注册
环信即时通讯云

环信即时通讯云

单聊、群聊、聊天室...
环信开发文档

环信开发文档

Demo体验

Demo体验

场景Demo,开箱即用
RTE开发者社区

RTE开发者社区

汇聚音视频领域技术干货,分享行业资讯
技术讨论区

技术讨论区

技术交流、答疑
资源下载

资源下载

收集了海量宝藏开发资源
iOS Library

iOS Library

不需要辛辛苦苦的去找轮子, 这里都有
Android Library

Android Library

不需要辛辛苦苦的去找轮子, 这里都有

血压飙升!记一次关于手机号存储的前后端讨论

起 事情是这样的,有一款产品需要实现国际化的注册需求,界面如下 : 涉及到的手机号码,因为要区分国家区号,所以和手机号是分开的,前端形态是分开存储的。一开始数据确实也是分开存的,么的问题。 本来是很简单的表单需求,结果出了幺蛾子。 承 对于前端来说,这就是两...
继续阅读 »


事情是这样的,有一款产品需要实现国际化的注册需求,界面如下 :




涉及到的手机号码,因为要区分国家区号,所以和手机号是分开的,前端形态是分开存储的。一开始数据确实也是分开存的,么的问题。


本来是很简单的表单需求,结果出了幺蛾子。



对于前端来说,这就是两个字段,在表单传值和数据逻辑处理时,直接用两个字段无疑是最直接和简单的:

const formData = {
country_code: '86',
phone: '13345431234'
...
}

但是,产品侧有一个强需求:手机号和其他的两个字段要做唯一性校验,不但要求三个至少存在一个,而且还要查别的业务库的数据表来校验这个注册人员的唯一性,有一套复杂的逻辑。总结一下就是,后端分开两个字段来判断很麻烦,这个也好理解,有耦合的逻辑重构要成本的。所以后端是这么要求数据的:

// (86)13345431234
phone: `(${country_code})${phone}`

将国家码放在前缀括号里拼接为一个字符串。这样一来,前端的成本就相对高了一些,不但在表单提交时要拼接,而且在列表查询时,要按照括号匹配解析出前缀来动态显示

const regex = /^\((\d+)\)(\d+)$/;
const matches = phoneNumber.match(regex);
// 如果匹配成功,返回国家码和号码的数组
if (matches) {
const countrycode = matches[1];
const number = matches[2];
return [countrycode, number];
}

就算这样,也可以接受,之前也不止一次挖过这种自己拼接自己解析的坑了。但是随后就有点让人血压上升了。



由于业务扩展,上面的手机号,要传递给一款即时通讯软件 xxx 的 API 来做下发,下发侧则规定了,需要不带括号但是要有国家区号前缀的手机号码

// 8613345431234
phone: `${country_code}${phone}`

这就有问题了,我们直接对接的后端需要括号的,下发侧对接的后端同学不需要括号。


第一阶段


血压上升 20%


讨论让后端换成两个字段存储,这样更灵活,方便拼接;或者给下发侧发数据时去掉括号也可以。后端很为难:”唯一性判断还有其他的逻辑很复杂,有耦合性,拆开字段就要重构,工期又紧,刚完成就要改,误了工期只有我背锅;去掉括号不是不可以,但地方有点多,还要考虑批量数据的性能,而且已经进入测试了,不接受临时需求“。我一想有道理啊,就去问下发侧同学。


第二阶段


血压上升 60%


问下发侧对接的同学,可以处理后再给 API下发吗?果断拒绝。他们也有理由的:”按照软件设计的规范,我们不应该非标处理这个问题的,下发的规范理应就是我们接口的规范,因为规范还有很多,这个地方妥协了,以后肯定还会有更多的非标处理,我们的代码就乱了。“


我草,我一想之前那那么多屎山的由来,觉得更有理了,那怎么办?弄得好像前端这里没有理了。


第三阶段


🔥 血压上升 120% 🔥


下发侧对接的同学反过来给我们前端提了建议,让我们在注册的时候,把前端两个字段就直接不用括号来拼接给后端,在需要解析的时候,使用特别手段解析出国家码。还好心的给我们搜索了一个解析手机号的 js 库:




我只能说,我 TM 谢谢你。😭


前端莫名其妙的凭空多了需求,而且加个库为了给后端擦屁股,还增加了打包体积,出了问题我们得首先背锅,这肯定不行。



这起事件牵涉的技术问题并没有多难,甚至有些幼稚,但是却蕴含了不少上班族哲学,最主要的一点是:如何学会背锅的问题。锅不是不能背,但是背锅换来的应该是产品成功后更多的功劳回报,而不是这种暗箱操作,老板压根不知道,只有扛不住的人吃暗亏。


个人觉得,这个应该是架构师或者技术总监的任务,提前了解上下游业务,并确定好字段与数据结构,而不是等用的时候才发现数据对不上。规划好用什么方案来开发。并将方案设计交于老板开会讨论得出定稿。之后再分派人员开发,就不会有这个事件了。



类似的问题还有很多,前后端与下游计算价格进制与单位不统一,每次都要非标转换;



最后的结局,产品出面调停,软磨硬泡,最终后端妥协了,夹在前端和下发侧之间, 确实为难。针对前端还是原来的逻辑不动,使用括号存储,他在下发侧拉取数据的时候做转换,去掉括号。其中涉及的性能问题和开发成本也申请了延长工期。


Happy Ending !!😁


血压恢复 0%





方案设计注意事项:

  • 注意产品迁移的问题。同类型的产品,如果针对的客户群体不同,其表现形态可能完全不一样,产品迁移过程中,要提前分析出新产品的业务场景的差异,做好风险分析。
  • 提前确定产品上下游生态,做好风险管控。比如下发侧有 API 对接的,需熟悉其 API 文档与使用限制,并在服务不可用时设计告警方案等。

作者:小肚肚肚肚肚哦
链接:https://juejin.cn/post/7275576074589880372
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

现在小厂实习都问的这么难了吗😱「万物心选一面(北京+电商)(vue+小程序)」

h5是怎么和微信小程序通信的?不同H5页面怎么通信的?webview是谁提供的?web-view 是往小程序中嵌入h5的容器。会自动铺满整个小程序页面。微信小程序和 uni-app 都提供了 web-view 组件。由于简历上有一...
继续阅读 »

h5是怎么和微信小程序通信的?不同H5页面怎么通信的?webview是谁提供的?

  1. web-view 是往小程序中嵌入h5的容器。会自动铺满整个小程序页面。微信小程序和 uni-app 都提供了 web-view 组件。
  2. 由于简历上有一段实习是移动端h5开发,让面试官误以为我做过微信小程序的内嵌h5页面,但其实实习中都是公司框架写的代码,页面也是直接嵌入公司的app中的,我并没有微信小程序开发的经验,现在整理答案也是死记硬背,mark一下看到的一篇讲的比较清楚的文章,以后学小程序了再来回顾。
    微信小程序web-view与H5 通信方式探索 - 掘金 (juejin.cn)
  3. h5 页面间通信其实就是前端跨页面通信(吧?)当时第一反应回答的是使用LocalStorage, 面试官又提出用户修改个人信息后返回页面更新信息的情况,回答的是我之前做表单有类似的场景,是向后端提交后在后端更新了数据,回退到原先的页面的时候在 created/activated 的时候获取数据。
    面试官:前端跨页面通信,你知道哪些方法? - 掘金 (juejin.cn)

什么是微任务?什么是宏任务?


如果说哪些操作是宏任务,哪些操作是微任务,那大部分同学都是比较清楚的:

  • 宏任务:script(整体代码)/setTimout/setInterval/setImmediate(node 独有)/requestAnimationFrame(浏览器独有)/IO/UI render(浏览器独有)
  • 微任务:process.nextTick(node 独有)/Promise.then()/Object.observe/MutationObserver

这里面试官还追问了一句,Promise本身是微任务吗


那这两者有具体的定义吗?老规矩,直接 mdn 开搜。mdn 中可以找到微任务(microtask),但是并没有宏任务或者(macrotask)的信息。但是在在 JavaScript 中通过 queueMicrotask() 使用微任务 - Web API 接口参考 | MDN 中我们可以发现,在文档中只有taskmicrotask,对应的就是事件循环中的任务队列task queue和微任务队列microtask queue


文档中还提到了JavaScript 中的 promise 和 Mutation Observer API 都使用微任务队列去运行它们的回调函数,想来面试官的意思就是,Promise本身只是一个代理,Promise()是他的构造函数,真正被放进微任务队列的是Promise的then方法中的回调函数。


文档中对任务和微任务的定义也比较冗长,我想能区分哪些是微任务,哪些是宏任务,说出他们分别会被放在任务队列和微任务队列以及他们的执行顺序(事件循环会持续调用微任务直至队列中没有留存的,再去调用任务队列)应该足够面试了。




遍历对象有哪些方法,如果是对象的原型链上的属性会不会被遍历到?有什么办法可以排除原型链上的属性?


直接上代码测试一波:

Object.prototype.age=18;   // 修改Object.prototype  
const person ={ name: "小明" };

// 输出 name, age
for(key in person){
console.log(key)
}

// 输出 name
Object.keys(person).forEach(key=>{
console.log(key);
})

// 输出 小明, 18
for(key in person){
console.log(person[key])
}

// 输出 小明
Object.values(person).forEach(value=>{
console.log(value);
})

// 输出 name: 小明
for (const [key, value] of Object.entries(person)) {
console.log(`${key}: ${value}`);
}

很明显,for...in 会遍历到原型链上的属性,Object上的keysvaluesentires方法不会。
看看 mdn 怎么说:



for...in 语句以任意顺序迭代一个对象的除Symbol以外的可枚举属性,包括继承的可枚举属性




Object.keys()  静态方法返回一个由给定对象 自身 的可枚举的字符串键属性名组成的数组。
Object.entries()  静态方法返回一个数组,包含给定对象 自有 的可枚举字符串键属性的键值对。


Object.values()  静态方法返回一个给定对象的 自有 可枚举字符串键属性值组成的数组。



那么如果仍然想使用 for...in 来遍历对象,并且不想要原型链上的属性,我们可以使用 Object.hasOwn 过滤掉它们:

for (key in person) {
if (person.hasOwn(key)) {
console.log(key);
}
}


如果指定的对象自身有指定的属性,则静态方法 Object.hasOwn()  返回 true。如果属性是继承的或者不存在,该方法返回 false



组件通信有哪些方法?依赖注入的数据是不是响应式的?有什么办法让他保持响应式的?

  • props / $emit
  • $emit / $on (eventBus)
  • provide / inject
  • $attrs / $listeners
  • ref / $ref
  • $parent / $children
  • vuex / pinia

Vue 组件间通信六种方式(完整版) - 掘金 (juejin.cn)


vue更新dom是异步还是同步?如何不使用nexttick实现nexttick的功能?vue的更新是哪一种微任务?


Vue更新DOM是异步的。这意味着我们在修改完data之后并不能立刻获取修改后的DOM元素。Vue需要通过nextTick方法才能获取最新的DOM。


Vue在调用Watcher更新视图时,并不会直接进行更新,而是把需要更新的Watcher加入到queueWatcher队列里,然后把具体的更新方法flushSchedulerQueue传给nextTick进行调用。nextTick只是单纯通过Promise、setTimeout等方法模拟的异步任务。


如果你想要不使用nextTick实现nextTick的功能,你可以使用Promise、setTimeout等方法来模拟异步任务。例如,你可以使用 Promise.resolve().then(callback) 或者 setTimeout(callback, 0) 来实现类似于nextTick的功能。


至于Vue的更新是哪一种微任务,它取决于浏览器兼容性。Vue会根据浏览器兼容性,选用不同的异步策略。例如,如果浏览器兼容Promise,那么Vue就会使用Promise来实现异步更新。如果浏览器不兼容Promise但兼容MutationObserver,那么Vue就会使用MutationObserver来实现异步更新。如果浏览器既不兼容Promise也不兼容MutationObserver,那么Vue就会使用setImmediatesetTimeout来实现异步更新。


vue能监听到数组的push方法吗?直接给响应式变量赋值一个新的数组会被监听到吗?


这里讨论的都是vue2vue3当中这些问题都已经被proxy解决了。



Vue 将被侦听的数组的变更方法进行了包裹,所以它们也将会触发视图更新。这些被包裹过的方法包括:

  • push()
  • pop()
  • shift()
  • unshift()
  • splice()
  • sort()
  • reverse()


替换整个对象或数组就和操作其他类型的响应式数据没区别了,自然是可以检测到的。


深入响应式原理 — Vue.js


如果要把数组api,比如push pop这些都改成async、await的异步函数要怎么做?怎么拿到这些方法?怎么传参?


这一问是我当时最蒙的,我到现在都不确定我是否领悟对了他要问什么,大致上的理解如下,如果有大佬知道的可以在评论区教学一下我这个小菜鸟。

// 保存数组原型上的push方法
const originalPush = Array.prototype.push;
// 重写数组原型上的push方法
Array.prototype.push = async function (...args) {
// 模拟异步操作
await new Promise(resolve => setTimeout(resolve, 500));
return originalPush.apply(this, args);
}
async function test() {
const arr = [1, 2, 3];
await arr.push(4);
console.log(arr);
}
test(); // [1, 2, 3, 4],需要延迟定时器中设定的时间才能打印出来

在沸点一位大佬的提醒下,面试官可能想问的是这个JavaScript 异步数组 - 个人文章 - SegmentFault 思否


总结


总结就是一个字,菜。

虽然是问的有点细致,但基本上都只能回答上来每一问的第一问,后面的深入追问就懵逼了。原因是因为自己基本上都是直接对着面经和八股文准备的,没有实践过,也没有看过相关的文档。之后还是要坚持把JavaScript高级程序设计和vue设计与实现啃完,不说把这些问题记得滚瓜烂熟能对答如流,起码也要在在面试官引导下应该有思路。


作者:炫饭第一名
链接:https://juejin.cn/post/7275141653420425277
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

血压飙升!记一次关于手机号存储的前后端讨论

web
本文是为了探讨技术架构管理的,不要带入实际生活 起 事情是这样的,有一款产品需要实现国际化的注册需求,界面如下 : 涉及到的手机号码,因为要区分国家区号,所以和手机号是分开的,前端形态是分开存储的。一开始数据确实也是分开存的,么的问题。 本来是很简单的表单...
继续阅读 »

本文是为了探讨技术架构管理的,不要带入实际生活




事情是这样的,有一款产品需要实现国际化的注册需求,界面如下 :


image.png


涉及到的手机号码,因为要区分国家区号,所以和手机号是分开的,前端形态是分开存储的。一开始数据确实也是分开存的,么的问题。


本来是很简单的表单需求,结果出了幺蛾子。



对于前端来说,这就是两个字段,在表单传值和数据逻辑处理时,直接用两个字段无疑是最直接和简单的:


const formData = {
country_code: '86',
phone: '13345431234'
...
}

但是,产品侧有一个强需求:手机号和其他的两个字段要做唯一性校验,不但要求三个至少存在一个,而且还要查别的业务库的数据表来校验这个注册人员的唯一性,有一套复杂的逻辑。总结一下就是,后端分开两个字段来判断很麻烦,这个也好理解,有耦合的逻辑重构要成本的。所以后端是这么要求数据的:


// (86)13345431234
phone: `(${country_code})${phone}`

将国家码放在前缀括号里拼接为一个字符串。这样一来,前端的成本就相对高了一些,不但在表单提交时要拼接,而且在列表查询时,要按照括号匹配解析出前缀来动态显示


const regex = /^\((\d+)\)(\d+)$/;
const matches = phoneNumber.match(regex);
// 如果匹配成功,返回国家码和号码的数组
if (matches) {
const countrycode = matches[1];
const number = matches[2];
return [countrycode, number];
}

就算这样,也可以接受,之前也不止一次挖过这种自己拼接自己解析的坑了。但是随后就有点让人血压上升了。



由于业务扩展,上面的手机号,要传递给一款即时通讯软件 xxx 的 API 来做下发,下发侧则规定了,需要不带括号但是要有国家区号前缀的手机号码


// 8613345431234
phone: `${country_code}${phone}`

这就有问题了,我们直接对接的后端需要括号的,下发侧对接的后端同学不需要括号。


第一阶段


血压上升 20%


讨论让后端换成两个字段存储,这样更灵活,方便拼接;或者给下发侧发数据时去掉括号也可以。后端很为难:”唯一性判断还有其他的逻辑很复杂,有耦合性,拆开字段就要重构,工期又紧,刚完成就要改,误了工期只有我背锅;去掉括号不是不可以,但地方有点多,还要考虑批量数据的性能,而且已经进入测试了,不接受临时需求“。我一想有道理啊,就去问下发侧同学。


第二阶段


血压上升 60%


问下发侧对接的同学,可以处理后再给 API下发吗?果断拒绝。他们也有理由的:”按照软件设计的规范,我们不应该非标处理这个问题的,下发的规范理应就是我们接口的规范,因为规范还有很多,这个地方妥协了,以后肯定还会有更多的非标处理,我们的代码就乱了。“


我草,我一想之前那那么多屎山的由来,觉得更有理了,那怎么办?弄得好像前端这里没有理了。


第三阶段


🔥 血压上升 120% 🔥


下发侧对接的同学反过来给我们前端提了建议,让我们在注册的时候,把前端两个字段就直接不用括号来拼接给后端,在需要解析的时候,使用特别手段解析出国家码。还好心的给我们搜索了一个解析手机号的 js 库:


企业微信截图_1bfc9849-e9f2-4289-a4cb-01098e3dcf2e.png


我只能说,我 TM 谢谢你。😭


前端莫名其妙的凭空多了需求,而且加个库为了给后端擦屁股,还增加了打包体积,出了问题我们得首先背锅,这肯定不行。



这起事件牵涉的技术问题并没有多难,甚至有些幼稚,但是却蕴含了不少上班族哲学,最主要的一点是:如何学会背锅的问题。锅不是不能背,但是背锅换来的应该是产品成功后更多的功劳回报,而不是这种暗箱操作,老板压根不知道,只有扛不住的人吃暗亏。


个人觉得,这个应该是架构师或者技术总监的任务,提前了解上下游业务,并确定好字段与数据结构,而不是等用的时候才发现数据对不上。规划好用什么方案来开发。并将方案设计交于老板开会讨论得出定稿。之后再分派人员开发,就不会有这个事件了。



类似的问题还有很多,前后端与下游计算价格进制与单位不统一,每次都要非标转换;



最后的结局,产品出面调停,软磨硬泡,最终后端妥协了,夹在前端和下发侧之间, 确实为难。针对前端还是原来的逻辑不动,使用括号存储,他在下发侧拉取数据的时候做转换,去掉括号。其中涉及的性能问题和开发成本也申请了延长工期。


Happy Ending !!😁


血压恢复 0%


39df05d4-0146-4fbc-9eda-384298424f19.jpg



方案设计注意事项:



  • 注意产品迁移的问题。同类型的产品,如果针对的客户群体不同,其表现形态可能完全不一样,产品迁移过程中,要提前分析出新产品的业务场景的差异,做好风险分析。

  • 提前确定产品上下游生态,做好风险管控。比如下发侧有 API 对接的,需熟悉其 API 文档与使用限制,并在服务不可用时设计告警方案等。


作者:小肚肚肚肚肚哦
来源:juejin.cn/post/7275576074589880372
收起阅读 »

为什么日本的网站看起来如此不同

web
该篇文章讨论了日本网站外观与设计的独特之处。作者指出日本网站设计与西方设计存在明显差异。文章首先强调了日本网站的视觉风格,包括丰富的色彩、可爱的角色和复杂的排版。作者解释了这种风格背后的文化和历史因素,包括日本的印刷传统和动漫文化。文章还讨论了日本网站的信息密...
继续阅读 »

该篇文章讨论了日本网站外观与设计的独特之处。作者指出日本网站设计与西方设计存在明显差异。文章首先强调了日本网站的视觉风格,包括丰富的色彩、可爱的角色和复杂的排版。作者解释了这种风格背后的文化和历史因素,包括日本的印刷传统和动漫文化。
文章还讨论了日本网站的信息密集型布局,这种布局适应了日本语言的特点,使得页面能够容纳大量文字和图像。此外,文章提到了日本网站的功能丰富性,如弹出式窗口和互动元素,以及这些元素在用户体验方面的作用。
作者强调了日本网站在技术和创新方面的进步,尽管在过去存在技术限制。最后,文章提出了一些关于如何将日本网站设计的元素应用到其他文化中的建议。


下面是正文~~~


多年来,我朋友与日本的网站有过许多接触——无论是研究签证要求、计划旅行,还是简单地在线订购东西。而我花了很长时间才适应这些网站上的大段文字、大量使用鲜艳颜色和10多种不同字体的设计,这些网站就像是直接冲着你扔过来的。


image.png


虽然有许多网站都采用了更简约、易于导航的设计,适应了西方网站的用户,但是值得探究的是为什么这种更复杂的风格在日本仍然盛行。


只是为了明确起见,这些不是过去的遗迹,而是维护的网站,许多情况下,它们最后一次更新是在2023年。


image.png


我们可以从几个角度来分析这种设计方法:



  • 字体和前端网站开发限制

  • 技术发展与停滞

  • 机构数字素养(或其缺乏)

  • 文化影响


与大多数话题一样,很可能没有一个正确的答案,而是这个网站设计是随着时间的推移而相互作用的各种因素的结果。


字体和前端网站开发限制


对于会一些基本排版知识、掌握适当软件并有一些空闲时间的人来说,为罗马化语言创造新字体可能是一项有趣的挑战。然而,对于日语来说,这是一个完全不同层次的努力。


要从头开始创建英文字体,需要大约230个字形——字形是给定字母的单个表示(A a a算作3个字形)——或者如果想覆盖所有基于拉丁字母表的语言,则需要840个字形。对于日语而言,由于其三种不同的书写系统和无数的汉字,需要7,000至16,000个字形甚至更多。因此,在日语中创建新字体需要有组织的团队合作和比其拉丁字母表的同行们更多的时间。



这并不令人意外,因此中文和(汉字)韩文字体也面临着类似的工作量,这导致这些语言通常被称为CJK字体所覆盖。



由于越来越少的设计师面对这个特殊的挑战,建立网站时可供选择的字体也越来越少。再加上缺乏大写字母和使用日文字体会导致加载时间较长,因为需要引用更大的库,这就不得不采用其他方式来创建视觉层次。


以美国和日本版的星巴克主页为例:


美国的:


image.png


日本的


image.png


就这样,我们就可以解释为什么许多日本网站倾向于用文字较多的图片来表示内容类别了。有时,你甚至会看到每个磁贴都使用自己定制的字体,尤其是在限时优惠的情况下。


image.png


技术发展/停滞与机构数字素养


如果你对日本感兴趣,你可能对现代与过时技术之间的鲜明对比有所了解。在许多地方,先进的技术与完全过时的技术并存。作为世界机器人领导者之一的国家,在台场人工岛上放置了一座真人大小的高达雕像,却仍然依赖软盘和传真机,面对2022年Windows资源管理器关闭时感到恐慌。


image.png


在德国,前总理安格拉·默克尔在2013年称互联网为“未知领域”后,遭到全国范围的嘲笑。然而,这在2018年被前网络安全部长樱田义孝轻易地超越,他声称自己从未使用过电脑,并且在议会被问及USB驱动器的概念时,他被引述为“困惑不解”(来源)。


对于那些尚未有机会窥探幕后幻象的人来说,这可能听起来很奇怪,但日本在技术素养方面严重落后于更新计划。因此,可以推断这些问题也在阻碍日本网站设计的发展。而具体来说,日本的网页设计正面临着这一挑战——只需在谷歌或Pinterest上搜索日本海报设计,就能看到一个非常不同和现代化的平面设计水平。


image.png


文化影响


在分析任何设计选择时,不应低估文化习俗、倾向、偏见和偏好的影响。然而,“这是文化”的说法可能过于简单化,并被用作为各种差异辩解的借口。而且,摆脱自己的观点偏见是困难的,甚至可能无法完全实现。


因此,从我们的角度来看,看这个网站很容易..


image.png


感觉不知所措,认为设计糟糕,然后就此打住。因为谁会使用这个混乱不堪的网站呢?


这就是因为无知而导致有趣的见解被忽视的地方。现在,我没有资格告诉你日本文化如何影响了这种设计。然而,我很幸运能够从与日本本土人士的交谈中获得启发,以及在日本工作和生活的经验。


与这个分析相关的一次对话实际上不是关于网站,而是关于YouTube的缩略图 - 有时候它们也同样令人不知所措。


image.png


对于习惯了许多西方频道所采用的极简和时尚设计——只有一个标题、重复的色彩搭配和有限的字体——上面的缩略图确实有些难以接受。然而,当我询问一个日本本土人士为什么许多极受欢迎频道的缩略图都是这样设计时,他对这种设计被视为令人困惑的想法感到惊讶。他认为日本的设计方法使视频看起来更加引人入胜,提供了一些信息碎片,从而使我们更容易做出是否有趣的明智决策。相比之下,我给他看的英文视频缩略图在他看来非常模糊和无聊。


也许正是这种寻求信息的态度导致了我们的观念如此不同。在日本,对风险的回避、反复核对和对迅速做出决策的犹豫明显高于西方国家。这与更加集体主义的社会心态紧密相连——例如,在将文件发送给商业伙伴之前进行两次(或三次)检查可能需要更长时间,但错误的风险显著降低,从而避免了任何参与者丢面子的情况发生。


尽管有人认为这只适用于足够高的赌注,而迷惑外国游客似乎不符合条件——搜索一下“Engrish”这个词,然后感谢我吧。


回到网站设计,这种文化角度有助于解释为什么在线购物、新闻和政府网站在外部观察者看来常常是“最糟糕的罪犯”。毕竟,这些正是需要大量细节直接对应于做出良好购买决策、高效地保持最新信息或确保你拥有某个特定程序的所有必要信息的情况。


有趣的是,关于美国人和中国/日本人如何感知信息,也有相当多的研究。几项研究的结果似乎表明,例如,日本人更加整体地感知信息,而美国人倾向于选择一个焦点来引导他们的注意力(来源)。这可能给我们提供了另一个线索,解释为什么即使在日语能力较高的情况下,西方人对这类网站也感到困难。


后但并非最不重要的是,必须说的是,网站并不是在一个在线真空中存在。而且,各种媒体,从小册子或杂志到地铁广告,也使用了尽可能多地压缩信息的布局,人们可能已经习惯了这种无处不在的方式,以至于没有人曾经想过质疑它。


长话短说,这并不是为了找到标题问题的绝对答案,也不是为了加强日本人独特性的观点,就像日本人论一样。相反,尤其是在看到了几次关注一个解释为“真正答案”的讨论之后,我想展示科技、历史和文化影响的广度,这些最终塑造了这种差异。


作者:王大冶
来源:juejin.cn/post/7272290608655941651
收起阅读 »

🤔️《你不知道的JavaScript》到底讲了些什么?

开始之前 在计算机科学的领域中,JavaScript是一门无法忽视的重要语言,深受许多开发者的喜爱。然而,它背后隐藏的复杂性和奥秘许多开发者并不为知。《你不知道的JavaScript》这三卷之作,对我个人而言真的算是常看常新,从刚从事前端开发到如今已经独当一面...
继续阅读 »

开始之前


在计算机科学的领域中,JavaScript是一门无法忽视的重要语言,深受许多开发者的喜爱。然而,它背后隐藏的复杂性和奥秘许多开发者并不为知。《你不知道的JavaScript》这三卷之作,对我个人而言真的算是常看常新,从刚从事前端开发到如今已经独当一面,从这本书中受益良多。因此在多次阅读后我选择用内容梗概+案例解析的形式将其精华部分记录下来,以供个人翻阅和与大家分享,那么我们开始吧


上卷


上卷主要针对语言核心的一些关键概念,如作用域、闭包、this等。本文将为笔者阅读过程中所总结和提炼的关键知识点与经典案例


1. 作用域是什么?


内容概览


本章介绍了JavaScript中的作用域概念,解释了变量如何被储存以及如何被引用。


实例分析

var a = 2;

function foo() {
var a = 3;
console.log(a); // 3
}

foo();

console.log(a); // 2

在这个例子中,我们看到a在全局作用域和foo函数的作用域中都有定义。函数内部的a不会影响到全局作用域中的a


2. 词法作用域


内容概览


词法作用域意味着作用域是由函数声明的位置来决定的,而不是函数调用的位置。


实例分析

function foo() {
console.log(a);
}

function bar() {
var a = 3;
foo();
}

var a = 2;

bar(); // 2

尽管foo函数在bar函数内部被调用,但foo函数的词法作用域仍然使其能够访问外部的变量a,所以输出为2。


3. 函数与块作用域


内容概览


介绍了函数作用域和块作用域,以及如何利用它们来避免变量冲突和其他问题。


实例分析

if (true) {
let a = 2;
console.log(a); // 2
}

console.log(a); // ReferenceError

使用let定义的变量具有块作用域,只能在声明它的块中访问。


4. 提升


内容概览


解释了提升(hoisting)现象,即变量和函数声明会被移动到它们所在的作用域顶部。


实例分析

foo(); // "Hello"

function foo() {
console.log("Hello");
}

尽管函数foo在调用之后被声明,但由于提升,它仍然可以正常调用。


5. 作用域闭包


内容概览


解释了闭包是如何工作的,以及它在JavaScript中的重要性。


实例分析

function makeGreeting(greeting) {
return function(name) {
console.log(greeting + ", " + name);
};
}

let sayHello = makeGreeting("Hello");
sayHello("Alice"); // "Hello, Alice"

sayHello函数是一个闭包,它记住了创建它时的作用域,因此能够访问greeting变量。


6. 词法分析和语法分析


实例分析


来看以下代码:

function add(x, y) {
return x + y;
}

let sum = add(5, 7);

在词法分析阶段,这段代码可能被分解为多个词法单元:function, add, (, x, ,, y, ), {, return, +, ;, }, let, =, 5, 7 等。然后,语法分析器会将这些词法单元组合成AST。


7. L查询与R查询


实例分析

function calculateArea(radius) {
const pi = 3.141592653589793;
return pi * radius * radius;
}

let r = 5;
let area = calculateArea(r);

在这个例子中,考虑let area = calculateArea(r);这行代码。对于calculateArea,它是RHS查询,因为我们需要获得这个函数的引用来执行它。而r也是RHS查询,因为我们正在获取它的值来传递给函数。


calculateArea函数内,pi和两次radius的查询都是RHS查询,因为我们获取它们的值来执行乘法操作。而return语句中的计算结果则赋值给了隐式的返回值,这涉及到LHS查询。


对于let r = 5;,这里的r是一个LHS查询,因为我们给它赋值了。


中卷


中卷的内容相比上卷来说更加深入且晦涩,其中包括令初学者头昏脑胀的面向对象编程与this原型链相关的知识,我将以更多的篇幅和更深入的案例来帮助大家进行理解


1. 对象


实例分析 1


使用工厂函数和构造器来创建对象:

function createPerson(name, age) {
return {
name,
age,
greet() {
console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
}
};
}

const person1 = createPerson('Alice', 30);
person1.greet();

深入分析


这是一个工厂函数的例子,允许我们快速创建具有相似属性和方法的对象。在此,greet方法是每个对象的一部分,这可能导致内存浪费,因为每次创建新对象时,都会为greet方法分配新的内存。


实例分析 2


使用getters和setters:

const book = {
title: 'In Search of Lost Time',
author: 'Marcel Proust',
get description() {
return `${this.title} by ${this.author}`;
},
set description(value) {
[this.title, this.author] = value.split(' by ');
}
};

book.description = '1984 by George Orwell';
console.log(book.title); // Outputs: 1984

深入分析


这个案例展示了如何利用对象的getters和setters来动态地管理对象的属性。通过setter,我们能够同时更新titleauthor,而getter则为我们提供了书的描述。


2. 类


实例分析 1


多态的使用:

class Animal {
makeSound() {
console.log('Some generic sound');
}
}

class Dog extends Animal {
makeSound() {
console.log('Woof');
}
}

const animal1 = new Animal();
const animal2 = new Dog();

animal1.makeSound(); // Outputs: Some generic sound
animal2.makeSound(); // Outputs: Woof

深入分析


多态是面向对象编程中的一个关键概念,允许我们创建能够以多种形式表现的对象。在此,我们看到Dog类重写了Animal类的makeSound方法,实现了多态。


实例分析 2


静态方法的使用:

class MathUtility {
static add(x, y) {
return x + y;
}
}

console.log(MathUtility.add(5, 3)); // Outputs: 8

深入分析


这个案例展示了如何在类中使用静态方法。与实例方法不同,静态方法不需要创建类的实例就可以被调用。它们通常用于执行与类的实例无关的操作。


3. 原型


实例分析


一个动态添加到原型的方法:

function Cat(name) {
this.name = name;
}

Cat.prototype.purr = function() {
console.log(`${this.name} is purring.`);
};

const whiskers = new Cat('Whiskers');
whiskers.purr(); // Outputs: Whiskers is purring.

深入分析


在此例中,我们后期将purr方法添加到Cat的原型中。这意味着即使在添加此方法后创建的所有Cat实例都可以访问它。这展示了原型继承的动态性质:我们可以在任何时候修改原型,这些更改会反映在所有继承了那个原型的对象上。


4. this和对象原型


JavaScript中的this是一个非常深入且经常被误解的主题。this并不是由开发者选择的,它是由函数调用时的条件决定的。


实例分析


考虑以下场景:

function showDetails() {
console.log(this.name);
}

const obj1 = {
name: 'Object 1',
display: showDetails
};

const obj2 = {
name: 'Object 2',
display: showDetails
};

obj1.display(); // Outputs: Object 1
obj2.display(); // Outputs: Object 2

深入分析


在这里,showDetails函数查看this.name。当它作为obj1的方法被调用时,this指向obj1。当它作为obj2的方法被调用时,this指向obj2。这说明了this的动态性质:它是基于函数如何被调用的。


5. 原型链


当试图访问一个对象的属性或方法时,JavaScript会首先在该对象本身上查找。如果未找到,它会在对象的原型上查找,然后是原型的原型,以此类推,直到找到该属性或到达原型链的末尾。


实例分析

function Animal(sound) {
this.sound = sound;
}

Animal.prototype.makeSound = function() {
console.log(this.sound);
}

function Dog() {
Animal.call(this, 'Woof');
}

Dog.prototype = Object.create(Animal.prototype);

const dog = new Dog();
dog.makeSound(); // Outputs: Woof

深入分析


当我们调用dog.makeSound()时,JavaScript首先在dog对象上查找makeSound。未找到后,它会在Dog的原型上查找。还是未找到,然后继续在Animal的原型上查找,最后找到并执行它。


6. 行为委托


行为委托是原型的一种使用模式,涉及到对象之间的关系,而不仅仅是克隆或复制。


实例分析

const Task = {
setID: function(ID) { this.id = ID; },
outputID: function() { console.log(this.id); }
};

const XYZ = Object.create(Task);

XYZ.prepareTask = function(ID, Label) {
this.setID(ID);
this.label = Label;
};

XYZ.outputTaskDetails = function() {
this.outputID();
console.log(this.label);
};

const task = Object.create(XYZ);
task.prepareTask(1, 'create demo for delegation');
task.outputTaskDetails(); // Outputs: 1, create demo for delegation

深入分析


XYZ不是Task的复制,它链接到Task。当我们在XYZ对象上调用setIDoutputID方法时,这些方法实际上是在Task对象上运行的,但this指向的是XYZ。这就是所谓的委托:XYZ在行为上委托给了Task


下卷


下卷的内容相较于中卷就基础了很多,更偏向于实际应用方向


1. 类型和语法


实例分析 - 类型转换


考虑以下的隐式类型转换:

var a = "42";
var b = a * 1;
console.log(typeof a); // "string"
console.log(typeof b); // "number"

深入分析


在这里,变量a是一个字符串,但当我们尝试与数字进行乘法操作时,它会被隐式地转换为一个数字。这是因为乘法操作符期望它的操作数是数字,因此JavaScript会尝试将字符串a转换为一个数字。


2. 异步和性能


实例分析 - Promises

function fetchData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve("Data fetched!");
}, 2000);
});
}

fetchData().then(data => {
console.log(data); // Outputs: "Data fetched!" after 2 seconds
});

深入分析


Promises 提供了一种更简洁、更具可读性的方式来处理异步操作。在上面的例子中,fetchData函数返回一个Promise。setTimeout模拟了异步数据获取,数据在2秒后可用。当数据准备好后,resolve函数被调用,then方法随后执行,输出数据。


3. ES6及其以上的特性


实例分析 - 使用箭头函数

const numbers = [1, 2, 3, 4];
const doubled = numbers.map(num => num * 2);
console.log(doubled); // [2, 4, 6, 8]

深入分析


箭头函数提供了一种更简洁的方式来定义函数,尤其是对于那些简短的、无状态的函数来说。在上述例子中,我们使用箭头函数简洁地定义了一个函数,该函数将其输入值乘以2,并使用map方法将其应用到一个数字数组中。


实例分析 - 使用async/await

async function fetchDataAsync() {
let response = await fetch('https://api.example.com/data');
let data = await response.json();
return data;
}

fetchDataAsync().then(data => console.log(data));

深入分析


async/await是ES7引入的特性,允许以同步的方式编写异步代码。在这个案例中,fetchDataAsync函数是一个异步函数,这意味着它返回一个Promise。await关键字使我们能够等待Promise解析,然后继续执行后面的代码。这消除了回调地狱,使异步代码更容易阅读和维护。


4. 迭代器和生成器


实例分析 - 使用生成器函数

function* numbersGenerator() {
yield 1;
yield 2;
yield 3;
}

const numbers = numbersGenerator();

console.log(numbers.next().value); // 1
console.log(numbers.next().value); // 2
console.log(numbers.next().value); // 3

深入分析


生成器函数使用function*声明,并且可以包含一个或多个yield表达式。每次调用生成器对象的next()方法时,函数都会执行到下一个yield表达式,并返回其值。这使我们能够按需产生值,非常适用于大数据集或无限数据流。


5. 增强的对象字面量


实例分析

const name = "Book";
const price = 20;

const book = {
name,
price,
describe() {
return `${this.name} costs ${this.price} dollars.`;
}
};

console.log(book.describe()); // "Book costs 20 dollars."

深入分析


增强的对象字面量允许我们在声明对象时使用更简洁的语法。在这里,我们直接使用变量名作为键,并使用简短的方法定义形式。这使得对象声明更为简洁和可读。


6. 解构赋值


实例分析

const user = {
firstName: "Alice",
lastName: "Smith"
};

const { firstName, lastName } = user;

console.log(firstName); // Alice
console.log(lastName); // Smith

深入分析


解构赋值允许我们从数组或对象中提取数据,并赋值给新的或已存在的变量。在此例中,我们从user对象中提取了firstNamelastName属性,并将它们赋值给了同名的新变量。


7. 模块


实例分析 - ES6模块导入和导出

// math.js
export function add(x, y) {
return x + y;
}

export function subtract(x, y) {
return x - y;
}

// app.js
import { add, subtract } from './math.js';

console.log(add(5, 3)); // 8
console.log(subtract(5, 3)); // 2

结语


经过对《你不知道的JavaScript》上、中、下三卷的深入探索,我们更加清晰地理解了JavaScript这门语言的复杂性、深度和强大之处。这不仅仅是关于语法或是新特性,更是关于理解其背后的哲学和设计思想。作为开发者,真正的掌握并不只是会用,而是要知其所以然。此书为我们打开了一扇探索JavaScript的大门,但真正的旅程,才刚刚开始。我们的每一步前行,都是为了更好地理解、更精准地应用,为编写出更高效、更优雅的代码而努力。


作者:J3LLYPUNK
链接:https://juejin.cn/post/7274839607656890429
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

👣 我在语雀做图册 - 更整齐的瀑布流算法

web
🙋🏻‍♀️ 编者按:本文作者是蚂蚁集团前端工程师亦池。介绍了语雀图册功能背后的更整齐的瀑布流算法,还有一段和 chatGPT 纠缠的故事 🤪,一起来看看~ 🏞️ 介绍一下图册 先来看看我们语雀年前上线的图册功能: 欢迎大家使用图册更好的展示自己的图片,...
继续阅读 »

🙋🏻‍♀️ 编者按:本文作者是蚂蚁集团前端工程师亦池。介绍了语雀图册功能背后的更整齐的瀑布流算法,还有一段和 chatGPT 纠缠的故事 🤪,一起来看看~



🏞️ 介绍一下图册


先来看看我们语雀年前上线的图册功能:
image.png


image.png



欢迎大家使用图册更好的展示自己的图片,秀秀最近的摄影作品、po一下最近的好吃的好玩的、晒几张靓照~
目前图册只是上了一个基础的初版,还有很多地方在体验和产品设计上也会继续优化,包括针对单张图的删除、排序,图册的尺寸调整,更快捷的把各种来源的图片放进图册里,大家有一些想法也欢迎提建议~



开发故事


🧐 瀑布流能不能再整齐一些


瀑布流是一个不新鲜的布局方式了,看到这个我第一反应自然是使用社区的开源库按需裁剪一下用起来。刚发布时也是这么上线的。扒过代码参考的开源库有:



但第一版本其实回发生下图左侧尴尬的情况:
image.png
肉眼可见我们要的是上图右侧的效果。


常见瀑布流算法的问题



原因:社区主流的瀑布流计算思路都是将已知高度的图片(实现上可以是图片加载完成后获取高度触发重新布局)分发了列容器里,每列记录实时高度,对于每一张新来的图片分发规则是放入最短的那一列。专业点说是贪心算法的思想。



所以当最后一张是长图时就会对布局的齐平性导致很大的冲击。(当然这不是说社区的方案都low,开源产品可能更多考虑了普适情况,譬如可能无法提前知道所有图片尺寸信息,全部加载完再重新布局一次又给用户带来干扰,甚至是懒加载场景更不好优雅的展示处理。)


在语雀编辑器场景,我们对于要布局的那批图片是能拿到宽高信息的,完全可以对所有图片整体考虑,计算一个最优结果再渲染,可以做到不被最后一张长图影响整体。


一开始我觉得这是个单纯的算法问题,可以抽象成将一个数字数组拆分成n个数组,使每个数组的数字和尽量接近,我觉得应该是有一种经典算法来解决这类问题的,譬如动态规划、背包问题之类的。


这么经典的问题不如问chatGPT吧,此处插入一段和chatGPT纠缠的故事。结论是它没给我找到靠谱答案。感兴趣的可以展开后面章节的折叠块看看这个让人哭笑不得的过程🙄。


💁‍♀️ 分析一下


chatGPT没能给我正确答案,我又是个基础算法的渣渣,想先找个方向再进去研究怎么实现,于是请教了一下一个酷爱刷算法题的师妹,得到的方向是:“这是个负载均衡类型问题,具有NP hard复杂度,无法获得最佳解,只能求近似最优解,用动态规划的思想是没错的”。


啥是NP hard复杂度,可以看后面的【基础知识】章节的科普。我也不清楚怎么证明这真的是一个NP hard复杂度的问题,但基础知识告诉我这类复杂度的问题往往复杂度是阶乘级别的,就是不是我们常见的O(n)、O(logn)、O(n^2)这种经典算法的复杂度,他们的复杂度叫做有多项式解。阶乘级别意味着暴力穷举,这往往是计算机无法接受的时间,也毫无算法可言。


咱这个问题,求解最优解时,每一张图片的摆放都影响着后面图片的位置,每张图之间都有关联,想想似乎确实只有穷举才能真正的找到最优解。加上对师妹算法水平的信任,我开始把问题缩减到动态规划领域。


那就拆解子问题,先计算子问题的近似最优解。


🏄‍ ♀️解决方案


核心思想:




  1. 计算平均值,让每一组的和尽量接近均值,最终每组和的差异才会最小

  2. 将原数组arr从大到小排序,降低分组时便利查找的复杂度

  3. 遍历原数组arr,从目标的n个分组的第一组开始装数据,直到它接近均值停止。这里注意接近的意思不是<=avg,而是在均值临界点,加上一个值num1 < avg后,和均值的差值是delta,往前遍历找(意味着num2 > num1)第一个没被分组的数据num2放入当前组后,num2 - avg < delta,如果是的则装num2,否则装num1。确保装的是最接近均值的数。

  4. 对于最后一个分组n-1要装数据时,需要确保arr的每一个数据都被分配完,并且各组结果最小,所以最后一组的策略不参考平均值,而是按和最小的分组去塞arr里的每一个数据。



另外注意,对于已经分好组的数据打个标,以免被重复分组。


这里我们是在拆解子问题



  • 把复杂的分组后每组方差最小的问题,转化为让每组和最接近平均值的问题,将整体的问题拆解成了n个组的问题

  • n个组塞值时,又是一个找数据使它最接近均值的子问题


其中为了降低复杂度不搞遍历的最优,确实只做到了近似最优解。譬如放值前先做了排序,只要当前数据放进去 < avg都先无脑放,就会出现,譬如剩下的数据有[48, 25, 25], 均值是50,本来我们可以放[25,25]得到最接近均值的数据,但现在只放入了48。


🤪 图片场景的特殊考虑因子


当我把一个纯数学解放入瀑布流场景时,发现事情并没有这么简单,算法的最优还是要为图片展示场景效果的最优做一些让步


参差感


譬如你看这个是最优解么?
image.png
因为我们先做了排序,并且按排序的数据顺序做分配,所以长图它它它它都跑到同一列去了。image.png
这个视觉上没有了参差美可受不了。


于是在接近最优的做法上妥协一步。先把排序前n的数据挨个放到n组,让个高的先均匀分布。


结合保留用户本来的顺序,是不是舒服一些:
image.png


这里依旧不是最佳效果,因为只取了前n个,试想我们如果是3组,5个长图,还是有一组全是长图。但长与短的边界实在无法敲定,除非再搞个每张图片高度的均值,大于均值一定阈值的数据先均匀分布到n组,但这种操作的数据越多,越影响到底部整体的平齐效果。所以还是只选了和组数相同的前n张这么处理。我估摸着大多数用户在文档里的图片是个辅助,不会搞出特别大数量级还夹杂很多长短分明的图。当前能保持一定数量级 (<10)展示上不会有太大问题。


排序


尽量得保证用户原图的顺序,所以需要记录原图的顺序,然后在分组完成后:




  1. 每列里按原图顺序重排下顺序

  2. 列与列之间按第一个图的顺序重排下顺序



能做到尽量接近原顺序但不绝对。


纯数字上[[25], [25], [25,25]][[25,25], [25], [25]]的分组没有差别。但是图片场景又不一样了:
image.png
这排列总透着一股奇怪image.png
于是再让步牺牲一下复杂度:



装最后一组数据分配余数之前,先把分配好的分组,先排序,组与组的和相等时优先放入排前面的数组。



当前版本优缺点


目前至少是在最平齐和图片参差感之间谋求的一个较优解,但绝不是最优解,理论上此类问题不穷举遍历获得不了最优解。但我们可以通过优化局部策略,使它更靠近最优解。不过一定是优于贪心算法把每张图放入高度最小列的做法。这里如果有深入研究过瀑布流的小伙伴有更优的方案,欢迎提供,让语雀的瀑布流更整齐~


做事情咱也不能只说好的,对问题缄口不言,目前的问题有:



  • 前面也说过,如果大量图片,并且存在 分组张数n 的与其他图片长度拉开巨大差距的图片,排版还是不够有参差感

  • 先按大小排序,后分组,会对原图顺序造成偏差,很难复原严格的行列顺序,但用户还是能一定程度的干预排序,只是无法满足一定要求图A和图B不放入同一列这种诉求。从这个角度说,顺序上不如贪心算法方案更接近原顺序,贪心方案的最后一张长图问题其实可以通过主动拖拽顺序把长图放到前面来解决掉,但是这对用户的理解力要求太高了。


anyway,以下的数据哪个算法也无法救🥲。目前列数是根据展示区宽度弹性计算的,这种想优雅可能要触发列数的改变规则了。
image.png


chatGPT的插曲


点我展开查看哭笑不得的过程### 第1轮
image.png
一开始它给了我个贪心算法的不是最优解,得让它进阶


第2轮


image.pngimage.png
看上去很高深,但这测试数据结果不对啊。
我换个说法?是不是不能理解什么叫数字加和尽量接近


第3轮


image.png
结果不对,继续让他换个解法


第4轮


image.png
还是肉眼可见的不对,虽然我肉眼分的也不是最优解,最后我的算法告诉我是可以分成三组和都是80的:[80], [32, 32, 12, 3, 1], [30, 21, 20, 9]


那么问题在哪呢,我尝试问了它一个很简单的问题:
image.png
原来加和都求不对,我放弃它了。。。


:::warning
综上:chatGPT能高效省事让你偷懒,但前提是你得能区分出它的答案靠不靠谱,如果你都不知道真相的问题仍给他,就会被忽悠了也不知道。另外别想用它帮你写笔试题了,它只根据语义生成,但并不真的运行代码,给的代码和结果可能完全不匹配。
:::


📔 基础知识


资料:




复杂度被分为两种级别:一种是O(1),O(log(n)),O(n^a)等,我们把它叫做多项式级的复杂度,因为它的规模n出现在底数的位置;另一种是O(a^n)和O(n!)型复杂度,它是非多项式级的,其复杂度计算机往往不能承受




P问题: 如果一个问题可以找到一个能在多项式的时间里解决它的算法,那么这个问题就属于P问题
NP问题: NP问题不是非P类问题。NP问题是指可以在多项式的时间里验证一个解的问题。NP问题的另一个定义是,可以在多项式的时间里猜出一个解的问题
NPC问题:同时满足下面两个条件的问题就是NPC问题。首先,它得是一个NP问题;然后,所有的NP问题都可以约化到它。NPC问题目前没有多项式的有效算法,只能用指数级甚至阶乘级复杂度的搜索。
**NP-Hard问题:**它满足NPC问题定义的第二条但不一定要满足第一条。NP-Hard问题同样难以找到多项式的算法,但它不列入我们的研究范围,因为它不一定是NP问题。即使NPC问题发现了多项式级的算法,NP-Hard问题有可能仍然无法得到多项式级的算法




约化:(Reducibility,有的资料上叫“归约”)。简单地说,一个问题A可以约化为问题B的含义即是,可以用问题B的解法解决问题A,或者说,问题A可以“变成”问题B。通过对某些问题的不断约化,我们能够不断寻找复杂度更高,但应用范围更广的算法来代替复杂度虽然低,但只能用于很小的一类问题的算法。



next:拼图


接下来我们还会上线更灵活的拼图能力。**拼图算法可以实现任何尺寸的图片,保持原比例不裁剪,用户任意摆放位置,最终绘制成整齐的矩形,**这个算法实现也远比瀑布流复杂。


譬如你可以这样:
image.png
也可以拖成这样:
image.png


还可以拖成这样:
image.png


甚至拖成这样:
image.png


等等等等...... 随意组合排序,最终都能整齐。


等上线后我再写写拼图的故事~


作者:支付宝体验科技
来源:juejin.cn/post/7198370695079903291
收起阅读 »

产品经理:实现一个微信输入框

web
近期在开发AI对话产品的时候为了提升用户体验增强了对话输入框的相关能力,产品初期阶段对话框只是一个单行输入框,导致在文本内容很多的时候体验很不好,所以进行体验升级,类似还原了微信输入框的功能(只是其中的一点点哈🤏)。 初期认为这应该改动不大,就是把input换...
继续阅读 »


近期在开发AI对话产品的时候为了提升用户体验增强了对话输入框的相关能力,产品初期阶段对话框只是一个单行输入框,导致在文本内容很多的时候体验很不好,所以进行体验升级,类似还原了微信输入框的功能(只是其中的一点点哈🤏)。


初期认为这应该改动不大,就是把input换成textarea吧。但是实际开发过程发现并没有这么简单,本文仅作为开发过程的记录,因为是基于uniapp开发,相关实现代码都是基于uniapp


简单分析我们大概需要实现以下几个功能点:



  • 默认单行输入

  • 可多行输入,但有最大行数限制

  • 超过限制行术后内容在内部滚动

  • 支持回车发送内容

  • 支持常见组合键在输入框内换行输入

  • 多行输入时高度自适应 & 页面整体自适应


单行输入


默认单行输入比较简单直接使用input输入框即可,使用textarea的时候目前的实现方式是通过设置行内样式的高度控制,如我们的行内高度是36px,那么就设置其高度为36px。为什么要通过这种方式设置呢?因为要考虑后续多行输入超出最大行数的限制,需要通过高度来控制textarea的最大高度。


<textarea style="{ height: 36px }" />


多行输入


多行输入核心要注意的就是控制元素的高度,因为不能随着用户的输入一直增加高度,我们需要设置一个最大的行数限制,超出限制后就不再增加高度,内容可以继续输入,可以在输入框内上下滚动查看内容。


这里需要借助于uniapp内置在textarea@linechange事件,输入框行数变化时调用并回传高度和行数。如果不使用uniapp则需要对输入文字的长度及当前行高计算出对应的行数,这种情况还需要考虑单行文本没有满一行且换行的情况。


代码如下,在linechange事件中获取到最新的高度设置为textarea的高度,当超出最大的行数限制后则不处理。


linechange(event) {
const { height, lineCount } = event.detail
if (lineCount < maxLine) {
this.textareaHeight = height
}
}

这是正常的输入,还有一种情况是用户直接粘贴内容输入的场景,这种时候不会触发@linechange事件,需要手动处理,根据粘贴文本后的textarea的滚动高度进行计算出对应的行数,如超出限制行数则设置为最大高度,否则就设置为实际的行数所对应的高度。代码如下:


const paddingTop = parseInt(getComputedStyle(textarea).paddingTop);
const paddingBottom = parseInt(getComputedStyle(textarea).paddingBottom);
const textHeight = textarea.scrollHeight - paddingTop - paddingBottom;
const numberOfLines = Math.floor(textHeight / lineHeight);

if (numberOfLines > 1 && this.lineCount === 1) {
const lineCount = numberOfLines < maxLine ? numberOfLines : maxLine
this.textareaHeight = lineCount * lineHeight
}

键盘发送内容


正常我们使用电脑聊天时发送内容都是使用回车键发送内容,使用ctrlshiftalt等和回车键的组合键将输入框的文本进行换行处理。所以接下来要实现的就是对键盘事件的监听,基于事件进行发送内容和内容换行输入处理。


首先是事件的监听,uniapp不支持keydown的事件监听,所以这里使用了原生JS做监听处理,为了避免重复监听,对每次开始监听前先进行移除事件的监听,代码如下:


this.$refs.textarea.$el.removeEventListener('keydown', this.textareaKeydownHandle)
this.$refs.textarea.$el.addEventListener('keydown', this.textareaKeydownHandle)

然后是对textareaKeydownHandle方法的实现,这里需要注意的是组合键对内容换行的处理,需要获取到当前光标的位置,使用textarea.selectionStart可获取,基于光标位置增加一个换行\n的输入即可实现换行,核心代码如下:


const cursorPosition = textarea.selectionStart;
if(
(e.keyCode == 13 && e.ctrlKey) ||
(e.keyCode == 13 && e.metaKey) ||
(e.keyCode == 13 && e.shiftKey) ||
(e.keyCode == 13 && e.altKey)
){
// 换行
this.content = `${this.content.substring(0, cursorPosition)}\n${this.content.substring(cursorPosition)}`
}else if(e.keyCode == 13){
// 发送
this.onSend();
e.preventDefault();
}

高度自适应


当多行输入内容时输入框所占据的高度增加,导致页面实际内容区域的高度减小,如果不进行动态处理会导致实际内容会被遮挡。如下图所示,红色区域就是需要动态处理的高度。



主要需要处理的场景就是输入内容行数变化的时候和用户粘贴文本的时候,这两种情况都会基于当前的可视行数计算输入框的高度,那么内容区域的高度就好计算了,使用整个窗口的高度减去输入框的高度和其他固定的高度如导航高度和底部安全距离高度即是真实内容的高度。


this.contentHeight = this.windowHeight - this.navBarHeight - this.fixedBottomHeight - this.textareaHeight;

最后


到此整个输入框的体验优化核心实现过程就结束了,增加了多行输入,组合键换行输入内容,键盘发送内容,整体内容高度自适应等。整体实现过程的细节功能点还是比较多,有实现过类似需求的同学欢迎留言交流~


看完本文如果觉得有用,记得点个赞支持,收藏起来说不定哪天就用上啦~


作者:南城FE
来源:juejin.cn/post/7267791228872753167
收起阅读 »

微信小程序 折叠屏适配

web
最近维护了将近的一年的微信小程序(某知名企业),突然提出要兼容折叠屏,这款小程序主要功能一些图表汇总展示,也就是专门给一些领导用的,也不知道为啥领导们为啥突然喜欢用折叠屏手机了,一句话需求,苦的还是咱们程序员,但没办法,谁让甲方是爸爸呢,硬着头皮改吧,好在最后...
继续阅读 »

最近维护了将近的一年的微信小程序(某知名企业),突然提出要兼容折叠屏,这款小程序主要功能一些图表汇总展示,也就是专门给一些领导用的,也不知道为啥领导们为啥突然喜欢用折叠屏手机了,一句话需求,苦的还是咱们程序员,但没办法,谁让甲方是爸爸呢,硬着头皮改吧,好在最后解决了,因为是甲方内部使用的小程序,这里不便贴图,但有官方案例图片,以供参考


查看了微信官网
大屏适配
响应显示区域变化


启用大屏模式


从小程序基础库版本 2.21.3 开始,在 Windows、Mac、车机、安卓 WMPF 等大屏设备上运行的小程序可以支持大屏模式。可参考小程序大屏适配指南。方法是:在 app.json 中添加 "resizable": true


看到这里我心里窃喜,就加个配置完事了?这也太简单了,但后面证明我想简单了,
主要有两大问题:



  • 1 尺寸不同的情况下内容展示效果兼容问题

  • 2 预览版和体验版 大屏模式冷启动会生效,但热启动 和 菜单中点击重新进入小程、授权操作,会失效变成窄屏


解决尺寸问题


因为css的长度单位大部分用的 rpx,窄屏和宽屏展示差异出入较大,别说客户不认,自己这关就过不了,简直都不忍直视,整个乱成一片,尤其登录页,用了定位,更是乱上加乱。


随后参考了官方的文档 小程序大屏适配指南自适应布局,方案对于微信小程序原生开发是可行的,但这个项目用的 uni-app开发的,虽然uni-app 也有对应的响应式布局组件,再加上我是个比较爱偷懒的人(甲方给的工期事件也有限制),不可能花大量时间把所有也页面重新写一遍布局,这是不现实的。


于是又转战到uni-app官网寻找解决方案 uni-app宽屏适配指南


内容缩放拉伸的处理 这一段中提出了两个策略



  • 1.局部拉伸:页面内容划分为固定区域和长宽动态适配区域,固定区域使用固定的px单位约定宽高,长宽适配区域则使用flex自动适配。当屏幕大小变化时,固定区域不变,而长宽适配区域跟着变化

  • 2.等比缩放:根据页面屏幕宽度缩放。rpx其实属于这种类型。在宽屏上,rpx变大,窄屏上rpx变小。


随后看到这句话特别符合我的需求,哈哈 省事 省事 省事


策略2省事,设计师按750px屏宽出图,程序员直接按rpx写代码即可。但策略2的实际效果不如策略1好。程序员使用策略1,分析下界面,设定好局部拉伸区域,这样可以有更好的用户体验


具体实现


1.配置 pages.json 的 globeStyle


{
"globalStyle": {
"rpxCalcMaxDeviceWidth": 1200, // rpx 计算所支持的最大设备宽度,单位 px,默认值为 960
"rpxCalcBaseDeviceWidth": 375, // rpx 计算使用的基准设备宽度,设备实际宽度超出 rpx 计算所支持的最大设备宽度时将按基准宽度计算,单位 px,默认值为 375
"rpxCalcIncludeWidth": 750 // rpx 计算特殊处理的值,始终按实际的设备宽度计算,单位 rpx,默认值为 750
},
}

2.单位兼容


还有一点官方也提出来了很重要,那就很多时候 会把宽度750rpx 当成100% 使用,这在宽屏的设备上就会有问题, uniapp给了两种解决方案



  • 750rpx 改为100%

  • 另一种是配置rpxCalcIncludeWidth,设置某个特定数值不受rpxCalcMaxDeviceWidth约束


想要用局部拉伸:页面内容划分为固定区域和长宽动态适配区域”的策略,单位必须用px


添加脚本


项目根目录新增文件 postcss.config.js 内容如下。则在编译时,编译器会自动转换rpx单位为px。


// postcss.config.js

const path = require('path')
module.exports = {
parser: 'postcss-comment',
plugins: {
'postcss-import': {
resolve(id, basedir, importOptions) {
if (id.startsWith('~@/')) {
return path.resolve(process.env.UNI_INPUT_DIR, id.substr(3))
} else if (id.startsWith('@/')) {
return path.resolve(process.env.UNI_INPUT_DIR, id.substr(2))
} else if (id.startsWith('/') && !id.startsWith('//')) {
return path.resolve(process.env.UNI_INPUT_DIR, id.substr(1))
}
return id
}
},
'autoprefixer': {
overrideBrowserslist: ["Android >= 4", "ios >= 8"],
remove: process.env.UNI_PLATFORM !== 'h5'
},
// 借助postcss-px-to-viewport插件,实现rpx转px,文档:https://github.com/evrone/postcss-px-to-viewport/blob/master/README_CN.md
// 以下配置,可以将rpx转换为1/2的px,如20rpx=10px,如果要调整比例,可以调整 viewportWidth 来实现
'postcss-px-to-viewport': {
unitToConvert: 'rpx',
viewportWidth: 200,
unitPrecision: 5,
propList: ['*'],
viewportUnit: 'px',
fontViewportUnit: 'px',
selectorBlackList: [],
minPixelValue: 1,
mediaQuery: false,
replace: true,
exclude: undefined,
include: undefined,
landscape: false
},
'@dcloudio/vue-cli-plugin-uni/packages/postcss': {}
}
}


大屏模式失效问题


下面重头戏来了,这期间经历 蜿蜒曲折 ,到头来发现都是无用功,我自己都有被wx蠢到发笑,唉,


样式问题解决后 开始着重钻研 大屏失效的问题,但看了官方的多端适配示例demo,人家的就是好的,那就应该有解决办法,于是转战github地址 下项目,谁知这项目暗藏机关,各种报错,让你跑不起来。。。。,让我一度怀疑腾讯也这么拉跨


还好issues 区一位大神有解决办法 感兴趣的老铁可以去瞅瞅
image


1693664649860.jpg


另外 微信小程序开发工具需要取消这两项,最后当项目跑起来后我还挺开心,模拟器上没有问题,但用真机预览的时候我啥眼了,还是窄屏,偶尔可以大屏,后面发现 冷启动是大屏,热启动和点击右上角菜单中的重新进入小程序按钮都会自己变成窄屏幕


官方案例.gif批量更新.gif

这是官方的项目啊,为啥人家的可以,我本地跑起来却不可以,让我一度怀疑这里有内幕,经过几轮测试还是不行,于是乎,我开始了各种询问查资料,社区、私聊、评论、github issues,最后甚至 统计出来了 多端适配示例demo 开发者的邮箱 挨个发了邮件,但都结果无一例外,全部石沉大海


1693666642117.jpgwx-github-issues-110.jpg
私聊.jpg评论.jpg
wx-mini-dev.jpgimage.png

结果就是,没有办法了,想看看是不是只有预览和体验版有问题,后面发布到正式版后,再看居然没问题了,就是这么神奇,也是无语!!!! 原来做了这么多无用功。。。。


作者:iwhao
来源:juejin.cn/post/7273764921456492581
收起阅读 »

详解JS判断页面是在手机端还是在PC端打开的方法

web
下面详细介绍一下如何判断页面是在手机端还是在PC端打开,并提供两条示例说明。 方法一:使用UA判断 UA(UserAgent)是指HTTP请求头中的一部分,用于标识客户端的一些信息,比如用户的设备类型、浏览器型号等等。因此,我们可以通过判断UA中的关键字来确定...
继续阅读 »

下面详细介绍一下如何判断页面是在手机端还是在PC端打开,并提供两条示例说明。


方法一:使用UA判断


UA(UserAgent)是指HTTP请求头中的一部分,用于标识客户端的一些信息,比如用户的设备类型、浏览器型号等等。因此,我们可以通过判断UA中的关键字来确定页面访问者的设备类型。下面是实现的代码:


const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);

if (isMobile) {
console.log('当前在手机端');
} else {
console.log('当前在PC端');
}

代码解析:


首先,我们使用正则表达式匹配navigator.userAgent中是否包含iPhoneiPadiPodAndroid这些关键字,如果匹配成功,则说明当前是在移动端。如果匹配失败,则说明当前是在PC端。


需要注意的是,该方法并不100%准确,因为用户可以使用PC浏览器模拟手机UA,也有可能使用移动端浏览器访问PC网站。


方法二:使用媒体查询判断


媒体查询是CSS3的一个新特性,可以根据不同的媒体类型(比如设备屏幕的宽度、高度、方向等)来设置不同的CSS样式。我们可以利用媒体查询来判断页面是在手机端还是在PC端打开。下面是实现的代码:


<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8"/>
<title>判断页面是在手机端还是在PC端</title>
<style>
/* 默认样式 */
p {
font-size: 24px;
color: yellow;
}
/* 移动端样式 */
@media (max-width: 767px) {
p {
font-size: 20px;
color: green;
}
}
</style>
</head>
<body>
<p>测试内容</p>
</body>
</html>

代码解析:


在CSS中,我们使用@media关键字定义了一个媒体查询,当浏览器宽度小于等于767px的时候,p元素的字体大小和颜色都会发生改变,从而实现了对移动端的识别。如果浏览器宽度大于767px,则会使用默认样式。


需要注意的是,该方法只能判断设备的屏幕宽度,不能确定设备的真实类型,因此并不太准确。


总的来说,两种方法各有优缺点,具体选择哪种方法要根据自己的需求和场景来决定。一般来说,如果只是想简单地判断页面访问者的设备类型,使用第一种方法即可。如果需要根据设备类型来优化网站的布局和样式,可以使用第二种方法。


作者:RuiRay
来源:juejin.cn/post/7273746154642014262
收起阅读 »

强大的css计数器,你确定不来看看?

web
强大的 css 计数器 css content 属性有很多实用的用法,这其中最为强大的莫过于是计数器了,它甚至可以实现连 javascript 都不能够实现的效果,下面我们一起来研究一下吧。 css 计数器主要有 3 个关键点需要掌握。如下: 首先需要一个计...
继续阅读 »

强大的 css 计数器


css content 属性有很多实用的用法,这其中最为强大的莫过于是计数器了,它甚至可以实现连 javascript 都不能够实现的效果,下面我们一起来研究一下吧。


css 计数器主要有 3 个关键点需要掌握。如下:



  1. 首先需要一个计数器的名字,这个名字由使用者自己定义。

  2. 计数器有一个计数规则,比如是 1,2,3,4...这样的递增方式,还是 1,2,1,2...这样的连续递增方式。

  3. 计数器的使用,即定义好了一个计数器名字和计数规则,我们就需要去使用它。


以上 3 个关键点分别对应的就是 css 计数器的 counter-reset 属性,counter-increment 属性,和 counter()/counters()方法。下面我们依次来介绍这三个玩意儿。


counter-reset 属性


counter-reset 属性叫做计数器重置,对应的就是创建一个计数器名字,如果可以,顺便也可以告诉计数器的计数起始值,也就是从哪个值开始计数,默认值是 0,注意是 0,而不是 1。例如以下一个示例:


html 代码如下:


<p>开始计数,计数器名叫counter</p>
<p class="counter"></p>

css 代码如下:


.counter {
counter-reset: counter;
}

.counter::before {
content: counter(counter);
}

在浏览器中运行以上示例,你会看到如下图所示:


counter-1.png


可以看到计数器的初始值就是 0,现在我们修改一下 css 代码,如下所示:


.counter {
counter-reset: counter 1;
}

在浏览器中运行以上示例,你会看到如下图所示:


counter-2.png


这次我们指定了计数器的初始值 1,所以结果就是 1,计数器的初始值同样也可以指定成小数,负数,如-2,2.99 之类,只不过 IE 和 FireFox 浏览器都会认为是不合法的数值,当做默认值 0 来处理,谷歌浏览器也会直接显示负数,如下图所示:


counter-3.png


低版本谷歌浏览器处理小数的时候是向下取整,比如 2.99 则显示 2,最新版本则当成默认值 0,来处理,如下图所示:


counter-4.png



ps: 当然不推荐指定初始值为负数或者小数。



你以为到这里就完了吗?还没有,计数器还可以指定多个,每一个计数器之间用空格隔开,比如以下代码:


.counter {
counter-reset: counter1 1 counter2 2;
}

.counter::before {
content: counter(counter1) counter(counter2);
}

在浏览器中运行以上示例,你会看到如下图所示:


counter-5.png


除此之外,计数器名还可以指定为 none 和 inherit,也就是取消计数和继承计数器,这没什么好说的。


counter-increment


顾名思义,该属性就是计数器递增的意思,也就是定义计数器的计数规则,值为计数器的名字,可以是一个或者多个,并且也可以指定一个数字,表示计数器每次变化的数字,如果不指定,默认就按照 1 来变化。比如以下代码:


.counter {
counter-reset: counter 1;
counter-increment: counter;
}

得到的结果就是: 1 + 1 = 2。如下图所示:


counter-6.png


再比如以下代码:


.counter {
counter-reset: counter 2;
counter-increment: counter 3;
}

得到的结果就是: 2 + 3 = 5,如下图所示:


counter-7.png


由此可见,计数器的规则就是: 计数器名字唯一,每指定一次计数规则,计数器就会加一,每指定二次计数规则,计数器就会加二,……以此类推。


计数规则不仅可以创建在元素上,也可以创建在使用计数器的元素上,比如以下代码:


.counter {
counter-reset: counter;
counter-increment: counter;
}

.counter::before {
content: counter(counter);
counter-increment: counter;
}

我们不仅在类名为 counter 元素上创建了一个计数器规则,同样的也在 before 伪元素上创建了一个计数器规则,因此最后的结果就是: 0 + 1 + 1 = 2。如下图所示:


counter-8.png


总而言之,无论位置在何处,只要有 counter-increment,对应的计数器的值就会变化, counter()只是输出而已!计数器的数值变化遵循 HTML 渲染顺序,遇到一个 increment 计数器就变化,什么时候 counter 输出就输出此时的计数值。


除此之外,计数器规则也可以和计数器一样,创建多个计数规则,也是以空格区分,比如以下示例代码:


.counter {
counter-reset: counter1 1 counter2 2;
counter-increment: counter1 2 counter2 3;
}

.counter::before {
content: counter(counter1) counter(counter2);
counter-increment: counter1 4 counter2 5;
}

此时的结果就应该是计数器 1: 1 + 2 + 4 = 7,计数器 2: 2 + 3 + 5 = 10。如下图所示:


counter-9.png


同样的,计数器规则的值也可以是负数,也就是递减效果了,比如以下代码:


.counter {
counter-reset: counter1 1 counter2 2;
counter-increment: counter1 -1 counter2 -3;
}

.counter::before {
content: counter(counter1) counter(counter2);
counter-increment: counter1 2 counter2 5;
}

此时的结果就应该是计数器 1: 1 - 1 + 2 = 2,计数器 2: 2 - 3 + 5 = 4。如下图所示:


counter-10.png


同样的计数规则的值也可以是 none 或者 inherit。


counter


counter 方法类似于 calc,主要用于定义计数器的显示输出,到目前为止,我们前面的示例都是最简单的输出,也就是如下语法:


counter(name); /* name为计数器名 */

实际上还有如下的语法:


counter(name,style);

style 参数和 list-style-type 的值一样,意思就是不仅可以显示数字,还可以显示罗马数字,中文字符,英文字母等等,值如下:


list-style-type: disc | circle | square | decimal | lower-roman | upper-roman |
lower-alpha | upper-alpha | none | armenian | cjk-ideographic | georgian |
lower-greek | hebrew | hiragana | hiragana-iroha | katakana | katakana-iroha |
lower-latin | upper-latin | simp-chinese-informal;

比如以下的示例代码:


.counter {
counter-reset: counter;
counter-increment: counter;
}

.counter::before {
content: counter(counter, lower-roman);
}

结果如下图所示:


counter-11.png


再比如以下的示例代码:


.counter {
counter-reset: counter;
counter-increment: counter;
}

.counter::before {
content: counter(counter, simp-chinese-informal);
}

结果如下图所示:


counter-12.png


同样的 counter 也可以支持级联,也就是说,一个 content 属性值可以有多个 counter 方法,如:


.counter {
counter-reset: counter;
counter-increment: counter;
}

.counter::before {
content: counter(counter) '.' counter(counter);
}

结果如下图所示:


counter-13.png


counters


counters 方法虽然只是比 counter 多了一个 s 字母,但是含义可不一样,counters 就是用来嵌套计数器的,什么意思了?我们平时如果显示列表符号,不可能只是单单显示 1,2,3,4...还有可能显示 1.1,1.2,1.3...前者是 counter 做的事情,后者就是 counters 干的事情。


counters 的语法为:


counters(name, string);

name 就是计数器名字,而第二个参数 string 就是分隔字符串,比如以'.'分隔,那 string 的值就是'.',以'-'分隔,那 string 的值就是'-'。来看如下一个示例:


html 代码如下:


<div class="reset">
<div class="counter">
javascript框架
<div class="reset">
<div class="counter">&nbsp;angular</div>
<div class="counter">&nbsp;react</div>
<div class="counter">
vue
<div class="reset">
<div class="counter">
vue语法糖
<div class="reset">
<div class="counter">&nbsp;@</div>
<div class="counter">&nbsp;v-</div>
<div class="counter">&nbsp;:</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>

css 代码如下:


.reset {
counter-reset: counter;
padding-left: 20px;
}

.counter::before {
content: counters(counter, '-') '.';
counter-increment: counter;
}

结果如下图所示:


counter-14.png


这种计数效果在模拟书籍的目录效果时非常实用,比如写文档,会有嵌套标题的情况,还有一个比较重要的点需要说明一下,就是显示 content 计数值的那个 DOM 元素在文档流中的位置一定要在 counter-increment 元素的后面,否则是没有计数效果的。


总而言之,content 计数器是非常强大的,以上都只是很基础的用法,真正掌握还需要大量的实践以及灵感还有创意。


作者:夕水
来源:juejin.cn/post/7275176987358265355
收起阅读 »

前端埋点实现方案

前端埋点的简介埋点是指在软件、网站或移动应用中插入代码,用于收集和跟踪用户行为数据。通过在特定位置插入埋点代码,开发人员可以记录用户在应用中的操作,如页面浏览、按钮点击、表单提交等。这些数据可以用于分析用户行为、优化产品功能、改进用户体验等目的。 埋点通常与...
继续阅读 »

前端埋点的简介

  • 埋点是指在软件、网站或移动应用中插入代码,用于收集和跟踪用户行为数据。

  • 通过在特定位置插入埋点代码,开发人员可以记录用户在应用中的操作,如页面浏览、按钮点击、表单提交等。

  • 这些数据可以用于分析用户行为、优化产品功能、改进用户体验等目的。




埋点通常与数据分析工具结合使用,如Google Analytics、Mixpanel等,以便对数据进行可视化和进一步分析。


前端埋点是指在前端页面中嵌入代码,用于收集和跟踪用户行为数据。


通过埋点可以获取用户在网页或应用中的点击、浏览、交互等动作,用于分析用户行为、优化产品体验和进行数据驱动的决策。


在前端埋点中,常用的方式包括:

  1. 页面加载埋点:用于追踪和监测页面的加载时间、渲染状态等信息。
  2. 点击事件埋点:通过监听用户的点击事件,记录用户点击了哪些元素、触发了什么操作,以及相关的参数信息。
  3. 表单提交埋点:记录用户在表单中输入的内容,并在用户提交表单时将这些数据发送到后台进行保存和分析。
  4. 页面停留时间埋点:用于记录用户在页面停留的时间,以及用户与页面的交互行为,如滚动、鼠标悬停等。
  5. AJAX请求埋点:在前端的AJAX请求中添加额外的参数,用于记录请求的发送和返回状态,以及相应的数据。

埋点数据可以通过后端API或第三方数据分析工具发送到服务器进行处理和存储。


在使用前端埋点时,需要注意保护用户隐私,遵守相关法律法规,并确保数据采集和使用的合法性和合规性。


同时,还需设计良好的数据模型和分析策略,以便从埋点数据中获得有价值的信息。


前端埋点设计


前面说过,前端埋点是一种数据追踪的技术,用于收集和分析用户的行为数据。


前端埋点设计方案有哪些?


下面简单介绍一下:

  1. 事件监听:通过监听用户的点击、滚动、输入等事件,记录用户的操作行为。可以使用JavaScript来实现事件监听,例如使用addEventListener()函数进行事件绑定。

  2. 自定义属性:在HTML元素中添加自定义属性,用于标识不同的元素或事件。 例如,在按钮上添加data-*属性,表示不同的按钮类型或功能。当用户与这些元素进行交互时,可以获取相应的属性值作为事件标识。

  3.  发送请求:当用户触发需要追踪的事件时,可以通过发送异步请求将数据发送到后台服务器。 可以使用XMLHttpRequest、fetch或者第三方的数据上报SDK来发送请求。

  4. 数据格式:确定需要采集的数据格式,包括页面URL、时间戳、用户标识、事件类型、操作元素等信息。 通常使用JSON格式来封装数据,方便后续的数据处理和分析。

  5. 用户标识:对于需要区分用户的情况,可以在用户首次访问时生成一个唯一的用户标识,并将该标识存储在浏览器的cookie中或使用localStorage进行本地存储。

  6. 数据上报:将采集到的数据发送到后台服务器进行存储和处理。可以自建后台系统进行数据接收和分析,也可以使用第三方的数据分析工具,例如百度统计、Google Analytics等。

  7.  隐私保护:在进行数据采集和存储时,需要注意用户隐私保护。

  8.  遵守相关的法律法规,对敏感信息进行脱敏处理或加密存储,并向用户明示数据采集和使用政策。


需要注意的是,在进行埋点时要权衡数据采集的成本与收益,确保收集到的数据具有一定的价值和合法性。


同时,要注意保护用户隐私,遵守相关法律法规,尊重用户的选择和权益。


前端埋点示例


以下是一个完整的前端埋点示例


展示了如何在网站上埋点统计页面浏览、按钮点击和表单提交事件

  • 在HTML中标识需要采集的元素或事件:
<button id="myButton" data-track-id="button1">Click Me</button>

<form id="myForm">
  <input type="text" name="username" placeholder="Username">
  <input type="password" name="password" placeholder="Password">
  <button type="submit">Submit</button>
</form>

在按钮和表单元素上添加了data-track-id自定义属性,用于标识这些元素。

  • 使用JavaScript监听事件并获取事件数据:
// 监听页面加载事件
window.addEventListener("load", function() {
  var pageUrl = window.location.href;
  var timestamp = new Date().getTime();
  var userData = {
    eventType: "pageView",
    pageUrl: pageUrl,
    timestamp: timestamp
    // 其他需要收集的用户数据
  };

  // 封装数据格式并发送请求
  sendData(userData);
});

// 监听按钮点击事件
document.getElementById("myButton").addEventListener("click", function(event) {
  var buttonId = event.target.getAttribute("data-track-id");
  var timestamp = new Date().getTime();
  var userData = {
    eventType: "buttonClick",
    buttonId: buttonId,
    timestamp: timestamp
    // 其他需要收集的用户数据
  };

  // 封装数据格式并发送请求
  sendData(userData);
});

// 监听表单提交事件
document.getElementById("myForm").addEventListener("submit", function(event) {
  event.preventDefault(); // 阻止表单默认提交行为

  var formId = event.target.getAttribute("id");
  var formData = new FormData(event.target);
  var timestamp = new Date().getTime();
  var userData = {
    eventType: "formSubmit",
    formId: formId,
    formData: Object.fromEntries(formData.entries()),
    timestamp: timestamp
    // 其他需要收集的用户数据
  };

  // 封装数据格式并发送请求
  sendData(userData);
});

通过JavaScript代码监听页面加载、按钮点击和表单提交等事件,获取相应的事件数据,包括页面URL、按钮ID、表单ID和表单数据等。

  • 发送数据请求:
function sendData(data) {
  var xhr = new XMLHttpRequest();
  xhr.open("POST", "/track", true);
  xhr.setRequestHeader("Content-Type", "application/json");

  xhr.onreadystatechange = function() {
    if (xhr.readyState === 4 && xhr.status === 200) {
      console.log("Data sent successfully.");
    }
  };

  xhr.send(JSON.stringify(data));
}

使用XMLHttpRequest对象发送POST请求,将封装好的数据作为请求的参数发送到后台服务器的/track接口。

  • 后台数据接收与存储:

后台服务器接收到前端发送的数据请求后,进行处理和存储。


可以使用后端开发语言(如Node.js、Python等)来编写接口逻辑,将数据存储到数据库或其他持久化存储中。


通过监听页面加载、按钮点击和表单提交等事件,并将相关数据发送到后台服务器进行存储和分析。


根据具体项目需求,可以扩展和定制各种不同类型的埋点事件和数据采集。


vue 前端埋点示例


在Vue中实现前端埋点可以通过自定义指令或者混入(mixin)来完成。


下面给出两种常见的Vue前端埋点示例:

  • 自定义指令方式:
// 在 main.js 中注册全局自定义指令 track
import Vue from 'vue';

Vue.directive('track', {
  bind(el, binding, vnode) {
    const { event, data } = binding.value;
    
    el.addEventListener(event, () => {
      // 埋点逻辑,例如发送请求或记录日志
      console.log("埋点事件:" + event);
      console.log("埋点数据:" + JSON.stringify(data));
    });
  }
});

在组件模板中使用自定义指令:

<template>
  <button v-track="{ event: 'click', data: { buttonName: '按钮A' } }">点击按钮A</button>
</template>

  • 1. 混入方式:
// 创建一个名为 trackMixin 的混入对象,并定义需要进行埋点的方法
const trackMixin = {
  methods: {
    trackEvent(event, data) {
      // 埋点逻辑,例如发送请求或记录日志
      console.log("埋点事件:" + event);
      console.log("埋点数据:" + JSON.stringify(data));
    }
  }
};

// 在组件中使用混入
export default {
  mixins: [trackMixin],
  mounted() {
    // 在需要进行埋点的地方调用混入的方法
    this.trackEvent('click', { buttonName: '按钮A' });
  },
  // ...
};

这两种方式都可以实现前端埋点,你可以根据自己的项目需求选择适合的方式。


在实际应用中,你需要根据具体的埋点需求来编写逻辑,例如记录页面浏览、按钮点击、表单提交等事件,以及相应的数据收集和处理操作。


使用自定义指令(Custom Directive)的方式来实现前端埋点


在Vue 3中,你可以使用自定义指令(Custom Directive)的方式来实现前端埋点。


一个简单的Vue 3的前端埋点示例:


  • 创建一个名为analytics.js的文件,用于存放埋点逻辑:
// analytics.js

export default {
  mounted(el, binding) {
    const { eventType, eventData } = binding.value;

    // 发送数据请求
    this.$http.post('/track', {
      eventType,
      eventData,
    })
    .then(() => {
      console.log('Data sent successfully.');
    })
    .catch((error) => {
      console.error('Error sending data:', error);
    });
  },
};

  • 在Vue 3应用的入口文件中添加全局配置:
import { createApp } from 'vue';
import App from './App.vue';
import axios from 'axios';

const app = createApp(App);

// 设置HTTP库
app.config.globalProperties.$http = axios;

// 注册全局自定义指令
app.directive('analytics', analyticsDirective);

app.mount('#app');

  • 在组件中使用自定义指令,并传递相应的事件类型和数据:
<template>
  <button v-analytics="{ eventType: 'buttonClick', eventData: { buttonId: 'myButton' } }">Click Me</button>
</template>

在示例中,我们定义了一个全局的自定义指令v-analytics,它接受一个对象作为参数,对象包含了事件类型(eventType)和事件数据(eventData)。当元素被插入到DOM中时,自定义指令的mounted钩子函数会被调用,然后发送数据请求到后台服务器。


注意,在示例中使用了axios作为HTTP库发送数据请求,你需要确保项目中已安装了axios,并根据实际情况修改请求的URL和其他配置。


通过以上设置,你可以在Vue 3应用中使用自定义指令来实现前端埋点,采集并发送相应的事件数据到后台服务器进行存储和分析。请根据具体项目需求扩展和定制埋点事件和数据采集。


使用Composition API的方式来实现前端埋点


以下是一个Vue 3的前端埋点示例,使用Composition API来实现:

  • 创建一个名为analytics.js的文件,用于存放埋点逻辑:
// analytics.js

import { ref, onMounted } from 'vue';

export function useAnalytics() {
  const trackEvent = (eventType, eventData) => {
    // 发送数据请求
    // 模拟请求示例,请根据实际情况修改具体逻辑
    console.log(`Sending ${eventType} event with data:`, eventData);
  };

  onMounted(() => {
    // 页面加载事件
    trackEvent('pageView', {
      pageUrl: window.location.href,
    });
  });

  return {
    trackEvent,
  };
}

  • 在需要进行埋点的组件中引入useAnalytics函数并使用:
import { useAnalytics } from './analytics.js';

export default {
  name: 'MyComponent',
  setup() {
    const { trackEvent } = useAnalytics();

    // 按钮点击事件
    const handleClick = () => {
      trackEvent('buttonClick', {
        buttonId: 'myButton',
      });
    };

    return {
      handleClick,
    };
  },
};

  • 在模板中使用按钮并绑定相应的点击事件:
<template>
  <button id="myButton" @click="handleClick">Click Me</button>
</template>

在示例中,我们将埋点逻辑封装在了analytics.js文件中的useAnalytics函数中。在组件中使用setup函数来引入useAnalytics函数,并获取到trackEvent方法进行埋点操作。在模板中,我们将handleClick方法绑定到按钮的点击事件上。


当页面加载时,会自动发送一个pageView事件的请求。当按钮被点击时,会发送一个buttonClick事件的请求。


注意,在示例中,我们只是模拟了数据请求操作,请根据实际情况修改具体的发送数据请求的逻辑。


通过以上设置,你可以在Vue 3应用中使用Composition API来实现前端埋点,采集并发送相应的事件数据。请根据具体项目需求扩展和定制埋点事件和数据采集。


react 前端埋点示例


使用自定义 Hook 实现


当然!以下是一个 React 的前端埋点示例,


使用自定义 Hook 实现:

  • 创建一个名为 useAnalytics.js 的文件,用于存放埋点逻辑:
// useAnalytics.js

import { useEffect } from 'react';

export function useAnalytics() {
  const trackEvent = (eventType, eventData) => {
    // 发送数据请求
    // 模拟请求示例,请根据实际情况修改具体逻辑
    console.log(`Sending ${eventType} event with data:`, eventData);
  };

  useEffect(() => {
    // 页面加载事件
    trackEvent('pageView', {
      pageUrl: window.location.href,
    });
  }, []);

  return {
    trackEvent,
  };
}

  • 在需要进行埋点的组件中引入 useAnalytics 自定义 Hook 并使用:
import { useAnalytics } from './useAnalytics';

function MyComponent() {
  const { trackEvent } = useAnalytics();

  // 按钮点击事件
  const handleClick = () => {
    trackEvent('buttonClick', {
      buttonId: 'myButton',
    });
  };

  return (
    <button id="myButton" onClick={handleClick}>Click Me</button>
  );
}

export default MyComponent;

在示例中,我们将埋点逻辑封装在了 useAnalytics.js 文件中的 useAnalytics 自定义 Hook 中。在组件中使用该自定义 Hook 来获取 trackEvent 方法以进行埋点操作。在模板中,我们将 handleClick 方法绑定到按钮的点击事件上。


当页面加载时,会自动发送一个 pageView 事件的请求。当按钮被点击时,会发送一个 buttonClick 事件的请求。


请注意,在示例中,我们只是模拟了数据请求操作,请根据实际情况修改具体的发送数据请求的逻辑。


通过以上设置,你可以在 React 应用中使用自定义 Hook 来实现前端埋点,采集并发送相应的事件数据。根据具体项目需求,你可以扩展和定制埋点事件和数据采集逻辑。


使用高阶组件(Higher-Order Component)实现


当然!以下是一个 React 的前端埋点示例,


使用高阶组件(Higher-Order Component)实现:

  • 创建一个名为 withAnalytics.js 的高阶组件文件,用于封装埋点逻辑:
// withAnalytics.js

import React, { useEffect } from 'react';

export function withAnalytics(WrappedComponent) {
  return function WithAnalytics(props) {
    const trackEvent = (eventType, eventData) => {
      // 发送数据请求
      // 模拟请求示例,请根据实际情况修改具体逻辑
      console.log(`Sending ${eventType} event with data:`, eventData);
    };

    useEffect(() => {
      // 页面加载事件
      trackEvent('pageView', {
        pageUrl: window.location.href,
      });
    }, []);

    return <WrappedComponent trackEvent={trackEvent} {...props} />;
  };
}

  • 在需要进行埋点的组件中引入 withAnalytics 高阶组件并使用:
import React from 'react';
import { withAnalytics } from './withAnalytics';

function MyComponent({ trackEvent }) {
  // 按钮点击事件
  const handleClick = () => {
    trackEvent('buttonClick', {
      buttonId: 'myButton',
    });
  };

  return (
    <button id="myButton" onClick={handleClick}>Click Me</button>
  );
}

export default withAnalytics(MyComponent);

在示例中,我们创建了一个名为 withAnalytics 的高阶组件,它接受一个被包裹的组件,并通过属性传递 trackEvent 方法。在高阶组件内部,我们在 useEffect 钩子中处理页面加载事件的埋点逻辑,并将 trackEvent 方法传递给被包裹组件。被包裹的组件可以通过属性获取到 trackEvent 方法,并进行相应的埋点操作。


在模板中,我们将 handleClick 方法绑定到按钮的点击事件上。


当页面加载时,会自动发送一个 pageView 事件的请求。当按钮被点击时,会发送一个 buttonClick 事件的请求。


请注意,在示例中,我们只是模拟了数据请求操作,请根据实际情况修改具体的发送数据请求的逻辑。


通过以上设置,你可以在 React 应用中使用高阶组件来实现前端埋点,采集并发送相应的事件数据。


当然根据具体项目需求,你还可以扩展和定制埋点事件和数据采集逻辑。


作者:朦胧之
链接:https://juejin.cn/post/7274084216286183460
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

如何突破技术瓶颈(适合P6以下)

前言 最近在更新react组件库源码的文章,其实vue和其他框架都一样,就是我发现自己在一年前直接看这些源码(不调试),还是有点吃力的,然后就放弃了。 可最近不知道为啥,看这些源码对我来说没啥太大难度,直接干就完事了,不需要啥调试。自己好好回想了一下,为什么会...
继续阅读 »

前言


最近在更新react组件库源码的文章,其实vue和其他框架都一样,就是我发现自己在一年前直接看这些源码(不调试),还是有点吃力的,然后就放弃了。


可最近不知道为啥,看这些源码对我来说没啥太大难度,直接干就完事了,不需要啥调试。自己好好回想了一下,为什么会有这样的变化?也算帮助一些想突破自己技术瓶颈的同学。


有新人在下面留言说看到很焦虑,刚进前端领域的同学,你们首要任务是能完成业务开发,此时业务开发带给你的提升是最明显的,文章更多的是帮助业务api用熟之后的想有突破的同学,不用焦虑,哈哈。而且话说回来了,我在平时工作中看到不想突破的人基本占百分90%,无论大小厂,所以不突破也没啥,大部分人只是仅仅当一个普通工作而已。


结论


首先我得出结论是:

  • 最开始不要自己去读源码,看别人的文章和视频即可,目的是先接触比自己能力层次高的代码,为超越现有的能力铺路(后面详细谈怎么做)
  • 平时注意积累一些手写题的思路,网上面经很多,主要不是写出来,是理解原理,理解大于一切,不理解的东西终究会忘记,我们要积累的是能力,能力是第一!(后面详细谈),设计模式里的发布订阅者模式必须要理解!这是写很多库常见的技巧。
  • 最后开始独立去看一些小的代码库,比如腾讯,阿里,字节的组件库,这些库大部分组件难度低。

去哪里看视频和文章学源码


视频


最简易的就是跟着视频学,因为视频会把代码敲一遍,给你思考的时间,讲解也是最细的,很适合刚开始想造轮子的同学了解一些有难度的源码。


举个例子:


我当时看了koa的源码,了解了koa中间件的原理,我自己造了一个自动化发布脚本就利用了这个原理,redux中间件也是类似的原理,在函数式编程领域叫做compose函数,koa是异步compose,redux是同步compose,


简单描述下什么是compose函数


我把大象装进冰箱是不是要
1、打开冰箱门
2、装进去大象
3、关冰箱门


那么很多同学就会写一个函数

function 装大象(){
// 打开冰箱
// 装大象
// 关闭冰箱门
}

compose函数会把这个过程拆开,并且抽象化

// 把装大象抽象为装东西函数
function 装东西();
function 打开冰箱();
function 关闭冰箱();

compose(打开冰箱函数, 装东西函数,关闭冰箱函数)

此时compose把上面三个函数抽象为一个打开冰箱往里面装东西的函数,我们只需要把参数大象穿进去就抽象了整个过程

compose(打开冰箱函数, 装东西函数,关闭冰箱函数)(大象)

具体内容我还写过一篇文章,有兴趣的同学可以去看看:


终极compose函数封装方案!


这个大家应该有自己的去处,我自己的话很简单,视频一般去b站,就是bilibili,有些同学以为这是一个二次元网站是吧,其实里面免费的学习资料一抓一大把呢,啥都有。


比如说我在b站看了很多linux入门教学视频,还有一个培训公开课,讲的都是源码,什么手写react hook,手写webpack,手写xxx,那个时候说实话,听了视频也不是很理解,但是我还是挺喜欢前端的,没咋理解就继续听。


记住,我们需要短时间内提升能力,所以视频算是其中最快的了,其他方法不可能有这个来的快,并且没理解就算了,能理解多少是多少。


学习是一个螺旋上升的过程,不是一下子就全懂或者全不懂的,都是每次比上一次更懂一点。除非你是天才,急不来的。


视频搜索第二大去处就是论坛,一些论坛有各种各样的培训视频,这种论坛太多了,你谷歌或者百度一抓一大把。


对了,谷歌是爸爸,你懂我意思,不要吝啬小钱。在搜索学习资料面前,百度就是个弟弟。


文章


文章一定记住,在精不在多。


切记,每个人都处在不同的学习阶段,不要盲目追求所谓的大神文章,不一定适合你,比如说有些人刚接触前端,你去看有些有深度的文章对你没啥好处,浪费时间,因为你理解不了,理解不了的知识相当于没学,过两天就忘了。


文章选择范围,比如掘金,知乎还有前端公众号,基本上就差不多了,选一两个你觉得你这个阶段能吸收的,好好精读,坚持个一年你会发现不一样的。


额外的知识储备


前端3年前主流的前端书我都读过,什么红宝书,权威指南都读了好几遍了。


但有一本从菜鸟到高级-资深前端很推荐的一本是:JavaScript设计模式与开发实践(图灵出品)(腾讯的一位大哥写的,不是百度的那位,这两本书我都看过)


里面的知识点很干很干,里面有非常多的技巧,比如说你的同事写了一个函数,你不想破坏函数,有什么办法拓展它(其实我觉得我想的这些题就比前端八股文好玩多了,是开放性的)

  • 技巧很多,比如面向切面编程,加个before或者after函数包装一下
  • 比如责任链模式
  • 比如刚才的compose函数
  • 比如装饰器模式

确立自己的发展方向


大家其实最后都要面对一个很现实的问题,就是35以后怎么办,我个人觉得你没有对标阿里P7的能力,落地到中小公司都难。


所以我们看源码,看啥都是为了提升能力,延长职业寿命。


那么如何在短时间内有效的提升,你就需要注意不能各种方向胡乱探索,前端有小游戏方向,数据可视化方向,B端后台系统方向,音视频方向等等


我是做b端,那b端整个链路我就需要打通,组件库是我这个方向,所以我探索这里,还有node端也是,写小工具是必须的,但是你们说什么deno,其他的技术,我根本不在乎,没时间浪费在这些地方,当然除了有些业务上需要,比如之前公司有个ai标注需求,用canvas写了一个类似画板的工具,也算开拓了知识点,但这也不是我重点发展的方向,不深入。


我做组件库是为了后面的低代码,低代码平台的整体设计思路我已经想好了,整体偏向国外开源的appsmith的那种方式,然后打通组件间通信的功能,我认为是能胜任稍微复杂的b端业务场景的,而且可以走很多垂直领域,比如网站建站,微信文章编辑器这种。所以我才开始研究组件库的,因为低代码大多数复杂功能都在组件上。


工作上勇于走出舒适圈


为什么这个跟看源码相关呢,如果你做过比较复杂的项目,你会发现很多现成的第三方库满足不了。比如说我自己遇到过的大型sass项目,ant design就满足不了,所以你才发现,源码看得少加上业务急,代码就烂,时间上就留不出自己偷偷学习的时间,如果你想长期从事软件开发,没有成长是一件很危险的事(钱多当我没说,哈哈),因为无论如何,有本事,总没错的。


当你的业务难度上去的时候,会逼着你去提升能力,所以你如果想前端走的更远,建议不要在自己的舒适区太久,业务上选择一家比较难的公司,后面再跳槽就是沉淀这段时间的知识点了,当你能够有自信说,我现在带团队,从0到1再遇到那么难的业务时,能从容应对,恭喜你,你可以去面下阿里p7,不是为了这个工作啊,可以检验下是不是达到这个职位的标准了,我就喜欢偶尔面一下,也不是换工作,就是看看自己进步没


作者:孟祥_成都
链接:https://juejin.cn/post/7168671474234949662
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

读完React新文档后的8条收获

不久前,React官网进行了重构。新官网将hook作为介绍的重头戏,重新通读了一遍react文档,有了一些自己的感悟。在此分享出来与各位共勉。 1. 换个角度认识Props与State Props与State是React中两个略有相似的概念。在一个React组...
继续阅读 »

不久前,React官网进行了重构。新官网将hook作为介绍的重头戏,重新通读了一遍react文档,有了一些自己的感悟。在此分享出来与各位共勉。


1. 换个角度认识Props与State


PropsState是React中两个略有相似的概念。在一个React组件中,它们作为数据来源可能同时存在,但它们之间有着巨大不同:

  1. Props更像函数中的参数。它主要用于组件之间的信息传递,它可以是任意类型的数据:数字、文本、函数、甚至于组件等等。
  2. State更像组件中的内存。它可以保存组件内部的状态,组件通过跟踪这些状态,从而渲染更新。
import React, { useState } from 'react';

// 父组件
const ParentComponent = () => {
const [count, setCount] = useState(0); // 使用state来追踪count的值

return (
<div>
<ChildComponent age={25} />
<p>Count: {count}</p>
</div>
);
};

// 子组件
const ChildComponent = (props) => {
const { age } = props; // 使用props来获取父组件传递的数据

return (
<div>
<p>Age: {age}</p>
</div>
);
};

2. 不要嵌套定义组件


在一个组件中直接定义其他组件,可以省去很多传递Props的工夫,看上去很好。但我们不应该嵌套定义组件,原因在于**嵌套定义组件会导致渲染速度变慢,也更容易出现BUG**。
我们在嵌套定义组件的情况下,当父组件更新时,内部嵌套的子组件也会重新生成并渲染,子组件内部的state也会丢失。针对这种情况,我们可以通过两种方式来解决:

  1. 为子组件包上useMemo,避免不必要的更新;
  2. 不再嵌套定义,将子组件提至父组件外,通过Props传参。

这里更推荐第二种方法,因为这样代码更少,结构也更清晰,组件迁移维护都会更方便一些。

//🔴 Bad Case
export default function Gallery() {
function Profile() {
// ...
}
// ...
}
//✅ Good Case
function Profile() {
// ...
}

export default function Gallery() {
// ...
}

3. 尽量不要使用匿名函数组件


因为类似export default () => {}的匿名函数组件书写虽然方便,但会让debug变得更困难
如下是两种不同类型组件出错时的控制台的表现:

  1. 具名组件出错时的提示,可直接的指出错的函数组件名称: 


  1. 匿名函数出错时的提示,无法直观确定页面内哪个组件出了错: 



4. 使用逻辑运算符&&编写JSX时,左侧最好不要是数字


运算符&&在JSX中的表现与JS略有不同:

  • 在JS中,只有在&&左侧的值0nullundefinedfalse''等假值时,才会返回右侧值;
  • 在JSX中,React对于falsenullundefined并不会做渲染处理,所以进行&&运算后,如果左侧值为假被返回后,会出现与直觉不同的渲染结果:可能会碰到页面上莫名奇妙出现了一个0的问题。为了避免出现这种情况,可以在书写JSX时,为左侧的值加上!!来进行强制类型转换。
const flag = 0
//🔴 Bad Case
{
flag && <div>123</div>
}
//✅ Good Case 1
{
!!flag && <div>123</div>
}
//✅ Good Case 2
{
flag > 0 && <div>123</div>
}

关于JSX对各种常见假值的渲染,这里进行了总结:

  1. nullundefinedfalse:这些值在JSX中会被视为空,不会渲染任何内容。它们会被忽略,并且不会生成对应的DOM元素。
  2. 0NaN:这些值会在页面上渲染为字符串"0"、"NaN"。
  3. ''[]{}:空字符串会被渲染为什么都没有,不会显示任何内容。


注:这里感谢@小明家的bin的评论提醒,他的见解对我起到了很大的启发作用。



5.全写的 Fragment标签上可以添加属性key


在map循环添加组件时,需要为组件添加key来优化性能。如果待循环的组件有多个时,可以在外层包裹<Fragment>...</Fragment>标签,在其上添加key添加在<Fragment>...</Fragment>来避免创建额外的组件。

const list = [1,2,3]
//🔴 Bad Case
//不能添加key
{
list.map(v=><> <div>1-1</div> <div>1-2</div> </>)
}
//🔴 Bad Case
//创建了额外的div节点
{
list.map(v=><div key={v}> <div>1-1</div> <div>1-2</div> <div/>)
}
//✅ Good Case
{
list.map(v=><Fragment key={v}> <div>1-1</div> <div>1-2</div> </Fragment>)
}



注意简写的Fragment标签<>...</>上不支持添加key



6. 可以使用updater function,来在下一次渲染之前多次更新同一个state


React中处理状态更新时采用了批处理机制,在下一次渲染之前多次更新同一个state,可能不会达到我们想要的效果。如下是一个简单例子:

// 按照直觉一次点击后button中的文字应展示为3,但实际是1
function Demo(){
const [a,setA] = useState(0)

function handler(){
setA(a + 1);
setA(a + 1);
setA(a + 1);
}

return <button onclick={handler}>{a}</button>
}

在某些极端场景下,我们可能希望state的值能够及时发生变化,这时便可以采用setA(n => n + 1)(这种形式被称为updater function)来进行更新:

// 一次点击后a的值会被更新为3
function Demo(){
const [a,setA] = useState(0)

function handler(){
setA(n => n + 1);
setA(n => n + 1);
setA(n => n + 1);
}

return <button onclick={handler}>{a}</button>
}

7. 管理状态的一些原则


更加结构化的状态有助于提高我们组件的健壮性,以下是一些管理组件内状态时可以参考的原则:

  1. 精简相关状态:如果在更新某个状态时总是需要同步更新其他状态变量,可以考虑将它们合并为一个状态变量。
  2. 避免矛盾状态:避免出现两个或多个状态的值互相矛盾的情况。
  3. 避免冗余状态:一个状态可以由其余状态计算而来时,其不应单独作为一个状态来进行管理。
  4. 避免状态重复:当相同的数据在多个状态变量之间或嵌套对象中重复时,很难使它们保持同步。尽可能减少重复。
  5. 避免深度嵌套状态:嵌套层级过深的状态更新时相对麻烦许多,尽量保持状态扁平化。

8. 使用useSyncExternalStore订阅外部状态


useSyncExternalStore是一个React18中新提供的一个Hook,可以用于订阅外部状态。
它的使用方式如下:

import { useSyncExternalStore } from 'react';

function MyComponent() {
const snapshot = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
// ...
}

useSyncExternalStore接受三个参数:

  • subscribe:一个订阅函数,用于订阅存储,并返回一个取消订阅的函数。
  • getSnapshot:一个从存储中获取数据快照的函数。在存储未发生变化时,重复调用getSnapshot应该返回相同的值。当存储发生变化且返回值不同时,React会重新渲染组件。
  • getServerSnapshot(可选):一个在服务器端渲染时获取存储初始快照的函数。它仅在服务器端渲染和在客户端进行服务器呈现内容的hydration过程中使用。如果省略该参数,在服务器端渲染组件时会抛出错误。

举一个具体的例子,我们可能需要获取浏览器的联网状态,并期望在浏览器网络状态发生变化时做一些处理。使用useSyncExternalStore可以很大程度上简化我们的代码,同时使逻辑更加清晰:

//🔴 Bad Case
function useOnlineStatus() {
// Not ideal: Manual store subscription in an Effect
const [isOnline, setIsOnline] = useState(true);
useEffect(() => {
function updateState() {
setIsOnline(navigator.onLine);
}

updateState();

window.addEventListener('online', updateState);
window.addEventListener('offline', updateState);
return () => {
window.removeEventListener('online', updateState);
window.removeEventListener('offline', updateState);
};
}, []);
return isOnline;
}

function ChatIndicator() {
const isOnline = useOnlineStatus();
// ...
}
function subscribe(callback) {
window.addEventListener('online', callback);
window.addEventListener('offline', callback);
return () => {
window.removeEventListener('online', callback);
window.removeEventListener('offline', callback);
};
}

// ✅ GoodCase
function useOnlineStatus() {
return useSyncExternalStore(
subscribe, // React won't resubscribe for as long as you pass the same function
() => navigator.onLine, // How to get the value on the client
() => true // How to get the value on the server
);
}

function ChatIndicator() {
const isOnline = useOnlineStatus();
// ...
}

结语


文章的最后,再来一次无废话总结:

  1. 更清晰地认识了Props与State之间的区别。Props更像是函数的参数,用于组件之间的信息传递;而State更像是组件内部的内存,用于保存组件的状态并进行渲染更新。
  2. 不推荐在一个组件内部嵌套定义其他组件,因为这样会导致渲染速度变慢并容易产生BUG。推荐将子组件提到父组件外部并通过Props传递数据。
  3. 尽量避免使用匿名函数组件,因为在出错时会增加调试的难度。具名组件的出错提示更加直观和准确。
  4. 在使用逻辑运算符&&编写JSX时,左侧最好不要是数字。在JSX中,0会被当作有效的值,而不是假值,为了避免出现问题,可以在左侧的值加上!!进行强制类型转换。
  5. 当在使用全写的Fragment标签时,可以给Fragment标签添加属性key,以优化性能和避免创建额外的组件。
  6. 使用updater function的方式进行状态更新,可以确保在下一次渲染之前多次更新同一个state。这样可以避免批处理机制带来的问题。
  7. 在管理组件内状态时,可以遵循一些原则,如精简相关状态、避免矛盾状态、避免冗余状态、避免状态重复等,以提高组件的健壮性和可维护性。
  8. 使用useSyncExternalStore可以订阅外部状态,它是React 18中新增的Hook。通过订阅函数、获取数据快照的函数以及获取服务器初始快照的函数,我们可以简化订阅外部状态的代码逻辑。

通过对React文档的深入学习和实践,我对React的理解更加深入了解,希望这些收获也能对大家在学习和使用React时有所帮助。


作者:星始流年
链接:https://juejin.cn/post/7233324859932442680
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

不要因bundle size太大责备开发者

前言 大家好我是飞叶,一个前端UP主,最近外网有个新的SSR框架Qwik比较火,GitHub上它的仓库star数以每天100的速度在增长,今天就通过翻译一篇文章来引入QWIK。 当然如果你不想读这些,我也录了一个视频讲这篇文章。讲解视频在这里 原文链接 ht...
继续阅读 »

前言


大家好我是飞叶,一个前端UP主,最近外网有个新的SSR框架Qwik比较火,GitHub上它的仓库star数以每天100的速度在增长,今天就通过翻译一篇文章来引入QWIK。


当然如果你不想读这些,我也录了一个视频讲这篇文章。讲解视频在这里



原文链接 http://www.builder.io/blog/dont-b…



不要因bundle size太大责备开发者


让我们谈谈我们构建 Web 应用程序所必须使用的工具,以及这些工具如何欺骗我们。


开发者们共同的故事


你要创建一个新项目,此时,你信心满满这个新站点会很快很流畅。
在一开始,事情看起来确实如此,但是很快你的应用就变大了变复杂了,应用的开启性能变慢了。
在不知不觉中,您手头上有一个巨大的应用程序,而您却无能为力地修复它。你哪里做错了?


我们用的每个工具/框架都承诺提供更好、更快的结果,
但我们通过访问整个互联网里的应用就知道,结果绝不是一个更好、更快的站点。
谁应该为此负责?开发者吗?


作为开发者,你是否有被告知:"就是你们搞砸了,你们偷工减料,才导致了一个性能差的站点。"


这不是你的错


如果一些网站速度较慢,而另一些网站速度较快,那么当然,责备开发人员可能是有道理的。
但真实情况是:所有网站都很慢!
当没有人成功时,你怎么能责怪开发者呢?问题是系统性的。也许这不是开发者的错。


这是一个关于我们如何构建应用程序、我们使用的工具、工具做出的承诺以及我们最终遇到的缓慢站点的故事。


只有一个结论。这些工具都过度承诺了,这是整个行业的系统性问题。这不仅仅是几个坏苹果,而是整个万维网。


代码太多了


我们都知道问题是什么:代码太多!我们非常擅长创建代码,浏览器无法跟上我们的脚步。
每个人都告诉你你的网站有太多的 JavaScript,但没有人知道如何缩小它。


这就像一个 YouTube 健身频道告诉你减肥所需要做的就是少摄入卡路里。
简单的建议,但成功率令人沮丧。
原因是该建议忽略了食欲。
当你又饿又虚弱并且只想到食物时,又有几个人能做到 减少卡路里摄入的意愿呢?
所以也许减肥成功的秘诀可能不是减少卡路里,而是如何控制你的食欲。


这个例子很类似于 JavaScript 膨胀的情况。
我们知道我们需要更少的 JavaScript,
但是我们有太多需求,除了代码要写,还有太多工具和轮子要用,(才能满足需求)
但是所有这些 代码 和工具 都会源源不断地使我们的应用越来越大。




打包的演变历史


让我们先看看我们是如何陷入这种境地的,然后再讨论前进的道路。


第 0 代:串联


在 ECMAScript 模块之前,什么都没有,只是文件。
打包过程很简单。这些文件被连接在一起并包装在 IIFE 中。


好处是很难向您的应用程序添加更多代码,因此bundle size保持较小。


第 1 代:打包器


ECMAScript 模块来了。
打包器也出现了:WebPack、Rollup 等。


然而,npm install 一个依赖并把它打包进去有点太容易了。很快,bundle size就成了一个问题。


庆幸的是,这些打包器知道如何进行tree shaking和死代码消除。这些功能确保只有用到的的代码才被打包。


第 2 代:延迟加载


意识到bundle size过大的问题, 打包器开始提供延迟加载。
延迟加载很棒,因为它允许将代码分解成许多chunks并根据需要交付给浏览器。
这很棒,因为它允许从最需要的部分开始 分批交付应用程序。


问题在于,在实践中,我们是使用框架来构建应用程序的,而框架对打包程序如何将我们的代码分解为延迟加载的块有很大影响。
问题在于延迟加载块需要引入异步API调用。
如果框架需要对您的代码进行同步引用,则打包器不能引入延迟加载的块。


所以我们需要明白,虽然打包器声称他们可以延迟加载代码,而且这也是真的,
但想做到延迟加载有个前提条件,即我们使用的框架得让开发者使用promise(来懒加载chunk),否则您可能没有太多选择。


第 3 代:延迟加载不在渲染树中的组件


框架迅速争先恐后地利用打包器的延迟加载功能,如今几乎所有人都知道如何进行延迟加载。
但是有一个很大的警告!框架只能延迟加载不在当前渲染树中的组件。




什么是渲染树?它是构成当前页面的一组组件。
应用程序通常具有比当前页面上更多的组件。
通常,渲染树包含视图(这是您当前在浏览器视口中看到的内容)内组件。
和一部分视图之外的组件。


假设一个组件在渲染树中。在这种情况下,框架必须下载组件,因为框架需要重建组件的渲染树,(这是hydration的一部分工作)。
框架只能延迟加载当前不在渲染树中的组件。


另一点是框架可以延迟加载组件,但总是包含行为。
因为组件包含了行为,这个懒加载的单位就太大了。如果可以延迟加载的单位更小会更好。
渲染组件不应要求下载组件的事件处理程序。
框架应该只在用户交互时才下载事件处理程序,而不是作为组件渲染方法的一部分。根
据您正在构建的应用程序的类型,事件处理程序可能代表您的大部分代码。
所以耦合组件的渲染和行为的下载是次优的。


问题的核心


仅在需要重新渲染组件时才延迟加载组件渲染函数,并且仅在用户与事件处理程序交互时才延迟加载事件处理程序。
这样才是最好的!
默认应该是所有内容都是延迟加载的。


但这种方法存在一个大问题。问题是框架需要协调其内部状态与 DOM。
这意味着至少需要一次hydration,来进行完整渲染以重建框架的内部状态。
在第一次渲染之后,框架可以对其更新进行更准确的把控,但问题已经产生了,因为代码已经下载了。所以我们有两个问题:

  • 框架需要下载并执行组件以在启动时重建渲染树。(请参阅hydration 是纯粹的开销)这会强制下载和执行渲染树中的所有组件。
  • 事件处理程序随组件一起提供,即使在渲染时不需要它们。包含事件处理程序会强制下载不必要的代码。

因此,当今框架的现状是,必须急切地下载和执行 SSR/SSG 渲染树中的每个组件(及其处理程序)。
使用当今的框架进行延迟加载有点说谎,因为您并不能在初始页面呈现时进行延迟加载。


值得指出的是,即使开发人员将延迟加载边界引入 SSR/SSG 初始页面,也无济于事。
框架仍需下载并执行 SSR/SSG 响应中的所有组件;因此,只要组件在渲染树中,框架就必须急切地加载开发人员试图延迟加载的组件。


渲染树中组件的急切下载是问题的核心,开发人员对此无能为力。
尽管如此,这并不能阻止开发人员因网站运行缓慢而受到指责。


下一代:细粒度的延迟加载


那么,我们该何去何从?显而易见的答案是我们需要更细粒度。该解决方案既明显又难以实施。我们需要:

  • 更改框架,这样它们就不会在hydration阶段急切地加载渲染树。
  • 允许组件渲染函数 独立于组件事件处理程序 单独下载。

如果您的框架可以完成上述两个部分,那么用户将看到巨大的好处。
应用程序的启动要求很少,因为启动时不需要进行渲染(内容已经在 SSR/SSG 处渲染)。
下载的代码更少:当框架确定需要重新渲染特定组件时,框架可以通过下载渲染函数来实现,而无需下载所有事件处理程序。


细粒度的延迟加载将是网站启动性能的巨大胜利。
它要快得多,因为下载的代码量将与用户交互性成正比,而不是与初始渲染树的复杂性成正比。
您的网站会变得更快,不是因为我们更擅长使代码更小,而是因为我们更擅长只下载我们需要的东西,而不是预先下载所有东西。




入口点 entry point


拥有一个可以进行细粒度延迟加载的框架是不够的。
因为,要利用细粒度的延迟加载,您必须首先拥有要延迟加载的bundles。


为了让打包器创建延迟加载的chunk,打包器需要每个块的入口点。
如果您的应用程序只有单个入口点,则打包器无法创建多个chunks。
如果您的应用程序只有单个入口点,即使你的框架可以进行细粒度的延迟加载,它也没有什么可以延迟加载的。


现在创建入口点很麻烦,因为它需要开发人员编写额外的代码。
在开发应用程序时,我们真的只能考虑一件事,那就是写功能。
让开发人员同时考虑他们正在构建的功能和延迟加载对开发人员来说是不公平的。
所以在实践中,为打包器创建入口点很麻烦。


所需要的是一个无需开发人员考虑就可以创建入口点的框架。
为打包程序创建入口点是框架的责任,而不是开发人员的责任。
开发人员的职责是构建功能。
该框架的职责是考虑应该如何完成该功能的底层实现。
如果框架不这样做,那么它就不能完全满足开发人员的需求。


担心切入点太多?


目标应该是创建尽可能多的入口点。
但是,有些人可能会问,这是不是就会导致下载很多小块而不是几个大块吗?答案是响亮的“不”。


如果没有入口点,打包器就无法创建chunk。
但是打包器可以将多个入口点放入一个chunk中。
您拥有的入口点越多,您以最佳方式组装bundle的自由度就越大。
入口点给了你优化bundle的自由。所以它们越多越好。


未来的框架


下一代框架将需要解决这些问题:

  • 拥有人们喜欢的开发体验DX。
  • 对代码进行细粒度的延迟加载。
  • 自动生成大量入口点以支持细粒度的延迟加载。

开发人员将像现在一样构建他们的网站,但这些网站不会在应用程序启动时用下载和执行一个很大的bundle来压倒浏览器。


Qwik是一个在设计时考虑到这些原则的框架。Qwik细粒度延迟加载是针对每个事件处理程序、渲染函数和effect的。


结论


我们的网站越来越大,看不到尽头。
它们之所以大,是因为这些网站今天比以前做得更多——更多的功能、动画等。并且这种趋势将继续下去。


上述问题的解决方案是对代码进行细粒度的延迟加载,这样浏览器就不会在初始页面加载时不堪重负。


我们的打包工具支持细粒度的延迟加载,但我们的框架不支持。
框架hydration强制渲染树中的所有组件在hydration时加载。(目前的SSR框架唯一的延迟加载是 当前不在渲染树中的组件。)
即使事件处理程序可能是代码的大部分,并且hydration并不需要事件处理器代码,现在的SSR框架还是随组件的下载一并下载了事件处理程序.


因为打包器可以细粒度的延迟加载,但我们的框架不能,我们无法识别其中的微妙之处。
导致的结果就是我们将网站启动缓慢归咎于开发人员,
因为我们错误地认为他们本可以采取一些措施来防止这种情况发生,尽管现实是他们在这件事上几乎没有发言权。


我们需要将细粒度延迟加载设计为框架核心功能的新型框架(例如Qwik )。
我们不能指望开发者承担这个责任;他们已经被各种功能淹没了。
框架需要考虑延迟加载运行时以及创建入口点,以便打包程序可以创建块以进行延迟加载。
下一代框架带来的好处将超过迁移到它们所花费的成本。


作者:飞叶_前端
链接:https://juejin.cn/post/7154948728871190535
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

为什么别人的 hooks 里面有那么多的 ref

前言 最近因为一些原因对useCallback有了一些新的认知,然后遇到一个朋友说他面试的时候被问到如何实现一个倒计时的hooks的时候,想到了这个问题,突然的茅塞顿开,对react 中的hooks有了新的了解,所以有了这篇文章,记录一下我个人的想法。 在...
继续阅读 »

前言



最近因为一些原因对useCallback有了一些新的认知,然后遇到一个朋友说他面试的时候被问到如何实现一个倒计时的hooks的时候,想到了这个问题,突然的茅塞顿开,对react 中的hooks有了新的了解,所以有了这篇文章,记录一下我个人的想法。



在学习一些开源的库的时候,很容易发现开源库中 hooks 里面会写很多的 ref 来存储hooks的参数。


使用了 ref 之后,使用变量的地方就需要 .current 才能拿到变量的值,这比我直接使用变量肯定是变得麻烦了。对于有代码洁癖的人来说,这肯定是很别扭的。


但是在开源库的 hooks 中频繁的使用了 ref,这肯定不是一个毫无原因的点,那么究竟是什么原因,让开源库也不得不使用 .current 去获取变量呢?


useCallback


先跑个题,什么时候我们需要使用 useCallback 呢?


每个人肯定有每个人心中的答案,我来讲讲我的心路历程吧。


第一阶段-这是个啥


刚开始学react的时候,写函数式组件,我们定义函数的时候,肯定是不会有意识的把这个函数使用 useCallback 包裹。就这样写了一段时间的代码。


突然有一天我们遇到了一个问题,useEffect无限调用,找了半天原因,原来是因为我们还不是很清楚useEffect依赖的概念,把使用到的所有的变量一股脑的塞到了依赖数组里面,碰巧,我们这次的依赖数组里面有一个函数,在react每一次渲染的时候,函数都被重新创建了,导致我们的依赖每一次都是新的,然后就触发了无限调用。


百度了一圈,原来使用 useCallback 缓存一下这个函数就可以了,这样useEffect中的依赖就不会每一次都是一个新值了。


小总结: 在这个阶段,我们第一次使用 useCallback ,了解到了它可以缓存一个函数。


第二阶段-可以缓存


可以缓存就遇到了两个点:

  1. 缓存是吧,不会每一次都重新创建是吧,这样是不是性能就能提高了!那我把我所有用到的函数都使用 useCallback缓存一下。
  2. react 每一次render的时候会导致子组件重新渲染,使用memo可以缓存这个子组件,在父组件更新的时候,会浅层的比较子组件的props,所以传给子组件的函数就需要使用缓存useCallback起来,那么父组件中定义函数的时候图方便,一股脑的都使用 useCallback缓存。

小总结: 在这里我们错误的认为了缓存就能够帮助我们做一些性能优化的事情,但是因为还不清楚根本的原因,导致我们很容易就滥用 useCallback


第三阶段-缓存不一定是好事


在这个阶段,写react也有一段时间了,我们了解到处处缓存其实还不如不缓存,因为缓存的开销不一定就比每一次重新创建函数的开销要小。


在这里肯定也是看了很多介绍 useCallback的文章了,推荐一下下面的文章


how-to-use-memo-use-callback,这个是全英文的,掘金有翻译这篇文章的,「好文翻译」


小总结: 到这里我们就大概的意识到了,处处使用useCallback可能并不是我们想象的那样,对正确的使用useCallback有了一定的了解


总结


那么究竟在何时应该使用useCallback呢?

  1. 我们知道 react 在父组件更新的时候,会对子组件进行全量的更新,我们可以使用 memo对子组件进行缓存,在更新的时候浅层的比较一下props,如果props没有变化,就不会更新子组件,那如果props中有函数,我们就需要使用 useCallback缓存一下这个父组件传给子组件的函数。
  2. 我们的useEffect中可能会有依赖函数的场景,这个时候就需要使用useCallback缓存一下函数,避免useEffect的无限调用

是不是就这两点呢?那肯定不是呀,不然就和我这篇文章的标题联系不起来了吗。


针对useEffect这个hooks补充一点react官方文档里面有提到,建议我们使用自定义的 hooks 封装 useEffect

  • 那使用useCallback的第三个场景就出现了,就是我们在自定义hooks需要返回函数的时候,建议使用 useCallback缓存一下,因为我们不知道用户拿我们返回的函数去干什么,万一他给加到他的useEffect的依赖里面不就出问题了嘛。

一个自定义hook的案例



实现一个倒计时 hooks



需求介绍


我们先简单的实现一个倒计时的功能,就模仿我们常见的发短息验证码的功能。页面效果




app.jsx




MessageBtn.jsx



 功能比较简单,按钮点击的时候创建了一个定时器,然后时间到了就清除这个定时器。


现在把 MessageBtn 中倒计时的逻辑写到一个自定义的hooks里面。


useCountdown


把上面的一些逻辑抽取一下,useCountdown主要接受一个倒计时的时长,返回当前时间的状态,以及一个开始倒计时的函数




这里的start函数用了useCallback,因为我们不能保证用户的使用场景会不会出问题,所以我们包一下


升级 useCountdown


现在我们期望useCountdown支持两个函数,一个是在倒计时的时候调用,一个是在倒计时结束的时候调用


预期的使用是这样的,通过一个配置对象传入 countdownCallBack函数和onEnd




改造 useCountdown

  • 然后我们这里count定义 0 有点歧义,0 不能准确的知道是一开始的 0 还是倒计时结束的 0,所以还需要加一个标志位来表示当前是结束的 0
    1. 使用 useEffect监听count的变化,变化的时候触发对应的方法

    实现如下, 新增了红框的内容




    提出问题


    那么,现在就有一个很严重的问题,onEndcountdownCallBack这两个函数是外部传入的,我们要不要把他放到我们自定义hookuseEffect依赖项里面呢


    我们不能保证外部传入的变量一定是一个被useCallback包裹的函数,那么肯定就不能放到useEffect依赖项里面。


    如何解决这个问题呢?


    答案就是使用useRef。(兜兜转转这么久才点题 (╥╯^╰╥))


    用之前我们可以看一下成熟的方案是什么


    比如ahooks里面的useLatestuseMemoizedFn的实现

    • useLatest 源码


    • useMemoizedFn 源码,主要看圈起来的地方就好了,本质也是用useRef记录传入的内容



    ok,我们使用一下 useLatest 改造一下我们的useCountdown,变动点被红框圈起来了




    总结


    其实这篇文章的核心点有两个

    1. 带着大家重新的学习了一下useCallback的使用场景。(useMemo类似)
    2. 编写自定义hooks时候,我们需要注意一下外部传入的参数,以及我们返回给用户的返回值,核心点是决不相信外部传入的内容,以及绝对要给用户一个可靠的返回值。

    作者:既见君子
    链接:https://juejin.cn/post/7271643757640007680
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    移动端的双击事件好不好用?

    web
    前言 2023年了,我不允许还有人不会自己实现移动端的双击事件。 过来,看这里,不足 50 行的代码实现的双击事件。 听笔者娓娓道来。 dblclick js原生有个dblclick双击事件,但是几乎不支持移动端。 而且,该dblclick事件在pc端鼠标双...
    继续阅读 »

    前言


    2023年了,我不允许还有人不会自己实现移动端的双击事件。


    过来,看这里,不足 50 行的代码实现的双击事件。


    听笔者娓娓道来。


    dblclick


    js原生有个dblclick双击事件,但是几乎不支持移动端。


    developer.mozilla.org_zh-CN_docs_Web_API_Element_dblclick_event.png


    而且,该dblclick事件在pc端鼠标双击时,会触发两次click与一次dblclick


    window.addEventListener('click', () => {
    console.log('click')
    });
    window.addEventListener('dblclick', () => {
    console.log('dblclick')
    });

    // 双击页面,打印:click✖️2 dblclick

    我们期望可以在移动端也能有双击事件,并且隔离单击与双击事件,双击时只触发双击事件,只执行双击回调函数,让注册双击事件像注册原生事件一样简单。


    点击穿透


    简单聊聊移动端的点击穿透。



    在移动端单击会依次触发touchstart->touchmove->touchend->click事件。



    有这样一段逻辑,在touchstart时出现全屏弹框,在click弹框时关闭弹框。实际上,在点击页面时,弹框会一闪而过,并没有出现正确的交互。在移动端单击时touchstart早于click,当弹框出现了,后来的click事件就落在了弹框上,导致弹框被关闭。这就是点击穿透的一种表现。


    笔者的业务需求是双击元素,出现全屏弹框,单击弹框时关闭弹框。因此基于这样的业务需求与现实的点击穿透问题,笔者选择采用click事件来模拟双击事件,并且适配pc端使用。大家也可以选择解决点击穿透问题,并采用touchstart模拟双击事件,可以更快地响应用户操作。



    采用touchstart模拟时,可以再考虑排除双指点击的情况。


    在实现上与下文代码除了事件对象获取位置属性有所不同外,其它代码基本一致,实现思路无差别。



    模拟双击事件


    采用click事件来模拟实现双击。


    双击事件定义:2次点击事件间隔小于200ms,并且点击范围小于10px的视为双击。这里的双击事件是自定义事件,为了区分原生的 dblclick,又优先满足移动端使用,则事件名定义为 dbltouch,后续可以使用window.addEventListener('dbltouch', ()=>{})来监听双击事件。



    这个间隔与位移限制大家可以根据自己的业务需求调整。通常采用的是300ms的间隔与10px的位移,笔者业务中发现200ms间隔也可使用。


    自定义事件名大家可以随意设置,满足语义化即可。





    1. 监听click事件,并在捕获阶段监听,目的是为了后续能够阻止click事件传播。


      window.addEventListener('click', handler, true);



    2. 监听函数中,第1次点击时,记录点击位置,并设置200ms倒计时。如果第2次点击在200ms后,则重新派发当前事件,让事件继续传播,使其它的监听函数可以继续处理对应事件。


      // 标识是否在等待第2次点击
      let isWaiting = false;

      // 记录点击位置
      let prevPosition = {};

      function handler(evt) {
      const { pageX, pageY } = evt;
      prevPostion = { pageX, pageY };
      // 阻止冒泡,不让事件继续传播
      evt.stopPropagation();
      // 开始等待第2次点击
      isWaiting = true;
      // 设置200ms倒计时,200ms后重新派发当前事件
      timer = setTimeout(() => {
      isWaiting = false;
      evt.target.dispatchEvent(evt);
      }, 200)
      }

      注意: 倒计时结束时evt.target.dispatchEvent(evt)派发的事件仍是原来的事件对象,即仍是click事件,会触发继续handler函数,进入了循环。


      这里需要破局,已知Event事件对象下有一个 isTrusted 属性,是一个只读属性,是一个布尔值。当事件是由用户行为生成的时候,这个属性的值为 true ,而当事件是由脚本创建、修改、通过 EventTarget.dispatchEvent()派发的时候,这个属性的值为 false 。


      因此,此处脚本派发的事件是希望继续传递的事件,不用handler内处理。


      function handler(evt) {
      // 如果事件是脚本派发的则不处理,将该事件继续传播
      if(!evt.isTrusted){
      return;
      }
      }



    3. 处理完第1次点击后,接着处理在200ms内的第2次点击事件。如果满足位移小于10px的条件,则视为双击。


      // 标识是否在等待第2次点击
      let isWaiting = false;

      // 记录点击位置
      const prevPosition = {};

      function handler(evt) {
      // 如果事件是脚本派发的则不处理,将该事件继续传播
      if(!evt.isTrusted){
      return;
      }
      const { pageX, pageY } = evt;
      if(isWaiting) {
      isWaiting = false;
      const diffX = Math.abs(pageX - prevPosition.pageX);
      const diffY = Math.abs(pageY - prevPosition.pageY);
      // 如果满足位移小于10,则是双击
      if(diffX <= 10 && diffY <= 10) {
      // 取消当前事件传递,并派发1个自定义双击事件
      evt.stopPropagation();
      evt.target.dispatchEvent(
      new PointerEvent('dbltouch', {
      cancelable: false,
      bubbles: true,
      })
      )
      }
      } else {
      prevPostion = { pageX, pageY };
      // 阻止冒泡,不让事件继续传播
      evt.stopPropagation();
      // 开始等待第2次点击
      isWaiting = true;
      // 设置200ms倒计时,200ms后重新派发当前事件
      timer = setTimeout(() => {
      isWaiting = false;
      evt.target.dispatchEvent(evt);
      }, 200)
      }
      }



    4. 以上便实现了双击事件,全局任意地方监听双击。


      window.addEventListener('dbltouch', () => {
      console.log('dbltouch');
      })
      window.addEventListener('click', () => {
      console.log('click');
      })
      // 使用鼠标、手指双击
      // 打印出 dbltouch
      // 而且不会打印有click



    笔者要在这里说句 但是: 由于200ms的延时,虽不多,但是对于操作迅速的用户来讲,还是会有不好的体验。


    优化双击事件


    由于是在window上注册的click函数,虽说注册双击事件像单击事件一样简单了,但却也导致整个产品页面的click事件都会推迟200ms执行。


    因此,我们应该只对需要处理双击的地方添加双击事件,至少只在局部发生延迟情况。稍微调整下代码,将需要注册双击事件的元素由开发决定,通过参数传递。而且事件处理函数也可以通过参数传递,即可以通过监听双击事件,也可以通过回调函数执行。


    以下是完整的代码。


    class RegisterDbltouchEvent {
    constructor(el, fn) {
    this.el = el || window;
    this.callback = fn;
    this.timer = null;
    this.prevPosition = {};
    this.isWaiting = false;

    // 注册click事件,注意this指向
    this.el.addEventListener('click', this.handleClick.bind(this), true);
    }
    handleClick(evt){
    if(this.timer) {
    clearTimeout(this.timer);
    this.timer = null;
    }
    if(!evt.isTrusted) {
    return;
    };
    if(this.isWaiting){
    this.isWaiting = false;
    const diffX = Math.abs(pageX - this.prevPosition.pageX);
    const diffY = Math.abs(pageY - this.prevPosition.pageY);
    // 如果满足位移小于10,则是双击
    if(diffX <= 10 && diffY <= 10) {
    // 取消当前事件传递,并派发1个自定义双击事件
    evt.stopPropagation();
    evt.target.dispatchEvent(
    new PointerEvent('dbltouch', {
    cancelable: false,
    bubbles: true,
    })
    );
    // 也可以采用回调函数的方式
    this.callback && this.callback(evt);
    }
    } else {
    this.prevPostion = { pageX, pageY };
    // 阻止冒泡,不让事件继续传播
    evt.stopPropagation();
    // 开始等待第2次点击
    this.isWaiting = true;
    // 设置200ms倒计时,200ms后重新派发当前事件
    this.timer = setTimeout(() => {
    this.isWaiting = false;
    evt.target.dispatchEvent(evt);
    }, 200)
    }
    }
    }

    只为需要实现双击逻辑的元素注册双击事件。可以通过传递回调函数的方式执行业务逻辑,也可以通过监听dbltouch事件的方式,也可以同时使用,it's up to you.


    const el = document.querySelector('#dbltouch');
    new RegisterDbltouchEvent(el, (evt) => {
    // 实现双击逻辑
    })

    最后


    采用的click事件模拟双击事件,因此在移动端和pc端都可以使用该构造函数。


    作者:Yue栎廷
    来源:juejin.cn/post/7274043371731796003
    收起阅读 »

    为什么我的页面鼠标一滑过,布局就错乱了?

    web
    前言 这天刚到公司,测试同事又在群里@我: 为什么页面鼠标一滑过,布局就错乱了? 以前是正常的啊? 刷新后也是一样 快看看怎么回事 同时还给发了一段bug复现视频,我本地跑个例子模拟下 可以看到,鼠标没滑过是正常的,鼠标一滑过图片就换行了,鼠标移出又正常了。...
    继续阅读 »

    前言


    这天刚到公司,测试同事又在群里@我:

    为什么页面鼠标一滑过,布局就错乱了?

    以前是正常的啊?

    刷新后也是一样

    快看看怎么回事


    同时还给发了一段bug复现视频,我本地跑个例子模拟下


    GIF 2023-8-28 11-23-25.gif


    可以看到,鼠标没滑过是正常的,鼠标一滑过图片就换行了,鼠标移出又正常了。


    正文


    首先说下我们的产品逻辑,我们产品的需求是:要滚动的页面,滚动条不应该占据空间,而是悬浮在滚动页面上面,而且滚动条只在滚动的时候展示。


    我们的代码是这样写:


      <style>
    .box {
    width: 630px;
    display: flex;
    flex-wrap: wrap;
    overflow: hidden; /* 注意⚠️ */
    height: 50vh;
    box-shadow: 0 0 5px rgba(93, 96, 93, 0.5);
    }
    .box:hover {
    overflow: overlay; /* 注意⚠️ */
    }
    .box .item {
    width: 200px;
    height: 200px;
    margin-right: 10px;
    margin-bottom: 10px;
    }
    img {
    width: 100%;
    height: 100%;
    }
    </style>
    <div class="box">
    <div class="item">
    <img src="https://p6.itc.cn/q_70/images03/20210422/229df67605934f719b5e2efe6f171d5f.jpeg" alt="">
    </div>
    <div class="item">
    <img src="https://p6.itc.cn/q_70/images03/20210422/229df67605934f719b5e2efe6f171d5f.jpeg" alt="">
    </div>
    <div class="item">
    <img src="https://p6.itc.cn/q_70/images03/20210422/229df67605934f719b5e2efe6f171d5f.jpeg" alt="">
    </div>
    <div class="item">
    <img src="https://p6.itc.cn/q_70/images03/20210422/229df67605934f719b5e2efe6f171d5f.jpeg" alt="">
    </div>
    </div>

    我们使用了overflow属性的overlay属性,滚动条不会占据空间,而是悬浮在页面之上,刚好吻合了产品的需求。


    image.png


    然后我们在滚动的区域默认是overflow:hidden,正常时候不会产生滚动条,hover的时候把overflow改成overlay,滚动区域可以滚动,并且不占据空间。


    简写代码如下:


      .box {
    overflow: hidden;
    }
    .box:hover {
    overflow: overlay;
    }

    然后我们是把它封装成sass的mixins,滚动区域使用这个mixins即可。


    上线后没什么问题,符合预期,获得产品们的一致好评。


    直接这次bug的出现。


    排查


    我先看看我本地是不是正常的,我打开一看,咦,我的布局不会错乱。


    然后我看了我的chrome的版本,是113版本


    然后我问了测试的chrome版本,她是114版本


    然后我把我的chrome升级到最新114版本,咦,滑过页面之后我的布局也乱了。


    初步判断,那就有可能是chrome版本的问题。


    去网上看看chrome的升级日志,看看有没有什么信息。


    image.png


    具体说明:


    image.png


    可以看到chrome114有了一个升级,就是把overflow:overlay当作overflow:auto的别名,也就是说,overlay的表现形式和auto是一致的。


    实锤了,auto是占据空间的,所以导致鼠标一滑过,布局就乱了。


    其实我们从mdn上也能看到,它旁边有个删除按钮,然后滑过有个tips:


    image.png


    解决方案


    第一种方式


    既然overflow:overlay表现形式和auto一致,那么我们得事先预留出滚动条的位置,然后设置滚动条的颜色是透明的,滑过才显示颜色,基本上也能符合产品的需求。


    代码如下:


      // 滚动条
    ::-webkit-scrollbar {
    background: transparent;
    width: 6px;
    height: 6px;
    }
    // 滚动条上的块
    ::-webkit-scrollbar-thumb {
    background-clip: padding-box;
    background-color: #d6d6d6;
    border: 1px solid transparent;
    border-radius: 10px;
    }
    .box {
    overflow: auto;
    }
    .box::-webkit-scrollbar-thumb {
    background-color: transparent;
    }
    .box:hover::-webkit-scrollbar-thumb {
    background-color: #d6d6d6;
    }

    第二种方式


    如果有用element-ui,它内部封装了el-scrollbar组件,模拟原生的滚动条,这个也不占据空间,而是悬浮,并且默认不显示,滑过才显示。



    element-ui没有el-scrollbar组件的文档说明,element-plus才有,不过都可以用。



    总结


    这次算是踩了一个坑,当mdn上面提示有些属性已经要废弃⚠️了,我们就要小心了,尽量不要使用这些属性,就算当前浏览器还是支持的,但是不能保证未来某个时候浏览器会不会废弃这个属性。(比如这次问题,改这些问题挺费时间的,因为用的地方挺多的。)


    因为一旦这些属性被废弃了,它预留的bug就等着你去解决,谨记!


    作者:答案cp3
    来源:juejin.cn/post/7273875079658209319
    收起阅读 »

    JS 获取页面尺寸

    web
    通过 JS 获取页面相关的尺寸是比较常见的操作,尤其是在动态计算页面布局时,今天我们就来学习一下几个获取页面尺寸的基本方法。 获取页面高度 function getPageHeight() { var g = document, a = g.bod...
    继续阅读 »

    通过 JS 获取页面相关的尺寸是比较常见的操作,尤其是在动态计算页面布局时,今天我们就来学习一下几个获取页面尺寸的基本方法。


    获取页面高度


    function getPageHeight() {
    var g = document,
    a = g.body,
    f = g.documentElement,
    d = g.compatMode == "BackCompat" ? a : g.documentElement;
    return Math.max(f.scrollHeight, a.scrollHeight, d.clientHeight);
    }

    获取页面scrollLeft


    function getPageScrollLeft() {
    var a = document;
    return a.documentElement.scrollLeft || a.body.scrollLeft;
    }

    获取页面scrollTop


    function getPageScrollTop() {
    var a = document;
    return a.documentElement.scrollTop || a.body.scrollTop;
    }

    获取页面可视宽度


    function getPageViewWidth() {
    var d = document,
    a = d.compatMode == "BackCompat" ? d.body : d.documentElement;
    return a.clientWidth;
    }

    获取页面可视高度


    function getPageViewHeight() {
    var d = document,
    a = d.compatMode == "BackCompat" ? d.body : d.documentElement;
    return a.clientHeight;
    }

    获取页面宽度


    function getPageWidth() {
    var g = document,
    a = g.body,
    f = g.documentElement,
    d = g.compatMode == "BackCompat" ? a : g.documentElement;
    return Math.max(f.scrollWidth, a.scrollWidth, d.clientWidth);
    }

    ~


    ~ 全文完


    ~


    作者:编程三昧
    来源:juejin.cn/post/7274856158175363126
    收起阅读 »

    一个有意思的点子

    web
    前言 前端基于运行时检测问题、定位原因、预警提示的可行性思考🤔 背景 部门要治理线上故障过多的问题,故障主要分后端异常以及前端的性能问题和业务问题。我们讨论的范围是前端业务故障,经过分析可以把它们大致分为UI、业务、工程技术、日志、三方依赖等。 首先要确定...
    继续阅读 »

    前言


    前端基于运行时检测问题、定位原因、预警提示的可行性思考🤔



    背景


    部门要治理线上故障过多的问题,故障主要分后端异常以及前端的性能问题和业务问题。我们讨论的范围是前端业务故障,经过分析可以把它们大致分为UI、业务、工程技术、日志、三方依赖等。



    首先要确定降低故障的指标,MTTI和MTTD是关键,因为只有及时发现和定位问题才能快速消灭它们。我们暂时没有统计线上MTTI、MTTD的数据,因为缺乏相关的预警所以问题的发现和定位耗时通常很久,下面的一些复盘统计显示发现问题的时间可以持续甚至好几个月。



    这些问题中UI和业务异常占比超过80%,这些问题发现的不及时,一方面是问题本身不会阻碍核心链路,另一方面说明团队对业务的稳定缺少监控手段。
    现有开发流程已经包含了Design QA验收交付的UI是否符合预期;开发工程师会和QA工程师一起执行Test Case验证业务的稳定并且在CI环节还有UT的保障。既然如此那为什么线上还是会不可避免的出现故障呢?


    问题归因


    在Dev和Stage阶段的验收能发现和处理绝显而易见的异常,但是这些验收的场景是有限的



    1. 开发环境数据集的局限

    2. 考虑到AB因素的影响,很难做到全场景全业务覆盖。

    3. 开发过程可能会无意间影响到其他业务,但是我们的注意力集中在现有的业务,也就导致问题难以被发现


    所以归根到底,验收环节中数据和场景的局限以及人治导致一些Edge case被遗漏。


    解决方案


    我们该如何解决数据和场景的局限呢?这个其实通过Monkey和数据流量回放就能解决。
    运行时阶段包含了所有业务和代码的上下文,所有在这个阶段发现问题、分析原因并预警效率是最高的,人治的问题可以得到改善。下面是这种机制执行的思路



    1. 自动化测试时,通过流量回放的形式模拟线上的数据和环境尽可能多的覆盖场景。

    2. 运行时阶段,前端通过UT代码验收业务(🤔UT只能在Unit Test阶段执行,运行时执行的原理后面会讲)

    3. 运行时阶段,分析UI元素间的关系并探测异常问题


    方案实现


    方案实现仅讨论前端的部分。
    UI和业务检测比较通用,因为它们的判别条件是客观的。比如我们完全可以复用UT代码检测业务。


    自动检测、定位原因、预警


    这个机制实现没有困难。考虑到自动检测的范围在不同项目中不尽相同,所以实现的思路可以是插件的形式,模块间通过协议解耦。
    主要功能模块有:



    1. 告警模块

    2. 日志生成模块

    3. 业务注册模块(接收业务自定义的检查日志)

    4. 内嵌的UI检测模块


    UI检测


    业务不同,遇到的UI问题会有差异,这部分需要具体问题具体分析,所以不做过多讨论。针对我们业务的现状Overlap、Truncate、Clip在UI中占比较高。我的做法是对显示的视图按多叉树遍历到叶子节点并分析子节点和兄弟节点间的关系,找到Overlap、Truncate、Clip问题。具体的实现可以参考代码LensWindowGuard.swift:31


    业务检测


    UT代码从逻辑上可以被分为三个部分:



    1. Give

    2. When

    3. Then


    Given表示输入的数据,可以是真实接口也可以是Mock数据。


    When表示调用业务函数,同时这里会产生一个输出结果。


    Then表示检验输入的数据是否和输出的结果匹配,如果不匹配会在UT环节报错。


    业务代码从逻辑上可以被分为两个部分



    1. Give

    2. When


    Given可以是上下文的变量也可以是API调用


    When表示执行业务的代码块


    Blank diagram (23).png
    如果把UT的Then引入到业务的函数里,就可以实现运行时执行UT的效果了😁。


    将UT代码引入到业务函数中,思路是首先把UT按照Given、When、Then三部分拆分,把Given和Then部分独立出去,独立的部分通过代码插桩的方式在UT和业务代码中复用。


    Blank diagram (24).png


    不过到这里遗留了几个问题,暂时还没有太好的思路🧐



    1. 异步回调 - 业务代码或者UT检测逻辑只能执行一个

    2. 业务方法名的修改 - 使用Swift Macro插桩方式等同新增加一个全新的方法,所以运行时执行UT需要更改业务逻辑

    3. UT代码被执行多次 - 上面的图可以看出来,新的UT代码被同时插到旧的UT和业务代码中,所以执行UT时会UT逻辑被执行了两次


    代码插桩


    我们项目基于Swift,且插桩需要有判别逻辑,基于此选用AST的方式在编译期修改FunctionDecliation,把代码通过字符串的形式插到function body的合适位置。正巧2023 WWDC苹果发布了Swift Macro,其中的@Attached(Peer)正好可以满足对FunctionDecliation可编程修改,通过实现自动以@Attached(Peer)以及增加DEBUG宏(Swift Macro不能读取工程配置信息)可以改变UT的流程。真想说Swift太香了。


    最终


    完整的项目的整体架构大致如下,主要分了三部分。



    1. Hubble - 主要职责是提供基础协议、日志、预警和一些协助分析问题的工具集

    2. HubbleCoordinator - 主要职责是作为业务和组件交互的中间层,业务相关的逻辑都放在这里,隔离业务和Hubble

    3. 业务 - 仅处理Hubble初始化工作,对业务代码没有任何侵入
      AR


    作者:tom猪
    来源:juejin.cn/post/7274140856034099252
    收起阅读 »

    刚咬了一口馒头,服务器突然炸了!

    首先,这个项目是完全使用Websocket开发,后端采用了基于Swoole开发的Hyperf框架,集成了Webscoket服务。 其次,这个项目是我们组第一次尝试使用Socket替换传统的HTTP API请求,前端使用了Vue3,前端同学定义了一套Socket...
    继续阅读 »

    首先,这个项目是完全使用Websocket开发,后端采用了基于Swoole开发的Hyperf框架,集成了Webscoket服务。
    其次,这个项目是我们组第一次尝试使用Socket替换传统的HTTP API请求,前端使用了Vue3,前端同学定义了一套Socket管理器。


    看着群里一群“小可爱”疯狂乱叫,我被吵的头都炸了,赶紧尝试定位问题。

    1. 查看是否存在Jenkins发版 -> 无
    2. 查看最新提交记录 -> 最后一次提交是下午,到晚上这段时间系统一直是稳定的
    3. 查看服务器资源,htop后,发现三台相关的云服务器资源都出现闲置状态
    4. 查看PolarDB后,既MySQL,连接池正常、吞吐量和锁正常
    5. 查看Redis,资源正常,无异常key
    6. 查看前端控制台,出现一些报错,但是这些报错经常会变化
    7. 查看前端测试环境、后端测试环境,程序全部正常
    8. 重启前端服务、后端服务、NGINX服务,好像没用,过了5分钟后,咦,好像可以访问了

    就在我们组里的“小可爱”通知系统恢复正常后,20分钟不到,再一次处于无法打开的状态,沃焯!!!
    完蛋了,找不出问题在哪里了,实在想不通问题究竟出在哪里。


    我不服啊,我不理解啊!


    咦,Nginx?对呀,我瞅瞅访问日志,于是我浏览了一下access.log,看起来貌似没什么问题,不过存在大量不同浏览器的访问记录,刷的非常快。


    再瞅瞅error.log,好像哪里不太对

    2023/04/20 23:15:35 [alert] 3348512#3348512: 768 worker_connections are not enough
    2023/04/20 23:33:33 [alert] 3349854#3349854: *3492 open socket #735 left in connection 1013

    这是什么?貌似是连接池问题,赶紧打开nginx.conf看一下配置

    events {
    worker_connections 666;
    # multi_accept on;
    }

    ???


    运维小可爱,你特么在跟我开玩笑?虽然我们这个系统是给B端用的,还是我们自己组的不到100人,但是这连接池给的也太少了吧!


    另外,前端为什么会开到 1000 个 WS 连接呢?后端为什么没有释放掉FD呢?


    询问后,才知道,前端的Socket管理器,会在连接超时或者其它异常情况下,重新开启一个WS连接。


    后端的心跳配置给了300秒

    Constant::OPTION_OPEN_WEBSOCKET_PROTOCOL => true, // websocket 协议
    Constant::OPTION_HEARTBEAT_CHECK_INTERVAL => 150,
    Constant::OPTION_HEARTBEAT_IDLE_TIME => 300,

    此时修改nginx.conf的配置,直接拉满!!!

    worker_connections 655350;

    重启Nginx,哇绰,好像可以访问了,但是每当解决一个问题,总会产生新的问题。


    此时error.log中出现了新的报错:

    2023/04/20 23:23:41 [crit] 3349767#3349767: accept4() failed (24: Too many open files)

    这种就不怕了,貌似和Linux的文件句柄限制有关系,印象中是1024个。
    至少有方向去排查,不是吗?而且这已经算是常规问题了,我只需小小的百度一下,哼哼~


    拉满拉满!!

    worker_rlimit_nofile 65535;

    此时再次重启Nginx服务,系统恢复稳定,查看当前连接数:

    netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'
    # 打印结果
    TIME_WAIT 1175

    FIN_WAIT1 52

    SYN_RECV 1

    FIN_WAIT2 9

    ESTABLISHED 2033

    经过多次查看,发现TIME_WAITESTABLISHED都在不断减少,最后完全降下来。


    本次问题排查结束,问题得到了解决,但是关于socket的连接池管理器,仍然需要优化,及时释放socket无用的连接。


    作者:兰陵笑笑生666
    链接:https://juejin.cn/post/7224314619865923621
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    弃用qiankun!看古茗中后台架构如何破局

    引言 我们团队之前发布过一篇文章,介绍了古茗前端到底在做什么,当然这只包含了我们团队内做的一部分事情。以端侧来划分,主要包括:中后台 web 端、H5 端、4 端小程序、Electron PC 端、Android/Flutter 客户端,我们需要为这些端侧方向...
    继续阅读 »

    引言


    我们团队之前发布过一篇文章,介绍了古茗前端到底在做什么,当然这只包含了我们团队内做的一部分事情。以端侧来划分,主要包括:中后台 web 端、H5 端、4 端小程序、Electron PC 端、Android/Flutter 客户端,我们需要为这些端侧方向做一些通用能力的解决方案,同时也需要深入每个端侧细分领域做它们特有的技术沉淀。本文主要介绍古茗在中后台技术方向的一些思考和技术沉淀。


    业务现状


    古茗目前有大量的中后台业务诉求,包括:权限系统、会员系统、商品中心、拓展系统、营运系统、财务系统、门店系统、供应链系统等多个子系统,服务于内部产品、技术、运营,及外部加盟商等用户群体,这些子系统分别由不同业务线的同学负责开发和维护,而这些子系统还没有系统性的技术沉淀。随着业务体量和业务复杂度的不断增大,在“降本增效”的大背景下,如何保证业务快速且高质量交付是我们面临的技术挑战。


    技术演进




    如上述技术演进路线图所示,我们的中后台技术架构大致经历了 4 个阶段:当业务起步时,我们很自然的使用了单应用开发模式;当业务发展到一定的体量,多人协同开发变得困难时,我们拆分了多个子系统维护,并使用了 systemjs 来加载子系统资源(微前端模式的雏形);当遇到有三方库的多版本隔离诉求时,我们又引入了 qiankun 微前端框架来隔离多个子系统,保证各子系统间互不影响;那是什么原因让我们放弃 qiankun,转向完全自研的一套中后台解决方案?


    弃用 qiankun?


    其实准确来说,qiankun 并不能算是一个完整的中后台解决方案,而是一个微前端框架,它在整个技术体系里面只充当子应用管理的角色。我们在技术演进过程中使用了 qiankun 尝试解决子应用的隔离问题,但同时也带来了一些新的问题:某些场景跳转路由后视图无反应;某些三方库集成进来后导致奇怪的 bug...。同时,还存在一些问题无法使用 qiankun 解决,如:子应用入口配置、样式隔离、运维部署、路由冲突、规范混乱、要求资源必须跨域等。


    探索方向


    我们重新思考了古茗中后台技术探索方向是什么,也就是中后台技术架构到底要解决什么问题,并确定了当下的 2 大方向:研发效率用户体验。由此我们推导出了中后台技术探索要做的第一件事情是“统一”,这也为我们整个架构的基础设计确立了方向。




    架构设计


    我们的整体架构是围绕着“统一”和“规范” 2 大原则来设计,目标是提升整个团队的研发效能。我们认为好的架构应该是边界清晰的,而不是一味的往上面堆功能,所以我们思考更多的是,如果没有这个功能,能否把这件事情做好。


    取个“好”名字


    我老板曾说过一个好的(技术)产品要做的第一件事就是取个“好”名字。我们给这套中后台架构方案取名叫「Mars」,将相关的 NPM 包、组件库、SDK、甚至部署路径都使用 mars 关键字来命名,将这个产品名字深入人心,形成团队内共同的产品认知。这样的好处是可以加强团队对这套技术方案的认同感,以及减少沟通负担,大家一提到 mars,就都知道在说哪一件事。


    框架设计




    正如上述大图所示,我们是基于微前端的思路做的应用框架设计,所以与市面上大多数的微前端框架的设计思路、分层结构大同小异。这里还是稍微介绍一下整个流程:当用户访问网站时,首先经过网关层,请求到基座应用的资源并渲染基础布局和菜单,当监听路由变化时,加载并渲染路由关联的子应用及页面。


    但是,市面上大部分的微前端框架往往需要在基座应用上配置子应用的 nameentryavtiveRule 等信息,因为框架需要根据这些信息来决定什么时机去加载哪一个子应用,以及如何加载子应用的资源。这就意味着每新增一个子应用,都需要去基座维护这份配置。更要命的是,不同环境的 entry 可能还要根据不同的环境做区别判断。如果遇到本地开发时需要跳转至其他子应用的场景,那也是非常不好的开发体验。所以,这是我们万万不能接受的。


    针对这一痛点,我们想到了 2 种解决思路:

    1. 把基座应用的配置放到云端,用 node 作为中间层来维护各子应用的信息,每次新增应用、发布资源后同步更新,问题就是这套方案实现成本比较高,且新增子应用还是需要维护子应用的信息,只是转移到在云端维护了。
    2. 使用约定式路由及部署路径,当我们识别到一个约定的路由路径时,可以反推它的应用 ID 及部署资源路径,完全 0 配置。很明显,我们选择了这种方案。

    约定式路由及部署路径


    路由约定


    我们制定了如下的标准 Mars 路由规范

      /mars/appId/path/some?name=ferret
    \_/ \_/ \_____/ \_______/
    | | | |
    标识 appId path query


    1. 路由必须以 /mars 开头(为了兼容历史路由包袱)

    2. 其后就是 appId ,这是子应用的唯一标识

    3. 最后的 pathquery 部分就是业务自身的路由和参数


    部署路径约定


    我们制定了如下的标准 Mars 子应用部署路径规范

      https://cdn.example.com/mars/[appId]/[env]/manifest.json
    \__________________/ \_/ \___/ \_/ \________/
    | | | | |
    cdn 域名 标识 appId 环境 入口资源清单

    从上述部署路径规范可以看出,整个路径就 appIdenv 2 个变量是不确定的,而 env 可以在发布时确定,因此可由 appId 推导出完整的部署路径。而根据路由约定,我们可以很容易的从路由中解析出 appId,由此就可以拿到完整的 manifest.json 部署路径 ,并以此获取到整个子应用的入口资源信息。


    编译应用


    虽然制定了上述 2 大规范,但是如何保障规范落地,防止规范腐化也是非常重要的一个问题。我们是通过编译手段来强制约束执行的(毕竟“人”往往是靠不住的😄)。


    依赖工程化体系



    提示:Kone 是古茗内部前端工程化的工具产品。



    首先,子应用需要配置一个工程配置文件,并注册 @guming/kone-plugin-mars 插件来完成子应用的本地开发、构建、发布等工程化相关的任务。其中:配置项 appId 就代表约定路由中的 appId 和 部署路径中的 appId,也是子应用的唯一标识。


    工程配置文件:kone.config.json

    {
    "plugins": ["@guming/kone-plugin-mars"],
    "mars": {
    "appId": "demo"
    }
    }

    编译流程


    然后,子应用通过静态化配置式(json 配置)注册路由,由编译器去解析配置文件,注册路由,以及生成子应用 mountunmount 生命周期方法。这样实现有以下 3 个好处:

    • 完整的路由 path 由编译生成,可以非常好的保障约定式路由落地
    • 生命周期方法由编译生成,减少项目中的模板代码,同样可以约束子应用的渲染和卸载按照预定的方式执行
    • 可以约束不规范的路由 path 定义,例如我们会禁用掉 :param 形式的动态路由

    应用配置文件:src/app.json

    {
    "routes": [
    {
    "path": "/some/list",
    "component": "./pages/list",
    "description": "列表页"
    },
    {
    "path": "/some/detail",
    "component": "./pages/detail",
    "description": "详情页"
    }
    ]
    }


    上述示例最终会生成路由:/mars/demo/some/list/mars/demo/some/detail



    webpack-loader 实现


    解析 src/app.json 需要通过一个自定义的 webpack-loader 来实现,部分示例代码如下:

    import path from 'path';
    import qs from 'qs';

    export default function marsAppLoader(source) {
    const { appId } = qs.parse(this.resourceQuery.slice(1));
    let config;
    try {
    config = JSON.parse(source);
    } catch (err) {
    this.emitError(err);
    return;
    }

    const { routes = [] } = config;

    const routePathSet = new Set();
    const routeRuntimes = [];
    const basename = `/mars/${appId}`;

    for (let i = 0; i < routes.length; i++) {
    const item = routes[i];
    if (routePathSet.has(item.path.toLowerCase())) {
    this.emitError(new Error(`重复定义的路由 path: ${item.path}`));
    return;
    }

    routeRuntimes.push(
    `routes[${i}] = { ` +
    `path: ${JSON.stringify(basename + item.path)}, ` +
    `component: _default(require(${JSON.stringify(item.component)})) ` +
    `}`
    );
    routePathSet.add(item.path.toLowerCase());
    }

    return `
    const React = require('react');
    const ReactDOM = require('react-dom');

    // 从 mars sdk 中引入 runtime 代码
    const { __internals__ } = require('@guming/mars');
    const { defineApp, _default } = __internals__;

    const routes = new Array(${routeRuntimes.length});
    ${routeRuntimes.join('\n')}

    // define mars app: ${appId}
    defineApp({
    appId: '${appId}',
    routes,
    });

    `.trim();
    }

    src/app.json 作为编译入口并经过此 webpack-loader 编译之后,将自动编译关联的路由组件,创建子应用路由渲染模板,注册生命周期方法等,并最终输出 manifest.json 文件作为子应用的入口(类似 index.html),根据入口文件的内容就可以去加载入口的 js、css 资源并触发 mount 生命周期方法执行渲染逻辑。生成的 manifest.json 内容格式如下:

    {
    "js": [
    "https://cdn.example.com/mars/demo/prod/app.a0dd6a27.js"
    ],
    "css": [
    "https://cdn.example.com/mars/demo/prod/app.230ff1ef.css"
    ]
    }

    聊聊沙箱隔离


    一个好的沙箱隔离方案往往是市面上微前端框架最大的卖点,我们团队内也曾引入 qiankun 来解决子应用间隔离的痛点问题。而我想让大家回归到自己团队和业务里思考一下:“我们团队需要隔离?不做隔离有什么问题”。而我们团队给出的答案是:不隔离 JS,要隔离 CSS,理由如下:

    1. 不隔离 JS 可能会有什么问题:window 全局变量污染?能污染到哪儿去,最多也就内存泄露,对于现代 B 端应用来说,个别内容泄露几乎可以忽略不计;三方库不能混用版本?如文章开头所提及的,我们要做的第一件事就是统一,其中就包括统一常用三方库版本,在统一的前提下这种问题也就不存在了。当然也有例外情况,比如高德地图 sdk 在不同子系统需要隔离(使用了不同的 key),针对这种问题我们的策略就是专项解决;当然,最后的理由是一套非常好的 JS 隔离方案实现成本太高了,需要考虑太多的问题和场景,这些问题让我们意识到隔离 JS 带来的实际价值可能不太高。
    2. 由于 CSS 的作用域是全局的,所以非常容易造成子应用间的样式污染,其次,CSS 隔离是容易实现的,我们本身就基于编译做了很多约束的事情,同样也可以用于 CSS 隔离方案中。实现方案也非常简单,就是通过实现一个 postcss 插件,将子应用中引入的所有 css 样式都加上特有的作用域前缀,例如:
    .red {
    color: red;
    }

    将会编译成:

    .mars__demo .red {
    color: red;
    }

    当然,某些场景可能就是需要全局样式,如 antd 弹层内容默认就会在子应用内容区外,造成隔离后的样式失效。针对这种场景,我们的解法是用隔离白名单机制,使用也非常简单,在最前面加上 :global 选择器,编译就会直接跳过,示例:

    :global {
    .some-modal-cls {
    font-size: 14px;
    }
    }

    将会编译成:

    .some-modal-cls {
    font-size: 14px;
    }

    除此之外,在子应用卸载的时候,还会禁用掉子应用的 CSS 样式,这是如何做到的?首先,当加载资源的时候,会找到该资源的 CSSStyleSheet 对象:

    const link = document.createElement('link');
    link.setAttribute('href', this.url);
    link.setAttribute('rel', 'stylesheet');
    link.addEventListener('load', () => {
    // 找到当前资源对应的 CSSStyleSheet 对象
    const styleSheets = document.styleSheets;
    for (let i = styleSheets.length - 1; i >= 0; i--) {
    const sheet = styleSheets[i];
    if (sheet.ownerNode === this.node) {
    this.sheet = sheet;
    break;
    }
    }
    });

    当卸载资源的时候,将该资源关联的 CSSStyleSheet 对象的 disabled 属性设置为 true 即可禁用样式:

    if (this.sheet) {
    this.sheet.disabled = true;
    }

    框架 SDK 设计




    框架 SDK 按照使用场景可以归为 3 类,分别是:子应用、基座应用、编译器。同样的遵循我们的一贯原则,如果一个 API 可以满足诉求,就不会提供 2 个 API,尽可能保证团队内的代码风格都是统一的。例如:路由跳转 SDK 只提供唯一 API,并通过编译手段禁用掉其他路由跳转方式(如引入 react-router-dom 的 API 会报错):

    import { mars } from '@guming/mars';

    // 跳转路由:/mars/demo/some/detail?a=123
    mars.navigate('/mars/demo/some/detail', {
    params: { a: '123' }
    });

    // 获取路由参数
    const { pathname, params } = mars.getLocation();
    // pathname: /mars/demo/some/detail
    // params: { a: '123' }

    当然,我们也会根据实际情况提供一些便利的 API,例如:跳转路由要写完整的 /mars/[appId] 路由前缀太繁琐,所以我们提供了一个语法糖来减少样板代码,在路由最前面使用 : 来代替 /mars/[appId] 前缀(仅在当前子应用内跳转有效):

    import { mars } from '@guming/mars';

    // 跳转路由:/mars/demo/some/detail?a=123
    mars.navigate(':/some/detail', {
    params: { a: '123' }
    });

    另外,值得一提的是在基座应用上使用的一个 API bootstrap(),得益于这个 API 的设计,我们可以快速创建多个基座应用(不同域名),还能使用这个 API 在本地开发的时候启动一个基座来模拟本地开发环境,提升开发体验。


    本地开发体验


    开发模拟器


    为更好的支持本地开发环境,我们提供了一套本地开发模拟器,在子应用启动本地服务的时候,会自动启动一个模拟的基座应用,拥有和真实基座应用几乎一样的布局,运行环境,集成的登录逻辑等。除此之外,开发模拟器还提供了辅助开发的「debug 小组件」,比如通过 debug 工具可以动态修改本地开发代理规则,保存之后立即生效。




    IDE 支持


    为了提升开发体验,我们分别开发了 Webstorm 插件 和 VSCode 插件服务于 Mars 应用,目前支持路由组件配置的路径补全、点击跳转、配置校验等功能。此外,我们会为配置文件提供 json schema,配置后将会获得 IDE 的自动补全能力和配置校验能力。




    历史项目迁移


    技术架构演进对于业务项目来说最大的问题在于,如何完成历史项目向新架构的迁移改造?我们团队投入 2 个人花了 2 个月时间将 12 个历史项目全部迁移至最新的架构上,这里分享一些我们在迁移过程中的经验。


    定目标


    首先要确定历史项目迁移这件事情一定是要做的,这是毋庸置疑的,而且要越快越好。所以我们要做的第一件事就是制定迁移计划,最后确定投入 2 个人力,大约花 2 个月时间将历史项目全部迁移至新的架构。第二件事情就是确定改造的范围(确定边界,不做非目标范围内的改造),对于我们的业务现状来说,主要包括:

    • 统一 reactreact-dom 版本为 17.0.2
    • 统一 antd 版本为 4.24.8
    • 统一路由
    • 统一接入 request 请求库
    • 统一接入工程化体系
    • 统一环境变量

    梳理 SOP


    因为迁移的流程不算简单,迁移要做的事情还挺多的,所以接下来要做的一件事就是梳理迁移流程 SOP,SOP 文档要细化到每种可能的场景,以及遇到问题对应的解法,让后续项目的迁移可以傻瓜式的按照标准流程去操作即可。我们的做法是,先以一个项目作为试点,一边迁移一边梳理 SOP,如果在迁移其他项目中发现有遗漏的场景,再持续补充这份 SOP 文档。


    例如:之前项目中使用了 dva 框架,但是它的 routermodel 是耦合的,这样就无法使用我们制定的统一路由方案,对此我们的解法是,通过 hack dva 的源代码,将 model 前置注入到应用中,完成与路由的解耦。


    上线方案


    由于业务迭代频繁,所以我们代码改造持续的时间不能太长,否则要多次合并代码冲突,而我们的经验就是,项目改造从拉分支到发布上线,要在 1 周内完成。当然,整个上线过程还遇到许多需要解决的问题,比如在测试参与较少的情况下如何保障代码质量,包括:业务回归的策略,回滚策略,信息同步等等。


    总结


    之前看到 Umi 4 设计思路文字稿 里面有句话我觉得特别有道理:“社区要开放,团队要约束”,我们团队也在努力践行“团队约束”这一原则,因为它为团队带来的收益是非常高的。


    没有最完美的方案,只有最适合自己的方案,以上这套架构方案只是基于当下古茗前端团队现状做的选择后的结果,可能并不适合每个团队,希望本文的这些思考和技术沉淀能对您有所帮助和启发。


    最后


    关注公众号「Goodme前端团队」,获取更多干货实践,欢迎交流分享~


    作者:古茗前端团队
    链接:https://juejin.cn/post/7269352663258284069
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    树形列表翻页,后端: 搞不了搞不了~~

    web
    背景 记得几年前做了一个报告,报告里面加载的是用户的历年作品还有会员信息,然后按照年月倒序展示出来,其中历年作品都要将作品的封面展示出来。一开始这个报告到没啥问题,而且一个时间轴下来感觉挺好,有用户的作品、会员记录、关注以及粉丝记录很全面。直到最近忽然有一批用...
    继续阅读 »

    背景


    记得几年前做了一个报告,报告里面加载的是用户的历年作品还有会员信息,然后按照年月倒序展示出来,其中历年作品都要将作品的封面展示出来。一开始这个报告到没啥问题,而且一个时间轴下来感觉挺好,有用户的作品、会员记录、关注以及粉丝记录很全面。直到最近忽然有一批用户说一进到这个报告页面就卡住不动了,上去一查发现不得了,都是铁杆用户,每年作品都几百个,导致几年下来,这个报告返回了几千个作品,包含上千的图片。


    问题分析


    上千的图片,肯定会卡,首先想到的是做图片懒加载。这个很简单,使用一个vue的全局指令就可以了。但是上线发现,没啥用,dom节点多的时候,懒加载也卡。


    然后就问服务端能不能支持分页,服务端说数据太散,连表太多,树形结构很难做分页。光查询出来就已经很费劲了。


    没办法于是想了一下如何前端来处理掉。


    思路




    1. 由于是app中的嵌入页面,首先考虑通过滚动进行分页加载。




    2. 一次性拿了全部的数据,肯定不能直接全部渲染,我们可以只渲染一部分,比如第一个节点,或者前几个节点。




    3. 随着滚动一个节点一个节点或者一批一批的渲染到dom中。




    实现


    本文仅展示一种基于vue的实现


    1. 容器

    设计一个可以进行滚动翻页的容器 然后绑定滚动方法OnPageScrolling



    <style lang="less" scoped>

    .study-backup {

    overflow-x: hidden;

    overflow-y: auto;

    -webkit-overflow-scrolling: touch;

    width: 100%;

    height: 100%;

    position: relative;

    min-height: 100vh;

    background: #f5f8fb;

    box-sizing: border-box;

    }

    </style>

    <template>

    <section class="report" @scroll="OnPageScrolling($event)">

    </section>

    </template>



    2.初始化数据

    这里定义一下树形列表的数据结构,实现初始化渲染,可以渲染一个树节点,或者一个树节点的部分子节点



    GetTreeData() {

    treeapi

    .GetTreeData({ ... })

    .then((result) => {

    // 处理结果

    const data = Handle(result)

    // 这里备份一份数据 不参与展示

    this.backTreeList = data.map((item) => {

    return {

    id: item.id,

    children: item.children

    }

    })

    // 这里可以初始化为第一个树节点

    const nextTree = this.backTreeList[0]

    const nextTansformTree = nextTree.children.splice(0)

    this.treeList = [{

    id: nextTree.id,

    children: nextTansformTree

    }]

    // 这里可以初始化为第一树节点 但是只渲染第一个子节点

    const nextTree = this.backTreeList[0]

    const nextTansformTree = nextTree.children.splice(0, 1)

    this.treeList = [{

    id: nextTree.id,

    children: nextTansformTree

    }]

    })

    },


    3.滚动加载

    这里通过不断的把 backTreeList 的子节点转存入 treeList来实现分页加载。



    OnPageScrolling(event) {

    const container = event.target

    const scrollTop = container.scrollTop

    const scrollHeight = container.scrollHeight

    const clientHeight = container.clientHeight

    // console.log(scrollTop, clientHeight, scrollHeight)

    // 判断是否接近底部

    if (scrollTop + clientHeight >= scrollHeight - 10) {

    // 执行滚动到底部的操作

    const currentReport = this.backTreeList[this.treeList.length - 1]

    // 检测匹配的当前树节点 treeList的长度作为游标定位

    if (currentReport) {

    // 判断当前节点的子节点是否还存在 如果存在则转移到渲染树中

    if (currentReport.children.length > 0) {

    const transformMonth = currentReport.children.splice(0, 1)

    this.treeList[this.treeList.length - 1].children.push(

    transformMonth[0]

    )

    // 如果不存在 则寻找下一树节点进行复制 同时复制下一节点的第一个子节点 当然如果寻找不到下一树节点则终止翻页

    } else if (this.treeList.length < this.backTreeList.length) {

    const nextTree = this.backTreeList[this.treeList.length]

    const nextTansformTree = nextTree.children.splice(0, 1)

    this.treeList.push({

    id: nextTree.id,

    children: nextTansformTree

    })

    }

    }

    }

    }


    4. 逻辑细节

    从上面代码可以看到,翻页的操作是树copy的操作,将备份树的子节点转移到渲染树中




    1. copy备份树的第一个节点到渲染树,同时将备份树的第一个节点的子节点的第一个节点转移到渲染树的第一个节点的子节点中




    2. 所谓转移操作,就是数组splice操作,从一颗树中删除,然后把删除的内容插入到另一颗树中




    3. 由于渲染树是从长度1开始的,所以我们可以根据渲染树的长度作为游标和备份树进行匹配,设渲染树的长度为当前游标




    4. 根据当前游标查询备份树,如果备份树的当前游标节点的子节点不为空,则进行转移




    5. 如果备份树的当前游标节点的子节点为空,则查找备份树的当前游标节点的下一节点,设为下一树节点




    6. 如果找到了备份树的当前游标节点的下一节点,扩展渲染树,将下一树节点复制到渲染树,同时将下一树节点的子节点的第一节点复制到渲染树




    7. 循环4-6,将备份树完全转移到渲染树,完成所有翻页




    扩展思路


    这个方法可以进行封装,将每次复制的节点数目和每次复制的子节点数目作为传参,一次可复制多个节点,这里就不做展开


    作者:CodePlayer
    来源:juejin.cn/post/7270503053358612520
    收起阅读 »

    创建一个可以循环滚动的文本,可能没这么简单。

    web
    如何创建一个可以向左循环滚动的文本? 创建如上图效果的滚动文本,你能想到几种方式? -------- 暂停阅读,不如你自己先试一下 -------- 方式一: 根据页面宽度,生成多个元素。每个元素通过js控制,每帧向左偏移一点像素。 如果偏移的元素不可见后...
    继续阅读 »

    如何创建一个可以向左循环滚动的文本?


    loop.gif


    创建如上图效果的滚动文本,你能想到几种方式?


    -------- 暂停阅读,不如你自己先试一下 --------


    方式一:


    根据页面宽度,生成多个元素。每个元素通过js控制,每帧向左偏移一点像素。

    如果偏移的元素不可见后,将此元素再移动到屏幕最右边。


    此方式容易理解,实现起来也不困难,但是有性能上的风险,因为每一帧都在修改元素的位置。


    方式二:


    根据页面宽度,生成多个元素。每个元素通过js控制,通过setInterval每一秒向左偏移一些像素。

    然后结合css的transition: all 1s linear;使得偏移更加顺滑。

    如果偏移的元素不可见后,将此元素再移动到屏幕最右边。


    使用此方法可以避免高频率计算元素位置,但是此方式控制起来更复杂,主要是因为,将元素移动到最右边的时候,也会触发transition ,需要额外逻辑控制在元素移到最右边的时候不触发transition

    并且在实际开发中发现。当窗口不可见时候动画实际会暂停,还需要控制当窗口隐藏时候,暂停setInterval


    方式三:


    换一种思路。按顺序排列元素,多个子元素首位相接。将每个子元素通过animation: xxx 10s linear infinite;

    从左到右移动。在一定范围内移动子元素,通过视觉错觉,像是整个大元素(盒子)都在移动。

    此方式简单,并且无需JS,性能较好。


    下面是完整代码(可以控制浏览器宽度,查看不同尺寸屏幕的效果)


    <!doctype html>  
    <html>
    <head>
    <title>LOOP</title>
    <style>
    @keyframes loop {
    0% {
    transform: translateX(0);
    }
    100% {
    transform: translateX(-100%);
    }
    }

    .box {
    white-space: nowrap;
    }

    .scrollContent {
    width: 600px;
    display: inline-block;
    text-align: center;
    animation: loop 3s linear infinite;
    }
    </style>
    </head>
    <body>
    <div class="box">
    <div class="scrollContent">
    这是一段可以滚动的文本
    </div>
    <div class="scrollContent">
    这是一段可以滚动的文本
    </div>
    <div class="scrollContent">
    这是一段可以滚动的文本
    </div>
    <div class="scrollContent">
    这是一段可以滚动的文本
    </div>
    <div class="scrollContent">
    这是一段可以滚动的文本
    </div>
    <div class="scrollContent">
    这是一段可以滚动的文本
    </div>
    </div>
    </body>

    </html>


    方式四:


    方式三会创建多份一样的文本内容,你可能会说,屏幕上同时出现这么多文本元素,当然要创建这么多一样的内容。

    其实还有一种性能更佳的方式:text-shadow: 600px 0 currentColor,通过此方式创建多份文本副本,达到类似效果。

    此方法性能最佳。但是对非文本无能为力。


    <!doctype html>  
    <html>
    <head>
    <title>LOOP</title>
    <style>
    @keyframes loop {
    0% {
    transform: translateX(0);
    }
    100% {
    transform: translateX(-100%);
    }
    }

    .box {
    white-space: nowrap;
    }

    .scrollContent {
    color: rebeccapurple;
    width: 600px;
    display: inline-block;
    text-align: center;
    animation: loop 3s linear infinite;
    text-shadow: 600px 0 currentColor, 1200px 0 currentColor, 1800px 0 currentColor, 2400px 0 currentColor;
    }
    </style>
    </head>
    <body>
    <div class="box">
    <div class="scrollContent">
    这是一段可以滚动的文本
    </div>
    </div>
    </body>

    </html>

    总结


    方式1:应该是最直接想到的方式。但是出于对性能的担忧。

    方式2:由于方式1性能优化得到,但是方式2过于复杂。
    方式3: 看上去非常易于实现,实际很难想到。
    方式4:如果对text-shadow和css颜色掌握不熟,根本难以实现。


    希望对你有所启发


    作者:wuwei123
    来源:juejin.cn/post/7273026570930257932
    收起阅读 »

    位运算,能不能一次记住!

    web
    写给新手朋友的位运算,你是不是也是总是不记得,记了又忘记,记住了又不知道怎么运用到编程中?那记一次理论+题目的方式理解记忆,搞清楚它吧! 我们接触最多的是十进制数,加减乘除这种四则远算其实就是二进制数据中的一种运算法则,当谈到位运算,它不是用来操作十进制数的,...
    继续阅读 »

    写给新手朋友的位运算,你是不是也是总是不记得,记了又忘记,记住了又不知道怎么运用到编程中?那记一次理论+题目的方式理解记忆,搞清楚它吧!


    我们接触最多的是十进制数,加减乘除这种四则远算其实就是二进制数据中的一种运算法则,当谈到位运算,它不是用来操作十进制数的,我们实际上是在操作二进制数的不同位。位运算在前端开发中可能不常用,但了解它们对你理解计算机底层运作和一些特定情况下的优化是有帮助的。


    接下来我们从几种常见的位运算开始,以及它们的使用场景,好好理解一番。


    1. 二进制转换


    既然是写给新手朋友也能看得明白的,那就顺带提一下二进制数吧(熟悉二进制的可以跳过这段)



    当计算机处理数据时,它实际上是在执行一系列的二进制操作,因为计算机内部使用的是电子开关,这些开关只能表示两个状态:开(表示1)和关(表示0)。因此,计算机中的所有数据最终都被转换为二进制表示。


    二进制(binary)是一种使用两个不同符号(通常是 0 和 1)来表示数字、字符、图像等信息的数字系统。这种二元系统是现代计算机科学的基础。





    • 十进制到二进制的转换:




    将十进制数转换为二进制数的过程涉及到不断地除以2,然后记录余数。最后,将这些余数按相反的顺序排列,就得到了对应的二进制数。


    例如,将十进制数 13 转换为二进制数:



    1. 13 除以 2 得商 6,余数 1

    2. 6 除以 2 得商 3,余数 0

    3. 3 除以 2 得商 1,余数 1

    4. 1 除以 2 得商 0,余数 1


    将这些余数按相反的顺序排列,得到二进制数 1101。


    或者你也可以这么想


    1. (1 || 0) * 2^n + (1 || 0) * 2^(n-1) + ... + (1 || 0) * 2^0 = 13

    2. 只需要满足以上公式,加出来你想要的值

    3. 2 的 4次方大于13,2的3次方小于13,那么就从2的3次方开始依次递减到0次方

    4. 1 * 2^3 + 1 * 2^2 + 1 * 2^1 + 1 * 2^0 显然 8 + 4 + 2 + 1 = 15已经超出了13,所以你得在这个式子中减少2

    5. 1 * 2^3 + 1 * 2^2 + 0 * 2^1 + 1 * 2^0 取该等式中的1,0;所以 13 的二进制是 1101


    以上两种方式都能得出一个数的二进制,看你喜欢




    • 二进制到十进制的转换:




    将二进制数转换为十进制数的过程涉及到将每个位上的数字与2的幂相乘,然后将这些结果相加。


    例如,将二进制数 1101 转换为十进制数:



    1. 第0位(最右边)上的数字是 1,表示 2^0 = 1

    2. 第1位上的数字是 0,表示 2^1 = 0

    3. 第2位上的数字是 1,表示 2^2 = 4

    4. 第3位上的数字是 1,表示 2^3 = 8


    将这些结果相加:1 + 0 + 4 + 8 = 13,得到十进制数 13。


    在编程中,通常会使用不同的函数或方法来实现十进制到二进制以及二进制到十进制的转换,这些转换可以帮助我们在计算机中处理和表示不同的数据。


    2. 按位与(&)


    按位与运算会将两个数字的二进制表示的每一位进行 操作,如果两个相应位都是 1,则结果为 1,否则为 0。


    使用场景: 常用于权限控制和掩码操作。


    image.png


    一道题让你更好的理解它的用法


    题目:判断一个整数是否是2的幂次方。


    问题描述:给定一个整数 n,判断它是否是2的幂次方,即是否满足 n = 2^k,其中 k 是非负整数。


    使用位运算中的按位与操作可以很巧妙地解决这个问题。


    思路:如果一个数 n 是2的幂次方,那么它的二进制表示一定只有一位是1,其他位都是0(例如:8的二进制是 1000)。而 n - 1 的二进制表示则是除了最高位的1之外,其他位都是1(例如:7的二进制是 0111)。如果我们对 nn - 1 进行按位与操作,结果应该是0。


    那我们可以这么写:


    image.png


    在这个示例中,我们巧妙的使用了 (n & (n - 1)) 来检查是否满足条件,如果结果为0,说明 n 是2的幂次方。


    希望这个示例能够帮助你更好地理解按位与运算的应用方式!


    2. 按位或(|)


    按位或运算会将两个数字的二进制表示的每一位进行或操作,如果两个相应位至少有一个是 1,则结果为 1,否则为 0。


    使用场景: 常用于设置选项和权限。


    image.png


    一道题让你更好的理解它的用法


    题目:如何将一个整数的特定位设置为1,而不影响其余位。


    问题描述:给定一个整数 num,以及一个表示要设置为1的位的位置 bitPosition(从右向左,最低位的位置为0),编写一个函数将 num 的第 bitPosition 位设置为1。


    我们可以使用按位或运算来实现这个效果


    image.png


    在这个示例中,我们首先创建了一个掩码 mask(这里用到了另一个位运算,左移,下面会讲到),它只有第 bitPosition 位是1,其他位都是0。然后,我们使用按位或运算 num | masknum 的第 bitPosition 位设置为1,得到了结果。


    这个问题演示了如何使用按位或运算来修改一个整数的特定位,而不影响其他位。希望这个示例能帮助你更好地理解按位或运算的应用方式!


    3. 按位异或(^)


    按位异或运算会将两个数字的二进制表示的每一位进行异或操作,如果两个相应位不相同则结果为 1,相同则为 0。


    使用场景: 常用于数据加密和校验。


    image.png


    一道题让你更好的理解它的用法


    题目:如何交换两个整数的值,而不使用额外的变量


    问题描述:给定两个整数 ab,编写一个函数来交换它们的值,而不使用额外的变量。


    我们可以使用按位异或运算来实现这个效果:


    image.png


    上述代码中,我们首先将 a 更新为 a ^ b,这使得 a 包含了 ab 的异或值。然后,我们使用同样的方法将 b 更新为 a 的原始值,最后,我们再次使用异或运算将 a 更新为 b 的原始值,完成了交换操作。



    此处应该沉思,思考清楚这个问题:(a ^ b) ^ b 得到的是 a 的原始值



    不使用额外的变量来做两个变量值的交换,这还是个面试题哦!


    4. 按位非(~)


    按位非运算会将一个数字的二进制表示的每一位取反,即 0 变成 1,1 变成 0。它将操作数转化为 32 位的有符号整型。


    image.png


    一道题让你更好的理解它的用法


    题目:反转二进制数的位,然后返回其对应的十进制数


    问题描述:给定一个二进制字符串,编写一个函数来反转该字符串的位,并返回其对应的十进制数。


    image.png


    这里你可能会有疑问,为什么13的二进制取反会的到-14,这里就不得不介绍一下 补码 的概念了


    5. 补码小插曲


    假设我们要求 -6 的二进制,那就相当于是求 -6 的补码


    因为负数的二进制表示通常使用二进制补码来表示。要计算-6的二进制补码表示,可以按照以下步骤操作:



    1. 首先,找到6的二进制表示。6的二进制表示是 00000110

    2. 然后,对6的二进制表示进行按位取反操作,即将0变成1,将1变成0。这将得到 11111001

    3. 最后,将取反后的结果加1。11111001 + 1 = 11111010


    所以,-6的二进制补码表示是 11111010。在补码中,最高位表示符号位,0表示正数,1表示负数,其余位表示数值的绝对值。因此,11111010 表示的是-6。


    注意:

    -6的二进制补码表示的位数不一定是8位。位数取决于数据类型和计算机系统的规定。在许多计算机系统中,整数的表示采用固定的位数,通常是32位或64位,但也可以是其他位数,例如16位。


    在常见的32位表示中,-6的二进制补码表示可能是 11111111111111111111111111111010。这是32位二进制,其中最高位是符号位(1表示负数),其余31位表示数值的绝对值。


    在64位表示中,-6的二进制补码表示可能是 1111111111111111111111111111111111111111111111111111111111110。这是64位二进制,同样,最高位是符号位,其余63位表示数值的绝对值。


    因此,-6的二进制补码表示的位数取决于计算机系统和数据类型的规定。不同的系统和数据类型可能采用不同的位数。


    6. 左移(<<)和右移(>>)


    左移运算将一个数字的二进制表示向左移动指定的位数,右移运算将二进制表示向右移动指定的位数。


    image.png



    注意:因为我们的计算可以是32位或者是64位的,所以理论上 5 的二进制应该是 00... 00000101, 整体长度为32或者64。 左移我们只是把有效值 101 向左拖动,右边补0,右移左边补 0, 但是要保证整体32或64位长度不能变,所以,右移会砍掉超出去的值



    一道题让你更好的理解它的用法


    题目: 如何实现整数的乘法和除法,使用左移和右移操作来提高效率。


    问题描述:编写一个函数,实现整数的乘法和除法运算,但是只能使用左移和右移操作,不能使用乘法运算符 * 和除法运算符 /


    这也是一道面试题,实现起来很简单


    image.png



    想清楚,一个数的二进制,每次左移一位的结果会怎么样?


    比如 6 的二进制是 00000110, 左移一次后变成 00001100,


    也就是说 从 2^2 + 2^1 变成了 2^3+ 2^2 。 4 + 2 变成了 8 + 4。


    所以每左移一位,都相当于是原数值本身放大了一倍



    这样你是否更清楚了用左移来实现乘法的效果了呢?


    最后


    以上列举的是常见的位运算方法,还有一些不常见的,比如:



    1. 位清零(Bit Clearing):将特定位设置为0,通常使用按位与运算和适当的掩码来实现。

    2. 位设置(Bit Setting):将特定位设置为1,通常使用按位或运算和适当的掩码来实现。

    3. 位翻转(Bit Flipping):将特定位取反,通常使用按位异或运算和适当的掩码来实现。

    4. 检查特定位:通过使用按位与运算和适当的掩码来检查特定位是否为1或0。

    5. 位计数:计算一个整数二进制表示中1的个数,这通常使用一种称为Brian Kernighan算法的技巧来实现。

    6. 位交换:交换两个整数的特定位,通常使用按位异或运算来实现。


    等等...有兴趣的可以自行摸索了


    作者:一个大蜗牛
    来源:juejin.cn/post/7274188187675902004
    收起阅读 »

    如何告诉后端出身的领导:这个前端需求很难实现

    本文源于一条评论。 有个读者评论说:“发现很多前端的大领导多是后端。他们虽然懂一点前端,但总觉得前端简单。有时候,很难向其证明前端不好实现。” 这位朋友让我写一写,那我就写一写。 反正,不管啥事儿,我高低都能整两句,不说白不说,说了也白说。本文暂且算是一条评...
    继续阅读 »

    本文源于一条评论。




    有个读者评论说:“发现很多前端的大领导多是后端。他们虽然懂一点前端,但总觉得前端简单。有时候,很难向其证明前端不好实现。


    这位朋友让我写一写,那我就写一写。


    反正,不管啥事儿,我高低都能整两句,不说白不说,说了也白说。本文暂且算是一条评论回复吧。愿意看扯淡的同学可以留一下。


    现象分析


    首先,前半句让我很有同感。因为,我就是前端出身,并且在研发管理岗上又待过。这个身份,让我既了解前端的工作流程,又经常和后端平级一起交流经验。这有点卧底的意思。


    有一次,我们几个朋友聚餐闲聊。他们也都是各个公司的研发负责人。大家就各自吐槽自己的项目。


    有一个人就说:“我们公司,有一个干前端的带项目了,让他搞得一团糟”


    另一个人听到后,就叹了一口气:“唉!这不是外行领导内行吗?!


    我当时听了感觉有点别扭。我扫视了一圈儿,好像就我一个前端。因此,我也点了点头(默念:我是卧底)。


    是啊,一般的项目、研发管理岗,多是后端开发。因为后端是业务的设计者,他掌握着基本的数据结构以及业务流转。而前端在他们看来,就是调调API,然后把结果展示到页面上。


    另外,一般项目遇到大事小情,也都是找后端处理。比如手动修改数据、批量生成数据等等。从这里可以看出,项目的“根基”在后端手里。


    互联网编程界流传着一条鄙视链。它是这样说的,搞汇编的鄙视写C语言的,写C的鄙视做Java的,写Java的鄙视敲Python的,搞Python的鄙视写js的,搞js的鄙视作图的。然后,作图的设计师周末带着妹子看电影,前面的那些大哥继续加班写代码。


    当然,我们提倡一个友好的环境,不提倡三六九等。这里只是调侃一下,采用夸张的手法来阐述一种现象。


    这里所谓的“鄙视”,其本质是源于谁更接近原理。


    比如写汇编的人,他认为自己的代码能直接调用“CPU”、“内存”,可以直接操纵硬件。有些前端会认为美工设计的东西,得依靠自己来实现,并且他们不懂技术就乱设计,还有脸要求100%还原。


    所以啊,有些只做过后端的人,可能会认为前端开发没啥东西。


    好了,上面是现象分析。关于谁更有优越感,这不能细究啊,因为这是没有结论的。如果搞一个辩论赛,就比方说”Python一句话,汇编写三年“之类论据,各有千秋。各种语言既然能出现,必定有它的妙用。


    我就是胡扯啊。不管您是哪个工种,请不要对号入座。如果您觉得被冒犯了,可以评论“啥都不懂”,然后离开。千万不要砸自己的电脑。


    下面再聊聊第二部分,面对这种情况,有什么好的应对措施?


    应对方法


    我感觉,后端出身的负责人,就算是出于人际关系,也是会尊重前端开发的


    “小张啊,对于前端我不是很懂。所以请帮我评估下前端的工期。最好列得细致一些,这样有利于我了解人员的工作量。弄完了,发给我,我们再讨论一下!”


    一般都是这么做。


    这种情况,我们就老老实实给他上报就行了,顶多加上10%~30%处理未知风险的时间。


    但是,他如果这样说:“什么?这个功能你要做5天?给你1天都多!


    这时,他是你的领导,对你又有考核,你怎么办?


    你心里一酸:“我离职吧!小爷我受不了这委屈!”


    这……当然也可以。


    如果你有更好的去处,可以这样。就算是没有这回事,有好去处也赶紧去。


    但是,如果这个公司整体还行呢?只是这个直接领导有问题。那么你可以考虑,这个领导是流水的,你俩处不了多长时间。哪个公司每年不搞个组织架构调整啊?你找个看上的领导,吃一顿饭,求个投靠呗。


    或者,这个领导只是因为不懂才这样。谁又能样样精通呢?给他说明白,他可能就想通了。人是需要说服的。


    如果你奔着和平友好的心态去,那么可以试试以下几点:


    第一,列出复杂原因


    既然你认为难以实现,那肯定是难以实现。具体哪里不好实现,你得说说


    记得刚工作时,我去找后端PK,我问他:“你这个接口怎么就难改了?你不改这个字段,我们得多调好几个接口,而且还对应不起来!”


    后端回复我:“首先,ES……;其次,mango……;最后,redis……”


    我就像是被反复地往水缸中按,听得”呜噜呜噜“的,一片茫然。


    虽然,我当时不懂后端,但我觉得他从气势上,就显得有道理


    到后来,还是同样的场景。我变成了项目经理,有一个年轻的后端也是这么回复我的。


    我说:“首先,你不用……;其次,数据就没在……;最后,只需要操作……”。他听完,挠了挠头说,好像确实可以这么做。


    所以,你要列出难以实现的地方。比如,没有成熟的UI组件,需要自己重新写一个。又或者,某种特效,业内都没有,很难做到。再或者,某个接口定得太复杂,循环调组数据,会产生什么样的问题。


    如果他说“我看到某某软件就是这样”。


    你可以说,自己只是一个初级前端,完成那些,得好多高级前端一起研究才行。


    如果你懂后端,可以做一下类比,比如哪些功能等同于多库多表查询、多线程并发问题等等,这可以辅助他理解。不过,如果能到这一步,他的位子好像得换你来坐。


    第二,给出替代方案


    这个方案,适用于”我虽然做不了,但我能解决你的问题“。


    就比如那个经典的打洞问题。需求是用电钻在墙上钻一个孔,我搞不定电钻,但是我用锤子和钉子,一样能搞出一个孔来。


    如果,你遇到难以实现的需求。比如,让你画一个很有特色的扇形玫瑰图。你觉得不好实现,不用去抱怨UI,这只会激化问题。咱可以给他提供几个业界成熟的组件。然后,告诉他,哪里能改,都能改什么(颜色、间距、字体……)。


    我们有些年轻的程序员很直,遇到不顺心的事情就怼。像我们大龄程序员就不这样。因为有房贷,上有老下有小。年龄大的程序员就想,到底是哪里不合适,我得怎样通过我的经验来促成这件事——这并不光彩,年轻人就得有年轻人的样子。


    第二招是给出替代方案。那样难以实现,你看这样行不行


    第三,车轮战,搞铺垫


    你可能遇到了一个硬茬。他就是不变通,他不想听,也不想懂,坚持“怎么实现我不管,明天之前要做完”。


    那他可能有自己的压力,比如老板就是这么要求他的。他转手就来要求你。


    你俩的前提可能是不一样。记住我一句话,没有共同前提,是不能做对比的。你是员工,他是领导,他不能要求你和他有一样的压力,这就像你不能要求,你和他有一样的待遇。多数PUA,就是拿不同的前提,去要求同样的结果。


    那你就得开始为以后扯皮找铺垫了。


    如果你们组有多个前端,可以发动大家去进谏。


    ”张工啊,这个需求,确实不简单,不光是小刘,我看了,也觉得不好实现“


    你一个人说了他不信,人多了可能就信了。


    如果还是不信。那没关系,已经将风险提前抛出了


    “这个需求,确实不好实现。非要这么实现,可能有风险。工期、测试、上线后的稳定性、用户体验等,都可能会出现一些问题”


    你要表现出为了项目担忧,而不是不想做的样子。如果,以后真发生了问题,你可以拿出“之前早就说过,多次反馈,无人理睬”这类的说辞。


    ”你居然不想着如何解决问题,反倒先想如何逃避责任?!“


    因此说,这是下下策。不建议程序员玩带有心机的东西。


    以上都是浅层次的解读。因为具体还需要结合每个公司,每个领导,每种局面。


    总之,想要解决问题,就得想办法


    作者:TF男孩
    链接:https://juejin.cn/post/7235966417564532794
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    京东一面:post为什么会发送两次请求?🤪🤪🤪

    web
    在前段时间的一次面试中,被问到了一个如标题这样的问题。要想好好地去回答这个问题,这里牵扯到的知识点也是比较多的。 那么接下来这篇文章我们就一点一点开始引出这个问题。 同源策略 在浏览器中,内容是很开放的,任何资源都可以接入其中,如 JavaScript 文件、...
    继续阅读 »

    在前段时间的一次面试中,被问到了一个如标题这样的问题。要想好好地去回答这个问题,这里牵扯到的知识点也是比较多的。


    那么接下来这篇文章我们就一点一点开始引出这个问题。


    同源策略


    在浏览器中,内容是很开放的,任何资源都可以接入其中,如 JavaScript 文件、图片、音频、视频等资源,甚至可以下载其他站点的可执行文件。


    但也不是说浏览器就是完全自由的,如果不加以控制,就会出现一些不可控的局面,例如会出现一些安全问题,如:



    • 跨站脚本攻击(XSS)

    • SQL 注入攻击

    • OS 命令注入攻击

    • HTTP 首部注入攻击

    • 跨站点请求伪造(CSRF)

    • 等等......


    如果这些都没有限制的话,对于我们用户而言,是相对危险的,因此需要一些安全策略来保障我们的隐私和数据安全。


    这就引出了最基础、最核心的安全策略:同源策略。


    什么是同源策略


    同源策略是一个重要的安全策略,它用于限制一个源的文档或者它加载的脚本如何能与另一个源的资源进行交互。


    如果两个 URL 的协议、主机和端口都相同,我们就称这两个 URL 同源。



    • 协议:协议是定义了数据如何在计算机内和之间进行交换的规则的系统,例如 HTTP、HTTPS。

    • 主机:是已连接到一个计算机网络的一台电子计算机或其他设备。网络主机可以向网络上的用户或其他节点提供信息资源、服务和应用。使用 TCP/IP 协议族参与网络的计算机也可称为 IP 主机。

    • 端口:主机是计算机到计算机之间的通信,那么端口就是进程到进程之间的通信。


    如下表给出了与 URL http://store.company.com:80/dir/page.html 的源进行对比的示例:


    URL结果原因
    http://store.company.com:80/dir/page.html同源只有路径不同
    http://store.company.com:80/dir/inner/another.html同源只有路径不同
    https://store.company.com:80/secure.html不同源协议不同,HTTP 和 HTTPS
    http://store.company.com:81/dir/etc.html不同源端口不同
    http://news.company.com:80/dir/other.html不同源主机不同

    同源策略主要表现在以下三个方面:DOM、Web 数据和网络。



    • DOM 访问限制:同源策略限制了网页脚本(如 JavaScript)访问其他源的 DOM。这意味着通过脚本无法直接访问跨源页面的 DOM 元素、属性或方法。这是为了防止恶意网站从其他网站窃取敏感信息。

    • Web 数据限制:同源策略也限制了从其他源加载的 Web 数据(例如 XMLHttpRequest 或 Fetch API)。在同源策略下,XMLHttpRequest 或 Fetch 请求只能发送到与当前网页具有相同源的目标。这有助于防止跨站点请求伪造(CSRF)等攻击。

    • 网络通信限制:同源策略还限制了跨源的网络通信。浏览器会阻止从一个源发出的请求获取来自其他源的响应。这样做是为了确保只有受信任的源能够与服务器进行通信,以避免恶意行为。


    出于安全原因,浏览器限制从脚本内发起的跨源 HTTP 请求,XMLHttpRequest 和 Fetch API,只能从加载应用程序的同一个域请求 HTTP 资源,除非使用 CORS 头文件


    CORS


    对于浏览器限制这个词,要着重解释一下:不一定是浏览器限制了发起跨站请求,也可能是跨站请求可以正常发起,但是返回结果被浏览器拦截了。


    浏览器将不同域的内容隔离在不同的进程中,网络进程负责下载资源并将其送到渲染进程中,但由于跨域限制,某些资源可能被阻止加载到渲染进程。如果浏览器发现一个跨域响应包含了敏感数据,它可能会阻止脚本访问这些数据,即使网络进程已经获得了这些数据。CORB 的目标是在渲染之前尽早阻止恶意代码获取跨域数据。



    CORB 是一种安全机制,用于防止跨域请求恶意访问跨域响应的数据。渲染进程会在 CORB 机制的约束下,选择性地将哪些资源送入渲染进程供页面使用。



    例如,一个网页可能通过 AJAX 请求从另一个域的服务器获取数据。虽然某些情况下这样的请求可能会成功,但如果浏览器检测到请求返回的数据可能包含恶意代码或与同源策略冲突,浏览器可能会阻止网页访问返回的数据,以确保用户的安全。


    跨源资源共享(Cross-Origin Resource Sharing,CORS)是一种机制,允许在受控的条件下,不同源的网页能够请求和共享资源。由于浏览器的同源策略限制了跨域请求,CORS 提供了一种方式来解决在 Web 应用中进行跨域数据交换的问题。


    CORS 的基本思想是,服务器在响应中提供一个标头(HTTP 头),指示哪些源被允许访问资源。浏览器在发起跨域请求时会先发送一个预检请求(OPTIONS 请求)到服务器,服务器通过设置适当的 CORS 标头来指定是否允许跨域请求,并指定允许的请求源、方法、标头等信息。


    简单请求


    不会触发 CORS 预检请求。这样的请求为 简单请求,。若请求满足所有下述条件,则该请求可视为 简单请求



    1. HTTP 方法限制:只能使用 GET、HEAD、POST 这三种 HTTP 方法之一。如果请求使用了其他 HTTP 方法,就不再被视为简单请求。

    2. 自定义标头限制:请求的 HTTP 标头只能是以下几种常见的标头:AcceptAccept-LanguageContent-LanguageLast-Event-IDContent-Type(仅限于 application/x-www-form-urlencodedmultipart/form-datatext/plain)。HTML 头部 header field 字段:DPR、Download、Save-Data、Viewport-Width、WIdth。如果请求使用了其他标头,同样不再被视为简单请求。

    3. 请求中没有使用 ReadableStream 对象。

    4. 不使用自定义请求标头:请求不能包含用户自定义的标头。

    5. 请求中的任意 XMLHttpRequestUpload 对象均没有注册任何事件监听器;XMLHttpRequestUpload 对象可以使用 XMLHttpRequest.upload 属性访问


    预检请求


    非简单请求的 CORS 请求,会在正式通信之前,增加一次 HTTP 查询请求,称为 预检请求


    需预检的请求要求必须首先使用 OPTIONS 方法发起一个预检请求到服务器,以获知服务器是否允许该实际请求。预检请求 的使用,可以避免跨域请求对服务器的用户数据产生未预期的影响。


    例如我们在掘金上删除一条沸点:


    20230822094049


    它首先会发起一个预检请求,预检请求的头信息包括两个特殊字段:



    • Access-Control-Request-Method:该字段是必须的,用来列出浏览器的 CORS 请求会用到哪些 HTTP 方法,上例是 POST。

    • Access-Control-Request-Headers:该字段是一个逗号分隔的字符串,指定浏览器 CORS 请求会额外发送的头信息字段,上例是 content-type,x-secsdk-csrf-token

    • access-control-allow-origin:在上述例子中,表示 https://juejin.cn 可以请求数据,也可以设置为* 符号,表示统一任意跨源请求。

    • access-control-max-age:该字段可选,用来指定本次预检请求的有效期,单位为秒。上面结果中,有效期是 1 天(86408 秒),即允许缓存该条回应 1 天(86408 秒),在此期间,不用发出另一条预检请求。


    一旦服务器通过了 预检请求,以后每次浏览器正常的 CORS 请求,就都跟简单请求一样,会有一个 Origin 头信息字段。服务器的回应,也都会有一个 Access-Control-Allow-Origin 头信息字段。


    20230822122441


    上面头信息中,Access-Control-Allow-Origin 字段是每次回应都必定包含的。


    附带身份凭证的请求与通配符


    在响应附带身份凭证的请求时:



    • 为了避免恶意网站滥用 Access-Control-Allow-Origin 头部字段来获取用户敏感信息,服务器在设置时不能将其值设为通配符 *。相反,应该将其设置为特定的域,例如:Access-Control-Allow-Origin: https://juejin.cn。通过将 Access-Control-Allow-Origin 设置为特定的域,服务器只允许来自指定域的请求进行跨域访问。这样可以限制跨域请求的范围,避免不可信的域获取到用户敏感信息。

    • 为了避免潜在的安全风险,服务器不能将 Access-Control-Allow-Headers 的值设为通配符 *。这是因为不受限制的请求头可能被滥用。相反,应该将其设置为一个包含标头名称的列表,例如:Access-Control-Allow-Headers: X-PINGOTHER, Content-Type。通过将 Access-Control-Allow-Headers 设置为明确的标头名称列表,服务器可以限制哪些自定义请求头是允许的。只有在允许的标头列表中的头部字段才能在跨域请求中被接受。

    • 为了避免潜在的安全风险,服务器不能将 Access-Control-Allow-Methods 的值设为通配符 *。这样做将允许来自任意域的请求使用任意的 HTTP 方法,可能导致滥用行为的发生。相反,应该将其设置为一个特定的请求方法名称列表,例如:Access-Control-Allow-Methods: POST, GET。通过将 Access-Control-Allow-Methods 设置为明确的请求方法列表,服务器可以限制哪些方法是允许的。只有在允许的方法列表中的方法才能在跨域请求中被接受和处理。

    • 对于附带身份凭证的请求(通常是 Cookie),


    这是因为请求的标头中携带了 Cookie 信息,如果 Access-Control-Allow-Origin 的值为 *,请求将会失败。而将 Access-Control-Allow-Origin 的值设置为 https://juejin。cn,则请求将成功执行。


    另外,响应标头中也携带了 Set-Cookie 字段,尝试对 Cookie 进行修改。如果操作失败,将会抛出异常。


    参考文章



    总结


    预检请求是在进行跨域资源共享 CORS 时,由浏览器自动发起的一种 OPTIONS 请求。它的存在是为了保障安全,并允许服务器决定是否允许跨域请求。


    跨域请求是指在浏览器中向不同域名、不同端口或不同协议的资源发送请求。出于安全原因,浏览器默认禁止跨域请求,只允许同源策略。而当网页需要进行跨域请求时,浏览器会自动发送一个预检请求,以确定是否服务器允许实际的跨域请求。


    预检请求中包含了一些额外的头部信息,如 Origin 和 Access-Control-Request-Method 等,用于告知服务器实际请求的方法和来源。服务器收到预检请求后,可以根据这些头部信息,进行验证和授权判断。如果服务器认可该跨域请求,将返回一个包含 Access-Control-Allow-Origin 等头部信息的响应,浏览器才会继续发送实际的跨域请求。


    使用预检请求机制可以有效地防范跨域请求带来的安全风险,保护用户数据和隐私。


    整个完整的请求流程有如下图所示:


    20230822122544


    最后分享两个我的两个开源项目,它们分别是:



    这两个项目都会一直维护的,如果

    作者:Moment
    来源:juejin.cn/post/7269952188927017015
    你也喜欢,欢迎 star 🥰🥰🥰

    收起阅读 »

    网易云音乐 Tango 低代码引擎正式开源!

    web
    📝 Tango 简介 Tango 是一个用于快速构建低代码平台的低代码设计器框架,借助 Tango 只需要数行代码就可以完成一个基本的低代码平台前端系统的搭建。Tango 低代码设计器直接读取前端项目的源代码,并以源代码为中心,执行和渲染前端视图,并为用户提供...
    继续阅读 »

    📝 Tango 简介


    Tango 是一个用于快速构建低代码平台的低代码设计器框架,借助 Tango 只需要数行代码就可以完成一个基本的低代码平台前端系统的搭建。Tango 低代码设计器直接读取前端项目的源代码,并以源代码为中心,执行和渲染前端视图,并为用户提供低代码可视化搭建能力,用户的搭建操作会转为对源代码的修改。借助于 Tango 构建的低代码工具或平台,可以实现 源码进,源码出的效果,无缝与企业内部现有的研发体系进行集成。


    Tango 低代码引擎开发效果


    如上图所示,Tango 低代码引擎支持可视化视图与源码双向同步,双向互转,为开发者提供 LowCode+ ProCode 无缝衔接的开发体验。


    ✨ 核心特性



    • 经历网易云音乐内网生产环境的实际检验,可灵活集成应用于低代码平台,本地开发工具等

    • 基于源码 AST 驱动,无私有 DSL 和协议

    • 提供实时出码能力,支持源码进,源码出

    • 开箱即用的前端低代码设计器,提供灵活易用的设计器 React 组件

    • 使用 TypeScript 开发,提供完整的类型定义文件


    🏗️ 基于源码的低代码搭建方案


    Tango 低代码引擎不依赖私有搭建协议和 DSL,而是直接使用源代码驱动,引擎内部将源码转为 AST,用户的所有的搭建操作转为对 AST 的遍历和修改,进而将 AST 重新生成为代码,将代码同步给在线沙箱执行。与传统的 基于 Schema 驱动的低代码方案 相比,不受私有 DSL 和协议的限制,能够完美的实现低代码搭建与源码开发的无缝集成。



    📄 源码进,源码出


    由于引擎内核完全基于源代码驱动实现,Tango 低代码引擎能够实现源代码进,源代码出的可视化搭建能力,不提供任何私有的中间产物。如果公司内部已经有了一套完善的研发体系(代码托管、构建、部署、CDN),那么可以直接使用 Tango 低代码引擎与现有的服务集成构建低代码开发平台。


    code in, code out


    🏆 产品优势


    与基于私有 Schema 的低代码搭建方案相比,Tango 低代码引擎具有如下优势:


    对比项基于 Schema 的低代码搭建方案Tango(基于源码 AST 转换)
    适用场景面向特定的垂直搭建场景,例如表单,营销页面等🔥 面面向以源码为中心的应用搭建场景
    语言能力依赖私有协议扩展,不灵活,且难以与编程语言能力对齐🔥 直接基于 JavaScript 语言,可以使用所有的语言特性,不存在扩展性问题
    开发能力LowCode🔥 LowCode + ProCode
    源码导出以 Schema 为中心,单向出码,不可逆🔥 以源码为中心,双向转码
    自定义依赖需要根据私有协议扩展封装,定制成本高🔥 原有组件可以无缝低成本接入
    集成研发设施定制成本高,需要额外定制🔥 低成本接入,可以直接复用原有的部署发布能力

    📐 技术架构


    Tango 低代码引擎在实现上进行了分层解藕,使得上层的低代码平台与底层的低代码引擎可以独立开发和维护,快速集成部署。此外,Tango 低代码引擎定义了一套开放的物料生态体系,开发者可以自由的贡献扩展组件配置能力的属性设置器,以及扩展低代码物料的二方三方业务组件。


    具体的技术架构如下图所示:


    low-code engine


    ⏰ 开源里程碑


    Tango 低代码引擎是网易云音乐内部低代码平台的核心构件,开源涉及到大量的核心逻辑解藕的工作,这将给我们正常的工作带来大量的额外工作,因此我们计划分阶段推进 Tango 低代码引擎的开源事项。



    1. 今天我们正式发布 Tango 低代码引擎的第一个社区版本,该版本将会包括 Tango 低代码引擎的核心代码库,TangoBoot 应用框架,以及基于 antd v4 适配的低代码组件库。

    2. 我们计划在今年的 9 月 30 日 发布低代码引擎的 1.0 Beta 版本,该版本将会对核心的实现面向社区场景重构,移除掉我们在云音乐内部的一些兼容代码,并将核心的实现进行重构和优化。

    3. 我们计划在今年的 10 月 30 日 发布低代码引擎的 1.0 RC 版本,该版本将会保证核心 API 基本稳定,不再发生 BREAKING CHANGE,同时我们将会提供完善翔实的开发指南、部署文档、和演示应用。

    4. 正式版本我们将在 2023 年 Q4 结束前 发布,届时我们会进一步完善我们的开源社区运营机制。


    milestones


    🤝 社区建设


    我们的开源工作正在积极推进中,可以通过如下的信息了解到我们的最新进展:



    欢迎大家加入到我们的社区中来,一起参与到 Tango 低代码引擎的开源建设中来。有任何问题都可以通过 Github Issues 反馈给我们,我们会及时跟进处理。


    💗 致谢


    感谢网易云音乐公共技术团队,大前端团队,直播技术团队,以及所有参与过 Tango 项目的同学们。


    感谢 CodeSandbox 提供的 Sandpack 项目,为 Tango 提供了强大的基于浏览器的代码构建与执行能力。

    作者:网易云音乐技术团队
    来源:juejin.cn/post/7273051203562749971

    收起阅读 »

    前端比localStorage存储还大的本地存储方案

    产品的原话就是“要又大又全”。既然存储量大,也要覆盖全多种设备多种浏览器。 方案选择既然要存储的数量大,得排除cookielocalStorage,虽然比cookie多,但是同样有上限(5M)左右,备选websql 使用简单,存储量大,兼容性差,备选index...
    继续阅读 »

    产品的原话就是“要又大又全”。既然存储量大,也要覆盖全多种设备多种浏览器。


    方案选择

    • 既然要存储的数量大,得排除cookie
    • localStorage,虽然比cookie多,但是同样有上限(5M)左右,备选
    • websql 使用简单,存储量大,兼容性差,备选
    • indexDB api多且繁琐,存储量大、高版本浏览器兼容性较好,备选

    既然罗列了一些选择,都没有十全十美的,那么有没有一种能够集合这多种方式的插件呢?渐进增强 or 优雅降级 的存在
    冲着这个想法,就去github和谷歌找了一下,还真的有这么一个插件。


    那就是 localforage


    localforage


    localForage 是一个 JavaScript 库,只需要通过简单类似 localStorage API 的异步存储来改进你的 Web 应用程序的离线体验。它能存储多种类型的数据,而不仅仅是字符串。 




    关于兼容性


    localForage 有一个优雅降级策略,若浏览器不支持 IndexedDB 或 WebSQL,则使用 localStorage。在所有主流浏览器中都可用:Chrome,Firefox,IE 和 Safari(包括 Safari Mobile)。下面是 indexDB、web sql、localStorage 的一个浏览器支持情况,可以发现,兼容性方面loaclForage基本上满足99%需求


    使用


    解决了兼容性和存储量的点,我们就来看看localforage的基础用法


    安装

    # 通过 npm 安装:
    npm install localforage
    // 直接引用
    <script src="localforage.js"></script>
    <script>console.log('localforage is: ', localforage);</script>

    获取存储


    getItem(key, successCallback)


    从仓库中获取 key 对应的值并将结果提供给回调函数。如果 key 不存在,getItem() 将返回 null。

    localforage.getItem('somekey').then(function(value) {
    // 当离线仓库中的值被载入时,此处代码运行
    console.log(value);
    }).catch(function(err) {
    // 当出错时,此处代码运行
    console.log(err);
    });

    // 回调版本:
    localforage.getItem('somekey', function(err, value) {
    // 当离线仓库中的值被载入时,此处代码运行
    console.log(value);
    });

    设置存储


    setItem(key, value, successCallback)


    将数据保存到离线仓库。你可以存储如下类型的 JavaScript 对象:

    • Array
    • ArrayBuffer
    • Blob
    • Float32Array
    • Float64Array
    • Int8Array
    • Int16Array
    • Int32Array
    • Number
    • Object
    • Uint8Array
    • Uint8ClampedArray
    • Uint16Array
    • Uint32Array
    • String
    localforage
    .setItem("somekey", "some value")
    .then(function (value) {
    // 当值被存储后,可执行其他操作
    console.log(value);
    })
    .catch(function (err) {
    // 当出错时,此处代码运行
    console.log(err);
    });

    // 不同于 localStorage,你可以存储非字符串类型
    localforage
    .setItem("my array", [1, 2, "three"])
    .then(function (value) {
    // 如下输出 `1`
    console.log(value[0]);
    })
    .catch(function (err) {
    // 当出错时,此处代码运行
    console.log(err);
    });

    // 你甚至可以存储 AJAX 响应返回的二进制数据
    req = new XMLHttpRequest();
    req.open("GET", "/photo.jpg", true);
    req.responseType = "arraybuffer";

    req.addEventListener("readystatechange", function () {
    if (req.readyState === 4) {
    // readyState 完成
    localforage
    .setItem("photo", req.response)
    .then(function (image) {
    // 如下为一个合法的 <img> 标签的 blob URI
    var blob = new Blob([image]);
    var imageURI = window.URL.createObjectURL(blob);
    })
    .catch(function (err) {
    // 当出错时,此处代码运行
    console.log(err);
    });
    }
    });

    删除存储


    removeItem(key, successCallback)


    从离线仓库中删除 key 对应的值。

    localforage.removeItem('somekey').then(function() {
    // 当值被移除后,此处代码运行
    console.log('Key is cleared!');
    }).catch(function(err) {
    // 当出错时,此处代码运行
    console.log(err);
    });

    清空存储


    clear(successCallback)


    从数据库中删除所有的 key,重置数据库。


    localforage.clear() 将会删除离线仓库中的所有值。谨慎使用此方法。

    localforage.clear().then(function() {
    // 当数据库被全部删除后,此处代码运行
    console.log('Database is now empty.');
    }).catch(function(err) {
    // 当出错时,此处代码运行
    console.log(err);
    });

    localforage是否万事大吉?


    用上了localforage一开始我也以为可以完全满足万恶的产品了,然而。。。翻车了.。


    内存不足的前提下,localforage继续缓存会怎么样?


    在这种状态下,尝试使用localforage,不出意外,抛错了 QuotaExceededError 的 DOMErro


    解决
    存储数据的时候加上存储的时间戳和模块标识,加时间戳一起存储

    setItem({
    value: '1',
    label: 'a',
    module: 'a',
    timestamp: '11111111111'
    })

    • 如果是遇到存储使用报错的情况,try/catch捕获之后,通过判断报错提示,去执行相应的操作,遇到内存不足的情况,则根据时间戳和模块标识清理一部分旧数据(内存不足的情况还是比较少的)
    • 在用户手机上产生脏数据的情况,想要清理的这种情况的 处理方式是:
    • 让后端在用户信息接口里面加上缓存有效期时间戳,当该时间戳存在,则前端会进行一次对本地存储扫描
    • 在有效期时间戳之前的数据,结合模块标识,进行清理,清理完毕后调用后端接口上报清理日志
    • 模块标识的意义是清理数据的时候,可以按照模块去清理(选填)

    作者:前端成长指南
    链接:https://juejin.cn/post/7273028474973012007
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    如何消除异步的传染性

    web
    本文的知识点是笔者在抖音看到 渡一前端 发布的视频学习到的。笔者觉得处理问题的思路非常值得学习,因此来掘金分享一下。 前言 各位掘友好!本文跟大家分享一个关于 消除异步传染性 的知识点,你可能不熟悉甚至没听过异步传染性这个词,其实笔者也是最近才看到的,因此想...
    继续阅读 »

    本文的知识点是笔者在抖音看到 渡一前端 发布的视频学习到的。笔者觉得处理问题的思路非常值得学习,因此来掘金分享一下。



    前言


    各位掘友好!本文跟大家分享一个关于 消除异步传染性 的知识点,你可能不熟悉甚至没听过异步传染性这个词,其实笔者也是最近才看到的,因此想着来分享一下!好了,接下来笔者会从两个方面来说这个知识点,一方面是概念,另一方面就是如何消除。


    什么是 异步传染性


    笔者通过一个例子来介绍异步传染性的概念。


    CleanShot 2023-08-30 at <a href=10.10.55@2x.png" loading="lazy" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/448eaad1c5f34f319bc3361fcad882ce~tplv-k3u1fbpfcp-zoom-in-crop-mark:1512:0:0:0.awebp#?w=1234&h=528&s=154964&e=png&b=282a35"/>


    上图中由于m2中的fetch是异步的,导致了使用m2m1变成了async functionmain 又使用了m1,从而main也变成了async function。类似这种现象就叫做异步的传染性。(可能你会觉得,为什么main不直接调m2,我们此处是为了理解这个概念,不要钻牛角尖😁)


    m2就好像病毒🦠,m1明知道到m2有毒,还要来挨着,结果就被传染了,main也是一样。


    那什么是消除传染性呢?就是希望不要 async/await,让mian、m1变成纯函数调用。也就是mian、m1不依赖fetch的状态。期望像下面这样调用:
    CleanShot 2023-08-30 at <a href=10.52.24@2x.png" loading="lazy" src="https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ba99c7db117e46319d778002889c51ee~tplv-k3u1fbpfcp-zoom-in-crop-mark:1512:0:0:0.awebp#?w=924&h=498&s=64284&e=png&b=282a35"/>



    纯函数:



    1. 输入决定输出: 纯函数的输出完全由输入决定,即相同的输入始终产生相同的输出。这意味着函数不依赖于外部状态,也不会对外部状态进行修改。

    2. 没有副作用: 纯函数没有副作用,即在函数的执行过程中不会对除函数作用域外的其他部分产生影响。它不会修改全局变量、改变输入参数或进行文件IO等操作。




    纯函数在函数式编程中具有重要作用,因为它们易于理解、测试和维护。由于不依赖于外部状态,纯函数可以很好地并行执行,也有助于避免常见的错误,例如竞态条件和不确定性行为。



    接下来咱们就分析一下要如何实现消除。


    如何消除


    当我们把async/await去掉之后,就变成了同步调用,那么m2返回的肯定是pending状态的promisemain得到的也是,肯定达不到我们想要的效果。


    那我们能不能等promise变成fulfilled/rejected状态再接着执行main


    可以,第一次调用main,我们直接throw,第一次调用就会终止,然后等promise变成fulfilled/rejected状态,我们将返回结果或错误信息缓存一下,再调用一次main,再次调用时存在缓存,直接返回缓存即可,此时也就变成了同步。流程图如下:


    CleanShot 2023-08-30 at <a href=11.30.26@2x.png" loading="lazy" src="https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/dc591e6d9bf24b4eb0d3e78fecf5dcc1~tplv-k3u1fbpfcp-zoom-in-crop-mark:1512:0:0:0.awebp#?w=1590&h=1048&s=165391&e=png&b=fdfdfd"/>


    具体实现如下:
    CleanShot 2023-08-30 at <a href=11.34.06@2x.png" loading="lazy" src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e9b4193e4c6c4a35850c487f1ad0bcbc~tplv-k3u1fbpfcp-zoom-in-crop-mark:1512:0:0:0.awebp#?w=1494&h=1336&s=353606&e=png&b=282a35"/>


    效果如下:
    CleanShot 2023-08-30 at 11.44.35.gif


    到此本次分享的内容就完了,感谢阅读!


    总结


    本文通过简单的例子,描述了什么是异步的传染性,以及如何利用缓存throw重写fetch实现了消除异步的传染性


    如果本文对你有一点点帮助,点个赞支持一下吧,你的每一个【】都是我创作的最大动力 ^_^<

    作者:Lvzl
    来源:juejin.cn/post/7272751454497996815
    /code>。

    收起阅读 »

    基于 Axios 封装一个完美的双 token 无感刷新

    web
    用户登录之后,会返回一个用户的标识,之后带上这个标识请求别的接口,就能识别出该用户。 标识登录状态的方案有两种: session 和 jwt。 session 是通过 cookie 返回一个 id,关联服务端内存里保存的 session 对象,请求时服务端取出...
    继续阅读 »

    用户登录之后,会返回一个用户的标识,之后带上这个标识请求别的接口,就能识别出该用户。


    标识登录状态的方案有两种: session 和 jwt。


    session 是通过 cookie 返回一个 id,关联服务端内存里保存的 session 对象,请求时服务端取出 cookie 里 id 对应的 session 对象,就可以拿到用户信息。



    jwt 不在服务端存储,会直接把用户信息放到 token 里返回,每次请求带上这个 token,服务端就能从中取出用户信息。



    这个 token 一般是放在一个叫 authorization 的 header 里。


    这两种方案一个服务端存储,通过 cookie 携带标识,一个在客户端存储,通过 header 携带标识。


    session 的方案默认不支持分布式,因为是保存在一台服务器的内存的,另一台服务器没有。



    jwt 的方案天然支持分布式,因为信息保存在 token 里,只要从中取出来就行。



    所以 jwt 的方案用的还是很多的。


    服务端把用户信息放入 token 里,设置一个过期时间,客户端请求的时候通过 authorization 的 header 携带 token,服务端验证通过,就可以从中取到用户信息。


    但是这样有个问题:


    token 是有过期时间的,比如 3 天,那过期后再访问就需要重新登录了。


    这样体验并不好。


    想想你在用某个 app 的时候,用着用着突然跳到登录页了,告诉你需要重新登录了。


    是不是体验很差?


    所以要加上续签机制,也就是延长 token 过期时间。


    主流的方案是通过双 token,一个 access_token、一个 refresh_token。


    登录成功之后,返回这两个 token:



    访问接口时带上 access_token 访问:



    当 access_token 过期时,通过 refresh_token 来刷新,拿到新的 access_token 和 refresh_token



    这里的 access_token 就是我们之前的 token。


    为什么多了个 refresh_token 就能简化呢?


    因为如果你重新登录,是不是需要再填一遍用户名密码?而有了 refresh_token 之后,只要带上这个 token 就能标识用户,不需要传用户名密码就能拿到新 token。


    而 access_token 一般过期时间设置的比较短,比如 30 分钟,refresh_token 设置的过期时间比较长,比如 7 天。


    这样,只要你 7 天内访问一次,就能刷新 token,再续 7 天,一直不需要登录。


    但如果你超过 7 天没访问,那 refresh_token 也过期了,就需要重新登录了。


    想想你常用的 APP,是不是没再重新登录过?


    而不常用的 APP,再次打开是不是就又要重新登录了?


    这种一般都是双 token 做的。


    知道了什么是双 token,以及它解决的问题,我们来实现一下。


    新建个 nest 项目:


     npx nest new token-test


    进入项目,把它跑起来:


    npm run start:dev

    访问 http://localhost:3000 可以看到 hello world,代表服务跑成功了:



    在 AppController 添加一个 login 的 post 接口:



    @Post('login')
    login(@Body() userDto: UserDto) {
    console.log(userDto);
    return 'success';
    }

    这里通过 @Body 取出请求体的内容,设置到 dto 中。


    dto 是 data transfer object,数据传输对象,用来保存参数的。


    我们创建 src/user.dto.ts


    export class UserDto {
    username: string;
    password: string;
    }

    在 postman 里访问下这个接口:



    返回了 success,服务端也打印了收到的参数:



    然后我们实现下登录逻辑:



    这里我们就不连接数据库了,就是内置几个用户,匹配下信息。


    const users = [
    { username: 'guang', password: '111111', email: 'xxx@xxx.com'},
    { username: 'dong', password: '222222', email: 'yyy@yyy.com'},
    ]

    @Post('login')
    login(@Body() userDto: UserDto) {
    const user = users.find(item => item.username === userDto.username);

    if(!user) {
    throw new BadRequestException('用户不存在');
    }

    if(user.password !== userDto.password) {
    throw new BadRequestException("密码错误");
    }

    return {
    userInfo: {
    username: user.username,
    email: user.email
    },
    accessToken: 'xxx',
    refreshToken: 'yyy'
    };
    }

    如果没找到,就返回用户不存在。


    找到了但是密码不对,就返回密码错误。


    否则返回用户信息和 token。


    测试下:


    当 username 不存在时:



    当 password 不对时:



    登录成功时:



    然后我们引入 jwt 模块来生成 token:


    npm install @nestjs/jwt

    在 AppModule 里注册下这个模块:



    JwtModule.register({
    secret: 'guang'
    })

    然后在 AppController 里就可以注入 JwtService 来用了:



    @Inject(JwtService)
    private jwtService: JwtService

    这个是 nest 的依赖注入功能。


    然后用这个 jwtService 生成 access_token 和 refresh_token:



    const accessToken = this.jwtService.sign({
    username: user.username,
    email: user.email
    }, {
    expiresIn: '0.5h'
    });

    const refreshToken = this.jwtService.sign({
    username: user.username
    }, {
    expiresIn: '7d'
    })

    access_token 过期时间半小时,refresh_token 过期时间 7 天。


    测试下:



    登录之后,访问别的接口只要带上这个 access_token 就好了。


    前面讲过,jwt 是通过 authorization 的 header 携带 token,格式是 Bearer xxxx


    也就是这样:



    我们再定义个需要登录访问的接口:


    @Get('aaa')
    aaa(@Req() req: Request) {
    const authorization = req.headers['authorization'];

    if(!authorization) {
    throw new UnauthorizedException('用户未登录');
    }
    try{
    const token = authorization.split(' ')[1];
    const data = this.jwtService.verify(token);

    console.log(data);
    } catch(e) {
    throw new UnauthorizedException('token 失效,请重新登录');
    }
    }

    接口里取出 authorization 的 header,如果没有,说明没登录。


    然后从中取出 token,用 jwtService.verify 校验下。


    如果校验失败,返回 token 失效的错误,否则打印其中的信息。


    试一下:


    带上 token 访问这个接口:



    服务端打印了 token 中的信息,这就是我们登录时放到里面的:



    试一下错误的 token:



    然后我们实现刷新 token 的接口:


    @Get('refresh')
    refresh(@Query('token') token: string) {
    try{
    const data = this.jwtService.verify(token);

    const user = users.find(item => item.username === data.username);

    const accessToken = this.jwtService.sign({
    username: user.username,
    email: user.email
    }, {
    expiresIn: '0.5h'
    });

    const refreshToken = this.jwtService.sign({
    username: user.username
    }, {
    expiresIn: '7d'
    })

    return {
    accessToken,
    refreshToken
    };

    } catch(e) {
    throw new UnauthorizedException('token 失效,请重新登录');
    }
    }

    定义了个 get 接口,参数是 refresh_token。


    从 token 中取出 username,然后查询对应的 user 信息,再重新生成双 token 返回。


    测试下:


    登录之后拿到 refreshToken:



    然后带上这个 token 访问刷新接口:



    返回了新的 token,这种方式也叫做无感刷新。


    那在前端项目里怎么用呢?


    我们新建个 react 项目试试:


    npx create-react-app --template=typescript token-test-frontend


    把它跑起来:


    npm run start


    因为 3000 端口被占用了,这里跑在了 3001 端口。



    成功跑起来了。


    我们改下 App.tsx


    import { useCallback, useState } from "react";

    interface User {
    username: string;
    email?: string;
    }

    function App() {
    const [user, setUser] = useState<User>();

    const login = useCallback(() => {
    setUser({username: 'guang', email: 'xx@xx.com'});
    }, []);

    return (
    <div className="App">
    {
    user?.username
    ? `当前登录用户: ${ user?.username }`
    : <button onClick={login}>登录button>

    }
    div>
    );
    }

    export default App;

    如果已经登录,就显示用户信息,否则显示登录按钮。


    点击登录按钮,会设置用户信息。


    这里的 login 方法因为作为参数了,所以用 useCallback 包裹下,避免不必要的渲染。



    然后我们在 login 方法里访问登录接口。


    首先要在 nest 服务里开启跨域支持:



    在 main.ts 里调用 enbalbeCors 开启跨域。


    然后在前端代码里访问下这个接口:


    先安装 axios


    npm install --save axios

    然后创建个 interface.ts 来管理所有接口:


    import axios from "axios";

    const axiosInstance = axios.create({
    baseURL: 'http://localhost:3000/',
    timeout: 3000
    });

    export async function userLogin(username: string, password: string) {
    return await axiosInstance.post('/login', {
    username,
    password
    });
    }

    async function refreshToken() {

    }
    async function aaa() {

    }

    在 App 组件里调用下:


    const login = useCallback(async () => {
    const res = await userLogin('guang', '111111');

    console.log(res.data);
    }, []);

    接口调用成功了,我们拿到了 userInfo、access_token、refresh_token



    然后我们把 token 存到 localStorage 里,因为后面还要用。


    const login = useCallback(async () => {
    const res = await userLogin('guang', '111111');

    const { userInfo, accessToken, refreshToken } = res.data;

    setUser(userInfo);

    localStorage.setItem('access_token', accessToken);
    localStorage.setItem('refresh_token', refreshToken);
    }, []);


    在 interface.ts 里添加 aaa 接口:


    export async function aaa() {
    return await axiosInstance.get('/aaa');
    }

    组件里访问下:



    const xxx = useCallback(async () => {
    const res = await aaa();

    console.log(res);
    }, []);


    点击 aaa 按钮,报错了,因为接口返回了 401。


    因为访问接口时没带上 token,我们可以在 interceptor 里做这个。


    interceptor 是 axios 提供的机制,可以在请求前、响应后加上一些通用处理逻辑:



    添加 token 的逻辑就很适合放在 interceptor 里:



    axiosInstance.interceptors.request.use(function (config) {
    const accessToken = localStorage.getItem('access_token');

    if(accessToken) {
    config.headers.authorization = 'Bearer ' + accessToken;
    }
    return config;
    })

    现在再点击 aaa 按钮,接口就正常响应了:



    因为 axios 的拦截器里给它带上了 token:



    那当 token 失效的时候,刷新 token 的逻辑在哪里做呢?


    很明显,也可以放在 interceptor 里。


    比如我们改下 localStorage 里的 access_token,手动让它失效。



    这时候再点击 aaa 按钮,提示的就是 token 失效的错误了:



    我们在 interceptor 里判断下,如果失效了就刷新 token:


    axiosInstance.interceptors.response.use(
    (response) => {
    return response;
    },
    async (error) => {
    let { data, config } = error.response;

    if (data.statusCode === 401 && !config.url.includes('/refresh')) {

    const res = await refreshToken();

    if(res.status === 200) {
    return axiosInstance(config);
    } else {
    alert(data || '登录过期,请重新登录');
    }
    } else {
    return error.response;
    }
    }
    )

    async function refreshToken() {
    const res = await axiosInstance.get('/refresh', {
    params: {
    token: localStorage.getItem('refresh_token')
    }
    });
    localStorage.setItem('access_token', res.data.accessToken);
    localStorage.setItem('refresh_token', res.data.refreshToken);
    return res;
    }

    响应的 interceptor 有两个参数,当返回 200 时,走第一个处理函数,直接返回 response。


    当返回的不是 200 时,走第二个处理函数 ,判断下如果返回的是 401,就调用刷新 token 的接口。


    这里还要排除下 /refresh 接口,也就是刷新失败不继续刷新。


    刷新 token 成功,就重发之前的请求,否则,提示重新登录。


    其他错误直接返回。


    刷新 token 的接口里,我们拿到新的 access_token 和 refresh_token 后,更新本地的 token。


    测试下:


    我手动改了 access_token 让它失效后,点击 aaa 按钮,发现发了三个请求:



    第一次访问 aaa 接口返回 401,自动调了 refresh 接口来刷新,之后又重新访问了 aaa 接口。


    这样,基于 axios interceptor 的无感刷新 token 就完成了。


    但现在还不完美,比如点击按钮的时候,我同时调用了 3 次 aaa 接口:



    这时候三个接口用的 token 都失效了,会刷新几次呢?



    是 3 次。


    多刷新几次也没啥,不影响功能。


    但做的再完美一点可以处理下:



    加一个 refreshing 的标记,如果在刷新,那就返回一个 promise,并且把它的 resolve 方法还有 config 加到队列里。


    当 refresh 成功之后,重新发送队列中的请求,并且把结果通过 resolve 返回。


    interface PendingTask {
    config: AxiosRequestConfig
    resolve: Function
    }
    let refreshing = false;
    const queue: PendingTask[] = [];

    axiosInstance.interceptors.response.use(
    (response) => {
    return response;
    },
    async (error) => {
    let { data, config } = error.response;

    if(refreshing) {
    return new Promise((resolve) => {
    queue.push({
    config,
    resolve
    });
    });
    }

    if (data.statusCode === 401 && !config.url.includes('/refresh')) {
    refreshing = true;

    const res = await refreshToken();

    refreshing = false;

    if(res.status === 200) {

    queue.forEach(({config, resolve}) => {
    resolve(axiosInstance(config))
    })

    return axiosInstance(config);
    } else {
    alert(data || '登录过期,请重新登录');
    }
    } else {
    return error.response;
    }
    }
    )

    axiosInstance.interceptors.request.use(function (config) {
    const accessToken = localStorage.getItem('access_token');

    if(accessToken) {
    config.headers.authorization = 'Bearer ' + accessToken;
    }
    return config;
    })

    测试下:



    现在就是并发请求只 refresh 一次了。


    这样,我们就基于 axios 的 interceptor 实现了完美的双 token 无感刷新机制。


    总结


    登录状态的标识有 session 和 jwt 两种方案。


    session 是通过 cookie 携带 sid,关联服务端的 session,用户信息保存在服务端。


    jwt 是 token 保存用户信息,在 authorization 的 header 里通过 Bearer xxx 的方式携带,用户信息保存在客户端。


    jwt 的方式因为天然支持分布式,用的比较多。


    但是只有一个 token 会有过期后需要重新登录的问题,为了更好的体验,一般都是通过双 token 来做无感刷新。


    也就是通过 access_token 标识用户身份,过期时通过 refresh_token 刷新,拿到新 token。


    我们通过 nest 实现了这种双 token 机制,在 postman 里测试了一下。


    在 react 项目里访问这些接口,也需要双 token 机制。我们通过 axios 的 interceptor 对它做了封装。


    axios.request.interceptor 里,读取 localStorage 里的 access_token 放到 header 里。


    axios.response.interceptor 里,判断返回的如果是 401 就调用刷新接口刷新 token,之后重发请求。


    我们还支持了并发请求时,如果 token 过期,会把请求放到队列里,只刷新一次,刷新完批量重发请求。


    这样,就是一个基于 Axios 的完美的双 token 无感刷新了。

    作者:zxg_神说要有光
    来源:juejin.cn/post/7271139265442021391

    收起阅读 »

    求求别再叫我切图仔了,我是前端开发!

    web
    ☀️ 前言 大家好我是小卢,前几天在群里见到有群友抱怨一周内要完成这么一个大概20~30页的小程序。 群友: 这20多个页面一个星期让我开发完,我是不相信😮‍💨。 群友1: 跑吧,这公司留着没用了,不然就只有自己加班。 群友2: 没有耕坏的田,只有累死...
    继续阅读 »

    ☀️ 前言





    • 大家好我是小卢,前几天在群里见到有群友抱怨一周内要完成这么一个大概20~30页的小程序。



      • 群友: 这20多个页面一个星期让我开发完,我是不相信😮‍💨。

      • 群友1: 跑吧,这公司留着没用了,不然就只有自己加班。

      • 群友2: 没有耕坏的田,只有累死的牛啊,老哥!🐮。

      • 群友3: 用CodeFun啊,分分钟解决你这种外包需求。

      • 群友2: 对喔!可以试一下CodeFun,省下来的时间开黑去。




    • 在我印象中智能生成页面代码的工具一般都不这么智能,我抱着怀疑的心态去调研了一下CodeFun看看是不是群友们说的这么神奇,试用了过后发现确实挺强大的,所以这次借此机会分享给大家。




    🤔 什么是 CodeFun



    • 大部分公司中我们前端现在的开发工作流大概是下面这几步。

      • 一般会有UI先根据产品提供的原型图产出设计稿。

      • 前端根据设计稿上的标注(大小,边距等)进行编写代码来开发。

      • 开发完后需要给UI走查来确认是不是他/她想要的效果。

      • 如果发现有问题之后又继续重复上面的工作->修改样式->走查。






    • 我们做前端的都知道,重复的东西都可以封装成组件来复用,而上面这种重复的劳作是我们最不想去做的。

    • 但是因为设计图的精细可能有时候会有1px的差异就会让产品UI打回重新编写代码的情况,久而久之就严重影响了开发效率。

    • 我时常会有这么一种疑惑,明明设计稿上都有样式了,为什么还要我重新手写一遍呢?那么有没有一种可能我们可以直接通过设计稿就自动生成代码呢?

    • 有的!通过我的调研过后发现,发现确实CodeFun在同类产品中更好的解决了我遇到的问题。




    • CodeFun是一款 UI 设计稿智能生成源代码的工具,可以将 SketchPhotoshopFigma 的设计稿智能转换为前端源代码。

    • 8 小时工作量,10 分钟完成是它的slogan,它可以精准还原设计稿,不再需要反复 UI 走查,我觉得在使用CodeFun后可以极大地程度减少工作流的复杂度,让我们的工作流变成以下这样:

      • UI设计稿产出。

      • CodeFun产出代码,前端开发略微修改交付。





    🖥 CodeFun 如何使用



    • 接下来我就演示一下如何快速的根据设计稿来产出前端代码,首先我们拿到一个设计稿,这里我就在网上搜了一套Figma的设计稿来演示。

    • 我们在Figma中安装了一个CodeFun的插件,选择对应CodeFun的项目后点击上传可以看到很轻松的就传到我们的CodeFun项目中,当然除了FigamaCodeFun还支持Sketch,PSD,即时设计等设计稿。

    • 我们随便进入一个页面,引入眼帘的是中间设计稿,而在左侧的列表相当于这个页面的节点,而我们点击一下右上角的生成代码可以看到它通过自己的算法很智能的生成了代码。

    • 我上面选择生成的是React的代码,当然啦,他还有很多种选择微信小程序Vueuni-app等等等等,简直就是多端项目的福音!不止是框架,连Css预处理器都可以选择适合自己的。

    • 将生成的代码复制到编辑器中运行,可以看到对于简单的页面完全不用动脑子,直接就渲染出来我们想要的效果了,如果是很复杂的页面进行一些微调即可,是不是很方便嘿嘿。

    • CodeFun不管是根据你选择的模块进行生成代码还是整页生成代码用户进行复制使用之外,它还提供了代码包下载功能,在下载界面可以选择不同页面,不同框架,不同Css预处理器,不同像素单位

    • 如果是React相关甚至还会帮你把脚手架搭建好,直接下载安装依赖使用即可,有点牛呀。



    🔥 CodeFun 好在哪



    • 笔者在这之前觉得想象中的AI生成前端代码的功能一直都挺简陋,用起来不会到达我的预期,到底能不能解决我的痛点,其实我是有以下固有思想的:

      • 生成代码就是很简单的帮你把HtmlCss写完嘛但是我们不同框架又不能生成。

      • 生成代码的变量名肯定不好看。

      • 生成的代码肯定固定了宽高,不同的手机端看的效果会差很多。

      • 平时习惯了v-for,wx:for,map遍历列表,这种生成代码肯定全部给你平铺出来吧。



    • 但是当我使用过CodeFun之后发现确实他可以解决我们很多的重复编写前端页面代码的场景,而且也打消了我对这类AI生成前端页面代码功能的一些固有思想,就如它的slogan所说:8 小时工作量,10 分钟完成


    多平台、多框架支持



    • 支持 Vue 等主流 Web 开发框架代码输出。

    • 支持微信小程序代码输出,当你选择小程序代码输出时,像素单位会新增一个rpx的选项供大家选择。

    • 使用最简单的复制代码功能,我们可以快速的将我们想要的样式复制到我们的项目中进行使用 。

    • 笔者在使用的过程中一直很好奇下载代码的功能,如果我选择了React难不成还会给我自动生成脚手架?结果一试,还真给我生成了脚手架,只需要安装依赖即可,可以说是很贴心了~。



    循环列表自动输出



    • 我们平时在写一个列表组件的时候都喜欢使用v-for,wx:for,map等遍历输出列表,而CodeFun也做到了这种代码的生成。

    • CodeFun在导入设计稿的时候会自动识别哪些是list组件,当然你也可以手动标记组件为List

    • 然后再开启“将 List 标签输出为循环列表”选项即可自动根据当前选择的框架生成对应的循环遍历语法,确实是很智能了~



    批量数据绑定




    • 在我们平时Coding的过程中都不会把数据写死,而是用变量来代替进行动态渲染,而CodeFun支持批量数据绑定功能,我们可以把任何在页面中看到的元素进行数据绑定和命名修改




    • 就拿上面的循环列表举例吧,在我们一开始识别的Html中,遍历循环了一个typeCards数组,每一个都展示对应的信息,我们可以看到这里一开始是写死的,而我们平时写的时候会将它用变量替代。




    • 我们只需要点击右上角的数据绑定进行可视化修改即可,我们可以看到它的全部写法都改成了变量动态渲染,这就很符合我们平时编码的套路了。





    一键预览功能



    • 有很多同学反馈在之前做小程序的情况下需要将代码编写完整并跑起来的情况下,使用微信的预览功能才可以看到效果,会比较繁琐

    • CodeFun支持直接预览,当我们导入设计稿后,选择右上角的预览功能可以直接生成小程序二维码扫码即可进行预览,好赞!。



    更加舒适的“生成代码”



    • CodeFun生成的代码中是会让人看起来比较舒适的。

      • 变量名可读性会比较强。

      • 布局一般不会固定死宽高,而是使用padding等属性来自适应屏幕百分比

      • 自动处理设计稿中的无用图层、不可见元素、错误的编组乃至不合理的文字排列。

      • 全智能切图,自动分离背景图层、图标元素。




    ✍🏻 一些思考与建议



    • 前端开发不仅仅是一个切图的工具人,如果你一直局限于视图的表现的时候,你的前端水平也就是curd工程师的水平了,我们前端更多的要深入一些性能优化前端插件封装等等有意思的事情🙋🏻。

    • 总之如果你想你的前端水平要更加精进的情况下,可以减少一些在页面上的投入时间,现在的工具越来越成熟,而这些切图工作完全可以交给现有的工具去帮助你完成

    • 在使用体验上来说,CodeFun确实可以解决大部分切图功能,减少大家进行切图的工作时间,大家可以去试一下~但是肯定会有一些小细节不符合自己的想法,表示理解吧,毕竟AI智能生成代码能做成CodeFun这种水平已经很厉害了👍🏻。

    • 在使用建议上来说,我建议大家可以把CodeFun当成一个助手,而不要完全依赖,过度依赖,去找到更合适自己使用CodeFun的使用方法可以大量减少开发时间从而去做👉🏻更有意义的事情。

    • 很多人会很排斥,觉得没自己写的好,但是时代已经变啦~我还是那句话,所有东西都是一个辅助,一个工具,它提供了这些优质的功能而使用的好不好是看使用者本身的,欢迎大家去使用一下CodeFun~支持国产!!




    • 记住我们是前端开发,不是切图仔!做前端,不搬砖!



    作者:快跑啊小卢_
    来源:juejin.cn/post/7145977342861508638

    收起阅读 »

    为了弄清楚几个现象,重新学习了 flex

    web
    flex 布局作为开发中的常规手段,使用起来简直不要太爽。其特点: 相对于常规布局(float, position),它具备更高的灵活性; 相对于 grid 布局,它具有更强的兼容性; 使用简单,几个属性就能解决常规布局需求(当然 grid 布局也可以哈) ...
    继续阅读 »

    flex 布局作为开发中的常规手段,使用起来简直不要太爽。其特点:



    • 相对于常规布局(float, position),它具备更高的灵活性;

    • 相对于 grid 布局,它具有更强的兼容性;

    • 使用简单,几个属性就能解决常规布局需求(当然 grid 布局也可以哈)


    但是在开发使用 flex 布局的过程中,也会遇到一些自己难以解释的现象;通俗表述:为什么会是这样效果,跟自己想象的不一样啊?


    那么针对自己提出的为什么,自己有去研究过?为什么是这样的效果?如何解决呢?


    自己也存在同样的问题。所以最近有时间,重新学习了一遍 flex,发现自己对 flex 的某些属性了解少之又少,也就导致针对一些现象确实说不清楚。


    下面我就针对自己遇到的几种疑惑现象进行学习,来对 flex 部分属性深入理解。



    每天多问几个为什么,总有想象不到的意外收获 ---我的学习座右铭



    回顾 flex 模型


    flex 的基本知识和基本熟悉就不介绍了,只需要简单的回顾一下 flex 模型。


    在使用 flex 布局的时候,脑海中就要呈现出清晰的 flex 模型,利于正确的使用 flex 属性,进行开发。


    flex1.png

    理解如下的几个概念:



    • 主轴(main axis)

    • 交叉轴(cross axis)

    • flex 容器(flex container)

    • flex 项(flex item)



    main size 也可以简单理解下,后面内容 flex-basis 会涉及到。



    还顺便理解一下 flex-item 的基本特点



    1. flex item 的布局将由 flex container 属性的设置来进行控制的。

    2. flex item 不在严格区分块级元素和行内级元素。

    3. flex item 默认情况下是包裹内容的,但是可以设置的高度和宽度。


    现象一:flex-wrap 换行引起的间距


    关键代码:


    <!-- css -->
    <style>
     .father {
       width: 400px;
       height: 400px;
       background-color: #ddd;
       display: flex;
       flex-wrap: wrap;
    }
     .son {
       width: 120px;
       height: 120px;
    }
    </style>

    <!-- html -->
    <body>
     <div class="father">
       <div class="son" style="background-color: aqua">1</div>
       <div class="son" style="background-color: blueviolet">2</div>
       <div class="son" style="background-color: burlywood">3</div>
       <div class="son" style="background-color: chartreuse">4</div>
     </div>
    </body>

    具体现象:


    flex2.png

    疑惑:为什么使用 flex-wrap 换行后,不是依次排列,而是排列之间存在间距?



    一般来说,父元素的高度不会固定的,而是由内容撑开的。但是我们也不能排除父元素的高度固定这种情况。



    排查问题:针对多行,并且在交叉轴上,不能想到是 align-content 属性的影响。但是又由于代码中根本都没有设置该属性,那么问题肯定出现在该属性的默认值身上。


    那么通过 MDN 查询:


    flex3.png

    align-content 的默认值为 normal,其解释是按照默认位置填充。这里默认位置填充到底代表什么呢,MDN 上没有明确说明。


    但是在 MDN 上查看 align-items 时,却发现了有用的信息(align-items 是单行,align-content 是多行),normal 在不同布局中有不同的表现形式。


    flex4.png

    可以发现,针对弹性盒子,normal 与 stretch 的表现形式一样。


    自己又去测试 align-content,果然发现 normal 和 stretch 的表现形式一样。那么看看 stretch 属性的解释:


    flex6.png

    那么只需简单的需改,去掉 height 属性,那么 height 属性默认值就为 auto。


    <!-- css -->
    <style>
     .son {
       width: 120px;
       /* 注释掉 height */
       /* height: 120px */
    }
    </style>

    看效果:


    flex5.png

    可以发现,子元素被拉伸了,这是子元素在默认情况下应该占据的空间大小。



    这里就需要理解 flex item 的特点之一:flex item 的布局将由 flex container 属性的设置来进行控制的



    那么当子元素设置高度时,是子元素自己把自己的高度限制了,但是并没有改变 flex container 对 flex item 布局占据的空间大小,所以就会多出一点空间,也就是所谓的间距。


    所以针对上面这个案例,换行存在间隔的现象也就理解了,因为第四个元素本身就排布在弹性盒子的正确位置,只是我们把子元素高度固定了,造成的现象是有存在间隔。



    可以想一下,如果子元素的高度加起来大于父元素的高度,又是什么效果呢?可以自己尝试一下,看自己能够解释不?



    现象二:flex item 拉伸?压缩?


    在使用 flex 时,最常见的现象是这样的:


    flex7.png

    当子元素为 3 个时,不会被拉伸,为什么呢?


    当子元素为 6 个事,会被压缩,又是为什么呢?


    其实上面这两个疑问❓,只需了解两个属性:flex-growflex-shrink。因为这两个属性不常用,所以容易忽略,从而不去了解,那么就会造成疑惑。


    flex-grow 属性指定了 flex 元素的拉伸规则。flex 元素当存在剩余空间时,根据 flex-grow 的系数去分配剩余空间。 flex-grow 的默认值为 0,元素不拉伸


    flex-shrink 属性指定了 flex 元素的收缩规则。flex 元素仅在默认宽度之和大于容器的时候才会发生收缩,其收缩的大小是依据 flex-shrink 的值。flex-shrink 的默认值为 1,元素压缩



    该两个属性都是针对 主轴方向的剩余空间



    所以



    • 当子元素数量较少时,存在剩余空间,但是又由于 flex-grow 的值为 0,所以子元素宽度不会进行拉伸。

    • 当子元素数量较多时,空间不足,但是又由于 flex-shrink 的值为 1,那么子元素就会根据相应的计算,来进行压缩。


    特殊场景: 当空间不足时,子元素一定会压缩?试试单词很长(字符串很长)的时候呢?


    flex8.png

    现象三:文本溢出,flex-basis?width?


    在布局中,如果指定了宽度,当内容很长的时候,就会换行。但是会存在一种特殊情况,就是如果一个单词很长为内容时,则不会进行换行;跟汉字是一样的道理,不可能从把汉字分成两半。


    那么在 flex 布局中,会存在两种情况:


    flex9.png

    可以发现:



    • 设置了固定的 width 属性,字符串超出宽度之后,就会截取。

    • 而设置了固定的 flex-basis 属性,字符串超出宽度之后,会自动扩充宽度。


    其实在这里可能有人会有疑惑:为什么把 width 和 flex-basis 进行对比?或者说 flex-basis 这个属性到底是干什么?



    其实我也是刚刚才熟悉到这个属性,哈哈哈,不知道吧!!!



    因为 flex-basis 是使用在 flex item 上,而 flex-basis(主轴上的基础尺寸)属性在大多数情况下跟 width 属性是等价的,都是设置 flex-item 的宽度。


    上面的案例就是属于特殊情况,针对单词超长不换行时,flex-basis 就会表现出不一样的形式,自动扩充宽度


    简单学习一下 flex-basis 的基本语法吧。


    flex-basis 属性值:



    • auto: 默认值,参照自身 width 或者 height 属性。

    • content: 自动尺寸,根据内容撑开。

    • <'width'>: 指定宽度。


    当一个属性同时设置 flex-basis(属性值不为 auto) 和 width 时,flex-basis 具有更高的优先级


    现象四:flex 平分


    当相对父容器里面的子元素进行平分时,我们会毫不犹豫的写出:


    .father {
     width: 400px;
     height: 400px;
     background-color: #ddd;
     display: flex;
    }
    .son {
     flex: 1; /* 平分 */
     height: 90px;
    }

    flex10.png

    那么我们是否会想过为什么会平分空间? 其中 flex:1 起了什么作用?


    我们也许都知道 flex 属性是一个简写,是 flex-growflex-shrinkflex-basis 的简写。所以,flex 的属性值应该是三个组合值。


    但是呢,flex 又类似于 font 属性一样,是一个很多属性的简写,其中一些属性值是可以不用写的,采用其默认值。


    所以 flex 的属性值就会分析三种情况:一个值,两个值,三个值。


    MDN 对其做了总结:


    flex11.png

    看图,规则挺多的,如果要死记的话,还是挺麻烦的。


    针对上面的规则,其实只需要理解 flex 的语法形式,还是能够完全掌握(有公式,谁想背呢)。


    flex = none | auto | [ <'flex-grow'> <'flex-shrink'>? || <'flex-basis'> ]  

    希望你能看懂这个语法,很多 api 都有类似的组合。



    • | 表示要么是 none, 要么是 auto, 要么是后面这一坨,三选一。

    • || 逻辑或

    • ? 可选


    理解上面这种语法之后,总结起来就是如下:


    一个值



    1. none(0 0 auto)auto(1 1 auto) 是需要单独记一下的,这个无法避免。

    2. 无单位,就是 flex-grow,因为存在单位,就是 flex-grow 属性值规定为一个 number 类型的

    3. 有单位,就是 flex-basis,因为类似 width 属性是需要单位的,不然没有效果。


    两个值



    1. 无单位,就是 flex-grow 和 flow-shrink,理由如上

    2. 其中一个有单位,就是 flex-grow 和 flex-basis,因为 flex-shrink 是可选的(这种情况是没有任何实际意义的,flex-basis设置了根本无效)。


    三个值


    三个值不用多说,一一对应。


    理解了上面的语法形式,再来看 flex: 1 的含义就轻而易举了。一个值,没有单位,就是 flex-grow,剩余空间平均分配


    现象五:多行,两边对齐布局


    无论是 app 开发,还是网页开发,遇到最多的场景就是这样的:


    flex12.png

    两边对齐,一行元素之间的间距相同;如果一行显示不下,就换行依次对齐排布。


    那么不难想到的就是 flex 布局,会写下如此代码:


    .father {
     display: flex;
     justify-content: space-between;
     flex-wrap: wrap;
    }
    .son {
     width: 90px;
     height: 90px;
    }

    那么你就会遇到如下情况:


    flex13.png

    其中的第二、三种情况布局是不可以接受,数据数量不齐的问题。但是数据是动态的,所以不能避免出现类似情况。


    你们遇到过这种类似的布局吗?会存在这种情况吗?是怎么解决的呢?


    第一种解决方案:硬算


    不使用 flex 的 justify-content 属性,直接算出元素的 margin 间隔。


    .father {
     width: 400px;
     background-color: #ddd;
     display: flex;
     flex-wrap: wrap;
    }
    .son {
     margin-right: calc(40px / 3); /* 40px 为 父元素的宽度 - 子元素的宽度总和,   然后平分剩余空间*/
     width: 90px;
     height: 90px;
     background-color: #5adacd;
     margin-bottom: 10px;
    }
    /* 针对一行最后一个,清空边距 */
    .son:nth-child(4n) {
     margin-right: 0;
    }

    缺点:只要其中的一个宽度发生变化,又要重新计算。


    第二种解决方案:添加空节点


    为什么要添加空节点呢?因为在 flex 布局中,没有严格的区分块级元素和行内元素。那么就可以使用空节点,来占据空间,引导正确的布局。


    <style>
     .father {
       width: 400px;
       background-color: #ddd;
       display: flex;
       justify-content: space-between;
       flex-wrap: wrap;
    }
     .son {
       width: 90px;
       height: 90px;
       background-color: #5adacd;
       margin-bottom: 10px;
    }
     
     /* height 设置为 0 */
     .father span {
       width: 90px; /*空节点也是 flex-item, width 是必须一致的,只是设置高度为0,不占据空间*/
    }
    </style>
    </head>
    <body>
     <div></div>
     <div class="father">
       <div class="son">1</div>
       <div class="son">2</div>
       <div class="son">3</div>
       <div class="son">4</div>
       <div class="son">5</div>
       <div class="son">6</div>
       <div class="son">7</div>
       <!-- 添加空节点,个数为 n-2 -->
       <i></i>
       <i></i>
     </div>
    </body>

    这样也能解决上面的问题。


    添加空节点的个数:n(一行的个数) - 2(行头和行尾,就是类似第一种情况和第四种情况本身就是正常的,就不需要空间点占据)


    缺点:添加了dom节点


    上面两种方案都解决问题,但是都有着各自的缺点,具体采用哪种方式,就看自己的选择了。


    那么你们还有其他的解决方案吗?


    总结


    其实本篇所解释的现象是自己对 flex 知识掌握不牢而造成的,从而记录此篇,提升熟悉度。也希望能够帮助对这些现象有困惑的码友。


    作者:copyer_xyf
    来源:juejin.cn/post/7273025171111444540
    >如果存在错误解释,评论区留言。

    收起阅读 »

    第一份只干了五天的前端工作

    可能是前面运气太好,工作五年了反而遇到奇怪的操作,首先是一些事情没有提前给我讲,入职当天才知道。当月工资要在下个月月末发,还会有绩效工资的变动,有减少的可能。我当时问了五险一金说是按照7000多交,结果公积金按最低2千多我去的第三天,后端管理A,负责给我分配任...
    继续阅读 »

    可能是前面运气太好,工作五年了反而遇到奇怪的操作,首先是一些事情没有提前给我讲,入职当天才知道。

    • 当月工资要在下个月月末发,还会有绩效工资的变动,有减少的可能。
    • 我当时问了五险一金说是按照7000多交,结果公积金按最低2千多
    • 我去的第三天,后端管理A,负责给我分配任务的,他早上和我讲,类似初创公司领导不愿意看到早下班,晚上也会找我过一下进度啥的,要定制一个学习计划,自己找时间去熟悉代码...

    面试过程倒还顺利,前端A问了些技术,比较偏实际应用的,对我github上做的东西也感兴趣。前端管理B问的有些偏人事的,然后问我下午有没有其它安排,没有的话不走流程叫领导A面一下。领导A就直接说了平薪,试用期3个月80%能不能接受,当时那种场景我就顺势而为说可以,我之前的工作都是少一千。


    我往回走的时候hr给我打电话,前面的没接到,我打过去得知是想问我在哪,想问一下总裁有没有时间,不然就要到周六。


    等到周六面完,下周一打电话说通过,让我周三去报到,我说太快要下周一。然后这家公司每个月月末的周六是组织学习培训的,我也参加了。


    第一天搭建环境,账号权限这些,电脑连不上WiFi,我问前台有没有网线,她说没有我也没去下面找,又换了一台,接口就不同了,又弄双屏的转接线。我找她一次,她都是从前台去存放物品的地方,发现不太对再找她已经回前台了,前后不到一分钟的时间吧。确实我对这个也不太熟,有些不好意思麻烦她,最后少一个转接器我就自己买了,否则屏幕显示不清晰,顺便买了两个增高架。


    前端管理B给我说了几个项目,我看了会代码,管理B找我谈了一会。


    九点上班六点下班,上班第一天我看快到六点半了就准备走,这时候管理B拉着我去展厅看了下公司的产品。


    第二天后端管理A请假了,前端管理B给我说了会代码,后面主要负责的项目,跑了下本地联调。


    基本上一个菜单一个项目,每个项目用iframe嵌套,然后有一些组件库这些,代码之间组件嵌套的比较深,多以component。数据走的是配置,流向很乱。接口的传递和返回都很庞大,有些还是json字符串,20-60多个kb,看结构话要单独复制出来。一些项目调试没有sourcemap,给我的感觉就是把简单的事做复杂了。


    第三天后端管理A给我安排了一些事情,就是口头说了一下,意思这种改动不需要ui、不需要产品自己就可以定,基本上就是我做完让他看一下,他觉得不好在改。这里面就有一个问题,就是到底改哪里没有全部列出来,我对项目又不熟,一般都有一个上下文的概念,可对不上的时候,才发现是另一个地方,他又是没有规划的那种。


    往往走配置基本会出现一种情况,就是一些东西需要单独处理,或者配置选项越加越多,或者当初实现的时候偷懒就给写死了,东改一下西改一下,而且这种封装太笨重了,不好优化,只能说熟悉了更快些,但是维护成本始终会处在一个固定的量级,而且随着功能迭代,补丁会越来越多。


    下午的时候管理B把我叫过去,让我协助后台A排查两个问题,说别的环境没有只有这个环境有,据说问题已经存在很久了,我找到问题A的代码所在地、以及问题的原因就花了很长时间,单从前端看,是因为一个代码报错导致的。因为这些东西要按照业务流程来,我不知道什么是对的,只能关注接口的返回然后找对应的前端组件渲染逻辑,对比差异反馈,本质上就是反推接口返回有哪些不对,找到问题已经晚上10点了,两个问题都是后台在处理用户操作后,前后id不一样导致拿到的数据不对所致。


    这种问题让一个刚入职的人排查显然浪费时间,中间链路太多了,我问了下后端A他之前都是和谁对接,得知前面的人离职了,我就想着这种情况是最难受的,总不可能我一上来就能接手他的工作,巧妇难为无米之炊,哪有那么丝滑的过度,总要有个渐进性的过程吧。


    每天要建tapd,再把tapd的内容复制出来写成日报,然后也要写周报,还要在领导有时间的时候找他汇报进度,或者等他来找你。


    第四天,后端管理A给我说了下今天的任务,有一些历史遗留问题,我处理的还是很快的,直到前端写完对接口的时候,他只是钉钉发了一些字段给我,发现还有另一个项目要改,找到代码熟悉,对好逻辑写好前端代码,我又在本地连了下测试环境,跑了下流程,接口报错了,我看六点半了就走了。


    第四天上午还过了个需求,虽然我也听不太懂,但是管理B直接说这个事情15个自然日还是工作日搞完。


    后端管理A评价我的日报,意思任务完不成要及时上报,晚上要和他汇报进度,我想着我都不知道一天到底有多少任务,也不知道完成任务花费多长时间,更不知道啥样算完成,我咋完成?


    第五天,前端管理B找我聊了一会,说是来了一周,我就把我的感触说了,他问我打算怎么处理,我就说这种强度我就不干了,感觉不值,他说他来处理。


    我对比了下我能够得到的和我将要面对的,平薪80%,三个月,我觉得有些不值得,待遇还不如我两年前,也没到山穷水尽的地步,受这罪干嘛?以前入职也不是没有压力大的时候,但待遇有所增长,看了代码啥的我也觉得对我是一种历练,即便不说我也会主动学习,因为我知道当我很熟悉的时候后面效率更高,算提早付出了,还是在时间不那么紧凑的时候,但这家公司给我一种压榨的感觉。


    后面我把项目分支发给后端管理A,部署发版耽搁了一会,后面是找的前端管理B解决的,我后面了解到走的是自己的搭建的运维系统,两个项目有不同的分支名,我把自己的分支手动合并,再找后端管理A就好了。


    接下来就是他发现一些问题让我改,持续到下午五点半左右,我再次提交代码时,发现gtilab账号已经被注销了,他让我把代码改动发给前端管理B,这个时候我的电脑已经重置,被前台收走了。还是有些遗憾,再次改动时发现轻车熟路了许多,前面还是花了不少精力的。


    公司提供了午休床,我贴了个标签,前端管理B贴心的给我个东西增加区分性,但我用过一次后没再找见,离职也是要交给前台的,找的时候还在想不会所有放午休床的地方都要找一遍吧,还好发现了破碎的标签,应该被别人用的时候弄碎了。


    其实周五不聊的话我可能想着再适应下,也没想到当天就能走完离职,清理tapd的时候发现有五十多个bug挂在我这,看到一个六月份的。


    幸亏我带了包过去,不然东西都不好拿,看着8月份4天32小时...。


    可能是太久没工作了吧,我便抱着试一试的态度,坦白的讲我也想过边干边找,入职的这几天有了新的方向,我github上写的工具依旧发挥稳定,替我节省了很多时间。


    作者:这次假期很长
    链接:https://juejin.cn/post/7263653558385868857
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    某法宝网站 js 逆向思路

    web
    本文章只做技术探讨, 请勿用于非法用途。 目标网站 近期为了深刻学习法律知识, 需要来下载一些法律条文相关的内容。 本文通过 某法宝网站来下载数据 http://www.pkulaw.com/。 网站分析 文章内容 详情页图片 可以看到下载的方式还蛮多的,...
    继续阅读 »

    本文章只做技术探讨, 请勿用于非法用途。



    目标网站


    近期为了深刻学习法律知识, 需要来下载一些法律条文相关的内容。


    本文通过 某法宝网站来下载数据 http://www.pkulaw.com/。


    网站分析


    文章内容


    image.png
    详情页图片


    可以看到下载的方式还蛮多的, 尝试复制全文, 得到内容保留原格式, 所以我选择使用复制全文的方式来得到文章内容。


    同时详情页没有看到明显的反扒措施, 不需要特殊处理。


    列表页


    image.png


    最终决定通过专题分类来获取所有的数据, 然后调试分析接口参数, 这里没有什么加密的参数, 确定需要关注的参数如下:


    {
    "Aggs.SpecialType": "", // 专题类型(编号)
    "VerifyCodeResult": "", // 验证码值(后边讲解)
    "Pager.PageIndex": "2", // 页码
    "RecordShowType": "List", // 显示方式(List 方式显示所有数据, 保持该值即可)
    "Pager.PageSize": 100, // 每页的数量(最大 100)
    }


    这里仅列出了需要关注的参数, 其他参数保持原值即可(需要的话可以自己调试对比参数值确定意义), 当 pageSize 设置为 100 时, 第 3 页之后的数据需要验证码才能查看。


    验证码


    为方便分析验证码的校验方式, 推荐使用无痕模式来调试, 获取验证码之前清空一次 cookie。


    image.png


    image.png


    image.png


    可以看到验证码的流程为:



    1. 请求验证码, 并返回 set-cookie 。

    2. 携带该 cookie 信息并校验验证码, 通过后得到 code。

    3. 携带 code 请求数据, 得到数据。


    开整


    确定了问题后, 就可以开始了, 一个一个解决。


    文章内容


    确定了要用复制的方式来得到数据, 那就分析下他的复制干了点啥。


    image.png


    查看他的页面元素, 发现了这两个东西, 全局搜索后很容易定位到处理函数(找不到的话刷新页面)。


    image.png


    然后接着定位这个 toCopyFulltext() 函数。


    image.png


    找到这个就对了, 然后可以看代码他是用的 jQuery 的选择器来做的, 我们只能来仿造一个页面结构来用它, 这里推荐使用 cheerio(nodejs) 库来做(jsdom 应该也可以?)。


    image.png


    伪造后直接调用就大功告成。


    列表页


    这个问题不大, 问题主要在于验证码。


    image.png


    通过查看页面元素可以得到专题编号。


    验证码


    请求


    // 验证码图片请求返回结果
    {
    "errcode": 0,
    "y": 131,
    "array": "14,12,11,7,6,2,13,15,9,8,16,0,1,3,4,18,17,5,19,10",
    "imgx": 300,
    "imgy": 200,
    "small": "data:image/jpg;base64,...", // 滑块图片
    "normal": "data:image/jpg;base64,..." // 背景图片
    }

    image.png
    得到的 base64 可以用 python 的 PIL 库来解析出来。


    image.png
    然后我解析出来的图片就是这个样子, 基本上就确定了这验证码就是老朋友了, 我们先来把他还原。



    还是简单解释一下这个, 图片被切割为了上下两部分, 每个部分又被切割为了 10 等份, 原图为 300x200, 也就是说这里被切割为 20 张 30x100 的图片, 然后打乱顺序拼接后返回, 我们需要先完成切割然后再按正确的顺序来拼接还原。



    "errcode": 0,
    "y": 131,
    "array": "14,12,11,7,6,2,13,15,9,8,16,0,1,3,4,18,17,5,19,10", // 图片的正确顺序
    "imgx": 300, // 图片宽
    "imgy": 200, // 图片高
    "small": "data:image/jpg;base64,...", // 滑块图片
    "normal": "data:image/jpg;base64,..." // 背景图片

    image.png


    image.png


    还原后的图片就是这个样子了。


    校验


    // 校验接口请求参数
    {
    "act": "check",
    "point": "197", // 缺口位置
    "timespan": "1067", // 滑动耗时
    "datelist": "1,1692848585282|10,1692848585288|20,1692848585295|34,1692848585305|50,1692848585312|58,1692848585320|74,1692848585328|90,1692848585338|95,1692848585344|107,1692848585355|117,1692848585360|124,1692848585371|126,1692848585376|130,1692848585388|133,1692848585393|136,1692848585404|137,1692848585409|138,1692848585416|139,1692848585425|139,1692848585433|140,1692848585441|140,1692848585449|140,1692848585457|141,1692848585465|141,1692848585473|141,1692848585481|142,1692848585489|142,1692848585498|142,1692848585506|143,1692848585514|143,1692848585522|143,1692848585530|144,1692848585539|145,1692848585546|146,1692848585554|146,1692848585562|149,1692848585572|151,1692848585579|154,1692848585589|157,1692848585596|160,1692848585604|161,1692848585611|164,1692848585621|167,1692848585627|170,1692848585639|172,1692848585643|174,1692848585655|177,1692848585659|179,1692848585667|181,1692848585676|182,1692848585683|184,1692848585692|185,1692848585699|186,1692848585710|187,1692848585716|188,1692848585724|189,1692848585732|189,1692848585740|190,1692848585748|191,1692848585758|192,1692848585764|192,1692848585773|193,1692848585781|194,1692848585789|195,1692848585797|195,1692848585806|196,1692848585813|196,1692848585821|197,1692848585829|197,1692848585839|197,1692848585845|197,1692848585855|197,1692848586219"
    } // 滑动轨迹(位置,时间戳)

    需要解决的参数为 point(缺口位置) 及 datelist(滑动轨迹)。


    point

    image.png


    推荐使用 ddddocr 库来识别, 准确率还可以吧, 挺方便的。


    datelist

    image.png


    轨迹方面自己设计算法来吧, 这里可以作为一个参考, 他的 datelist 的长度不固定, 一般也就是一百多些轨迹点吧, 可以通过调整参数来达到效果, 反正就是多测试吧, 这个方法大概有 百分之九十 左右的通过率吧, 暂时够用。


    VerifyCodeResult


    // 校验请求成功后返回数据
    {
    "state": 0,
    "info": "正确",
    "data": 197
    }
    // VerifyCodeResult: YmRmYl8xOTc=

    解决了验证码, 惊喜的发现还是不咋对, 这个返回的 data 明显长得和要用的 VerifyCodeResult 不太像, 就接着来找。


    image.png


    全局搜索, 找到两个 js 文件, 都打上断点来调试。


    image.png


    可以看到我们成功断到, 并得到是由 (new Base64).encode("bdfb_" + y) 这种方式来生成的 code 值, 这个 y 值就是上一步返回的 data, 接下来只需要把 Base64 的代码那里扣下来, 或者自己实现就行了, 方便些, 这里直接抠下来了, 然后拿到 code 带上验证码请求返回的 cookie, 就能正常拿到数据了。


    结语


    整体来说不算困难, 没有什么加密啊混淆啊之类的东西, 确定方向之后很快就能搞定了, 用来练手还是很不错的, 有什么问题欢迎交流, 不知道这个得几年啊。。。



    请洒潘江,各倾陆海云

    作者:Glommer
    来源:juejin.cn/post/7270702261293039635
    尔。


    收起阅读 »

    JS长任务(耗时操作)导致页面卡顿,优化方案大比拼!

    web
    抛出问题 前端是直接和客户交互的界面,前端的流畅性对于客户的体验和留存率至关重要,可以说,前端界面的流畅性是一款产品能不能成功的关键因素之一。要解决界面卡顿,首先我们先要知道造成页面卡顿的原因: 什么是长任务? 长任务是指JS代码执行耗时超过50ms,能让...
    继续阅读 »

    抛出问题


    前端是直接和客户交互的界面,前端的流畅性对于客户的体验和留存率至关重要,可以说,前端界面的流畅性是一款产品能不能成功的关键因素之一。要解决界面卡顿,首先我们先要知道造成页面卡顿的原因:




    • 什么是长任务?


      长任务是指JS代码执行耗时超过50ms,能让用户感知到页面卡顿的代码执行流。




    • 长任务为什么会造成页面卡顿?


      UI界面的渲染由UI线程控制,UI线程和JS线程是互斥的,所以在执行JS代码时,UI线程无法工作,就表现出页面卡死状态。




    我们现在来模拟一个长任务,看看它是怎么影响页面流畅性的:



    • 先看效果(GIF),这里我们给div加了个滚动的动画,当我们开始执行长任务后,页面卡住了,等待执行完后才恢复,总耗时3秒左右,记住总耗时,后面会用到。


    动画.gif



    • 再看代码(有点长,主要看JS部分,后面的优化方案代码只展示优化过的JS函数)


    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <style>
    .myDiv {
    width: 100px;
    height: 100px;
    margin: 50px;
    background-color: blue;
    position: relative;
    animation: my-animation 5s linear infinite;
    }
    @keyframes my-animation {
    from {
    left: 0%;
    rotate: 0deg;
    }
    to {
    left: 100%;
    rotate: 360deg;
    }
    }
    </style>
    </head>
    <body>
    <div class="myDiv"></div>
    <button onclick="longTask()">执行长任务</button>

    <script>
    // 模拟耗时操作,大概10ms
    function myFunc() {
    const startTime = Date.now();
    while (Date.now() - startTime < 10) {}
    }

    // 长任务,循环执行myFunc300次,耗时3秒左右
    function longTask() {
    console.log("开始长任务");
    const startTime = Date.now();
    for (let i = 0; i < 300; i++) {
    myFunc();
    }
    console.log(`长任务结束,总耗时:${Date.now() - startTime}ms`);
    }
    </script>
    </body>
    </html>


    本段代码有一个模拟耗时的函数,有一个模拟长任务的函数(调用300次耗时函数),后面的优化方案都会基于这段代码来进行。


    优化方案


    setTimeout 宏任务方案


    第一个优化方案,我们将长任务拆成多个宏任务来执行,这里我们用setTimeout函数。为什么拆成多个宏任务可以优化卡顿问题呢?


    正如我们上文所说,页面卡顿的原因是因为JS执行线程占用了控制权,导致UI线程无法工作。在浏览器的事件轮询(EventLoop)机制中,每一个宏任务执行完之后会将控制权重新交给UI线程,待UI线程执行完渲染任务后,才会继续执行下一个宏任务。浏览器轮询机制流程图如下所示,想要深入了解浏览器轮询机制,可以参考我的另一篇文章:从进程和线程入手,我彻底明白了EventLoop的原理! image.png



    • 先看效果(GIF),执行长任务的同时,页面也很流畅,没有了先前卡顿的感觉,总耗时4.4秒。


    动画.gif



    • 再看代码


    // setTimeout方案 递归,循环300次
    function timeOutTask(i, startTime) {
    setTimeout(() => {
    if (!startTime) {
    console.log("开始长任务");
    i = 0;
    startTime = Date.now();
    }
    if (i === 300) {
    console.log(`长任务结束,总耗时:${Date.now() - startTime}ms`);
    return;
    }
    myFunc();
    timeOutTask(++i, startTime);
    });
    }

    把代码改为多个宏任务之后,解决了页面卡顿的问题,但是总耗时比之前多了1.4秒,主要原因是因为递归调用需要不断地向下开栈,会增加开销。当我们每个任务都不依赖于上一个任务的执行结果时,就可以不使用递归,直接使用循环创建宏任务。



    • 先看效果(GIF),耗时缩短到了3.1秒,但是可以看到明显掉帧。


    动画.gif



    • 再看代码


    // setTimeout不递归方案,循环300次
    function timeOutTask2() {
    console.log("开始长任务");
    const startTime = Date.now();

    for (let i = 0; i < 300; i++) {
    setTimeout(() => {
    myFunc();
    if (i === 300 - 1) {
    console.log(`长任务结束,总耗时:${Date.now() - startTime}ms`);
    }
    });
    }
    }

    使用300个循环同时创建宏任务后,虽然耗时降低了,但是div滚动会出现明显掉帧,这也是我们不愿意看到的,那执行代码速度和页面流畅度就没办法兼得了吗?很幸运,requestIdleCallback函数可以帮你解决这个难题。


    requestIdleCallback 函数方案


    requestIdleCallback提供了由浏览器决定,在空闲的时候执行队列任务的能力,从而不会影响到UI线程的正常运行,保证了页面的流畅性。


    它的用法也很简单,第一个参数是一个函数,浏览器空闲的时候就会把函数放到队列执行,第二个参数为options,包含一个timeout,则超时时间,即使浏览器非空闲,超时时间到了,也会将任务放到事件队列。
    下面我们把setTimeout替换为requestIdleCallback



    • 先看效果(GIF),耗时3.1秒,也没有出现掉帧的情况。


    动画.gif



    • 再看代码


    // requestIdleCallback不递归方案,循环300次
    function callbackTask() {
    console.log("开始长任务");
    const startTime = Date.now();

    for (let i = 0; i < 300; i++) {
    requestIdleCallback(() => {
    myFunc();
    if (i === 300 - 1) {
    console.log(`长任务结束,总耗时:${Date.now() - startTime}ms`);
    }
    });
    }
    }

    requestIdleCallback解决了setTimeout方案掉帧的问题,这两种方案都需要拆分任务,有没有一种不需要拆分任务,还能不影响页面流畅度的方法呢?Web Worker满足你。


    Web Worker 多线程方案


    WebWorker是运行在后台的javascript,独立于其他脚本,不会影响页面的性能。



    • 先看效果,耗时不到3.1秒,页面也没有受到影响。


    动画.gif



    • 再看代码,需要额外创建一个js文件。(注意,浏览器本地直接运行HTML会被当成跨域,需要开一个服务运行,我使用的http-server)


    task.js 文件代码


    // 模拟耗时
    function myFunc() {
    const startTime = Date.now();
    while (Date.now() - startTime < 10) {}
    }

    // 循环执行300次
    for (let i = 0; i < 300; i++) {
    myFunc();
    }

    // 通知主线程已执行完
    self.postMessage("我执行完啦");

    主文件代码


    // Web Worker 方案
    function workerTask() {
    console.log("开始长任务");
    const startTime = Date.now();
    const worker = new Worker("./task.js");

    worker.addEventListener("message", (e) => {
    console.log(`长任务结束,总耗时:${Date.now() - startTime}ms`);
    });
    }

    WebWorker方案额外增加的耗时很少,也不需要拆分代码,也不会影响页面性能,算是很完美的一种方案了。
    但它也有一些缺点:



    • 浏览器兼容性差

    • 不能访问DOM,即不能更新UI

    • 不能跨域加载JS


    总结


    三种方案中,如果不需要访问DOM的话,我认为最好的方案为WebWorker方案,其次requestIdleCallback方案,最后是setTimeout方案。
    WebWorker和requestIdleCallback属于比较新的特性,并非所有浏览器都支持,所以我们需要先进行判断,代码如下:


    if (typeof Worker !== 'undefined') {
    //使用 WebWorker
    }else if(typeof requestIdleCallback !== 'undefined'){
    //使用 requestIdleCallback
    }else{
    //使用 setTimeout
    }

    希望本文对您有帮助,其他所有代码可在下方直接执行。(WebWorker不支持)

    作者:TuYuHao
    来源:juejin.cn/post/7272632260180377634
    n>

    收起阅读 »

    你看这个圆脸😁,又大又可爱~ (Compose低配版)

    web
    前言 阅读本文需要一定compose基础,如果没有请移步Jetpack Compose入门详解(实时更新) 在网上看到有人用css写出了下面这种效果;原文链接 我看到了觉得很好玩,于是用Compose撸了一个随手势移动眼珠的版本。 一、Canvas画图 这...
    继续阅读 »


    前言


    阅读本文需要一定compose基础,如果没有请移步Jetpack Compose入门详解(实时更新)


    在网上看到有人用css写出了下面这种效果;原文链接


    请添加图片描述


    我看到了觉得很好玩,于是用Compose撸了一个随手势移动眼珠的版本。




    一、Canvas画图


    这种笑脸常用的控件肯定实现不了,我们只能用Canvas自己画了


    笑脸


    我们先画脸



    下例当中的size和center都是onDraw 的DrawScope提供的属性,drawCircle则是DrawScope提供的画圆的方法



    Canvas(modifier = modifier
    .size(300.dp),
    onDraw = {

    // 脸
    drawCircle(
    color = Color(0xfffecd00),
    radius = size.width / 2,
    center = center
    )

    })

    属性解释



    • color:填充颜色

    • radius: 半径

    • center: 圆心坐标


    这里我们半径取屏幕宽度一半,圆心取屏幕中心,画出来的脸效果如下


    在这里插入图片描述


    微笑


    微笑是一个弧形,我们使用drawArc来画微笑


    // 微笑
    val smilePadding = size.width / 4

    drawArc(
    color = Color(0xffb57700),
    startAngle = 0f,
    sweepAngle = 180f,
    useCenter = true,
    topLeft = Offset(smilePadding, size.height / 2 - smilePadding / 2),
    size = Size(size.width / 2, size.height / 4)
    )

    属性解释



    • color:填充颜色

    • startAngle: 弧形开始的角度,默认以3点钟方向为0度

    • sweepAngle:弧形结束的角度,默认以3点钟方向为0度

    • useCenter :指示圆弧是否要闭合边界中心的标志(上例加不加都无所谓)

    • topLeft :相对于当前平移从0的本地原点偏移,0开始

    • size:要绘制的圆弧的尺寸


    效果如下
    在这里插入图片描述


    眼睛和眼珠子


    眼睛也是drawCircle方法,只是位置不同,这边就不再多做解释


                // 左眼
    drawCircle(
    color = Color.White,
    center = Offset(smilePadding, size.height / 2 - smilePadding / 2),
    radius = smilePadding / 2
    )


    //左眼珠子
    drawCircle(
    color = Color.Black,
    center = Offset(smilePadding, size.height / 2 - smilePadding / 2),
    radius = smilePadding / 4
    )



    // 右眼
    drawCircle(
    color = Color.White,
    center = Offset(smilePadding * 3, size.height / 2 - smilePadding / 2),
    radius = smilePadding / 2
    )


    //右眼珠子
    drawCircle(
    color = Color.Black,
    center = Offset(smilePadding * 3, size.height / 2 - smilePadding / 2),
    radius = smilePadding / 4
    )


    整个笑脸就画出来了,效果如下


    在这里插入图片描述


    二、跟随手势移动


    在实现功能之前我们需要介绍transformableanimateFloatAsStatetranslate


    transformable


    transformablemodifier用于平移、缩放和旋转的多点触控手势的修饰符,此修饰符本身不会转换元素,只会检测手势。


    animateFloatAsState


    animateFloatAsState 是通过Float状态变化来控制动画 的状态


    知道了这两个玩意过后我们就可以先通过transformable监听手势滑动然后通过translate方法和animateFloatAsState方法组成一个平移动画来实现眼珠跟随手势移动


    完整的代码:


    @Composable
    fun SmileyFaceCanvas(
    modifier: Modifier
    )
    {

    var x by remember {
    mutableStateOf(0f)
    }

    var y by remember {
    mutableStateOf(0f)
    }

    val state = rememberTransformableState { _, offsetChange, _ ->

    x = offsetChange.x
    if (offsetChange.x >50f){
    x = 50f
    }

    if (offsetChange.x < -50f){
    x=-50f
    }

    y = offsetChange.y
    if (offsetChange.y >50f){
    y= 50f
    }

    if (offsetChange.y < -50f){
    y=-50f
    }
    }

    val animTranslateX by animateFloatAsState(
    targetValue = x,
    animationSpec = TweenSpec(1000)
    )

    val animTranslateY by animateFloatAsState(
    targetValue = y,
    animationSpec = TweenSpec(1000)
    )



    Canvas(
    modifier = modifier
    .size(300.dp)
    .transformable(state = state)
    ) {



    // 脸
    drawCircle(
    Color(0xfffecd00),
    radius = size.width / 2,
    center = center
    )

    // 微笑
    val smilePadding = size.width / 4

    drawArc(
    color = Color(0xffb57700),
    startAngle = 0f,
    sweepAngle = 180f,
    useCenter = true,
    topLeft = Offset(smilePadding, size.height / 2 - smilePadding / 2),
    size = Size(size.width / 2, size.height / 4)
    )

    // 左眼
    drawCircle(
    color = Color.White,
    center = Offset(smilePadding, size.height / 2 - smilePadding / 2),
    radius = smilePadding / 2
    )

    translate(left = animTranslateX, top = animTranslateY) {
    //左眼珠子
    drawCircle(
    color = Color.Black,
    center = Offset(smilePadding, size.height / 2 - smilePadding / 2),
    radius = smilePadding / 4
    )
    }


    // 右眼
    drawCircle(
    color = Color.White,
    center = Offset(smilePadding * 3, size.height / 2 - smilePadding / 2),
    radius = smilePadding / 2
    )

    translate(left = animTranslateX, top = animTranslateY) {
    //右眼珠子
    drawCircle(
    color = Color.Black,
    center = Offset(smilePadding * 3, size.height / 2 - smilePadding / 2),
    radius = smilePadding / 4
    )
    }


    }
    }

    为了不让眼珠子从眼眶里蹦出来,我们将位移的范围限制在了50以内,运行效果如下


    在这里插入图片描述




    总结


    通过Canvas中的一些方法配合简单的动画API实

    作者:我怀里的猫
    来源:juejin.cn/post/7272550100139098170
    现了这个眼珠跟随手势移动的笑脸😁

    收起阅读 »

    pdf为什么不能被修改

    web
    PDF简介 PDF是Portable Document Format 的缩写,可翻译为“便携文件格式”,由Adobe System Incorporated 公司在1992年发明。 PDF文件是一种编程形式的文档格式,它所有显示的内容,都是通过相应的操作符进...
    继续阅读 »

    PDF简介



    • PDF是Portable Document Format 的缩写,可翻译为“便携文件格式”,由Adobe System Incorporated 公司在1992年发明。

    • PDF文件是一种编程形式的文档格式,它所有显示的内容,都是通过相应的操作符进行绘制的。

    • PDF基本显示单元包括:文字,图片,矢量图,图片

    • PDF扩展单元包括:水印,电子署名,注释,表单,多媒体,3D

    • PDF动作单元:书签,超链接(拥有动作的单元有很多个,包括电子署名,多媒体等等)


    PDF的优点



    • 一致性:在所有可以打开PDF的机器上,展示的效果是完全一致,不会出现段落错乱、文字乱码这些排版问题。尤其是文档中,本身可以嵌入字体,避免了客户端没有对应字体,而导致文字显示不一致的问题。所以,在印刷行业,绝大多数用的都是PDF格式。

    • 不易修改:用过PDF文件的人,都会知道,对已经保存之后的PDF文件,想要进行重新排版,基本上就不可能的,这就保证了从资料源发往外界的资料,不容易被篡改。

    • 安全性:PDF文档可以进行加密,包括以下几种加密形式:文档打开密码,文档权限密码,文档证书密码,加密的方法包括:RC4,AES,通过加密这种形式,可以达到资料防扩散等目的。

    • 不失真:PDF文件中,使用了矢量图,在文件浏览时,无论放大多少倍,都不会导致使用矢量图绘制的文字,图案的失真。

    • 支持多种压缩方式:为了减少PDF文件的size,PDF格式支持各种压缩方式:asciihex,ascii85,lzw,runlength,ccitt,jbig2,jpeg(DCT),jpeg2000(jpx)

    • 支持多种印刷标准:支持PDF-A,PDF-X


    PDF格式


    根据PDF官方指南,理解PDF格式可以从四个方面下手——Objects(对象)、File structure(物理文件结构)、Document structure(逻辑文件结构)、Content streams(内容流)。


    对象


    物理文件结构




    • 整体上分为文件头(Header)、对象集合(Body)、交叉引用表(Xref table)、文件尾(Trailer)四个部分,结构如图。修改过的PDF结构会有部分变化。




    • 未经修改






    编辑


    img




    • 经修改






    编辑


    img


    文件头



    • 文件头是PDF文件的第一行,格式如下:


    %PDF-1.7

    复制



    • 这是个固定格式,表示这个PDF文件遵循的PDF规范版本,解析PDF的时候尽量支持高版本的规范,以保证支持大多数工具生成的PDF文件。1.7版本支持1.0-1.7之间的所有版本。


    对象集合



    • 这是一个PDF文件最重要的部分,文件中用到的所有对象,包括文本、图象、音乐、视频、字体、超连接、加密信息、文档结构信息等等,都在这里定义。格式如下:


    2 0 obj
    ...
    end obj

    复制



    • 一个对象的定义包含4个部分:前面的2是对象序号,其用来唯一标记一个对象;0是生成号,按照PDF规范,如果一个PDF文件被修改,那这个数字是累加的,它和对象序号一起标记是原始对象还是修改后的对象,但是实际开发中,很少有用这种方式修改PDF的,都是重新编排对象号;obj和endobj是对象的定义范围,可以抽象的理解为这就是一个左括号和右括号;省略号部分是PDF规定的任意合法对象。

    • 可以通过R关键字来引用任何一个对象,比如要引用上面的对象,可以使用2 0 R,需要主意的是,R关键字不仅可以引用一个已经定义的对象,还可以引用一个并不存在的对象,而且效果就和引用了一个空对象一样。

    • 对象主要有下面几种

    • booleam 用关键字true或false表示,可以是array对象的一个元素,或dictionary对象的一个条目。也可以用在PostScript计算函数里面,做为if或if esle的一个条件。

    • numeric


    包括整形和实型,不支持非十进制数字,不支持指数形式的数字。例: 1)整数 123 4567 +111 -2 范围:正2的31次方-1到负的2的31次方 2)实数 12.3 0.8 +6.3 -4.01 -3. +.03 范围:±3.403 ×10的38次方 ±1.175 × 10的-38次方



    • 注意:如果整数超过表示范围将转化成实数,如果实数超过范围就会出错

    • string


    由一系列0-255之间的字节组成,一个string总长度不能超过65535.string有以下两种方式:



    • 十六进制字串 由<>包含起来的一个16进制串,两位表示一个字符,不足两位用0补齐。例: \ 表示AA和BB两个字符 \ 表示AA和B0两个字符

    • 直接字串 由()包含起来的一个字串,中间可以使用转义符"/"。例:(abc) 表示abc (a//) 表示a/ 转义符的定义如下:


    转义字符含义
    /n换行
    /r回车
    /t水平制表符
    /b退格
    /f换页(Form feed (FF))
    /(左括号
    /)右括号
    //反斜杠
    /ddd八进制形式的字符



    • 对象类别(续)




    • name 由一个前导/和后面一系列字符组成,最大长度为127。和string不同的是,name是不可分割的并且是唯一的,不可分割就是说一个name对象就是一个原子,比如/name,不能说n就是这个name的一个元素;唯一就是指两个相同的name一定代表同一个对象。从pdf1.2开始,除了ascii的0,别的都可以用一个#加两个十六进制的数字表示。例: /name 表示name /name#20is 表示name is /name#200 表示name 0




    • array 用[]包含的一组对象,可以是任何pdf对象(包括array)。虽然pdf只支持一维array,但可以通过array的嵌套实现任意维数的array(但是一个array的元素不能超过8191)。例:[549 3.14 false (Ralph) /SomeName]




    • Dictionary 用"<<"和">>"包含的若干组条目,每组条目都由key和value组成,其中key必须是name对象,并且一个dictionary内的key是唯一的;value可以是任何pdf的合法对象(包括dictionary对象)。例: << /IntegerItem 12 /StringItem (a string) /Subdictionary << /Item1 0.4 /Item2 true /LastItem (not!) /VeryLastItem (OK) >> >>




    • stream 由一个字典和紧跟其后面的一组关键字stream和endstream以及这组关键字中间包含一系列字节组成。内容和string很相似,但有区别:stream可以分几次读取,分开使用不同的部分,string必须作为一个整体一次全部读取使用;string有长度限制,但stream却没有这个限制。一般较大的数据都用stream表示。需要注意的是,stream必须是间接对象,并且stream的字典必须是直接对象。从1.2规范以后,stream可以以外部文件形式存在,这种情况下,解析PDF的时候stream和endstream之间的内容就被忽略掉。例: dictionary stream…data…endstreamstream字典中常用的字段如下: 字段名类型值Length整形(必须)关键字stream和endstream之间的数据长度,endstream之前可能会有一个多余的EOL标记,这个不计算在数据的长度中。Filter名字 或 数组(可选)Stream的编码算法名称(列表)。如果有多个,则数组中的编码算法列表顺序就是数据被编码的顺序。DecodeParms字典 或 数组(可选)一个参数字典或由参数字典组成的一个数组,供Filter使用。如果仅有一个Filter并且这个Filter需要参数,除非这个Filter的所有参数都已经给了默认值,否则的话 DecodeParms必须设置给Filter。如果有多个Filter,并且任意一个Filter使用了非默认的参数, DecodeParms 必须是个数组,每个元素对应一个Filter的参数列表(如果某个Filter无需参数或所有参数都有了默认值,就用空对象代替)。如果没有Filter需要参数,或者所有Filter的参数都有默认值,DecodeParms 就被忽略了。F文件标识(可选)保存stream数据的文件。如果有这个字段, stream和endstream就被忽略,FFilter将会代替Filter, FDecodeParms将代替DecodeParms。Length字段还是表示stream和endstream之间数据的长度,但是通常此刻已经没有数据了,长度是0.FFilter名字 或 字典(可选)和filter类似,针对外部文件。FDecodeParms字典 或 数组(可选)和DecodeParams类似,针对外部文件。




    • Stream的编码算法名称(列表)。如果有多个,则数组中的编码算法列表顺序就是数据被编码的顺序。且需要被编码。编码算法主要如下:






    编辑切换为居中


    img


    编码可视化主要显示为乱码,所以提供了隐藏信息的机会,如下图的steam内容为乱码。




    编辑切换为居中


    img



    • NULL 用null表示,代表空。如果一个key的值为null,则这个key可以被忽略;如果引用一个不存在的object则等价于引用一个空对象。


    交叉引用表



    • 交叉引用表是PDf文件内部一种特殊的文件组织方式,可以很方便的根据对象号随机访问一个对象。其格式如下:


    xref
    0 1
    0000000000 65535 f
    4 1
    0000000009 00000 n
    8 3
    0000000074 00000 n
    0000000120 00000 n
    0000000179 00000 n

    复制



    • 其中,xref是开始标志,表示以下为一个交叉引用表的内容;每个交叉引用表又可以分为若干个子段,每个子段的第一行是两个数字,第一个是对象起始号,后面是连续的对象个数,接着每行是这个子段的每个对象的具体信息——每行的前10个数字代表这个这个对象相对文件头的偏移地址,后面的5位数字是生成号(用于标记PDF的更新信息,和对象的生成号作用类似),最后一位f或n表示对象是否被使用(n表示使用,f表示被删除或没有用)。上面这个交叉引用表一共有3个子段,分别有1个,1个,3个对象,第一个子段的对象不可用,其余子段对象可用。


    文件尾



    • 通过trailer可以快速的找到交叉引用表的位置,进而可以精确定位每一个对象;还可以通过它本身的字典还可以获取文件的一些全局信息(作者,关键字,标题等),加密信息,等等。具体形式如下:


    trailer
    <<
    key1 value1
    key2 value2
    key3 value3

    >>
    startxref
    553
    %%EOF

    复制



    • trailer后面紧跟一个字典,包含若干键-值对。具体含义如下:


    值类型值说明
    Size整形数字所有间接对象的个数。一个PDF文件,如果被更新过,则会有多个对象集合、交叉引用表、trailer,最后一个trailer的这个字段记录了之前所有对象的个数。这个值必须是直接对象。
    Prev整形数字当文件有多个对象集合、交叉引用表和trailer时,才会有这个键,它表示前一个相对于文件头的偏移位置。这个值必须是直接对象。
    Root字典Catalog字典(文件的逻辑入口点)的对象号。必须是间接对象。
    Encrypt字典文档被保护时,会有这个字段,加密字典的对象号。
    Info字典存放文档信息的字典,必须是间接对象。
    ID数组文件的ID


    • 上面代码中的startxref:后面的数字表示最后一个交叉引用表相对于文件起始位置的偏移量

    • %%EOF:文件结束符


    逻辑文件结构




    编辑切换为居中


    img


    catalog根节点



    • catalog是整个PDF逻辑结构的根节点,这个可以通过trailer的Root字段定位,虽然简单,但是相当重要,因为这里是PDF文件物理结构和逻辑结构的连接点。Catalog字典包含的信息非常多,这里仅就最主要的几个字段做个说明。 字段类型值Typename(必须)只能为Pages 。Parentdictionary(如果不是catalog里面指定的跟节点,则必须有,并且必须是间接对象) 当前节点的直接父节点。Kidsarray(必须)一个间接对象组成的数组,节点可能是page或page tree。Countinteger(必须) page tree里面所包含叶子节点(page 对象)的个数。从以上字段可以看出,Pages最主要的功能就是组织所有的page对象。Page对象描述了一个PDF页面的属性、资源等信息。Page对象是一个字典,它主要包含一下几个重要的属性:

    • Pages字段 这是个必须字段,是PDF里面所有页面的描述集合。Pages字段本身是个字典,它里面又包含了一下几个主要字段:


    字段类型
    Typename(必须)必须是Page。
    Parentdictionary(必须;并且只能是间接对象)当前page节点的直接父节点page tree 。
    LastModifieddate(如果存在PieceInfo字段,就必须有,否则可选)记录当前页面被最后一次修改的日期和时间。
    Resourcesdictionary(必须; 可继承)记录了当前page用到的所有资源。如果当前页不用任何资源,则这是个空字典。忽略所有字段则表示继承父节点的资源。
    MediaBoxrectangle(必须; 可继承)定义了要显示或打印页面的物理媒介的区域(default user space units)
    CropBoxrectangle(可选; 可继承)定义了一个可视区域,当前页被显示或打印的时候,它的内容会被这个区域裁剪。默认值就是 MediaBox。
    BleedBoxrectangle(可选) 定义了一个区域,当输出设备是个生产环境( production environment)的时候,页面显示的内容会被裁剪。默认值是 CropBox.
    Contentsstream or array(可选) 描述页面内容的流。如果这个字段缺省,则页面上什么也不会显示。这个值可以是一个流,也可以是由几个流组成的一个数组。如果是数组,实际效果相当于所有的流是按顺序连在一起的一个流,这就允许PDF生成的时候可以随时插入图片或其他资源。流之间的分割只是词汇上的一个分割,并不是逻辑上或者组织形式的切割。
    Rotateinteger(可选; 可继承) 顺时钟旋转的角度数,这个必须是90的整数倍,默认是0。
    Thumbstream(可选)定义当前页的缩略图。
    Annotsarray(可选) 和当前页面关联的注释。
    Metadatastream(可选) 当前页包含的元数据。


    • 一个简单例子:


    3 0 obj
    << /Type /Page
    /Parent 4 0 R
    /MediaBox [ 0 0 612 792 ]
    /Resources <</Font<<
    /F3 7 0 R /F5 9 0 R /F7 11 0 R
    >>
    /ProcSet [ /PDF ]
    >>
    /
    Contents 12 0 R
    /Thumb 14 0 R
    /Annots [ 23 0 R 24 0 R]
    >>
    endobj

    复制



    • Outlines字段 Outline是PDF里面为了方便用户从PDF的一部分跳转到另外一部分而设计的,有时候也叫书签(Bookmark),它是一个树状结构,可以直观的把PDF文件结构展现给用户。用户可以通过鼠标点击来打开或者关闭某个outline项来实现交互,当打开一个outline时,用户可以看到它的所有子节点,关闭一个outline的时候,这个outline的所有子节点会自动隐藏。并且,在点击的时候,阅读器会自动跳转到outline对应的页面位置。Outlines包含以下几个字段: 字段类型值Typename(可选)如果这个字段有值,则必须是Outlines。Firstdictionary(必须;必须是间接对象) 第一个顶层Outline item。Lastdictionary(必须;必须是间接对象)最后一个顶层outline item。Countinteger(必须)outline的所有层次的item的总数。

    • Outline是一个管理outline item的顶层对象,我们看到的,其实是outline item,这个里面才包含了文字、行为、目标区域等等。一个outline item主要有一下几个字段: 字段类型值Titletext string(必须)当前item要显示的标题。Parentdictionary(必须;必须是间接对象) outline层级中,当前item的父对象。如果item本身是顶级item,则父对象就是它本身。Prevdictionary(除了每层的第一个item外,其他item必须有这个字段;必须是间接对象)当前层级中,此item的前一个item。Nextdictionary(除了每层的最后一个item外,其他item必须有这个字段;必须是间接对象)当前层级中,此item的后一个item。Firstdictionary(如果当前item有任何子节点,则这个字段是必须的;必须是间接对象) 当前item的第一个直接子节点。Lastdictionary(如果当前item有任何子节点,则这个字段是必须的;必须是间接对象) 当前item的最后一个直接子节点。Destname,byte string, or array(可选; 如果A字段存在,则这个不能被会略)当前的outline item被激活的时候,要显示的区域。Adictionary(可选; 如果Dest 字段存在,则这个不能被忽略)当前的outline item被激活的时候,要执行的动作。

    • URI字段 URI(uniform resource identifier),定义了文档级别的统一资源标识符和相关链接信息。目录和文档中的链接就是通过这个字段来处理的.

    • Metadata字段 文档的一些附带信息,用xml表示,符合adobe的xmp规范。这个可以方便程序不用解析整个文件就能获得文件的大致信息。

    • 其他 Catalog字典中,常用的字段一般有以下一些:


    字段类型
    Typename(必须)必须为Catalog。
    Versionname(可选)PDF文件所遵循的版本号(如果比文件头指定的版本号高的话)。如果这个字段缺省或者文件头指定的版本比这里的高,那就以文件头为准。一个PDF生成程序可以通过更新这个字段的值来修改PDF文件版本号。
    Pagesdictionary(必须并且必须为间接对象)当前文档的页面集合入口。
    PageLabelsnumber tree(可选) number tree,定义了页面和页面label对应关系。
    Namesdictionary(可选)文档的name字典。
    Destsdictionary(可选;必须是间接对象)name和相应目标对应关系字典。
    ViewerPreferencesdictionary(可选)阅读参数配置字典,定义了文档被打开时候的行为。如果缺省,则使用阅读器自己的配置。
    PageLayoutname(可选) 指定文档被打开的时候页面的布局方式。SinglePageDisplay 单页OneColumnDisplay 单列TwoColumnLeftDisplay 双列,奇数页在左TwoColumnRightDisplay 双列,奇数页在右TwoPageLeft 双页,奇数页在左TwoPageRight 双页,奇数页在右缺省值: SinglePage.
    PageModename(可选) 当文档被打开时,指定文档怎么显示Use 目录和缩略图都不显示UseOutlines 显示目录UseThumbs 显示缩略图FullScreen 全屏模式,没有菜单,任何其他窗口UseOC 显示Optional content group 面板UseAttachments显示附件面板缺省值: Use.
    Outlinesdictionary(可选;必须为间接对象)文档的目录字典
    Threadsarray(可选;必须为间接对象)文章线索字典组成的数组。
    OpenActionarray or dictionary(可选) 指定一个区域或一个action,在文档打开的时候显示(区域)或者执行(action)。如果缺省,则会用默认缩放率显示第一页的顶部。
    AAdictionary(可选)一个附加的动作字典,在全局范围内定义了响应各种事件的action。
    URIdictionary(可选)一个URI字典包含了文档级别的URI action信息。
    AcroFormdictionary(可选)文档的交互式form (AcroForm)字典。
    Metadatastream(可选;必须是间接对象)文档包含的元数据流。

    具体组成


    1 Header部分


    PDF文件的第一行应是由5个字符“%PDF-”后跟“1.N”的版本号组成的标题,其中N是0到7之间的数字。例如下面的:


    %PDF–1.0   %PDF–1.1   %PDF–1.2   %PDF–1.3   %PDF–1.4   %PDF–1.5   %PDF–1.6   %PDF–1.7


    从PDF 1.4开始,应使用文档目录字典中的Version 条目(通过文件Trailer部分的Root条目指定版本),而不是标题中指定的版本。


    2 Body部分


    PDF文件的正文应由表示文件内容的一系列间接对象组成,例如字体、页面和采样图像。从PDF 1.5开始,Body还可以包含对象流,每个对象流包含一系列间接对象。例如下面这样:


    1 0 obj
    << /Type /Catalog
      /Outlines 2 0 R
      /Pages 3 0 R
    >>
    endobj
    2 0 obj
    << /Type Outlines
      /Count 0
    >>
    endobj
    3 0 obj
    << /Type /Pages
    /Kids [4 0 R]
    /Count 1
    >>
    endobj
    4 0 obj
    << /Type /Page
      /Parent 3 0 R
      /MediaBox [0 0 612 792]
      /Contents 5 0 R
      /Resources << /ProcSet 6 0 R >>
    >>
    endobj
    5 0 obj
    << /Length 35 >>
    stream
      …Page-marking operators…
    endstream
    endobj
    6 0 obj
    [/PDF]
    endobj

    3 Cross-Reference Table 交叉引用表部分


    交叉引用表包含文件中间接对象的信息,以便允许对这些对象进行随机访问,因此无需读取整个文件即可定位任何特定对象。


    交叉引用表以xref开始,紧接着是一个空格隔开的两个数字,然后每一行就是一个对象信息:


    xref
    0 7
    0000000000 65535 f
    0000000009 00000 n
    0000000074 00000 n
    0000000120 00000 n
    0000000179 00000 n
    0000000300 00000 n
    0000000384 00000 n

    上面第二行中的两个数字“0 7”,0表示下面的对象从0号对象开始,7表示对象的数量,也就是说表示从0到6共7个对象。


    每行一个对象信息的格式如下:


    nnnnnnnnnn ggggg n eol


    • nnnnnnnnnn 长度10个字节,表示对象在文件的偏移地址;

    • ggggg 长度5个字节,表示对象的生成号;

    • n (in-use)表示对象被引用,如果此值是f (free),表示对象未被引用;

    • eol 就是回车换行


    交叉引用表中的第一个编号为0的对象始终是f(free)的,并且生成号为65535;除了编号0的对象外,交叉引用表中的所有对象最初的生成号应为0。删除间接对象时,应将其交叉引用条目标记为“free”,并将其添加到free条目的链表中。下次创建具有该对象编号的对象时,条目的生成号应增加1,最大生成号为65535;当交叉引用条目达到此值时,它将永远不会被重用。


    交叉引用表也可以是这样的:


    xref
    0 1
    0000000000 65535 f
    3 1
    0000025325 00000 n
    23 2
    0000025518 00002 n
    0000025635 00000 n
    30 1
    0000025777 00000 n

    [


    4 Trailer部分


    PDF阅读器是从PDF的尾部开始解析文件的,通过Trailer部分能够快速找到交叉引用表和某些特殊对象。如下所示:


    trailer
    << /Size 7
    /Root 1 0 R
    >>
    startxref
    408
    %%EOF

    文件的最后一行应仅包含文件结束标记%%EOF。关键字startxref下面的数字表示最后一个交叉引用表的xref关键字开头的字节偏移量。trailer和startxref之间是尾部字典,由包含在双尖括号(<<…>>)中的键值对组成。


    为什么不容易被修改



    1. 文件结构和编码:PDF文件采用了一种复杂的文件结构和编码方式,这使得在未经授权的情况下修改PDF文件变得非常困难。PDF文件采用二进制格式存储,而不是像文本文件那样以可读的形式存储。这导致无法直接编辑和修改PDF文件,需要使用特定的软件或工具。

    2. 加密和安全特性:PDF文件可以使用密码进行加密和保护,以确保只有授权的用户才能进行修改。加密可以防止未经授权的访问和修改,使得修改PDF文件变得更加困难和复杂。

    3. 文件签名和验证:PDF文件可以使用数字签名进行验证,以确保文件的完整性和可信性。数字签名可以证明文件的来源和真实性,一旦数字签名验证失败,即表明文件已被篡改。

    4. 版本兼容性和规范:PDF格式被国际标准化组织(ISO)制定为ISO 32000标准。这个标准确保了不同版本和软件之间的PDF文件的兼容性,并定义了丰富的功能和规范,包括页面布局、字体嵌入、图形和图像处理等。这些严格的规范使得对PDF文件进行修改变得复杂和具有挑战性。


    如何修改pdf


    因为pdf的局限性是无法进行修改的,所以我们只能通过将他转换为其他类型的文件进行查看修改,当然,转换的过程不可能是百分百完美的进行转换的。


    下面推荐这俩个可以进行转换的网站


    PDF转换成Word在线转换器 - 免费 - CleverPDF


    Convert PDF to Word for free |

    Smallpdf.com

    收起阅读 »

    你会用nginx部署前端项目吗

    web
    前端项目的部署以前一直是把静态资源放到后端工程中,随后端部署一起部署。随着前后端分离开发模式的流行,前端项目可以单独部署了,目前最流行的方式使用nginx来部署。 对于前端项目来说,nginx主要有两个功能: 对静态资源做托管,即作为一个静态资源服务器; 对...
    继续阅读 »

    前端项目的部署以前一直是把静态资源放到后端工程中,随后端部署一起部署。随着前后端分离开发模式的流行,前端项目可以单独部署了,目前最流行的方式使用nginx来部署。


    对于前端项目来说,nginx主要有两个功能:



    • 对静态资源做托管,即作为一个静态资源服务器

    • 对动态资源做反向代理,即代理后台接口服务,防止跨域


    路由配置


    nginx配置最多就是路由配置,路由配置又有几种写法。


    1. =


    location = /111/ {
    default_type text/plain;
    return 200 "111 success";
    }

    location 和路径之间加了个 =,代表精准匹配,也就是只有完全相同的 url 才会匹配这个路由。


    image.png


    在路径后面添加了aa,那么就不是精确匹配了,所以是404


    image.png


    2. 不带 =


    代表根据前缀匹配,后面可以是任意路径


    location /222 {
    default_type text/plain;
    // 这里的 $uri 是取当前路径。
    return 200 $uri;
    }

    image.png


    3. 支持正则匹配~


    // 匹配以/333/bbb开头,以.html结尾的路径
    location ~ ^/333/bbb.*\.html$ {
    default_type text/plain;
    return 200 $uri;
    }

    image.png


    上面的~是区分大小写的,如果不区分大小写是~*


    // 匹配以/333/bbb开头,以.html结尾的路径
    location ~* ^/333/bbb.*\.html$ {
    default_type text/plain;
    return 200 $uri;
    }

    4. ^~代表优先级


    下面的配置有两个路径是以/444开头的:


    location ~* ^/444/AAA.*\.html$ {
    default_type text/plain;
    return 200 $uri;
    }
    location ~ /444/ {
    default_type text/plain;
    return 200 $uri;
    }

    如果访问/444/AAA45.html,就会直接命中第一个路由,如果我想命中/444/呢? 加上^就好了。


    location ^~ /444/ {
    default_type text/plain;
    return 200 $uri;
    }

    也就是说 ^~ 能够提高前缀匹配的优先级。


    总结一下,location 语法一共有四种:




    1. location = /aaa 是精确匹配 /aaa 的路由;




    2. location /bbb 是前缀匹配 /bbb 的路由。




    3. location ~ /ccc.*.html 是正则匹配,可以再加个 * 表示不区分大小写 location ~* /ccc.*.html;




    4. location ^~ /ddd 是前缀匹配,但是优先级更高。




    这 4 种语法的优先级是这样的:


    精确匹配(=) > 高优先级前缀匹配(^~) > 正则匹配(~ / ~*) > 普通前缀匹配


    root 与 alias


    nginx指定文件路径有两种方式rootaliasrootalias主要区别在于nginx如何解释location后面的uri,这会使两者以不同的方式将请求映射到服务器文件上。



    1. root的处理结果是:root路径 + location路径;

    2. alias的处理结果是:使用alias路径替换location路径;


    alias是一个目录别名的定义,root则是最上层目录的定义。


    需要注意的是alias后面必须要用/结束,否则会找不到文件的,而root则可有可无。另外,alias只能位于location块中。


    root示例:


    location ^~ /test/ {
    root /www/root/html/;
    }

    如果一个请求的 uri 是 /test/a.html时,web服务器将会返回服务器上的/www/root/html/test/a.html的文件。


    alias示例:


    location ^~ /test/ {
    alias /www/root/html/new_test/;
    }

    如果一个请求的 uri 是 /test/a.html 时,web服务器将会返回服务器上的/www/root/html/new_test/a.html的文件。


    注意, 这里是new_test,因为alias会把location后面配置的路径丢弃掉,把当前匹配到的目录指向到指定的目录。


    二级目录


    有时候需要在一个端口下,部署多个项目,那么这时可以采用二级目录的形式来部署。


    采用二级目录来部署会有一些坑,比如,当我请求 http://xxxxxxxx.com/views/basedata 的时候,浏览器自动跳转到了http://xxxxxxxxm:8100/store/views/basedata/


    这是什么原因呢?


    最根本的问题是因为http://xxxxxxxx.com/views/basedata后面没有/,所以就触发了nginx301重定向,重定向到了http://xxxxxxxxm:8100/store/views/basedata/,因此,只要避免触发重定向即可。


    如果你能忍受直接使用如 http://xxxxxxxxm:8100/store/views/basedata/ 这样最后带/的地址,那也就没有什么问题了。


    那为什么会触发重定向呢?


    当用户请求 http.xxxxxx.cn/osp 时,这里的 $uri 就是 /ospnginx 会尝试到alias或 root 指定的目录下找这个文件。


    如果存在名为 {alias指定目录}/osp 的文件,注意这里是文件,不是目录,就直接把这个文件的内容发送给用户。


    很显然,目录中没有叫 osp 的文件,于是就看 osp/,增加了一个 /,也就是看有没有名为 {alias指定目录}/osp/ 的目录。


    即,当我们访问 uri 时,如果访问资源为一个目录,并且 uri 没有以正斜杠 / 结尾,那么nginx 服务就会返回一个301跳转,目标地址就是要加一个正斜杠/


    一种最简单的方式就是直接访问一个具体的文件,如 http.xxxxxx.cn/osp/index.html,这样就不会发生重定向了。但是,这样方式不够优雅,每次都要输入完整的文件路径。


    另一种方式是调整 nginx 中关于重定向的配置,nginx 重定向中的三个配置:absolute_redirectserver_name_in_redirectport_in_redirect


    absolute_redirect通过该指令控制 nginx 发出的重定向地址是相对地址还是绝对地址:


    1、如果设置为 off,则 nginx 发出的重定向将是相对的,没有域名和端口, 也就没有server_name_in_redirectport_in_redirect什么事儿了。


    image.png


    加了这个配置后,尽管也会发生重定向,但是不会在路径上加上域名和端口了。


    2、如果设置为 on,则 nginx 发出的重定向将是绝对的;只有 absolute_redirect 设置为 onserver_name_in_redirectport_in_redirect 的设置才有作用。


    image.png


    nginx 开启 gzip 静态压缩提升效率


    gzip 是一种格式,也是一种 linux 下的解压缩工具,我们使用 gzipapp.js 文件压缩后,原始文件就变为了以.gz结尾的文件,同时文件大小从 42571 减小到 11862。


    image.png


    目前,对静态资源压缩有两种形式:



    • 动态压缩: 服务器在返回任何的静态文件之前,由服务器对每个请求压缩在进行输出。

    • 静态压缩:服务器直接使用现成的扩展名为 .gz 的预压缩文件,直接进行输出。


    我们知道 gzipCPU 密集型的,实时动态压缩比较消耗 CPU 资源。为进一步提高 nginx 的性能,我们可以使用静态 gzip 压缩,提前将需要压缩的文件压缩好,当请求到达时,直接发送压缩好的.gz文件,如此就减轻了服务器 CPU 的压力,提高了性能。


    因此,我们一般采用静态压缩的方式,实现静态压缩有以下两个步骤:


    1. 生成gzip压缩文件


    在利用webpack打包的时候,我们就把文件进行压缩,配置如下:


    const isProduction = process.env.NODE_ENV === 'production'

    if (isProduction) {
    config.plugins.push(
    new CompressionWebpackPlugin({
    // 采用gzip进行压缩
    algorithm: 'gzip',
    test: /\.js$|\.html$|\.json$|\.css/,
    threshold: 10240
    })
    )
    }

    可以看到,多生成了一个以.gz结尾的文件,然后把.gz后缀的文件上传到服务器中即可。


    image.png


    2. 在 nginx 开启支持静态压缩的模块


    nginx配置中加上如下配置:


    gzip_static on;

    如果不加的话,访问的时候就会找不到,报404错误,因为服务器只有.gz的文件,没有原始文件。


    总结


    前端项目nginx部署主要的配置基本上就是上面提到的这些。


    首先是location路由的四种写法;


    接着就是分清楚rootalias的区别;


    当项目较多时需要使用二级路由时,需要注意重定向的配置;


    如果你的项目文件较大,可以开启gzip

    作者:小p
    来源:juejin.cn/post/7270902621065560120
    压缩提升传输效率。

    收起阅读 »

    坏了!要长脑子了...这么多前端框架没听过

    web
    市面上有很多不同的 JavaScript 框架,很难对它们一一进行跟踪。在本文中,我们将重点介绍最受欢迎的几种,并探讨开发人员喜欢或不喜欢它们的原因。 React 官网链接 React 是一个用于构建用户界面的 JavaScript 库。它由 Faceboo...
    继续阅读 »

    市面上有很多不同的 JavaScript 框架,很难对它们一一进行跟踪。在本文中,我们将重点介绍最受欢迎的几种,并探讨开发人员喜欢或不喜欢它们的原因。


    React



    官网链接


    React 是一个用于构建用户界面的 JavaScript 库。它由 Facebook 和一个由个人开发者和公司组成的社区维护。React 可以作为开发单页或移动应用程序的基础。然而,React 只关心将数据呈现给 DOM,因此创建 React 应用程序通常需要使用额外的库来进行状态管理、路由和与 API 的交互。React 还用于构建可重用的 UI 组件。从这个意义上讲,它的工作方式很像 Angular 或 Vue 等 JavaScript 框架。然而,React 组件通常以声明式方式编写,而不是使用命令式代码,这使得它们更容易阅读和调试。正因为如此,许多开发人员更喜欢使用 React 来构建 UI 组件,即使他们不使用它作为整个前端框架。


    优点:



    • React 快速而高效,因为它使用虚拟 DOM 而不是操纵真实的 DOM。

    • React 很容易学习,因为它的声明性语法和清晰的文档。

    • React 组件是可重用的,使代码维护更容易。


    缺点:



    • React 有一个很大的学习曲线,因为它是一个复杂的 JavaScript 库。

    • React 不是一个成熟的框架,因此它需要使用额外的库来完成许多任务。


    Next.js



    官网链接


    Next.js 是一个 javascript 库,支持 React 应用程序的服务器端渲染。这意味着 next.js 可以在将 React 应用程序发送到客户端之前在服务器上渲染它。这有几个好处。首先,它允许您预呈现组件,以便当用户请求它们时,它们已经在客户机上可用。其次,它允许爬虫更容易地索引你的内容,从而为你的 React 应用程序提供更好的 SEO。最后,它可以通过减少客户机为呈现页面而必须执行的工作量来提高性能。


    以下是开发者喜欢 Next.js 的原因:




    • js 使得无需做任何配置就可以轻松地开始服务器端渲染。




    • js 会自动对应用程序进行代码拆分,以便每个页面只在被请求时加载,这可以提高性能。
      缺点:




    • 如果不小心,next.js 会使应用程序代码库变得更复杂,更难以维护。




    • 一些开发人员发现 next.js 的内置特性固执己见且不灵活。




    Vue.js



    官网链接


    Vue.js 是一个用于构建用户界面和单页应用程序的开源 JavaScript 框架。与 React 和 Angular 等其他框架不同,Vue.js 被设计为轻量级且易于使用。Vue.js 库可以与其他库和框架结合使用,也可以作为创建前端 web 应用程序的独立工具使用。Vue.js 的一个关键特性是它的双向数据绑定,当模型发生变化时,它会自动更新视图,反之亦然。这使得它成为构建动态用户界面的理想选择。此外,Vue.js 还提供了许多内置功能,如模板系统、响应系统和事件总线。这些特性使创建复杂的应用程序成为可能,而不必依赖第三方库。因此,Vue.js 已经成为近年来最流行的 JavaScript 框架之一。


    优点:



    • Vue.js 很容易学习,因为它的小尺寸和清晰的文档。

    • Vue.js 组件是可重用的,这使得代码维护更容易。

    • 由于虚拟 DOM 和异步组件加载,Vue.js 应用程序非常快。


    缺点:



    • 虽然 Vue.js 很容易学习,但如果你想掌握它的所有功能,它有一个很大的学习曲线。

    • Vue.js 没有像其他框架那样多的库和工具。


    Angular



    官网链接


    Angular 是一个 JavaScript 框架,用于用 JavaScript、html 和 Typescript 构建 web 应用和应用。Angular 是由 Google 创建和维护的。Angular 提供了双向数据绑定,这样对模型的更改就会自动传播到视图。它还提供了一种声明性语法,使构建动态 ui 变得容易。最后,Angular 提供了许多有用的内置服务,比如 HTTP 请求处理,以及对路由和模板的支持。


    优点:



    • Angular 有一个庞大的社区和许多可用的库和工具。

    • Angular 很容易学习,因为它有组织良好的文档和清晰的语法。


    缺点:



    • 虽然 Angular 很容易学习,但如果你想掌握它的所有特性,它有一个很大的学习曲线。

    • Angular 不像其他一些框架那样轻量级。


    Svelte



    官网链接


    简而言之,Svelte 是一个类似于 React、Vue 或 Angular 的 JavaScript 框架。然而,这些框架使用虚拟 DOM(文档对象模型)来区分视图之间的变化,而 Svelte 使用了一种称为 DOM 区分的技术。这意味着它只更新 DOM 中已更改的部分,从而实现更高效的呈现过程。此外,Svelte 还包括一些其他框架没有的内置优化,例如自动批处理 DOM 更新和代码分割。这些特性使 Svelte 成为高性能应用程序的一个很好的选择。


    优点:




    • Svelte 有其他框架没有的内置优化,比如代码分割。




    • 由于其清晰的语法和组织良好的文档,Svelte 很容易学习。
      缺点:




    • 虽然 Svelte 很容易学习,但如果你想掌握它的所有功能,它有一个很大的学习曲线。




    • Svelte 没有像其他框架那样多的库和工具。




    Gatsby



    官网链接


    Gatsby 是一个基于 React 的免费开源框架,可以帮助开发人员构建快速的网站和应用程序。它使用尖端技术,使建立网站和应用程序的过程更加高效。它的一个关键特性是能够预取资源,以便在需要时立即可用。这使得盖茨比网站非常快速和响应。使用 Gatsby 的另一个好处是,它允许开发人员使用 GraphQL 查询来自任何来源的数据,从而使构建复杂的数据驱动应用程序变得容易。此外,Gatsby 附带了许多插件,使其更易于使用,包括用于 SEO,分析和图像优化的插件。所有这些因素使 Gatsby 成为构建现代网站和应用程序的一个非常受欢迎的选择。


    优点:




    • 由于使用了预取,Gatsby 网站的速度和响应速度非常快。




    • 由于对 GraphQL 的支持,Gatsby 使构建复杂的数据驱动应用程序变得容易。




    • Gatsby 附带了许多插件,使其更易于使用。
      缺点:




    • 虽然 Gatsby 很容易使用,但如果你想掌握它的所有功能,它有一个很大的学习曲线。




    • Gatsby 没有像其他框架那样多的库和工具。




    Nuxt.js



    官网链接


    js 是一个用于构建 JavaScript 应用程序的渐进式框架。它基于 Vue.js,并附带了一组工具和库,可以轻松创建可以在服务器端和客户端呈现的通用应用程序。js 还提供了一种处理异步数据和路由的方法,这使得它非常适合构建高度交互的应用程序。此外,nuxt.js 附带了一个 CLI 工具,可以很容易地构建新项目并构建、运行和测试它们。使用 nuxt.js,您可以创建快速、可靠和可扩展的令人印象深刻的 JavaScript 应用程序。


    优点:




    • js 易于使用和扩展。




    • 由于服务器端渲染,nuxt.js 应用程序快速响应。
      缺点:




    • 虽然 nuxt.js 很容易使用,但如果你想掌握它的所有功能,它有一个很大的学习曲线。




    • nuxt.js 没有像其他框架那样多的库和工具。




    Ember.js



    官网链接


    Ember.js 以其优于配置的约定方法而闻名,这使得开发人员更容易开始使用该框架。它还为数据持久化和路由等常见任务提供了内置库,从而加快了开发速度。尽管 Ember.js 有一个陡峭的学习曲线,但它为开发人员提供了创建富 web 应用程序的灵活性和强大功能。如果你正在寻找一个前端 JavaScript 框架来构建 spa, Ember.js 绝对值得考虑。


    优点:




    • Ember.js 使用约定而不是配置,这使得它更容易开始使用框架。




    • Ember.js 为数据持久化和路由等常见任务提供了内置库。




    • Ember.js 为开发人员创建富 web 应用程序提供了很大的灵活性和能力。
      缺点:




    • Ember.js 有一个陡峭的学习曲线。




    • Ember.js 没有像其他框架那样多的库和工具。




    Backbone.js



    官网链接


    Backbone.js 是一个轻量级 JavaScript 库,允许开发人员创建单页面应用程序。它基于模型-视图-控制器(MVC)体系结构,这意味着它将数据和逻辑从用户界面中分离出来。这使得代码更易于维护和扩展,也使创建复杂的应用程序变得更容易。Backbone.js 还包含了许多使其成为开发移动应用程序的理想选择的特性,例如将数据绑定到 HTML 元素的能力以及对触摸事件的支持。因此,对于想要创建快速响应的应用程序的开发人员来说,Backbone.js 是一个受欢迎的选择。


    优点:




    • js 是轻量级的,只是一个库,而不是一个完整的框架。




    • js 很容易学习和使用。




    • Backbone.js 具有很强的可扩展性,可以使用许多第三方库。
      缺点:




    • Backbone.js 不像其他框架那样提供那么多的内置功能。




    • 与其他一些框架相比,Backbone.js 只有一个小社区。




    结论


    总之,虽然有许多不同的 JavaScript 框架可供选择,但最流行的框架仍然相对稳定。每一种都有自己的优点和缺点,开发人员在决定在他们的项目中使用哪一种时必须权衡。虽然没有一个框架是完美的,但每个框架都有一些可以使开发更容易或更快的东西。


    在选择框架时,每个人都应该考虑他们项目的具体需求,以及他们团队的技能和他们必须投入学习新框架的时间。通过考虑所有这些因素,您可以为您的项目选择最好的 JavaScript 框架!


    参考链接:
    blog.risingstack.com/

    best-javasc…

    收起阅读 »

    虚拟dom

    vue中的虚拟dom 简介 首先vue会把模板编译成render函数,运行render函数生成虚拟dom 虚拟dom通过 diff算法 对比差异 渲染不同的部分 然后更新视图 为什么会需要虚拟dom 在主流框架 Angular , Vue.js (1.0)...
    继续阅读 »

    vue中的虚拟dom


    简介



    首先vue会把模板编译成render函数,运行render函数生成虚拟dom


    虚拟dom通过 diff算法 对比差异 渲染不同的部分 然后更新视图



    为什么会需要虚拟dom


    在主流框架 Angular , Vue.js (1.0)React 中都有一个共同点,那就是它们都不知道哪些状态(state)变了。因此就需要进行比对,在React中使用的虚拟dom比对, Angular 中使用的是脏检查的流程



    而在 Vue.js中使用的是变化侦测的方式,它在一定程度上知道具体哪些状态发生了变化,这样就可以通过更细粒度的绑定来更新视图。也就是说,在Vue.js中,当状态发生变化时,它在一定程度上知道哪些节点使用了这个状态,从而对这些节点进行更新操作,根本不需要比对



    但是这样做的代价就是,粒度太细,每一个都有对应的 watcher 来观察状态变化,这样就会浪费一些内存开销,绑定的越多开销越大,如果这运用在一个大型项目中,那么他的开销无疑是非常大的


    因此从 Vue.js 2.0 开始借鉴 React 中的虚拟DOM ,组件级别是一个watcher实例,就是说即便一个组件内有10个节点使用了某个状态,但其实也只有一个watcher在观察这个状态的变化。


    什么是虚拟dom


    虚拟DOM是通过状态生成一个虚拟节点树(vnode) ,然后使用虚拟节点树进行渲染。 在渲染之前,会使用新生成的虚拟节点树和上一次生成的虚拟节点树进行对比 (diff算法) ,只渲染不同的部分



    虚拟节点树其实是由组件树建立起来的整个虚拟节点(Virtual Node,也经常简写为vnode)树



    在Vue.js中,我们使用模板来描述状态DOM之间的映射关系。Vue.js通过编译将模板转换成渲染函数(render),执行渲染函数就可以得到一个虚拟节点树,使用这个虚拟节点树就可以渲染页面


    模板编译成render函数


    将模板编译成渲染函数可以分两个步骤,先将模板解析成AST(Abstract Syntax Tree,抽象语法树),然后再使用AST生成渲染函数。


    但是由于静态节点不需要总是重新渲染,所以在生成AST之后、生成渲染函数之前这个阶段,需要做一个操作,那就是遍历一遍AST,给所有静态节点做一个标记,这样在虚拟DOM中更新节点时,如果发现节点有这个标记,就不会重新渲染它。所以,在大体逻辑上,模板编译分三部分内容:

    • 将模板解析为AST
    • 遍历AST标记静态节点
    • 使用AST生成渲染函数

    虚拟dom做了什么


    虚拟DOM在Vue.js中所做的事情其实并没有想象中那么复杂,它主要做了两件事。

    • 提供与真实DOM节点所对应的虚拟节点vnode。
    • 将虚拟节点vnode和旧虚拟节点oldVnode进行比对,然后更新视图。

    对两个虚拟节点对比是虚拟dom 中最核心的算法 (diff),它可以判断出哪些节点发生了变化,从而只对发生了变化的节点进行更新操作


    小结


    虚拟DOM是将状态映射成视图的众多解决方案中的一种,它的运作原理是使用状态生成虚拟节点,然后使用虚拟节点渲染视图。


    为什么会需要虚拟dom

    框架设计

    Vue 和 React 框架设计理念都是基于数据驱动的,当数据发生变化时 就要去更新视图,要想知道在页面众多元素中改动数据的元素 并根据改动后的数据去更新视图 是非常困难的



    所以 Vue 和 React 中都会有一个 Render函数 或者类似于Render函数的功能,当数据变化时 全量生成Dom 元素
    如果是直接操作 真实Dom 的话 是很昂贵的,就会严重拖累效率,所以就不生成真实的Dom,而是生成虚拟的Dom
    当数据变化时就是 对象 和 对象 进行一个对比 ,这样就能知道哪些数据发生了改变 从而去操作改变的数据后的Dom元素



    这也是一个 “妥协的结果”


    跨平台

    现阶段的框架他不仅仅能在浏览器里面使用,在小程序,移动端,或者桌面端也可以使用,但是真实Dom仅仅指的是在浏览器的环境下使用,因此他不能直接生成真实Dom ,所以选择生成一个在任何环境下都能被认识的虚拟Dom
    最后根据不同的环境,使用虚拟Dom 去生成界面,从而实现跨平台的作用 --- 一套代码在多端运行


    作者:明天休息吗
    链接:https://juejin.cn/post/7258171660145868860
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    不会封装hook? 看下ahooks这6个hook是怎么做的

    1 useUpdate 在react函数组件中如何强制组件进行刷新? 虽然react没有提供原生的方法,但是我们知道当state值变化的时候,react函数组件就会刷新,所以useUpdate就是利用了这一点,源码如下:import { useCallback...
    继续阅读 »

    1 useUpdate


    在react函数组件中如何强制组件进行刷新? 虽然react没有提供原生的方法,但是我们知道当state值变化的时候,react函数组件就会刷新,所以useUpdate就是利用了这一点,源码如下:

    import { useCallback, useState } from 'react';

    const useUpdate = () => {
    const [, setState] = useState({});

    return useCallback(() => setState({}), []);
    };

    export default useUpdate;

    可以看到useUpdate的返回值函数,就是每次都用一个新的对象调用setState,触发组件的更新。


    2 useMount


    react函数组件虽然没有了mount的生命周期,但是我们还会有这种需求,就是在组件第一次渲染之后执行一次的需求,就可以封装useEffect实现这个需求, 只需要把依赖项设置成空数组,那么就只在渲染结束后,执行一次回调:

    import { useEffect } from 'react';

    const useMount = (fn: () => void) => {

    useEffect(() => {
    fn?.();
    }, []);
    };

    export default useMount;


    3 useLatest


    react函数组件是一个可中断,可重复执行的函数,所以在每次有state或者props变化的时候,函数都会重新执行,我们知道函数的作用域是创建函数的时候就固定下来的,如果其中的内部函数是不更新的,那么这些函数获取到的外部变量就是不会变的。例如:

    import React, { useState, useEffect } from 'react';
    import { useLatest } from 'ahooks';


    export default () => {
    const [count, setCount] = useState(0);

    useEffect(() => {
    const interval = setInterval(() => {
    setCount(count + 1);
    }, 1000);
    return () => clearInterval(interval);
    }, []);

    return (
    <>
    <p>count: {count}</p>
    </>
    );
    };

    这是一个定时更新count值的例子,但是上边的代码只会让count一直是1,因为setInterval中的函数在创建的时候它的作用域就定下来了,它拿到的count永远是0, 当执行了setCount后,会触发函数的重新执行,重新执行的时候,虽然count值变成了1,但是这个count已经不是它作用域上的count变量了,函数的每次执行都会创建新的环境,而useState, useRef 等这些hooks 是提供了函数重新执行后保持状态的能力, 但是对于那些没有重新创建的函数,他们的作用域就永远的停留在了创建的时刻。 如何让count正确更新, 简单直接的方法如下,在setCount的同时,也直接更新count变量,也就是直接改变这个闭包变量的值,这在JS中也是允许的。

    import React, { useState, useEffect } from 'react';
    import { useLatest } from 'ahooks';


    export default () => {
    let [count, setCount] = useState(0);

    useEffect(() => {
    const interval = setInterval(() => {
    count = count + 1
    setCount(count + 1);
    }, 1000);
    return () => clearInterval(interval);
    }, []);

    return (
    <>
    <p>count: {count}</p>
    </>
    );
    };

    setCount是为了让函数刷新,并且更新函数的count值,而直接给count赋值,是为了更新定时任务函数中维护的闭包变量。 这显然不是一个好的解决办法,更好的办法应该是让定时任务函数能够拿到函数最新的count值。
    useState返回的count每次都是新的变量,也就是变量地址是不同的,应该让定时任务函数中引用一个变量地址不变的对象,这个对象中再记录最新的count值,而实现这个功能就需要用到了useRef,它就能帮助我们在每次函数刷新都返回相同变量地址的对象, 实现方式如下:

    import React, { useState, useEffect, useRef } from 'react'

    export default () => {
    const [count, setCount] = useState(0)

    const latestCount = useRef(count)
    latestCount.current = count

    useEffect(() => {
    const interval = setInterval(() => {
    setCount(latestCount.current + 1)
    }, 1000)
    return () => clearInterval(interval)
    }, [])

    return (
    <>
    <p>count: {count}</p>
    </>
    )
    }


    可以看到定时函数获取的latestCount永远是定义时的变量,但因为useRef,每次函数执行它的变量地址都不变,并且还把count的最新值,赋值给了latestCount.current, 定时函数就可以获取到了最新的count值。
    所以这个功能可以封装成了useLatest,获取最新值的功能。

    import { useRef } from 'react';

    function useLatest<T>(value: T) {
    const ref = useRef(value);
    ref.current = value;

    return ref;
    }

    export default useLatest;


    上边的例子是为了说明useLatest的作用,但针对这个例子,只是为了给count+1,还可以通过setCount方法本身获取,虽然定时任务函数中的setCount页一直是最开始的函数,但是它的功能是可以通过传递函数的方式获取到最新的count值,代码如下:

      const [count, setCount] = useState(0)
    useEffect(() => {
    const interval = setInterval(() => {
    setCount(count=>count+1)
    }, 1000)
    return () => clearInterval(interval)
    }, [])

    4 useUnMount


    有了useMount就会有useUnmount,利用的就是useEffect的函数会返回一个cleanup函数,这个函数在组件卸载和useEffect的依赖项变化的时候触发。 正常情况 我们应该是useEffect的时候做了什么操作,返回的cleanup函数进行相应的清除,例如useEffect创建定时器,那么返回的cleanup函数就应该清除定时器:

     const [count, setCount] = useState(0);
    useEffect(() => {
    const interval = setInterval(() => {
    count = count + 1
    setCount(count + 1);
    }, 1000);
    return () => clearInterval(interval);
    }, []);

    所以useUnMount就是利用了这个cleanup函数实现useUnmount的能力,代码如下:

    import { useEffect } from 'react';
    import useLatest from '../useLatest';

    const useUnmount = (fn: () => void) => {
    const fnRef = useLatest(fn);

    useEffect(
    () => () => {
    fnRef.current();
    },
    [],
    );
    };

    export default useUnmount;


    使用了useLatest存放fn的最新值,写了一个空的useEffect,依赖是空数组,只在函数卸载的时候执行。


    5 useToggle和useBoolean


    useToggle 封装了可以state在2个值之间的变化,useBoolean则是利用了useToggle,固定2个值只能是true和false。 看下他们的源码:

    function useToggle<D, R>(defaultValue: D = false as unknown as D, reverseValue?: R) {
    const [state, setState] = useState<D | R>(defaultValue);

    const actions = useMemo(() => {
    const reverseValueOrigin = (reverseValue === undefined ? !defaultValue : reverseValue) as D | R;

    const toggle = () => setState((s) => (s === defaultValue ? reverseValueOrigin : defaultValue));
    const set = (value: D | R) => setState(value);
    const setLeft = () => setState(defaultValue);
    const setRight = () => setState(reverseValueOrigin);

    return {
    toggle,
    set,
    setLeft,
    setRight,
    };
    // useToggle ignore value change
    // }, [defaultValue, reverseValue]);
    }, []);

    return [state, actions];
    }

    可以看到,调用useToggle的时候可以设置初始值和相反值,默认初始值是false,actions用useMemo封装是为了提高性能,没有必要每次渲染都重新创建这些函数。setLeft是设置初始值,setRight是设置相反值,set是用户随意设置,toggle是切换2个值。
    useBoolean则是在useToggle的基础上进行了封装,让我们用起来对更加的简洁方便。

    export default function useBoolean(defaultValue = false): [boolean, Actions] {
    const [state, { toggle, set }] = useToggle(defaultValue);

    const actions: Actions = useMemo(() => {
    const setTrue = () => set(true);
    const setFalse = () => set(false);
    return {
    toggle,
    set: (v) => set(!!v),
    setTrue,
    setFalse,
    };
    }, []);

    return [state, actions];
    }

    总结


    本文介绍了ahooks中封装的6个简单的hook,虽然简单,但是可以通过他们的做法,学习到自定义hook的思路和作用,就是把一些能够重用的逻辑封装起来,在实际项目中我们有这个意识就可以封装出适合项目的hook。


    作者:前端良文
    链接:https://juejin.cn/post/7248073216512901178
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    你的代码不堪一击!太烂了!

    前言 小王,你的页面白屏了,赶快修复一下。小王排查后发现是服务端传回来的数据格式不对导致,无数据时传回来不是 [] 而是 null, 从而导致 forEach 方法报错导致白屏,于是告诉测试,这是服务端的错误导致,要让服务端来修改,结果测试来了一句:“服务端返...
    继续阅读 »

    前言


    小王,你的页面白屏了,赶快修复一下。小王排查后发现是服务端传回来的数据格式不对导致,无数据时传回来不是 [] 而是 null, 从而导致 forEach 方法报错导致白屏,于是告诉测试,这是服务端的错误导致,要让服务端来修改,结果测试来了一句:“服务端返回数据格式错误也不能白屏!!” “好吧,千错万错都是前端的错。” 小王抱怨着把白屏修复了。


    刚过不久,老李喊道:“小王,你的组件又渲染不出来了。” 小王不耐烦地过来去看了一下,“你这个属性data 格式不对,要数组,你传个对象干嘛呢。”老李反驳: “ 就算 data 格式传错,也不应该整个组件渲染不出来,至少展示暂无数据吧!” “行,你说什么就是什么吧。” 小王又抱怨着把问题修复了。


    类似场景,小王时不时都要经历一次,久而久之,大家都觉得小王的技术太菜了。小王听到后,倍感委屈:“这都是别人的错误,反倒成为我的错了!”


    等到小王离职后,我去看了一下他的代码,的确够烂的,不堪一击!太烂了!下面来吐槽一下。


    一、变量解构一解就报错


    优化前

    const App = (props) => {
    const { data } = props;
    const { name, age } = data
    }

    如果你觉得以上代码没问题,我只能说你对你变量的解构赋值掌握的不扎实。



    解构赋值的规则是,只要等号右边的值不是对象或数组,就先将其转为对象。由于 undefinednull 无法转为对象,所以对它们进行解构赋值时都会报错。



    所以当 dataundefinednull 时候,上述代码就会报错。


    优化后

    const App = (props) => {
    const { data } = props || {};
    const { name, age } = data || {};
    }

    二、不靠谱的默认值


    估计有些同学,看到上小节的代码,感觉还可以再优化一下。


    再优化一下

    const App = (props = {}) => {
    const { data = {} } = props;
    const { name, age } = data ;
    }

    我看了摇摇头,只能说你对ES6默认值的掌握不扎实。



    ES6 内部使用严格相等运算符(===)判断一个变量是否有值。所以,如果一个对象的属性值不严格等于 undefined ,默认值是不会生效的。



    所以当 props.datanull,那么 const { name, age } = null 就会报错!


    三、数组的方法只能用真数组调用


    优化前:

    const App = (props) => {
    const { data } = props || {};
    const nameList = (data || []).map(item => item.name);
    }

    那么问题来了,当 data123 , data || [] 的结果是 123123 作为一个 number 是没有 map 方法的,就会报错。


    数组的方法只能用真数组调用,哪怕是类数组也不行。如何判断 data 是真数组,Array.isArray 是最靠谱的。


    优化后:

    const App = (props) => {
    const { data } = props || {};
    let nameList = [];
    if (Array.isArray(data)) {
    nameList = data.map(item => item.name);
    }
    }

    四、数组中每项不一定都是对象


    优化前:

    const App = (props) => {
    const { data } = props || {};
    let infoList = [];
    if (Array.isArray(data)) {
    infoList = data.map(item => `我的名字是${item.name},今年${item.age}岁了`);
    }
    }

    一旦 data 数组中某项值是 undefinednull,那么 item.name 必定报错,可能又白屏了。


    优化后:

    const App = (props) => {
    const { data } = props || {};
    let infoList = [];
    if (Array.isArray(data)) {
    infoList = data.map(item => `我的名字是${item?.name},今年${item?.age}岁了`);
    }
    }

    ? 可选链操作符,虽然好用,但也不能滥用。item?.name 会被编译成 item === null || item === void 0 ? void 0 : item.name,滥用会导致编辑后的代码大小增大。


    二次优化后:

    const App = (props) => {
    const { data } = props || {};
    let infoList = [];
    if (Array.isArray(data)) {
    infoList = data.map(item => {
    const { name, age } = item || {};
    return `我的名字是${name},今年${age}岁了`;
    });
    }
    }

    五、对象的方法谁能调用


    优化前:

    const App = (props) => {
    const { data } = props || {};
    const nameList = Object.keys(data || {});
    }

    只要变量能被转成对象,就可以使用对象的方法,但是 undefinednull 无法转换成对象。对其使用对象方法时就会报错。


    优化后:

    const _toString = Object.prototype.toString;
    const isPlainObject = (obj) => {
    return _toString.call(obj) === '[object Object]';
    }
    const App = (props) => {
    const { data } = props || {};
    const nameList = [];
    if (isPlainObject(data)) {
    nameList = Object.keys(data);
    }
    }

    六、async/await 错误捕获


    优化前:

    import React, { useState } from 'react';

    const App = () => {
    const [loading, setLoading] = useState(false);
    const getData = async () => {
    setLoading(true);
    const res = await queryData();
    setLoading(false);
    }
    }

    如果 queryData() 执行报错,那是不是页面一直在转圈圈。


    优化后:

    import React, { useState } from 'react';

    const App = () => {
    const [loading, setLoading] = useState(false);
    const getData = async () => {
    setLoading(true);
    try {
    const res = await queryData();
    setLoading(false);
    } catch (error) {
    setLoading(false);
    }
    }
    }

    如果使用 trycatch 来捕获 await 的错误感觉不太优雅,可以使用 await-to-js 来优雅地捕获。


    二次优化后:

    import React, { useState } from 'react';
    import to from 'await-to-js';

    const App = () => {
    const [loading, setLoading] = useState(false);
    const getData = async () => {
    setLoading(true);
    const [err, res] = await to(queryData());
    setLoading(false);
    }
    }

    七、不是什么都能用来JSON.parse


    优化前:

    const App = (props) => {
    const { data } = props || {};
    const dataObj = JSON.parse(data);
    }

    JSON.parse() 方法将一个有效的 JSON 字符串转换为 JavaScript 对象。这里没必要去判断一个字符串是否为有效的 JSON 字符串。只要利用 trycatch 来捕获错误即可。


    优化后:

    const App = (props) => {
    const { data } = props || {};
    let dataObj = {};
    try {
    dataObj = JSON.parse(data);
    } catch (error) {
    console.error('data不是一个有效的JSON字符串')
    }
    }

    八、被修改的引用类型数据


    优化前:

    const App = (props) => {
    const { data } = props || {};
    if (Array.isArray(data)) {
    data.forEach(item => {
    if (item) item.age = 12;
    })
    }
    }

    如果谁用 App 这个函数后,他会搞不懂为啥 dataage 的值为啥一直为 12,在他的代码中找不到任何修改 dataage 值的地方。只因为 data 是引用类型数据。在公共函数中为了防止处理引用类型数据时不小心修改了数据,建议先使用 lodash.clonedeep 克隆一下。


    优化后:

    import cloneDeep from 'lodash.clonedeep';

    const App = (props) => {
    const { data } = props || {};
    const dataCopy = cloneDeep(data);
    if (Array.isArray(dataCopy)) {
    dataCopy.forEach(item => {
    if (item) item.age = 12;
    })
    }
    }

    九、并发异步执行赋值操作


    优化前:

    const App = (props) => {
    const { data } = props || {};
    let urlList = [];
    if (Array.isArray(data)) {
    data.forEach(item => {
    const { id = '' } = item || {};
    getUrl(id).then(res => {
    if (res) urlList.push(res);
    });
    });
    console.log(urlList);
    }
    }

    上述代码中 console.log(urlList) 是无法打印出 urlList 的最终结果。因为 getUrl 是异步函数,执行完才给 urlList 添加一个值,而 data.forEach 循环是同步执行的,当 data.forEach 执行完成后,getUrl 可能还没执行完成,从而会导致 console.log(urlList) 打印出来的 urlList 不是最终结果。


    所以我们要使用队列形式让异步函数并发执行,再用 Promise.all 监听所有异步函数执行完毕后,再打印 urlList 的值。


    优化后:

    const App = async (props) => {
    const { data } = props || {};
    let urlList = [];
    if (Array.isArray(data)) {
    const jobs = data.map(async item => {
    const { id = '' } = item || {};
    const res = await getUrl(id);
    if (res) urlList.push(res);
    return res;
    });
    await Promise.all(jobs);
    console.log(urlList);
    }
    }

    十、过度防御


    优化前:

    const App = (props) => {
    const { data } = props || {};
    let infoList = [];
    if (Array.isArray(data)) {
    infoList = data.map(item => {
    const { name, age } = item || {};
    return `我的名字是${name},今年${age}岁了`;
    });
    }
    const info = infoList?.join(',');
    }

    infoList 后面为什么要跟 ?,数组的 map 方法返回的一定是个数组。


    优化后:

    const App = (props) => {
    const { data } = props || {};
    let infoList = [];
    if (Array.isArray(data)) {
    infoList = data.map(item => {
    const { name, age } = item || {};
    return `我的名字是${name},今年${age}岁了`;
    });
    }
    const info = infoList.join(',');
    }

    后续


    以上对小王代码的吐槽,最后我只想说一下,以上的错误都是一些 JS 基础知识,跟任何框架没有任何关系。如果你工作了几年还是犯这些错误,真的可以考虑转行。


    作者:红尘炼心
    链接:https://juejin.cn/post/7259007674520158268
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    🤭新的广告方式,很新很新!

    哈哈 会爬树的金鱼,树上的金鱼呦😀 前言 老板:针对上半年业绩发表一下大家的看法,大家自由发言,已经定好晚餐和夜宵了有需要的自取୧(๑•̀⌄•́๑)૭ 产品A:根据大数据分析公司产品的广告收入较低拖后腿,建议扩宽曝光渠道! 产品B:对! 大数据分析公司产品的广...
    继续阅读 »

    哈哈 会爬树的金鱼,树上的金鱼呦😀


    前言


    老板:针对上半年业绩发表一下大家的看法,大家自由发言,已经定好晚餐和夜宵了有需要的自取୧(๑•̀⌄•́๑)૭


    产品A:根据大数据分析公司产品的广告收入较低拖后腿,建议扩宽曝光渠道!


    产品B:对! 大数据分析公司产品的广告大部分来自移动端,pc端曝光率较少,可以增加一下曝光率,据统计移动端大部分广告收入来自首屏广告,我觉得pc也可以加上


    程序员: PC哪有首屏广告啊,行业都没有先例


    老板:这个好,没有先例! 我们又可以申请专利了Ψ( ̄∀ ̄)Ψ 搞起!!今年公司专利指标有着落了


    程序员: 这也太影响体验了


    产品A:就说能不能做吧, 今天面试的那个应届生不错能倒背chromium源码,还能手写react源码并且指出优化方案


    程序员: 我做!!! ╭( ̄m ̄*)╮


    先来个全屏遮罩🤔


    这还不简单,直接一个定位搞定

    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>XX百货</title>
    <style>
    #ADBox {
    background: #fff;
    position: fixed;
    width: 100%;
    height: 100%;
    display: none;
    }
    </style>
    </head>
    <body>
    <div id="ADBox">广告</div>
    </body>
    </html>


    搞定提测!


    重来没接过这么简单的需求,送业绩这是╮(╯﹏╰)╭


    第一次提测🤐


    测试A: 送业绩这是? 产品说要和移动端一模一样,你这哪一样了?? 直系看需求文档!!


    程序员: 需求文档就一句话, 和移动端一样的开屏广告, 这那不一样了?


    测试A: 这哪一样了?? 你家移动端广告露个顶部出来?看看哪个app广告不是全屏的???


    程序员: 啥? 还要全屏?? 行...


    // 必须用点击事件触发才能全屏
    document.addEventListener("click", async (elem) => {
    const box = document.getElementById("ADBox");
    if (box.requestFullscreen) {
    box.requestFullscreen();
    }
    setTimeout(() => {
    const state = !!document.fullscreenElement;
    // 是否全屏状态
    if (state) {
    // 取消全屏
    if (document.exitFullscreen) {
    document.exitFullscreen();
    }
    }
    }, 5 * 1000);
    });

    搞定提测


    第二次提测🙄


    产品A: 嗯...有点感觉了,这鼠标去掉都遮住广告了,万一广告商不满意投诉怎么办?


    程序员: 鼠标这么小这么能遮住广告??


    产品B: 看我鼠标? (大米老鼠标PC主题)


    程序员: ...

    <style>
    #ADBox {
    background: #fff;
    position: fixed;
    width: 100%;
    height: 100%;
    // 隐藏广告Box让用户点任意地方激活
    opacity: 0;

    }
    </style>

    提测...


    第三次提测🤕


    测试A: 为啥还有鼠标???


    程序员: 怎么那可能还有?


    测试A: 过来看,鼠标不动的话还是会显示鼠标哦,动一下才消失


    程序员: ##..行, 那我直接锁指针

        <script>
    let pointerLockElement = null;
    // 指针锁定或解锁
    document.addEventListener(
    "pointerlockchange",
    () => {
    // 锁定的元素是否为当前的元素 -- 没啥也意义可以去掉
    if (document.pointerLockElement === pointerLockElement) {
    console.log("指针锁定成功了。");
    } else {
    pointerLockElement = null;
    console.log("已经退出锁定。");
    }
    },
    false
    );
    // 锁定失败事件
    document.addEventListener(
    "pointerlockerror",
    () => {
    console.log("锁定指针时出错。");
    },
    false
    );

    // 锁定指针,锁定指针的元素必须让用户点一下才能锁定
    function lockPointer(elem) {
    // 如果已经存锁定的元素则不操作
    if (document.pointerLockElement) {
    return;
    }
    if (elem) {
    pointerLockElement = elem;
    elem.requestPointerLock();
    }
    }

    // 解除锁定
    function unlockPointer() {
    document.exitPointerLock();
    }

    // 必须用点击事件触发才能全屏
    document.addEventListener("click", async () => {
    const box = document.getElementById("ADBox");
    if (box.requestFullscreen) {
    box.requestFullscreen();
    box.style.opacity = 1;
    box.style.display = "block";
    lockPointer(box);
    }
    // 5秒后解除锁定
    setTimeout(() => {
    const state = !!document.fullscreenElement;
    // 是否全屏状态
    if (state) {
    // 取消全屏
    if (document.exitFullscreen) {
    document.exitFullscreen();
    unlockPointer();
    box.style.display = "none";
    }
    }
    }, 5 * 1000);
    });
    </script>

    提测...


    第四次提测😤


    测试A: Safari上失效哦


    程序员: 额....

    <script>

    // requestFullscreen 方法兼容处理
    function useRequestFullscreen(elem) {
    const key = ['requestFullscreen', 'mozRequestFullScreen', 'webkitRequestFullscreen', 'msRequestFullscreen']
    for (const value of key) {
    if (elem[value]) {
    elem[value]()
    return true
    }
    }
    return false
    }

    // document.exitFullscreen 方法兼容处理
    document.exitFullscreenUniversal = document.exitFullscreen || document.webkitExitFullscreen || document.mozCancelFullScreen

    // fullscreenElement 对象兼容处理
    function getFullscreenElement() {
    const key = ['fullscreenElement', 'webkitFullscreenElement']
    for (const value of key) {
    if (document[value]) {
    return document[value]
    }
    }
    return null
    }

    // fullscreenchange 事件兼容处理
    addEventListener("fullscreenchange", endCallback);
    addEventListener("webkitfullscreenchange", endCallback);

    // requestPointerLock 方法在Safari下不可与 requestFullscreen 方法共用一个事件周期 暂无解决方法,必须让用户点两次鼠标,第一次全屏,第二次锁鼠标
    // 同一事件周期内会出现的问题: 1.有小机率会正常执行, 2.顶部出现白条(实际上是个浏览器锁鼠标的提示语,但显示异常了) 3.锁定鼠标失败

    </script>


    结尾😩


    产品A: 效果不错,但还有点小小的瑕疵,为啥要鼠标点一下才能弹广告,改成进入就弹窗吧


    程序员: 要不还是找上次那个应届生来吧,改chromium源码应该能实现╭∩╮(︶︿︶)╭∩╮


    效果预览: http://www.npmstart.top/BSOD.html


    作者:会爬树的金鱼
    链接:https://juejin.cn/post/7270082131962183739
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    项目部署之后页面没有刷新怎么办?

    web
    最近项目部署成功之后,突然产品找我,上线之后,页面没有生效,这是怎么回事?我这是第一次部署这个项目,也不太清楚历史问题,接下来就慢慢寻找答案吧, 如果心急的可以直接看后面的总结,下面我们好好聊聊缓存的问题。 浏览器输入url之后,就会进行下面一系列判断,来实现...
    继续阅读 »

    最近项目部署成功之后,突然产品找我,上线之后,页面没有生效,这是怎么回事?我这是第一次部署这个项目,也不太清楚历史问题,接下来就慢慢寻找答案吧, 如果心急的可以直接看后面的总结,下面我们好好聊聊缓存的问题。


    浏览器输入url之后,就会进行下面一系列判断,来实现页面渲染。



    首先讲一下常见的http缓存~


    HTTP缓存常见的有两类:



    • 强缓存:可以由这两个字段其中一个决定





      • expires

      • cache-control(优先级更高)





    • 协商缓存:可以由这两对字段中的一对决定





      • Last-Modified,If-Modified-Since

      • Etag,If--Match(优先级更高)




    强缓存


    使用的是express框架


    expires


    app.get('/login', function(req, res){
    // 设置 Expires 响应头
    const time = new Date(Date.now() + 300000).toUTCString()
    res.header('Expires', time)
    res.render('login');
    });

    然后我们在前端页面刷新,我们可以看到请求的资源的响应头里多了一个expires的字段, 取消Disable cache



    刷新



    勾选Disable cache



    但是,Expires已经被废弃了。对于强缓存来说,Expires已经不是实现强缓存的首选。


    因为Expires判断强缓存是否过期的机制是:获取本地时间戳,并对先前拿到的资源文件中的Expires字段的时间做比较。来判断是否需要对服务器发起请求。这里有一个巨大的漏洞:“如果我本地时间不准咋办?”


    是的,Expires过度依赖本地时间,如果本地与服务器时间不同步,就会出现资源无法被缓存或者资源永远被缓存的情况。所以,Expires字段几乎不被使用了。现在的项目中,我们并不推荐使用Expires,强缓存功能通常使用cache-control字段来代替Expires字段。


    cache-control


    其实cache-control跟expires效果差不多,只不过这两个字段设置的值不一样而已,前者设置的是秒数,后者设置的是毫秒数


    app.get('/login', function(req, res){
    // 设置 Expires 响应头
    // const time = new Date(Date.now() + 300000).toUTCString()
    // res.header('Expires', time)
    // 设置 Cache-Control 响应头
    res.header('Cache-Control', 'max-age=300')
    res.render('login');
    });

    前端页面响应头多了cache-control这个字段,且300s内都走本地缓存,不会去请求服务端



    Cache-Control:max-age=N,N就是需要缓存的秒数。从第一次请求资源的时候开始,往后N秒内,资源若再次请求,则直接从磁盘(或内存中读取),不与服务器做任何交互。


    Cache-control中因为max-age后面的值是一个滑动时间,从服务器第一次返回该资源时开始倒计时。所以也就不需要比对客户端和服务端的时间,解决了Expires所存在的巨大漏洞。


    Cache-control的多种属性:developer.mozilla.org/zh-CN/docs/…


    但是使用最多的就是no-cache和no-store,接下来就重点学习这两种


    no-cache和no-store


    no_cache是Cache-control的一个属性。它并不像字面意思一样禁止缓存,实际上,no-cache的意思是强制进行协商缓存。如果某一资源的Cache-control中设置了no-cache,那么该资源会直接跳过强缓存的校验,直接去服务器进行协商缓存。而no-store就是禁止所有的缓存策略了。


    app.get('/login', function(req, res){
    // 设置 Expires 响应头
    // const time = new Date(Date.now() + 300000).toUTCString()
    // res.header('Expires', time)
    // 设置 Cache-Control 响应头
    res.header('Cache-Control', 'no-cache')
    res.render('login');
    });

    no-cache(进行协商缓存,下次再次请求,没有勾选控制台Disable cache,状态码是304)



    app.get('/login', function(req, res){
    // 设置 Cache-Control 响应头
    res.header('Cache-Control', 'no-store')
    res.render('login');
    });

    no-store(每次都请求服务器的最新资源,没有缓存策略)



    强制缓存就是以上这两种方法了。现在我们回过头来聊聊,Expires难道就一点用都没有了吗?也不是,虽然Cache-control是Expires的完全替代品,但是如果要考虑向下兼容的话,在Cache-control不支持的时候,还是要使用Expires,这也是我们当前使用的这个属性的唯一理由。


    协商缓存


    与强缓存不同的是,强缓存是在时效时间内,不走服务端,只走本地缓存;而协商缓存是要走服务端的,如果请求某个资源,去请求服务端时,发现命中缓存则返回304,否则则返回所请求的资源,那怎么才算命中缓存呢?接下来讲讲


    Last-Modified,If-Modified-Since


    简单来说就是:



    • 第一次请求资源时,服务端会把所请求的资源的最后一次修改时间当成响应头中Last-Modified的值发到浏览器并在浏览器存起来

    • 第二次请求资源时,浏览器会把刚刚存储的时间当成请求头中If-Modified-Since的值,传到服务端,服务端拿到这个时间跟所请求的资源的最后修改时间进行比对

    • 比对结果如果两个时间相同,则说明此资源没修改过,那就是命中缓存,那就返回304,如果不相同,则说明此资源修改过了,则没命中缓存,则返回修改过后的新资源


    基于last-modified的协商缓存实现方式是:



    1. 首先需要在服务器端读出文件修改时间,

    2. 将读出来的修改时间赋给响应头的last-modified字段。

    3. 最后设置Cache-control:no-cache


    三步缺一不可。


    app.get('/login', function(req, res){
    // 设置 Expires 响应头
    // const time = new Date(Date.now() + 300000).toUTCString()
    // res.header('Expires', time)

    const { mtime } = fs.statSync(path.join(__dirname, 'public/index.css')) // 读取最后修改时间
    console.log(mtime.toUTCString(), '--------')
    // 响应头的last-modified字段
    res.header('last-modified', mtime.toUTCString())
    // 设置 Cache-Control 响应头
    res.header('Cache-Control', 'no-cache')
    res.render('login');
    });


    当index.css发生改变再次请求时



    终端输出的时间变化



    服务端的时间跟last-modified的值是一致的



    使用以上方式的协商缓存已经存在两个非常明显的漏洞。这两个漏洞都是基于文件是通过比较修改时间来判断是否更改而产生的。


    1.因为是更具文件修改时间来判断的,所以,在文件内容本身不修改的情况下,依然有可能更新文件修改时间(比如修改文件名再改回来),这样,就有可能文件内容明明没有修改,但是缓存依然失效了。


    2.当文件在极短时间内完成修改的时候(比如几百毫秒)。因为文件修改时间记录的最小单位是秒,所以,如果文件在几百毫秒内完成修改的话,文件修改时间不会改变,这样,即使文件内容修改了,依然不会 返回新的文件。


    为了解决上述的这两个问题。从http1.1开始新增了一个头信息,ETag(Entity 实体标签)


    Etag,If--Match


    ETag就是将原先协商缓存的比较时间戳的形式修改成了比较文件指纹。


    其实Etag,If--Match跟Last-Modified,If-Modified-Since大体一样,区别在于:



    • 后者是对比资源最后一次修改时间,来确定资源是否修改了

    • 前者是对比资源内容,来确定资源是否修改


    那我们要怎么比对资源内容呢?我们只需要读取资源内容,转成hash值,前后进行比对就行了!


    app.get('/login', function(req, res){
    // 设置 Expires 响应头
    // const time = new Date(Date.now() + 300000).toUTCString()
    // res.header('Expires', time)

    // const { mtime } = fs.statSync(path.join(__dirname, 'public/index.css')) // 读取最后修改时间
    // console.log(mtime.toUTCString(), '--------')
    // 响应头的last-modified字段
    // res.header('last-modified', mtime.toUTCString())


    // 设置ETag
    const ifMatch = req.header['if-none-match']
    const hash = crypto.createHash('md5')
    const fileBuf = fs.readFileSync(path.join(__dirname, 'public/index.css'))
    hash.update(fileBuf, 'utf8')
    const etag = `"${hash.digest('hex')}"`
    console.log(etag, '---etag----')
    // 对比hash值
    if (ifMatch === etag) {
    res.status = 304
    } else {
    res.header('etag', etag)
    // ctx.body = fileBuffer
    }
    // 设置 Cache-Control 响应头
    res.header('Cache-Control', 'no-cache')
    res.render('login');
    });


    当资源发生改变时,状态码变成200,更新缓存


    比如更改css样式



    ETag也有缺点



    • ETag需要计算文件指纹这样意味着,服务端需要更多的计算开销。如果文件尺寸大,数量多,并且计算频繁,那么ETag的计算就会影响服务器的性能。显然,ETag在这样的场景下就不是很适合。

    • ETag有强验证和弱验证,所谓将强验证,ETag生成的哈希码深入到每个字节。哪怕文件中只有一个字节改变了,也会生成不同的哈希值,它可以保证文件内容绝对的不变。但是,强验证非常消耗计算量。ETag还有一个弱验证,弱验证是提取文件的部分属性来生成哈希值。因为不必精确到每个字节,所以他的整体速度会比强验证快,但是准确率不高。会降低协商缓存的有效性。


    值得注意的一点是,不同于cache-control是expires的完全替代方案(说人话:能用cache-control就不要用expiress)。ETag并不是last-modified的完全替代方案。而是last-modified的补充方案(说人话:项目中到底是用ETag还是last-modified完全取决于业务场景,这两个没有谁更好谁更坏)。


    disk cache & memory cache


    磁盘缓存+内存缓存,这两种缓存不属于http缓存,而是本地缓存了~


    我们直接打开掘金官网,点击network,类型选择all



    可以看的很多请求,这里请求包括了静态资源+接口请求


    这里我们能够看的很多请求的size中有很多是disk cache(磁盘缓存)


    也有一些图片是memory cache(内存缓存)



    这两者有什么区别呢?


    disk cache: 磁盘缓存,很明显将内容存储在计算机硬盘中,很明显,这种缓存可以占用比较大的空间,但是由于是读取硬盘,所以速度低于内存


    memory cache: 内存缓存,速度快,优先级高,但是大小受限于计算机的内存大小,很大的资源还是缓存到硬盘中


    上面的浏览器缓存已经有三个大点了,那它们的优先级是什么样的呢?


    缓存的获取顺序如下:


    1.内存缓存


    2.磁盘缓存


    3.强缓存


    4.协商缓存


    如果勾选了Disable cache,那磁盘缓存都不存在了,之还有内存缓存



    我还发现,勾选了Disable cache,就base64图片一定会在内存缓存中,其他图片则会发起请求;而不勾选了Disable cache,则大多数图片都在内存缓存中




    CDN缓存


    CDN缓存是一种服务端缓存,CDN服务商可以将源站上的资源缓到其各地的边缘服务器节点上。当用户访问该资源时,CDN再通过负载均衡将用户的请求调度到最近的缓存节点上,有效减少了链路回源,提高了资源访问效率及可用性,降低带宽消耗。


    如果客户端没有命中缓存,那接下来就要发起一次网络请求,根据网络环境,一般大型站点都会配置CDN,CDN会找一个最合适的服务节点接管网络请求。CDN节点都会在本地缓存静态文件数据,一旦命中直接返回,不会穿透去请求应用服务器。并且CDN会通过在不同的网络,策略性地通过部署边缘服务器和应用大量的技术和算法,把用户的请求指定到最佳响应节点上。所以会减少非常多的网络开销和响应延迟。


    如果没有部署CDN或者CDN没有命中,请求最终才会落入应用服务器,现在的http服务器都会添加一层反向代理,例如nginx,在这一层同样会添加缓存层,代表技术是squid,varnish,当然nginx作为http服务器而言也支持静态文件访问和本地缓存技术,当然也可以使用远程缓存,如redis,memcache,这里缓存的内容一般为静态文件或者由服务器已经生成好的动态页面,在返回用户之前缓存。


    如果前面的缓存机制全部失效,请求才会落入真正的服务器节点。


    总结


    1.如果页面是协商缓存,如何获取页面最新内容?


    协商缓存比较好办,那就刷新页面,不过需要勾选Disable cache,但是用户不知道打开控制台怎么办?


    那就右击页面的刷新按钮,然后选择硬性重新加载,或者清空缓存并硬性重新加载,页面就获取到最新资源了



    2.如果页面没有设置cache-control,那默认的缓存机制是什么样的?



    默认是协商缓存,这也符合浏览器设计,缓存可以减少宽度流量,加快响应速度


    3.如果项目重新部署还是没有更新,怎么办?


    在确定项目已经部署成功


    这样子,可以去问一下公司的运维同事,这个项目是否有CDN缓存


    如果项目的域名做了CDN缓存,就需要刷新CDN目录,来解决缓存问题了,不然就只能等,等CDN策略失效,来请求最新的内容


    向如下配置的缓存策略,只有过30天才会去真正服务器去请求最新内容



    当然你可以测试一下是否为CDN缓存,在url后面拼接一个参数,就能够获取到最新资源了,比如有缓存的链接是baidu.com/abc


    你可以在浏览器中输入baidu.com/abc&t=1234来…


    当然特定场景,我们不能随意给链接后面添加参数,所以这也只适用于测试一下是否有CDN缓存


    所以最好的解决办法还是需要让运维同事去刷新目录,这样就能快速解决CDN缓存问题。


    参考链接


    juejin.cn/post/712719…


    juejin.cn/post/717756…


    xiaolincoding.com/

    network/2_h…

    收起阅读 »

    Android 时钟翻页效果

    web
    背景 今天逛掘金,发现了一个有意思的web view效果,想着Android能不能实现一下捏。 原文链接:juejin.cn/post/724435… 具体实现分析请看上文原文链接,那我们开始吧! 容器 val space = 10f //上下半间隔 val...
    继续阅读 »

    背景


    今天逛掘金,发现了一个有意思的web view效果,想着Android能不能实现一下捏。


    image.png
    原文链接:juejin.cn/post/724435…


    具体实现分析请看上文原文链接,那我们开始吧!


    容器


    val space = 10f //上下半间隔
    val bgBorderR = 10f //背景圆角
    //上半部分
    val upperHalfBottom = height.toFloat() / 2 - space / 2
    canvas.drawRoundRect(
    0f,
    0f,
    width.toFloat(),
    upperHalfBottom,
    bgBorderR,
    bgBorderR,
    bgPaint
    )
    //下半部分
    val lowerHalfTop = height.toFloat() / 2 + space / 2
    canvas.drawRoundRect(
    0f,
    lowerHalfTop,
    width.toFloat(),
    height.toFloat(),
    bgBorderR,
    bgBorderR,
    bgPaint
    )

    image.png


    绘制数字


    我们首先居中绘制数字4


    val number4 = "4"
    textPaint.getTextBounds(number4, 0, number4.length, textBounds)
    //居中显示
    val x = (width - textBounds.width()) / 2f - textBounds.left
    val y = (height + textBounds.height()) / 2f - textBounds.bottom
    canvas.drawText(number4, x, y, textPaint)

    image.png


    接下来我们将数字切分为上下两部分,分别绘制。


    val number4 = "4"
    textPaint.getTextBounds(number4, 0, number4.length, textBounds)
    val x = (width - textBounds.width()) / 2f - textBounds.left
    val y = (height + textBounds.height()) / 2f - textBounds.bottom
    // 上半部分裁剪
    canvas.save()
    canvas.clipRect(
    0f,
    0f,
    width.toFloat(),
    upperHalfBottom
    )
    canvas.drawText(number4, x, y, textPaint)
    canvas.restore()
    // 下半部分裁剪
    canvas.save()
    canvas.clipRect(
    0f,
    lowerHalfTop,
    width.toFloat(),
    height.toFloat()
    )
    canvas.drawText(number4, x, y, textPaint)

    image.png


    翻转卡片


    如何实现让其旋转呢?
    而且还得是3d的效果了。我们选择Camera来实现。
    我们先让数字'4'旋转起来。


    准备工作,通过属性动画来改变旋转的角度。


    private var degree = 0f //翻转角度
    private val camera = Camera()
    private var flipping = false //是否处于翻转状态
    ...
    //动画
    val animator = ValueAnimator.ofFloat(0f, 360f)
    animator.addUpdateListener { animation ->
    val animatedValue = animation.animatedValue as Float
    setDegree(animatedValue)
    }
    animator.doOnStart {
    flipping = true
    }
    animator.doOnEnd {
    flipping = false
    }
    animator.duration = 1000
    animator.interpolator = LinearInterpolator()
    animator.start()
    ...

    private fun setDegree(degree: Float) {
    this.degree = degree
    invalidate()
    }

    让数字'4'旋转起来:


      override fun onDraw(canvas: Canvas) {
    super.onDraw(canvas)
    // 居中绘制数字4
    val number4 = "4"
    textPaint.getTextBounds(number4, 0, number4.length, textBounds)
    val x = (width - textBounds.width()) / 2f - textBounds.left
    val y = (height + textBounds.height()) / 2f - textBounds.bottom

    if (!flipping) {
    canvas.drawText(number4, x, y, textPaint)
    } else {
    camera.save()
    canvas.translate(width / 2f, height / 2f)
    camera.rotateX(-degree)
    camera.applyToCanvas(canvas)
    canvas.translate(-width / 2f, -height / 2f)
    camera.restore()
    canvas.drawText(number4, x, y, textPaint)
    }
    }

    file.gif

    我们再来看一边效果图:
    我们希望将卡片旋转180度,并且0度-90度由上半部分完成,90度-180度由下半部分完成。


    我们调整一下代码,先处理一下上半部分:


    ...
    val animator = ValueAnimator.ofFloat(0f, 180f)
    ...
    override fun onDraw(canvas: Canvas) {
    super.onDraw(canvas)
    val space = 10f //上下半间隔
    //上半部分
    val upperHalfBottom = height.toFloat() / 2 - space / 2
    ...
    // 居中绘制数字4
    val number4 = "4"
    textPaint.getTextBounds(number4, 0, number4.length, textBounds)
    val x = (width - textBounds.width()) / 2f - textBounds.left
    val y = (height + textBounds.height()) / 2f - textBounds.bottom

    if (!flipping) {
    //上半部分裁剪
    canvas.save()
    canvas.clipRect(
    0f,
    0f,
    width.toFloat(),
    upperHalfBottom
    )
    canvas.drawText(number4, x, y, textPaint)
    canvas.restore()
    } else {
    if (degree < 90) {
    //上半部分裁剪
    canvas.save()
    canvas.clipRect(
    0f,
    0f,
    width.toFloat(),
    upperHalfBottom
    )
    camera.save()
    canvas.translate(width / 2f, height / 2f)
    camera.rotateX(-degree)
    camera.applyToCanvas(canvas)
    canvas.translate(-width / 2f, -height / 2f)
    camera.restore()
    canvas.drawText(number4, x, y, textPaint)
    canvas.restore()
    }
    }
    }

    效果如下:


    upper.gif

    接下来我们再来看一下下半部分:


    override fun onDraw(canvas: Canvas) {
    super.onDraw(canvas)
    val space = 10f //上下半间隔
    //下半部分
    val lowerHalfTop = height.toFloat() / 2 + space / 2

    // 居中绘制数字4
    val number4 = "4"
    textPaint.getTextBounds(number4, 0, number4.length, textBounds)
    val x = (width - textBounds.width()) / 2f - textBounds.left
    val y = (height + textBounds.height()) / 2f - textBounds.bottom

    if (!flipping) {
    // 下半部分裁剪
    canvas.save()
    canvas.clipRect(
    0f,
    lowerHalfTop,
    width.toFloat(),
    height.toFloat()
    )
    canvas.drawText(number4, x, y, textPaint)
    canvas.restore()
    } else {
    if (degree > 90) {
    canvas.save()
    canvas.clipRect(
    0f,
    lowerHalfTop,
    width.toFloat(),
    height.toFloat()
    )
    camera.save()
    canvas.translate(width / 2f, height / 2f)
    val bottomDegree = 180 - degree
    camera.rotateX(bottomDegree)
    camera.applyToCanvas(canvas)
    canvas.translate(-width / 2f, -height / 2f)
    camera.restore()
    canvas.drawText(number4, x, y, textPaint)
    canvas.restore()
    }
    }
    }

    lower.gif

    那我们将上下部分结合起来,效果如下:


    all.gif

    数字变化


    好!我们完成了翻转部分,现在需要在翻转的过程中将数字改变:

    我们还是举例说明:数字由'4'变为'5'的情况。我们思考个问题,什么时候需要改变数字?

    上半部分在翻转开始的时候,上半部分底部显示的数字就应该由'4'变为'5',但是旋转的部分还是应该为'4',
    下半部分开始旋转的时候底部显示的数字还是应该为'4',而旋转的部分该为'5'。


    canvas.save()
    canvas.clipRect(
    0f,
    0f,
    width.toFloat(),
    upperHalfBottom
    )
    canvas.drawText(number5, x, y, textPaint)
    canvas.restore()
    // 下半部分裁剪
    canvas.save()
    canvas.clipRect(
    0f,
    lowerHalfTop,
    width.toFloat(),
    height.toFloat()
    )
    canvas.drawText(number4, x, y, textPaint)
    canvas.restore()
    //=====⬆️=====上述的代码显示的上下底部显示的内容,即上半部分地步显示5,下半部分显示4
    if (degree < 90) {
    //上半部分裁剪
    canvas.save()
    canvas.clipRect(
    0f,
    0f,
    width.toFloat(),
    upperHalfBottom
    )
    camera.save()
    canvas.translate(width / 2f, height / 2f)
    camera.rotateX(-degree)
    camera.applyToCanvas(canvas)
    canvas.translate(-width / 2f, -height / 2f)
    camera.restore()
    canvas.drawText(number4, x, y, textPaint)
    canvas.restore()
    //=====⬆️=====上述的代码表示上半部分旋转显示的内容,即数字4
    } else {
    canvas.save()
    canvas.clipRect(
    0f,
    lowerHalfTop,
    width.toFloat(),
    height.toFloat()
    )
    camera.save()
    canvas.translate(width / 2f, height / 2f)
    val bottomDegree = 180 - degree
    camera.rotateX(bottomDegree)
    camera.applyToCanvas(canvas)
    canvas.translate(-width / 2f, -height / 2f)
    camera.restore()
    canvas.drawText(number5, x, y, textPaint)
    canvas.restore()
    //=====⬆️=====上述的代码表示下半部分旋转显示的内容,即数字5
    }

    效果图如下:大伙可以在去理一下上面数字的变化的逻辑。


    a.gif

    最后我们加上背景再看一下效果:


    a.gif

    小结


    上述代码仅仅提供个思路,仅为测试code,正式代码可不

    作者:蹦蹦蹦
    来源:juejin.cn/post/7271518821809438781
    能这么写哦 >..<

    收起阅读 »

    虚拟列表 or 时间分片

    前言 最近在做一个官网,原本接口做的都是分页的,但是客户提出不要分页,之前看过虚拟列表这个东西,所以进行一下了解。 为啥要用虚拟列表呢! 在日常工作中,所要渲染的也不单单只是一个li那么简单,会有很多嵌套在里面。但数据量过多,同时渲染式,会在 渲染样式 跟 布...
    继续阅读 »

    前言


    最近在做一个官网,原本接口做的都是分页的,但是客户提出不要分页,之前看过虚拟列表这个东西,所以进行一下了解。


    为啥要用虚拟列表呢!


    在日常工作中,所要渲染的也不单单只是一个li那么简单,会有很多嵌套在里面。但数据量过多,同时渲染式,会在 渲染样式 跟 布局计算上花费太多时间,体验感不好,那你说要不要优化嘛,不是你被优化就是你优化它。


    进入正题,啥是虚拟列表?


    可以这么理解,根据你视图能显示多少就先渲染多少,对看不到的地方采取不渲染或者部分渲染。




    这时候你完成首次加载,那么其他就是在你滑动时渲染,就可以通过计算,得知此时屏幕应该显示的列表项。


    怎么弄?


    备注:很多方案对于动态不固定高度、网络图片以及用户异常操作等形式处理的也并不好,了解下原理即可。


    虚拟列表的实现,实际上就是在首屏加载的时候,只加载可视区域内需要的列表项,当滚动发生时,动态通过计算获得可视区域内的列表项,并将非可视区域内存在的列表项删除。


    1、计算当前可视区域起始数据索引(startIndex)

    2、计算当前可视区域结束数据索引(endIndex)

    3、计算当前可视区域的数据,并渲染到页面中

    4、计算startIndex对应的数据在整个列表中的偏移位置startOffset并设置到列表上


    由于只是对可视区域内的列表项进行渲染,所以为了保持列表容器的高度并可正常的触发滚动,将Html结构设计成如下结构:

    <div class="infinite-list-container">
    <div class="infinite-list-phantom"></div>
    <div class="infinite-list">
    <!-- item-1 -->
    <!-- item-2 -->
    <!-- ...... -->
    <!-- item-n -->
    </div>
    </div>


    • infinite-list-container 为可视区域的容器

    • infinite-list-phantom 为容器内的占位,高度为总列表高度,用于形成滚动条

    • infinite-list 为列表项的渲染区域

      接着,监听infinite-list-containerscroll事件,获取滚动位置scrollTop

    • 假定可视区域高度固定,称之为screenHeight

    • 假定列表每项高度固定,称之为itemSize

    • 假定列表数据称之为listData

    • 假定当前滚动位置称之为scrollTop

    •   则可推算出:

      • 列表总高度listHeight = listData.length * itemSize
      • 可显示的列表项数visibleCount = Math.ceil(screenHeight / itemSize)
      • 数据的起始索引startIndex = Math.floor(scrollTop / itemSize)
      • 数据的结束索引endIndex = startIndex + visibleCount
      • 列表显示数据为visibleData = listData.slice(startIndex,endIndex)

        当滚动后,由于渲染区域相对于可视区域已经发生了偏移,此时我需要获取一个偏移量startOffset,通过样式控制将渲染区域偏移至可视区域中。

      • 偏移量startOffset = scrollTop - (scrollTop % itemSize);

    时间分片


    那么虚拟列表是一方面可以优化的方式,另一个就是时间分片。


    先看看我们平时的情况


    1.直接开整,直接渲染。




    诶???我们可以发现,js运行时间为113ms,但最终 完成时间是 1070ms,一共是 js 运行时间加上渲染总时间。

    PS:

    • 在 JS 的 EventLoop中,当JS引擎所管理的执行栈中的事件以及所有微任务事件全部执行完后,才会触发渲染线程对页面进行渲染
    • 第一个 console.log的触发时间是在页面进行渲染之前,此时得到的间隔时间为JS运行所需要的时间
    • 第二个 console.log是放到 setTimeout 中的,它的触发时间是在渲染完成,在下一次 EventLoop中执行的

    那我们改用定时器


    上面看是因为我们同时渲染,那我们可以分批看看。

    let once = 30
    let ul = document.getElementById('testTime')
    function loopRender (curTotal, curIndex) {
    if (curTotal <= 0) return
    let pageCount = Math.min(curTotal, once) // 每页最多20条
    setTimeout(_ => {
    for (let i=0; i<pageCount;i++) {
    let li = document.createElement('li')
    li.innerHTML = curIndex + i
    ul.appendChild(li)
    }
    loopRender(curTotal - pageCount, curIndex + pageCount)
    }, 0)
    }
    loopRender(100000, 0)

    这时候可以感觉出来渲染很快,但是如果渲染复杂点的dom会闪屏,为什么会闪屏这就需要清楚电脑刷新的概念了,这里就不详细写了,有兴趣的小朋友可以自己去了解一下。

    可以改用 requestAnimationFrame 去分批渲染,因为这个关于电脑自身刷新效率的,不管你代码的事,可以解决丢帧问题。

    let once = 30
    let ul = document.getElementById('container')
    // 循环加载渲染数据
    function loopRender (curTotal, curIndex) {
    if (curTotal <= 0) return
    let pageCount = Math.min(curTotal, once) // 每页最多20条
    window.requestAnimationFrame(_ => {
    for (let i=0; i<pageCount;i++) {
    let li = document.createElement('li')
    li.innerHTML = curIndex + i
    ul.appendChild(li)
    }
    loopRender(curTotal - pageCount, curIndex + pageCount)
    })
    }
    loopRender(100000, 0)

    还可以改用 DocumentFragment


    什么是 DocumentFragment



    DocumentFragment,文档片段接口,表示一个没有父级文件的最小文档对象。它被作为一个轻量版的 Document使用,用于存储已排好版的或尚未打理好格式的XML片段。最大的区别是因为 DocumentFragment不是真实DOM树的一部分,它的变化不会触发DOM树的(重新渲染) ,且不会导致性能等问题。

    可以使用 document.createDocumentFragment方法或者构造函数来创建一个空的 DocumentFragment

    ocumentFragments是DOM节点,但并不是DOM树的一部分,可以认为是存在内存中的,所以将子元素插入到文档片段时不会引起页面回流。



    当 append元素到 document中时,被 append进去的元素的样式表的计算是同步发生的,此时调用 getComputedStyle 可以得到样式的计算值。而 append元素到 documentFragment 中时,是不会计算元素的样式表,所以 documentFragment 性能更优。当然现在浏览器的优化已经做的很好了, 当 append元素到 document中后,没有访问 getComputedStyle 之类的方法时,现代浏览器也可以把样式表的计算推迟到脚本执行之后。

    let once = 30 
    let ul = document.getElementById('container')
    // 循环加载渲染数据
    function loopRender (curTotal, curIndex) {
    if (curTotal <= 0) return
    let pageCount = Math.min(curTotal, once) // 每页最多20条
    window.requestAnimationFrame(_ => {
    let fragment = document.createDocumentFragment()
    for (let i=0; i<pageCount;i++) {
    let li = document.createElement('li')
    li.innerHTML = curIndex + i
    fragment.appendChild(li)
    }
    ul.appendChild(fragment)
    loopRender(curTotal - pageCount, curIndex + pageCount)
    })
    }
    loopRender(100000, 0)

    其实同时渲染十万条数据这个情况还是比较少见的,就当做个了解吧。


    作者:NIIBLE
    链接:https://juejin.cn/post/7263009476058742840
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    CSS命名太头疼?这个Vite插件自动生成,让你解放双手!

    web
    CSS样式一直以来都是一个让前端开发者头疼的问题,随着前端工程化的发展,使用原子CSS进行样式开发正变得越来越流行。相比传统的CSS样式书写方式,原子CSS可以让我们以更模块化和可复用的方式进行样式的编码。但是手动编写大量原子类样式也比较烦琐。有没有办法自动生...
    继续阅读 »

    CSS样式一直以来都是一个让前端开发者头疼的问题,随着前端工程化的发展,使用原子CSS进行样式开发正变得越来越流行。相比传统的CSS样式书写方式,原子CSS可以让我们以更模块化和可复用的方式进行样式的编码。但是手动编写大量原子类样式也比较烦琐。有没有办法自动生成原子CSS类呢?今天我要介绍的Vite插件atom-css-generator就可完美实现这一功能。


    原子CSS简介


    原子CSS(Atomic CSS)将传统的CSS类拆分成一个个独立的、原子级的类,每个类仅包含一个CSS属性,例如:



    .p-10 {
    padding: 10px;
    }

    .bg-red {
    background: red;
    }

    相比于传统的CSS类,原子类具有以下特点:



    • 原子:每个类只包含一个CSS属性,拆分到最小粒度

    • 独立:类名语义明确,可以任意组合使用而不会产生冲突

    • 可复用:一个原子类可以重复使用在不同的组件中


    使用原子CSS的优势在于:



    • 更模块化:样式属性高内聚、解耦

    • 更可维护:不同类名称、不同文件,避免影响

    • 更灵活:组件样式由原子类组合,更容易扩展和维护


    但是编写大量原子类也比较麻烦,多达几千个类定义都可能出现。有没有自动生成的方式呢?


    atom-css-generator插件介绍


    atom-css-generator是一个Vite插件,它可以通过解析Vue组件中的class,自动生成对应的原子CSS定义


    安装和配置


    使用npm或yarn安装:


    Copy code

    npm install atom-css-generator

    在vite.config.js中引入插件:


    js

    Copy code

    import atomCssGenerator from 'atom-css-generator';

    export default {
    plugins: [
    atomCssGenerator({
    outputPath: 'assets/styles'
    })
    ]
    }

    主要的配置项有:



    • outputPath:指定生成的CSS文件输出目录,默认为public


    使用方式



    1. 在Vue组件的template中,使用特定格式的class,例如:


    html

    Copy code

    <template>
    <div class="bg-red fc-white p-20">
    <!-- ... -->
    </div>
    </template>


    1. 构建项目时,插件会自动生成对应的原子CSS类定义:


    css

    Copy code

    .bg-red {
    background-color: red;
    }

    .fc-white {
    color: white;
    }

    .p-20 {
    padding: 20px;
    }


    1. style.css会被自动生成到指定的outputPath中,并注入到HTML文件头部。


    支持的类名格式


    插件支持多种格式的类名规则生成,包括:



    • 颜色类名:bg-red、fc-333

    • 间距类名:p-20、ml-10

    • 尺寸类名:w-100、h-200

    • Flexbox类名:jc-center、ai-stretch

    • 边框类名:bc-333、br-1-f00-solid

    • 布局类名:p-relative、p-fixed

    • 文字类名:fs-14、fw-bold


    等等,非常全面。


    而且也内置了一些预设的实用样式类,比如文字截断类te-ellipsis。


    原理简析


    插件主要通过以下处理流程实现自动生成原子CSS:



    1. 使用@vue/compiler-sfc解析Vue文件,获取模板内容

    2. 通过正则表达式提取模板中的class名称

    3. 根据特定类名规则,生成对应的CSS定义

    4. 将CSS写入style.css文件中,并注入到HTML中


    同时,插件还会在热更新时自动检查新添加的类名,从而动态更新style.css。


    总结


    通过atom-css-generator这个插件,我们可以非常轻松地在Vue项目中使用原子CSS样式,而不需要手动编写。它省去了我们大量重复的工作,使得样式的维护和扩展更加简单。


    如果你也想尝试在自己的项目中引入原子CSS,不妨试试这个插件。相信它能给你带来意想不到的便利!
    GitHub地址

    收起阅读 »

    自建”IT兵器库”,你值得一看!

    web
    现在市面的组件库,我们用的越发熟练,越发爆嗨,只需cv就能完成需求,为何不爆嗨!!! 常用库有 element、antd、iView、antd pro,这些组件库都是在以往的需求开发当中进行总结提炼,一次次符合我们的需求,只要有文档,只要有示例,页面开发都是小...
    继续阅读 »

    现在市面的组件库,我们用的越发熟练,越发爆嗨,只需cv就能完成需求,为何不爆嗨!!!


    常用库有 element、antd、iView、antd pro,这些组件库都是在以往的需求开发当中进行总结提炼,一次次符合我们的需求,只要有文档,只要有示例,页面开发都是小问题,对吧,各位优秀开发前端工程师。


    接下来,根据开发需求,一步步完成一个组件的开发,当然可能自己的思路,并不是最好的,欢迎大家留言讨论,一起进步


    需求:


    动态列表格,就是一个表格,存在默认列,但是支持我们操控,实现动态效果


    实现效果


    默认表格配置


    image.png


    默认列配置


    image.png


    动态列组件支持查询


    image.png


    动态列组件支持勾选


    image.png


    动态列组件支持清空


    image.png


    动态列组件支持一键全选


    image.png


    动态列组件支持一键清空


    image.png


    功能点划分



    • 表格默认列和动态列组件默认选中项 实现双向绑定

    • 动态列组件 增删改与表格 实现双向绑定

    • 动态列组件 实现搜索

    • 动态列组件 实现单点控制 添加与删除

    • 动态列组件 实现一键控制功能 全选与清空

    • 动态列组件 实现恢复初始态


    使用到组件(Antd 组件库哈)



    • Table

    • Pagination

    • Modal

    • Input

    • Button

    • Checkbox


    动态列组件区域划分



    • 头部标题

    • 头部提示语

    • 核心内容区



      • 核心区域头部功能按钮





      • 搜索区域





      • 左边所有内容项





      • 待选内容项




    动态列组件最终可支持配置项


      open?: boolean // Modal状态
    setOpen?: React.Dispatch> // 控制Modal状态
    modalTitle?: string | React.ReactNode
    modalWidth?: number
    modalHeadContent?: React.ReactNode
    leftHeadContent?: React.ReactNode | string
    rightHeadContent?: React.ReactNode | string
    modalBodyStyle?: any
    searchPlaceholder?: string
    modalOk?: (val, isUseDefaultData?: boolean) => void // 第二个参数 内部数据处理支持
    enableSelectAll?: boolean // 是否开启全选功能
    selectData: SelectItem[] // 下拉框数据
    isOutEmitData?: boolean
    defaultSelectKeyList?: string[] // 默认选中的key(当前表格列)自定义数据(外部做逻辑处理)
    initSelectKey?: string[] // 初始表格选中的key 自定义数据(外部做逻辑处理)
    curColumns?: any[] // 当前表格列 内部做逻辑处理
    originColumns?: any[] // 原始表格列 内部做逻辑处理
    isDelHeadCol?: boolean // 删除头部columnKey 例如序号 只有内部处理数据时生效
    isDelTailCol?: boolean // 删除尾部columnKey 例如操作 只有内部处理数据时生效
    isDelHeadAndTail?: boolean // 删除头尾部columnKey 只有内部处理数据时生效

    动态列组件布局



        

    // 头部内容区
    {modalHeadContent}

    // 以下维核心区


    // 核心区-左边

    // 核心区-功能按钮 - 一键全选
    {enableSelectAll && (

    onCheckBoxChange([], e)} checked={checkAll} indeterminate={indeterminate}>
    全选


    )}
    {leftHeadContent || ''}



    // 核心区-左搜索
    {childSearchRender({ curData: leftSelectData })}

    // 核心区-左列表区域
    {selectItemRender(leftSelectData)}



    // 核心区-右边


    {rightHeadContent || ''}

    // 核心区-功能按钮 - 一键清空
    handleRightClearSelectData()}>
    清空



    // 核心区-右搜索
    {childSearchRender({ curData: rightSelectData }, true)}

    // 核心区-右列表区域
    {selectItemRender(rightSelectData, true)}






    动态列组件-列表渲染


    const selectItemRender = (listArr = [], isRight = false) => {
    return (


    // 数据遍历形式
    {listArr?.map(({ label, value, disabled = false }) => (

    {!isRight && (

    {label}

    )}
    // 判断是否是 右边列表区域 添加删除按钮
    {isRight && {label}}
    {isRight && (



    )}

    ))}


    )
    }

    动态列组件-搜索渲染


    const childSearchRender = (childSearchProps: any, isRight = false) => {
    // eslint-disable-next-line react/prop-types
    const { curData } = childSearchProps
    return (
    {
    onSearch(e, curData, isRight)
    }}
    allowClear
    />
    )
    }

    动态列组件样式


    .content-box {
    width: 100%;
    height: 550px;
    border: 1px solid #d9d9d9;
    }
    .content-left-box {
    border-right: 1px solid #d9d9d9;
    }
    .content-left-head {
    padding: 16px 20px;
    background: #f5f8fb;
    height: 48px;
    box-sizing: border-box;
    }
    .content-right-head {
    padding: 16px 20px;
    background: #f5f8fb;
    height: 48px;
    box-sizing: border-box;

    &-clear {
    color: #f38d29;
    cursor: pointer;
    }
    }
    .content-right-box {
    }
    .content-left-main {
    padding: 10px 20px 0 20px;
    height: calc(100% - 50px);
    box-sizing: border-box;
    }
    .content-right-main {
    padding: 10px 20px 0 20px;
    height: calc(100% - 50px);
    box-sizing: border-box;
    }
    .right-head-content {
    font-weight: 700;
    color: #151e29;
    font-size: 14px;
    }
    .modal-head-box {
    color: #151e29;
    font-size: 14px;
    height: 30px;
    }
    .icon-box {
    color: #f4513b;
    }
    .ant-checkbox-group {
    flex-wrap: nowrap;
    }
    .left-select-box {
    height: 440px;
    padding-bottom: 10px;
    }
    .right-select-box {
    height: 440px;
    padding-bottom: 10px;
    }
    .ant-checkbox-wrapper {
    align-items: center;
    }
    .display-box {
    height: 22px;
    }

    功能点逐一拆解实现


    点1:表格默认列和动态列组件默认选中项 实现双向绑定



    • 首先,先写一个表格啦,确定列,这个就不用代码展示了吧,CV大法

    • 其次,把表格原始列注入动态列组件当中,再者注入当前表格列当前能选择的所有项

    • 当前能选择所有项内容参数示例


    [
    { label: '项目编码', value: 'projectCode' },
    { label: '项目名称', value: 'projectName' },
    { label: '项目公司', value: 'company' },
    { label: '标段', value: 'lot' },
    ]




    • 动态组件内部默认选中当前表格列

    • 这里需要把表格列数据 进行过滤 映射成 string[]


       内部是通过checkbox.Grop 实现选中 我们只需要 通过一个状态去控制即可 `selectKey`
    <Checkbox.Group onChange={onCheckChange} className="flex-col h-full flex" value={selectKey}>
    ...
    Checkbox.Group>

    动态列组件 单点控制 增删改



    • 增,删,改就是实现 左右边列表的双向绑定

    • 监听 左边勾选事件 + 右边删除事件 + 一键清空事件

    • 通过左右两边的状态 控制数据即可

    • 状态


      const [originSelectData, setOriginSelectData] = useState([]) // 下拉原始数据 搜索需要
    const [originRightSelectData, setOriginRightSelectData] = useState([])
    const [rightSelectData, setRightSelectData] = useState([])
    const [selectKey, setSelectKey] = useState([])
    const [transferObj, setTransferObj] = useState({})
    const [indeterminate, setIndeterminate] = useState(false)
    const [checkAll, setCheckAll] = useState(false)
    const [leftSelectData, setLeftSelectData] = useState([])
    const [defaultSelectKey, setDefaultSelectKey] = useState([])
    const [originSelectKey, setOriginSelectKey] = useState([])

    const onCheckChange = checkedValues => {
    // 往右边追加数据
    const selectResArr = checkedValues?.map(val => transferObj[val])
    setSelectKey(checkedValues) // 我们选中的key (选项)
    setRightSelectData(selectResArr) // 右边列表数据
    setOriginRightSelectData(selectResArr) // 右边原数据(搜索时候需要)
    }

    const deleteRightData = key => {
    const preRightData = rightSelectData
    const filterResKeyArr = preRightData?.filter(it => it.value !== key).map(it => it.value) // 数据处理 只要你不等于删除的key 保留
    const filterResItemArr = preRightData?.filter(it => it.value !== key)
    setRightSelectData(filterResItemArr) // 更新右边数据 即可刷新右边列表
    setOriginRightSelectData(filterResItemArr) // 更新右边数据
    setSelectKey(filterResKeyArr) // 更新选中的key 即可刷新左边选中项
    }

      const handleRightClearSelectData = () => {
    // 这就暴力了塞
    setSelectKey([])
    setRightSelectData([])
    setOriginRightSelectData([])
    }

    动态列组件 实现搜索



    • 搜索,就是改变一下数据 视觉上看起来有过滤的效果塞

    • 刚才我们不是多存了一份数据源嘛

    • 出来见见啦~


    const onSearch = (val, curData, isRight = false) => {
    const searchKey = val
    // 这个是同时支持 左右两边
    // 做个判断
    if (!isRight) {
    // 在判断一下是否有搜索内容 因为也需要清空的啦
    if (searchKey) {
    // 有,我就过滤呗
    const searchResArr = curData?.filter(item => item.label.includes(searchKey))
    setLeftSelectData(searchResArr)
    }
    if (!searchKey) {
    // 没有 我就把原本数据还给你呗
    setLeftSelectData(originSelectData)
    }
    }
    // 右边 一样
    if (isRight) {
    if (searchKey) {
    const searchResArr = curData?.filter(item => item.label.includes(searchKey))
    setRightSelectData(searchResArr)
    }
    if (!searchKey) {
    setRightSelectData(originRightSelectData)
    }
    }
    }

    动态列组件 增删改与表格 实现数据绑定



    • 里面的数据 处理好了 直接再关闭的时候 丢给外面嘛

    • 把右边的内容(也就是选中的key)返回给表格

    • 表格再自己构造


      const handleOk = (colVal, isUseDefaultCol) => {
    `colVal` : 选中的列key
    `isUseDefaultCol`:是否使用默认列
    // table column字段组装
    const normalColConstructor = (
    title,
    dataIndex,
    isSort = true,
    colWidth = 150,
    isEllipsis = false,
    render = null
    ) => {
    const renderObj = render ? { render } : {}
    return {
    title,
    dataIndex,
    sorter: isSort,
    width: colWidth,
    ellipsis: isEllipsis,
    key: dataIndex,
    ...renderObj,
    }
    }
    const statusRender = text => approvalStatusRender(text)
    const dateRender = (text, record) => {dayjs(text).format('YYYY-MM-DD')}
    const newColArr = []
    // 定制化处理 (其实还有2.0)
    colVal?.forEach(({ label, value }, index) => {
    let isSort = false
    let renderFn = null
    const isSubmissionAmount = value === 'submissionAmount'
    const isApprovalAmount = value === 'approvalAmount'
    const isReductionRate = value === 'reductionRate'
    const isInitiationTime = value === 'initiationTime'

    // 特定的业务场景 特殊状态渲染
    const isStatus = value === 'status'
    // 特定的业务场景 时间类型 加上排序
    if (isApprovalAmount || isInitiationTime || isReductionRate || isSubmissionAmount) {
    isSort = true
    }
    if (isStatus) {
    renderFn = statusRender
    }
    // 普通列 已就绪
    // 普通列 标题 拿label就ok
    newColArr.push(normalColConstructor(label, value, isSort, 100, true, renderFn))
    })

    // 最后在头部追加一个序号
    newColArr.unshift({
    title: '序号',
    dataIndex: 'orderCode',
    width: 45,
    render: (_: any, record: any, index) => tablePageSize * (tablePage - 1) + index + 1,
    })
    // 最后在尾部部追加一个操作
    newColArr.push({
    title: '操作',
    dataIndex: 'action',
    fixed: 'right',
    width: 50,
    render: (text, row: DataType) => (



    ),
    })

    if (colVal?.length) {
    if (isUseDefaultCol) {
    setColumns([...originColumns])
    } else {
    setColumns([...newColArr])
    }
    } else {
    setColumns([...originColumns])
    }
    }

    // 解决表格存在子列 -- 先搞定数据结构 -- 在解决表格列内容填充 -- 无需捆绑
    // 遍历拿到新增列数组 匹配上代表 需要子数组 进行转换
    // eslint-disable-next-line consistent-return
    colVal?.forEach(({ label, value }, index) => {
    // DesignHomeDynamicCol[value] 返回的值能匹配到 说明存在 嵌套列
    const validVal = DesignHomeDynamicClassify[DesignHomeDynamicCol[value]]
    const isHasChild = newColChildObj[validVal]
    const titleText = DesignHomeDynamicLabel[value]
    if (validVal) {
    // 如果已经有孩子 追加子列
    if (isHasChild) {
    newColChildObj[validVal] = [...isHasChild, normalColConstructor(titleText, value)]
    } else {
    // 则 新增
    newColChildObj[validVal] = [normalColConstructor(titleText, value)]
    }
    } else {
    // 普通列 已就绪
    // 普通列 标题 拿label就ok
    newColArr.push(normalColConstructor(label, value, false, 100, true))
    }
    })

    动态列组件 实现恢复初始态 实现双向绑定



    • 这个就更简单啦 再点击确定的时候 传一个 isUseDefaultData:true

    • 只是这个isUseDefaultData 的逻辑判断问题

    • 当动态列组件 点击恢复默认列 我们只需把 当初传进来的 原始列数据 更新到 selectKey 状态即可


    const handleDefaultCol = () => {
    // 这里是考虑到组件灵活性 数据可由自己处理好在传入
    if (isOutEmitData) {
    setSelectKey(initSelectKey)
    } else {
    // 这里是使用 内部数据处理逻辑
    setSelectKey(originSelectKey)
    }
    }

    const handleOk = () => { 
    // 数据比对 是否使用默认校验
    // originColumnMapSelectKey 源数据与传出去的数据 进行比对
    const originRightMapKey = originRightSelectData?.map(it => it.value)
    // 采用 lodash isEqual 方法
    const isSame = isEqual(originSelectKey, originRightMapKey)
    // 判断外部是否有传 确定事件 handleOk
    if (modalOk) {
    modalOk(originRightSelectData, isSame)
    }
    setOpen(false)
    }

    const handleOk = (colVal, isUseDefaultCol) => {
    ... 一堆代码
    // 当用户清空以后 还是恢复表格默认状态
    if (colVal?.length) {
    // 恢复默认列
    if (isUseDefaultCol) {
    setColumns([...originColumns])
    } else {
    // 否则就拿新数据更新
    setColumns([...newColArr])
    }
    } else {
    setColumns([...originColumns])
    }
    }

    动态列组件 实现一键控制功能 全选与清空



    • 这就是Vip版本的噻

    • 但是也简单 无非就是操作多选框 无非多选框就三种态

    • 未选 半选 全选

    • 既然我们下面的逻辑已处理好 这个其实也很快的锅

    • 首先,就是下面数据变化的时候 我们上面需要去感应

    • 其次就是 上面操作的时候 下面也需要感应

    • 最后 双向数据绑定 就能搞定 没有那么神秘

    • 一步一步来 先分别把 上下事件处理好


    const onCheckBoxChange = (dataArr = [], e = null) => {
    // 判断所有数据长度
    const allLen = originSelectData?.length
    // 根据当前选中数据长度 判断是 多选框三种状态当中的哪一种
    const checkLen = e ? selectKey?.length : dataArr?.length // 全选
    const isAllSelect = allLen === checkLen // 半选
    const isHalfSelect = allLen > checkLen
    // 然后再判断一下是点击一键全选事件 触发还是 点击下面选项的时候触发
    // 点击一键全选 能拿到事件的 e.target 从而来判断
    // 这里是操作下面按钮的时候 触发
    if (!e) {
    // 如果没有选中
    if (checkLen === 0) {
    // 恢复未选状态
    setCheckAll(false)
    setIndeterminate(false)
    return ''
    }
    if (isAllSelect) {
    // 如果是全选 改为全选态
    setCheckAll(true)
    setIndeterminate(false)
    }
    if (isHalfSelect) {
    // 半选态
    setIndeterminate(true) // 这个控制 多选框的半选态
    setCheckAll(false)
    }
    }
    // 这个就是用户操作 一键全选按钮触发
    if (e) {
    // 如果当前长度为0 那么应该更新为全选
    if (checkLen === 0) {
    setCheckAll(true)
    setIndeterminate(false)
    setSelectKey(originSelectData?.map(it => it.value))
    }
    // 如果已经全选 就取消全选
    if (isAllSelect) {
    setCheckAll(false)
    setIndeterminate(false)
    setSelectKey([])
    }
    // 如果是半选态 就全选
    if (isHalfSelect) {
    setCheckAll(true)
    setIndeterminate(false)
    setSelectKey(originSelectData?.map(it => it.value))
    }
    }
    }

    const onCheckChange = checkedValues => {
    // 往右边追加数据
    const selectResArr = checkedValues?.map(val => transferObj[val]) setSelectKey(checkedValues)
    setRightSelectData(selectResArr)
    setOriginRightSelectData(selectResArr)
    }


    • 我们两个事件都处理好 那么开始进行联动

    • 意思就是 我们拿什么去控制这两个机关 核心是不是就是 选中的选项啊

    • 有两种解法,第二种可能有点绕



      • 一、就是在下面每个选项改变的时候 都去调一下 上面一键全选事件;然后上面变化的时候 也去调一下 下面选项的事件 (常规)缺点:容易漏 一变多改





      • 二、监听选项key的变化 去触发开关 避免我们第一种缺点 (优解)同时解决了确保外面传进来的 表格列 使得下拉框选中这些项 生效




    // 这里通过 useEffect() 实现监听 确保外面传进来的 表格列 使得下拉框选中这些项 生效 
    useEffect(() => {
    onCheckBoxChange(selectKey)
    onCheckChange(selectKey)
    // eslint-disable-next-line react-hooks/exhaustive-deps },
    [selectKey]
    )

    结束


    都看到这里了,不留点痕迹,是怕我发现么?

    作者:造更多的轮子
    来源:juejin.cn/post/7266463919139684367

    收起阅读 »

    卸下if-else 侠的皮衣!- 适配器模式

    web
    🤭当我是if-else侠的时候 😶怕出错 给我一个功能,我总是要写很多if-else,虽然能跑,但是维护起来确实很难受,每次都要在一个方法里面增加逻辑,生怕搞错,要是涉及到支付功能,分分钟炸锅 😑难调试 我总是不知道之前写的逻辑在哪里,一个方法几百行逻辑,而且...
    继续阅读 »

    🤭当我是if-else侠的时候


    😶怕出错


    给我一个功能,我总是要写很多if-else,虽然能跑,但是维护起来确实很难受,每次都要在一个方法里面增加逻辑,生怕搞错,要是涉及到支付功能,分分钟炸锅


    😑难调试


    我总是不知道之前写的逻辑在哪里,一个方法几百行逻辑,而且是不同功能点冗余在一起!这可能让我牺牲大量时间在这查找调试中


    🤨交接容易挨打


    当你交接给新同事的时候,这个要做好新同事的白眼和嘲讽,这代码简直是坨翔!这代码简直是个易碎的玻璃,一碰就碎!这代码简直是个世界十大奇迹!


    🤔脱下if-else侠的皮衣


    先学习下开发的设计原则


    单一职责原则(SRP)



    就一个类而言,应该仅有一个引起他变化的原因



    开放封闭原则(ASD)



    类、模块、函数等等应该是可以扩展的,但是不可以修改的



    里氏替换原则(LSP)



    所有引用基类的地方必须透明地使用其子类的对象



    依赖倒置原则(DIP)



    高层模块不应该依赖底层模块



    迪米特原则(LOD)



    一个软件实体应当尽可能的少与其他实体发生相互作用



    接口隔离原则(ISP)



    一个类对另一个类的依赖应该建立在最小的接口上



    在学习下设计模式


    大致可以分三大类:创建型结构型行为型

    创建型:工厂模式 ,单例模式,原型模式

    结构型:装饰器模式,适配器模式,代理模式

    行为型:策略模式,状态模式,观察者模式


    上篇文章学习了 策略模式,有兴趣可以过去看看,下面我们来学习适配器模式


    场景:将一个接口返回的数据,转化成列表格式,单选框数据格式,多选框数据格式


    用if-else来写,如下


    let list = []
    let selectArr = []
    let checkedArr = []

    http().then(res =>{
    //处理成列表格式
    this.list = this.handel(res,0)
    //处理成下拉框模式
    this.selectArr = this.handel(res,1)
    //处理成多选框模式
    this.checkedArr = this.handel(res,2)
    })
    handel(data,num){
    if(num == 0){
    ....
    }else if(num ==1){
    ....
    }else if(num ==2){
    ....
    }
    }

    分下下问题,假如我们多了几种格式,我们又要在这个方法体里面扩展,违反了开放封闭原则(ASD),所以我们现在来采用适配器模式来写下:


    //定义一个Adapter
    class Adpater {
    data = [];
    constructor(){
    this.data = data
    }
    toList(col){
    //col代表要的字段,毕竟整个data的字段不是我们都想要的
    //转列表
    return this.data.map(item =>{
    const obj = {}
    for(let e of col){
    const f = e.f
    obj[f] = item[f];
    }
    return obj
    })
    }
    //用了map的省略写法,来处理转化单选的格式
    toSelect(opt){
    const {label,value} = opt
    return this.data.map(item => ({
    label,
    value
    }))
    }
    //同上处理转化多选的格式
    toChecked(opt){
    const {field} = opt
    return this.data.map(item => ({
    checked:false,
    value:item[field]
    }))
    }
    }

    //下面是调用这个适配类
    let list = []
    let selectArr = []
    let checkedArr = []
    http.then(data =>{
    const adapter = new Adatpter(data)
    //处理列表
    list = adapter.toList(['id','name','age'])
    //处理下拉
    selectArr = adapter.toSelect({
    label:'name'
    value:'id'
    })
    //处理多选
    checkedArr = adapter.toChecked({
    field:'id'
    })
    })

    这个扩展性就能大大提高,看着也会好看很多,可以通过继承等等方式来扩展,继承方式下次有机会再来写代码,文章先到这里!


    结尾


    遵守设计规则,脱掉if-else的皮衣,善用设计模式,加

    作者:向乾看
    来源:juejin.cn/post/7265694012962537513
    油,骚年们!给我点点赞,关注下!

    收起阅读 »

    当文字成为雨滴:HTML、CSS、JS创作炫酷的"文字雨"动画!

    web
    简介 大家好,今天要给大家带来一个Super Cool的玩意儿😎! 在本篇技术文章中,将介绍如何使用HTML、CSS和JavaScript创建一个独特而引人注目的"文字(字母&数字)"雨🌨️动画效果。通过该动画,展现出的是一系列随机字符将从云朵中下落...
    继续阅读 »

    简介


    大家好,今天要给大家带来一个Super Cool的玩意儿😎!


    rain-preview.gif


    在本篇技术文章中,将介绍如何使用HTML、CSS和JavaScript创建一个独特而引人注目的"文字(字母&数字)"雨🌨️动画效果。通过该动画,展现出的是一系列随机字符将从云朵中下落像是将文字变成雨滴从天而降,营造出与众不同的视觉效果;


    HTML


    创建一个基本的HTML结构,这段HTML代码定义了一个容器,其中包含了"云朵"和"雨滴"(即文字元素)。基本结构如下:



    • 首先是类名为container的容器,表示整个动画的容器;

    • 其次是类名为cloud的容器,表示云朵的容器;

    • 接着是cloud容器中的文字元素,表示雨滴(即文字元素);
      然后引入外部创建的css和js文件,可以先定义几个text容器,用于调整样式;


    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Text Rain Animation</title>

    <link rel="stylesheet" href="./css/style.css">
    </head>
    <body>
    <div class="container">
    <div class="cloud">
    <!-- <div class="text">a</div> -->
    <!-- <div class="text">b</div> -->
    <!-- <div class="text">c</div> -->
    <!-- 雨滴将会在这里出现 -->
    </div>
    </div>

    <script src="./js/main.js"></script>
    </body>
    </html>

    CSS


    CSS是为文字雨效果增色添彩的关键,使动画效果更加丰富,关于一些 CSS 样式:



    • 使用了自定义的颜色变量来为背景色和文本颜色提供值,有助于使代码易于维护和修改;

    • 利用CSS的阴影效果和动画功能,创造逼真的"云朵"和流畅的"雨滴"动画;


    * {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
    }

    :root {
    --body-color: #181c1f;
    --primary-color: #ffffff;
    }

    body {
    display: flex;
    justify-content: center;
    align-items: center;
    min-height: 100vh;
    background-color: var(--body-color);
    }

    .container {
    width: 100%;
    height: 400px;
    display: flex;
    justify-content: center;
    border-bottom: 1px solid rgba(255, 255, 255, .1);
    /* 添加一个从下往上线性渐变的镜像效果,增加视觉层次感 */
    -webkit-box-reflect: below 1px linear-gradient(transparent, transparent, transparent, transparent, #0005);
    }

    .cloud {
    position: relative;
    top: 50px;
    z-index: 100;

    /* 横向云朵 */
    width: 320px;
    height: 100px;
    background-color: var(--primary-color);
    border-radius: 100px;

    /* drop-shadow函数将阴影效果应用于投影图像 */
    filter: drop-shadow(0 0 30px var(--primary-color));
    }
    .cloud::before {
    content: "";
    /* 左侧小云朵 */
    width: 110px;
    height: 110px;
    background-color: var(--primary-color);
    border-radius: 50%;
    position: absolute;
    top: -50px;
    left: 40px;

    /* 右侧大云朵 */
    box-shadow: 90px 0 0 30px var(--primary-color);
    }

    .cloud .text {
    position: absolute;
    top: 40px;
    height: 20px;
    line-height: 20px;

    text-transform: uppercase;
    color: var(--primary-color);
    /* 为文字添加阴影,看上去发光,增加视觉效果 */
    text-shadow: 0 0 5px var(--primary-color), 0 0 15px var(--primary-color), 0 0 30px var(--primary-color);
    transform-origin: bottom;
    animation: animate 2s linear forwards;
    }

    @keyframes animate {
    0% {
    transform: translateX(0);
    }

    70% {
    transform: translateY(290px);
    }

    100% {
    transform: translateY(290px);
    }
    }

    通过关键帧动画 @keyframes animate 定义文字运动的过程,在这里是垂直移动290px,也就是向下移动,模拟下雨的状态。当然为了让文字雨效果更加好看,还可以引入一下字体库;



    Warning


    -webkit-box-reflect:可将元素内容在特定方向上进行轴对称反射;


    但是该特性是非标准的,请尽量不要在生产环境中使用它!


    目前只有webkit内核的浏览器支持,如:谷歌浏览器、Safari浏览器。在火狐浏览器中是不支持的;



    JavaScript


    最后,使用JavaScript来实现文字雨的效果。通过动态生成并随机选择字符,可以实现让这些字符(雨滴)从.cloud(云朵)中降落的效果。JavaScript 脚本逻辑:



    • 首先,定义函数 generateText() 并创建字符集,定义函数 randomText() 通过从给定的字符集中随机选择一个字符返回;

    • 接下来,编写 rain() 函数,在函数内部,首先选取 .cloud 元素同时创建一个新的 <div>元素作为字符节点,设置元素文本内容为函数返回的字符,并添加类名;

    • 然后,利用 Math.random() 方法生成一些随机值,将这些随机值应用到创建的 <div> 元素上,包括:

      • 字符距离左侧位置,在 .cloud 容器的宽度区间;

      • 字体大小,最大不超过32px;

      • 动画周期所需的时间(动画持续时间),2s内;



    • 最后,将其<div>添加到 .cloud 元素中,使用 setTimeout() 函数在2秒后将文字节点从 .cloud 元素中移除,模拟雨滴落地消失效果;


    定时器: 为了让字符(雨滴)持续下落,使用 setInterval 函数和一个时间间隔值来调用 rain() 函数。这样就是每20毫秒就会生成一个新的字符(雨滴)节点并添加到云朵中。


    // 生成字母和数字数组
    function generateText() {
    const letters = [];
    const numbers = [];

    const a = "a".charCodeAt(0);

    for (let i = 0; i < 26; i++) {
    letters.push(String.fromCharCode(a + i));

    if (i < 9) {
    numbers.push(i + 1);
    }
    };

    return [...letters, ...numbers];
    };

    // 从生成的数组中随机取出一个字符
    function randomText() {
    const texts = generateText();
    const text = texts[Math.floor(Math.random() * texts.length)];

    return text;
    };

    function rainEffect() {
    const cloudEle = document.querySelector(".cloud");
    const textEle = document.createElement("div");

    textEle.innerText = randomText();
    textEle.classList.add("text");

    const left = Math.floor(Math.random() * 310);
    const size = Math.random() * 1.5;
    const duration = Math.random();
    const styleSheets = {
    left: `${left}px`,
    fontSize: `${0.5 + size}em`,
    animationDuration: `${1 + duration}s`,
    };
    Object.assign(textEle.style, styleSheets);

    cloudEle.appendChild(textEle);
    setTimeout(() => {
    cloudEle.removeChild(textEle);
    }, 2000);
    };

    // 每隔20ms创建一个雨滴元素
    setInterval(() => rainEffect(), 20);

    结论


    通过HTML、CSS和JS的紧密合作,成功创建了一个炫酷的"文字雨"动画效果,这个动画可以增加网页的吸引力! 不要犹豫🖐️,动手尝试一下,或者甚至你也可以根据自己的需求对文字、样式和动画参数进行调整,进一步改善和扩展这个效果;


    希望这篇文章对你在开发类似动画效果时有所帮助!另外如果你对这个案例还有任何问题,欢迎在评论区留

    作者:掘一
    来源:juejin.cn/post/7270648629378367528
    言或联系(私信)我。谢谢阅读!🎉

    收起阅读 »