注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

js的垃圾回收机制

web
概论 对于js的垃圾回收,很多人的理解还停留在引用计数和标记清除的阶段。 有人会说,学习这个,对业务代码开发没啥作用。但是我想说,了解了这些基础的东西之后,才能更好地组织代码,在写代码的时候,才能做到心中能有个框架,知道浏览器到底发生了什么。 我也不是科班出身...
继续阅读 »

概论


对于js的垃圾回收,很多人的理解还停留在引用计数和标记清除的阶段。


有人会说,学习这个,对业务代码开发没啥作用。但是我想说,了解了这些基础的东西之后,才能更好地组织代码,在写代码的时候,才能做到心中能有个框架,知道浏览器到底发生了什么。


我也不是科班出身,很多东西不清不楚的。但我感觉计算机行业有个很好的地方,就是学了的知识很快就能得到验证,就能在生产上应用。这种成就感是我当年干机械的时候所无法体验到的。


前几天就有个报障说有个项目越用越卡,但是排查不出问题,我最近正好在学习垃圾回收内存泄漏,就立马能分析出来是不是内存不足产生的影响,就很开心。


本文我会采用图解的方式,尽量照着js垃圾回收的演变历史讲解。


一,什么是垃圾回收


GCGarbage Collection,也就是我们常说的垃圾回收。


我们知道,js是v8引擎编译执行的,而代码的执行就需要内存的参与,内存往往是有限的,为了更好地利用内存资源,就需要把没用的内存回收,以便重新使用。


比如V8引擎在执行代码的过程中遇到了一个函数,那么我们会创建一个函数执行上下文环境并添加到调用栈顶部,函数的作用域里面包含了函数中所有的变量信息,在执行过程中我们分配内存创建这些变量,当函数执行完毕后函数作用域会被销毁,那么这个作用域包含的变量也就失去了作用,而销毁它们回收内存的过程,就叫做垃圾回收。


如下代码:


var testObj1={
a:1
}
testObj1={
b:2
}

对应的内存情况如下:


1,垃圾的产生.drawio.png
其中堆中的{a:1}就变成了垃圾,需要被GC回收掉。


在C / C++中,需要开发者跟踪内存的使用和管理内存。而js等高级语言,代码在执行期间,是V8引擎在为我们执行垃圾回收。


那么既然已经有v8引擎自动给我们回收垃圾了,为啥我们还需要了解V8引擎的垃圾回收机制呢?这是因为依据这个机制,还有些内存无法回收,会造成内存泄漏。具体的表现就是随着项目运行时间的变成,系统越来越卡滞,需要手动刷新浏览器才能恢复。


了解V8的垃圾回收机制,才能让我们更好地书写代码,规避不必要的内存泄漏。


二,内存的生命周期


如上所说,内存应该存在这样三个生命周期:




  1. 分配所需要的内存:在js代码执行的时候,基本数据类型存储在栈空间,而引用数据类型存储在堆空间。




  2. 使用分配的空间:可能对对应的值做一些修改。




  3. 不需要时将其释放回收。


    如下代码:


    function fn(){
    //创建对象,分配空间
    var testObj={
    a:1
    }
    //修改内容
    testObj.a=2
    }
    fn()//调用栈执行完毕,垃圾回收

    对应的内存示意图:




2,内存的生命周期.drawio.png


三,垃圾回收的策略


当函数执行完毕,js引擎是通过移动ESP(ESP:记录当前执行状态的指针)来销毁函数保存在栈当中的执行上下文的,栈顶的空间会被自动回收,不需要V8引擎的垃圾回收机制出面。


然而,堆内存的大小是不固定的,那堆内存中的数据是如何回收的呢?


这就引出了垃圾回收的策略。


通常用采用的垃圾回收有两种方法:引用计数(reference counting)标记清除(mark and sweep)


3.1,引用计数(reference counting)


如上文第二节中所说,testObj对象存放在堆空间,我们想要使用的时候,都是通过指针来访问,那么是不是只要没有额外的指针指向它,就可以判定为它不再被使用呢?


基于这个想法,人们想出了引用计数的算法。


它工作原理是跟踪每个对象被引用的次数,当对象的引用次数变为 0 时,则判定该对象为无用对象, 可以被垃圾回收机制进行回收。


    function fn(){
//创建对象,分配空间
var testObj1={
a:1
}//引用数:1
var testObj2=testObj1//引用数:2
var testObj3=testObj1//引用数:3
var testObj4={
b:testObj1
}//引用数:4
testObj1=null//引用数:3
testObj2=null//引用数:2
testObj3=null//引用数:1
testObj4=null//引用数:1
}
fn()//调用栈执行完毕,垃圾回收

如上代码,引用次数变成0后,堆内存中的对应内存就会被GC。


如下图,当testObj1-4都变成null后,原来的testObj4引用数变成0,而{a:1}这时候的引用数还为1(有一个箭头指向它),而{b:1002}被回收后,它的引用数就变成0,故而最后也被垃圾回收。


3,引用计数的计数数量.drawio.png


引用计数的优点:


引用计数看起来很简单,v8引擎只需要关注计数器即可,一旦对象的引用数变成0,就立即回收。

但是很明显的,引用计数存在两个缺点:


1,每个对象都需要维护一个计数器去记录它的引用数量。
2,如果存在相互循环引用的对象,因为各自的引用数量无法变成0(除非手动改变),因而无法被垃圾回收。

对于第二点,如下代码:


function fn(){
//创建对象,分配空间
var testObj1={
a:testObj2
}
var testObj2={
b:testObj1
}
}
fn()

当fn执行完毕后的内存情况如下,因为两个对象相互引用,导致引用数到不了0,就无法被GC:


4.循环引用.drawio.png


因为引用计数的弊端,后续的浏览器开始寻找新的垃圾回收机制,从2012年起,所有现代浏览器都使用了标记清除垃圾回收算法。所有对JavaScript垃圾回收算法的改进都是基于标记清除算法的改进。


3.2,标记清除(mark and sweep)


标记清除是另一种常见的垃圾回收机制。


它工作原理是找出所有活动对象并标记它们,然后清除所有未被标记的对象


其实现步骤如下:



  1. 根节点:垃圾回收机制的起点是一组称为根的对象(有很多根对象),根通常是引擎内部全局变量的引用,或者是一组预定义的变量名,例如浏览器环境中的 Window 对象和 Document 对象。

  2. 遍历标记:从根开始遍历引用的对象,将其标记为活动对象。每个活动对象的所有引用也必须被遍历并标记为活动对象。

  3. 清除:垃圾回收器会清除所有未标记的对象,并使空间可用于后续使用。


因为能从根节点开始被遍历到的(有被使用到的),就是有用的活动对象,而剩余不能被链接到的则是无用的垃圾,需要被清除。


对于前文引用计数中循环引用的例子,就因为从根对象触发,无法遍历到堆空间中的那两个循环引用的对象,就会把它判定为垃圾对象,从而回收。


如下代码:


var obj1={
a:{
b:{
c:3
}
}
}
var obj2={
d:1
}
obj2=null

如下图,从根节点无法遍历到obj2了,就会把d垃圾回收。


5,标记清除.png


按照这个思路,标记清除算法有一个很大的缺点,就是在清除之后,剩余的对象内存位置是不变的,会导致空闲内存空间是不连续的,出现了 内存碎片(如下图),并且由于剩余空闲内存不是一整块,它是由不同大小内存组成的内存列表,这就牵扯出了内存分配的问题:当新对象需要空间存储时,需要遍历空间以找到能够容纳对象大小size的区域:


6,标记清除新增对象.png


这样效率比较低,因而又有了标记整理(Mark-Compact)算法 ,它的标记阶段和标记清除算法没有什么不同,只是标记结束后,标记整理算法会先将活动对象向内存的一端移动,然后再回收未标记的垃圾内存:


7,标记整理算法.png


四,V8引擎的分代回收


如上文所说,在每次垃圾回收时都要检查内存中所有的对象,这样的话对于一些占用空间大、存活时间长的对象,要是和占用空间小、存活时间短的对象一起检查,那不是平白浪费很多不必要的检查资源嘛。


因为前者需要时间长并且不需要频繁进行清理,后者恰好相反,那怎么优化呢?


类似于信誉分,信誉分高的,检查力度就应该小一些嘛。把信誉分抽象一下,其实说的就是分层级管理,于是就有了弱分代假设。


4.1,弱分代假设(The Weak Generational Hypothesis)



  1. 多数对象的生命周期短

  2. 生命周期长的对象,一般是常驻对象


V8的GC也是基于假设将对象分为两代: 新生代和老生代。


对不同的分代执行不同的算法可以更有效的执行垃圾回收。


V8 的垃圾回收策略主要基于分代式垃圾回收机制,将堆内存分为新生代和老生代两区域,采用不同的策略来管理垃圾回收。


他们的内存大小如下:


64位操作系统32位操作系统
V8内存大小1.3G(1432MB)0.7g(716MB)
新生代空间32MB16MB
老生代空间1400MB700MB

4.2,新生代的垃圾回收策略


新生代对象是通过一个名为 Scavenge 的算法进行垃圾回收。


在JavaScript中,任何对象的声明分配到的内存,将会先被放置在新生代中,而因为大部分对象在内存中存活的周期很短,所以需要一个效率非常高的算法。Scavenge算法是一个典型的牺牲空间换取时间的复制算法,在占用空间不大的场景上非常适用。


Scavenge 算法中将新生代内存一分为二,Semi space FromSemi space To,新生区通常只支持 1~8M 的容量。这块区域使用副垃圾回收器来回收垃圾。


工作方式也很简单:


1,等From空间满了以后,垃圾回收器就会把活跃对象打上标记。
2,把From空间已经被标记的活动对象复制到To空间。
3,将From空间的所有对象垃圾回收。
4,两个空间交换,To空间变成From空间,From变成To空间。以此往复。

而判断是否是活跃对象的方法,还是利用的上文说的从根节点遍历,满足可达性则是活跃对象。


具体流程如下图所示,假设有蓝色指针指向的是From空间,没有蓝色指针指向的是To空间:


8,新生代的垃圾回收策略.drawio.png


从上图可以明显地看到,这种方式解决了上文垃圾回收后内存碎片不连续的问题,相当于是利用空间换时间。


现在新生代空间的垃圾回收策略已经了解,那新生代空间中的对象又如何进入老生代空间呢?


4.3,新生代空间对象晋升老生代空间的条件


1,复制某个对象进入to区域时,如果发现内存占用超过to区域的25%,则将其晋升老生代空间。(因为互换空间后要留足够大的区域给新创建对象)
2,经过两次fromto互换后,还存活的对象,下次复制进to区域前,直接晋升老生代空间。

4.4,老生代空间的垃圾回收策略


老生代空间最初的回收策略很简单,这在我们上文也讲过,就是标记整理算法。


1,先根据可达性,给所有的老生代空间中的活动对象打上标记。
2,将活动对象向内存的一端移动,然后再回收未标记的垃圾内存。

这样看起来已经很完美了,但是我们知道js是个单线程的语言,就目前而言,我们的垃圾回收还是全停顿标记:js是运行在主线程上的,一旦垃圾回收生效,js脚本就会暂停执行,等到垃圾回收完成,再继续执行


这样很容易造成页面无响应的情况,尤其是在多对象、大对象、引用层级过深的情况下。


于是在这基础上,又有了增量标记的优化。


五,V8优化


5.1,增量标记


前文所说,我们给老生代空间中的所有对象打上活动对象的标记,是从一组根节点出发,根据可达性遍历而得。这就是全量地遍历,一次性完成,


但因为js是单线程,为了避免标记导致主线程卡滞。于是人们想出来和分片一样的思路:主线程每次遍历一部分,就去干其他活,然后再接着遍历。如下图:


9,增量标记.png


增量标记就是将一次 GC 标记的过程,分成了很多小步,每执行完一小步就让应用逻辑执行一会儿,这样交替多次后完成一轮 GC 标记,这样就算页面卡滞,因为时间很短,使用者也感受不到,体验就好了很多。


但是这又引发了一个新的问题:每次遍历一部分节点就停下来,下次继续怎么识别到停顿点,然后继续遍历呢?


V8又引入了三色标记法。


5.2,三色标记法


首先要明确初心:三色标记法要解决的问题是遍历节点的暂停与恢复。


使用两个标志位编码三种颜色:白色(00),灰色(10)和黑色(11)。


白色:指的是未被标记的对象,可以回收


灰色:指自身被标记,成员变量(该对象的引用对象)未被标记,即遍历到它了,但是它的下线还没遍历。不可回收


黑色:自身和成员变量都被标记了,是活动对象。不可回收


1,从已知对象开始,即roots(全局对象和激活函数), 将所有非root对象标记置为白色
2,将root对象变黑,同时将root的直接引用对象abc标记为灰色
3,将abc标记为黑色,同时将它们的直接引用对象标记为灰色
4,直到没有可标记灰色的对象时,开始回收所有白色的对象

10,三色标记法.drawio.png


如上图所示,如果第一次增量标记只标记到(2),下次开始时,只要找到灰色节点,继续遍历标记即可。


而遍历标记完成的标志就是内存中不再有灰色的。于是这时候就可以把白色的垃圾回收掉。


那这样就解决了遍历节点的暂停与恢复问题,同时支持增量标记。


(ps:其实这里我有个疑惑,暂停后重新开始的时候,不也要遍历寻找灰色节点嘛,每次恢复都要遍历找灰色节点,不是也耗时嘛?)


5.3,写屏障


按照上文对标记的描述,其实有一个前提条件:在标记期间,代码运行不会变更对象的引用情况。


比如说我采用的是增量标记,前脚刚做好的标记,后脚就被js脚本修改了引用关系,那不是会导致标记结果不可信嘛?如下图:


11,写屏障.drawio.png


就像上图一样,D已经被判定成垃圾了,但是下一个分片的js又引用了它,这时候如果删除,必然不对,所以V8 增量使用 写屏障 (Write-barrier) 机制,即一旦有黑色对象引用白色对象,该机制会强制将引用的白色对象改为灰色,从而保证下一次增量 GC 标记阶段可以正确标记,这个机制也被称作 强三色不变性


那在我们上图的例子中,将对象 B 的指向由对象 C 改为对象 D 后,白色对象 D 会被强制改为灰色。


这样一来,就不会将D判定为垃圾 ,并且图中新增的垃圾C在本轮垃圾回收中也不会回收,而是在下一轮回收了。


5.4,惰性清理


上文的增量标记和三色标记法以及写屏障只是对标记方式的优化。目的是采用分片的思想将标记的流程碎片化。


而清理阶段同样可以利用这个思想。


V8的懒性清理,也称为惰性清理(Lazy Sweeping),是一种垃圾回收机制,用于延迟清理未标记对象所占用的内存空间,以减少垃圾回收期间的停顿时间。


当增量标记结束后,假如当前的可用内存足以让我们快速的执行代码,其实我们是没必要立即清理内存的,于是可以将清理的过程延迟一下,让JavaScript逻辑代码先执行;也无需一次性清理完所有非活动对象内存,垃圾回收器可以按需逐一进行清理,直到所有的页都清理完毕。


六,垃圾回收总结


6.1,初始的垃圾回收策略:从引用计数到标记清除


对于js的垃圾回收,最开始的时候,是采用引用计数的算法,但是因为引用计数存在循环引用导致垃圾无法清除,于是又引入了标记清除算法,而标记清除算法存在碎片空间问题,于是又优化成标记整理算法。


随着技术的发展,v8引擎的垃圾回收机制也在不断完善。


6.2,弱分代假设,划分新老生代空间采用不同策略


第一次完善是采用弱分代假设,为了让内存占用大、存活时间长的对象减少遍历,采用分代模型,分成了新分代和老分代空间,垃圾回收采取不同的策略。


新生代空间以空间换时间,拆分成from和to空间互换位置,解决垃圾回收后内存不连续的问题。


将满足条件的对象晋升到老生代空间。而老生代空间采用标记整理算法。


6.3,从全停顿到引入分片思想


因为js是单线程,如果垃圾回收耗时过长,就会阻塞页面响应。


为了解决标记阶段的全停顿问题,引入了增量标记算法。但是非黑即白的标记算法在下一次重新开始标记时无法找到上次的中断点,所以使用三色标记法。此外,为了避免增量标记过程中js脚本变更引用关系,v8又增加了写屏障。


同样的,为了解决清理阶段的全停顿问题,引入了惰性清理。


七,本系列其他文章


最近在整理js基础,下面是已经完成的文章:


js从编译到执行过程 - 掘金 (juejin.cn)


从异步到promise - 掘金 (juejin.cn)


从promise到await - 掘金 (juejin.cn)


浅谈异步编程中错误的捕获 - 掘金 (juejin.cn)


作用域和作用域链 - 掘金 (juejin.cn)


原型链和原型对象 - 掘金 (juejin.cn)


this的指向原理浅谈 - 掘金 (juejin.cn)


js的函数传参之值传递 - 掘金 (juejin.cn)


js的事件循环机制 - 掘金 (juejin.cn)


从作用域链和内存角度重新理解闭包 - 掘金 (juejin.cn)


八,本文参考文章:


「硬核JS」你真的了解垃圾回收机制吗 - 掘金 (juejin.cn)


一文带你快速掌握V8的垃圾回收机制 - 掘金 (juejin.cn)


[深入浅出]JavaScript GC 垃圾回收机制 - 掘金 (juej

in.cn)

收起阅读 »

什么,产品让我实现自动播放?

web
前言 最近,在逛一些技术群时,看到有人在吐槽,这个video媒体标签设置autoplay属性怎么不生效。不生效就算了,为什么我在dom渲染完成时去获取video元素(假设获取到的元素为el),然后执行el.paly()也不生效,why???? 那为什么我通过控...
继续阅读 »

前言


最近,在逛一些技术群时,看到有人在吐槽,这个video媒体标签设置autoplay属性怎么不生效。不生效就算了,为什么我在dom渲染完成时去获取video元素(假设获取到的元素为el),然后执行el.paly()也不生效,why????


那为什么我通过控制台去执行这两行代码的时候,它又生效了!!!!???


带着满脸疑惑,我们来了解一下浏览器的自动播放策略。


1、浏览器的自动播放策略


以谷歌浏览器为例:


在某些特定的情况下,浏览器是允许自动播放的:




  1. 静音状态下始终允许自动播放




  2. 有声音自动播放时:



    • 用户进行了页面点击等与界面发生交互行为后。

    • 达到媒体参与指数, 也就是用户之前在本站播放过有声音的视频。

    • 用户将页面添加到移动设备的主屏幕上或者在PC上安装了PWA




  3. 主站可以将自动播放权限委托给它们的 iframe,以允许自动播放声音。




2、静音播放。


在静音状态下,浏览器是允许自动播放的,代码如下:


<video controls class="videoItem" width="100%" autoplay muted loop="loop" src="./tets.mp4"></video>

属性说明:


muted: 是否静音播放,默认为false


autoplay:是否自动播放,默认为false


control:控制器是否显示,默认为false(可不写)。


loop:是否循环播放,默认为false(可不写)。


唉,好像可以自动播放了,于是我拿去应付产品,产品给我泼了一盆冷水,说能让它播放起来有声音吗?

于是,又开始了我们的与非静音状态自动播放功能的探索。


3、非静音自动播放


我们在上面有了解到非静音自动播放有四种情况:



  1. 用户进行了页面点击等与界面发生交互行为后。

  2. 达到媒体参与指数MEI.

  3. 用户将页面添加到移动设备的主屏幕上或者在PC上安装了PWA

  4. 主站可以将自动播放权限委托给它们的 iframe。


3.1 用户进行了页面交互行为


这个就可以理解成用户触碰了页面之后,我们就可以进行有声音的自动播放了。看代码


HTML部分:


<video controls class="videoItem" width="100%"  loop="loop" src="./tets.mp4"></video>

JS部分:


const vdo = document.querySelector('video')
// 播放函数
async function playAudio() {
const res = await vdo.play()
}
// 监听用户点击
document.addEventListener('click',playAudio)
// 监听媒体播放
vdo.addEventListener('play', function () { //播放开始执行的函数
console.log("开始播放");
// 移除点击监听事件
document.removeEventListener("click",playAudio)
})

这里我们就有很大的发挥空间了,比如说视频不完全在可见区,或者用户看视频前给他弹个框?


3.2 达到媒体参与指数MEI


MEI是浏览器根据我们对一些网站的浏览行为打分,越高,就表示我们喜欢观看这个网站的视频,可以通过about://media-engagement来查询,不可更改。
注意:该策略只对PC端浏览器有效。


3.3 用户将页面添加到移动设备的主屏幕上或者在PC上安装了PWA


用户将页面添加到移动设备的主屏幕上或者在PC上安装了PWA,这个我们基本可以不用去考虑了,在国内是比较少见的,感兴趣的可以去百度。


3.4 主站可以将自动播放权限委托给它们的 iframe。


这个就是通过iframe来控制媒体的自动播放。代码如下:


    <!-- 允许自动播放 -->
<iframe src="跨源地址" allow="autoplay">

<!-- 允许自动播放和设置全屏 -->
<iframe src="跨源地址" allow="autoplay; fullscreen">

需要注意的是,我们可能需要做一些本地代理,不然可能会出现跨域问题。


4 补充


上面还有一点是没有提到的,就是同源下点击页面跳转后可实现自动播放。


比如:我在http://www.bilibili.com页面点击一个视频跳转到了一个新页面,就可以实现自动播放(注意:不能打开新窗口,否则非

作者:清_秋
来源:juejin.cn/post/7244818202214416443
静音自动播放将失效)

收起阅读 »

面试:(简单粗暴点)百度一面,直接问痛我

web
前言 这次的百度面试挺紧张的,在写算法题的时候脑子都有点空白,还是按照脑海中那点残存的算法技巧才写出来,不至于太尴尬,以及第一次面试百度这种级别的公司,难免出现了一些平常不至于出现的问题或没注意的缺点,在这里分享给大家。 百度一面 1. 如何用chatgpt提...
继续阅读 »

前言


这次的百度面试挺紧张的,在写算法题的时候脑子都有点空白,还是按照脑海中那点残存的算法技巧才写出来,不至于太尴尬,以及第一次面试百度这种级别的公司,难免出现了一些平常不至于出现的问题或没注意的缺点,在这里分享给大家。


百度一面


1. 如何用chatgpt提升前端开发效率



因为我嘴贱,平时习惯了使用chatgpt,然后自我介绍说了一句,由于之前面得公司都没问过,导致我没怎么往这方面准备,以至于答得时候牛头不对马嘴,所以说不愧是大厂啊。




  1. 问题解答和指导ChatGPT可以帮助回答与前端开发相关的问题。当你在编写代码的时候,当一时忘记了某个API怎么用,就可以向ChatGPT提问,并获得解答和指导,甚至还会给出一些更加深入且性能更好的应用。这可以帮助更快地解决问题和理解前端开发中的概念。

  2. 代码片段和示例ChatGPT可以帮助你生成常见的前端代码片段和示例。你可以描述你想要实现的功能或解决的问题,然后向ChatGPT请求相关代码片段。这样,您可以更快地获得一些基础代码,从而加快开发速度。

  3. 自动生成文档ChatGPT可以帮助你生成前端代码的文档。你可以描述一个函数、组件或类,并向ChatGPT请求生成相关的文档注释。这可以帮助您更轻松地为你的代码添加文档,提高代码的可读性和可维护性。

  4. 问题排查和调试:在开发过程中,您可能会遇到问题或错误。您可以向ChatGPT描述您遇到的问题,或者直接把代码交给它,并请求帮助进行排查和调试。ChatGPT可以提供一些建议和指导,帮助您更快地找到问题的根本原因并解决它们。

  5. 学习资源和最新信息ChatGPT可以为你提供关于前端开发的学习资源和最新信息。你可以向ChatGPT询问关于前端开发的最佳实践、最新的框架或库、前端设计原则等方面的问题。这可以帮助我们不断学习和更新自己的前端开发知识,从而提高效率。


2. [1, 2, 3, 4, 5, 6, 7, 8, 9] => [[1, 2, 3],[4, 5, 6],[7, 8, 9]],把一个一维数组变成三个三个的二维数组


在JavaScript中,可以使用数组的slice方法和一个循环来将一个一维数组转换为一个二维数组。下面是一个示例代码:


    function convertTo2DArray(arr, chunkSize) {
var result = [];
for (var i = 0; i < arr.length; i += chunkSize) {
result.push(arr.slice(i, i + chunkSize));
}
return result;
}

var inputArray = [1, 2, 3, 4, 5, 6, 7, 8, 9];
var outputArray = convertTo2DArray(inputArray, 3);

console.log(outputArray);

输出结果将是:


    [[1, 2, 3], [4, 5, 6], [7, 8, 9]]


slice 不会修改原数组,只会返回一个浅复制了原数组中的元素的一个新数组,不信的话自己可以编译一下。



这段代码中的convertTo2DArray函数接受两个参数:arr表示输入的一维数组,chunkSize表示每个子数组的大小。它使用slice方法来从输入数组中提取每个子数组,并使用循环来遍历整个数组并构建输出二维数组。最后,它返回生成的二维数组。


3. 输出结果,为什么?


    const obj3 = {a: 1};
const obj4 = {b: 2};
console.log(obj3 == obj4); // false
console.log(obj3 === obj4); // false

结果:


false,false


原因:


在这段代码中,obj3obj4分别是两个独立的对象,它们开辟的堆内存地址是完全不一样。==运算符用于比较两个操作数是否相等,而===运算符用于比较两个操作数是否严格相等。


根据对象的比较规则,当使用==运算符比较两个对象时,它们将会进行类型转换后再进行比较。由于obj3obj4是不同的对象,即使它们的属性值相同,它们的引用也不同,因此在进行类型转换后,它们会被视为不相等的对象。因此,console.log(obj3 == obj4);的输出结果将会是false


而在使用===运算符比较两个对象时,不会进行类型转换,而是直接比较两个操作数的值和类型是否完全相同。由于obj3obj4是不同的对象,且类型也不同,即使它们的属性值相同,它们也不会被视为严格相等的对象。因此,console.log(obj3 === obj4);的输出结果同样会是false



总结起来,无论是使用==运算符还是===运算符,obj3obj4都不会被视为相等或严格相等的对象,因为它们是不同的对象。



4. this有关 输出结果,为什么?


    const obj1 = {
fn: () => {
  return this
}
}
const obj2 = {
fn: function(){
  return this
}
}

console.log(obj1.fn());
console.log(obj2.fn());

输出结果:



  1. window || undefined

  2. obj2


原因是:


在箭头函数 fn 中的 this 关键字指向的是定义该函数的上下文,而不是调用该函数的对象。因此,当 obj1.fn() 被调用时,由于箭头函数没有它自己的this,当你调用fn()函数时,this指向会向上寻找,因此箭头函数中的 this 指向的是全局对象(在浏览器环境下通常是 window 对象),因此返回的是 undefined


而在普通函数 fn 中的 this 关键字指向的是调用该函数的对象。在 obj2.fn() 中,函数 fn 是作为 obj2 的方法被调用的,所以其中的 this 指向的是 obj2 对象本身,因此返回的是 obj2


需要注意的是,在严格模式下,普通函数中的 this 也会变为 undefined,因此即使是 obj2.fn() 也会返回 undefined。但在示例中没有明确指定使用严格模式,所以默认情况下运行在非严格模式下。


5. Promise有关输出结果,为什么?


    console.log('1');
function promiseFn() {
return new Promise((resolve, reject) => {
  setTimeout(()=> {
    console.log('2');
  })
  resolve('3');
  console.log('4')
})
}

promiseFn().then(res => {
console.log(res);
});

输出结果: 1 4 3 2


原因是:



  1. 首先,代码从上往下执行,把console.log('1')放入同步任务

  2. 再调用promiseFn(),因为new Promise是同步任务,所以放入同步任务,继续执行

  3. 遇到setTimout这个宏任务,放入宏任务队列中

  4. 遇到resolve('3'),把res返回

  5. 之后再执行.then(),因为promise.then是微任务,所以放入微任务队列

  6. 代码是先执行同步任务,再执行微任务,之后再是宏任务

  7. 所以输出结果为1 4 3 2



这里涉及到了EventLoop的执行机制,如果不是太清楚可以看看我的面试题:小男孩毕业之初次面试第二家公司第一题



6. 实现斐波那契的第N个值(从0开始),要求时间复杂度为O(n)



首先,说到斐波那契第一个想到的肯定是如下的算法,但这可是百度啊,如果只是这种程度的话如何能和同样面相同岗位的人竞争呢,所以我们得想到如下算法有什么缺点,然后如何优化



function fib(n) {
if (n == 0 || n === 1) return 1;
return fib(n - 1) + fib(n - 2);
};

console.log(fib(3)); // 5
console.log(fib(5)); // 8

单纯的使用递归看似没什么问题,也能运算出结果,但是里面有个致命的问题,首先,时间复杂度就不对,递归思想的复杂度为 O(2^n) ,它不为O(n),然后还有会重复计算,比如计算n=3时,会计算fib(1) + fib(2),再次计算fib(4)时,会先算fib(3) = fib(1) + fib(2),然后再计算fib(4) = fib(1) + fib(2) + fib(3),在这里,fib(1)和fib(2)重复计算了两次,对于性能损耗极大。此时的你如果对动态规划敏感的话,就会从中想到动态规划其中最关键的特征——重叠子问题



因此,使用动态规划来规避重复计算问题,算是比较容易想到较优的一种解法,并且向面试官展现了你算法能力中有动态规划的思想,对于在面试中的你加分是极大的。



以下是动态规划思路的算法,状态转移方程为dp[i] = dp[i-1] + dp[i-2]


function fibonacci(n) { 
if (n <= 1) return n;
let fib = [0, 1]; // 保存斐波那契数列的结果
for (let i = 2; i <= n; i++) {
fib[i] = fib[i - 1] + fib[i - 2]; // 计算第i个斐波那契数
}
return fib[n];
}



当然,你可能会说,在面试中怎么可能一下子就能想到动态规划,所以在面试前你需要背一背相关的状态转移方程,当你对算法问题分析到一定程度时,就能够记忆起这些状态转移方程,提高你写算法的速度。



在面试中,动态规划的常用状态转移方程可以根据问题的具体情况有所不同。以下是几个常见的动态规划问题和它们对应的状态转移方程示例:




  1. 斐波那契数列(Fibonacci Sequence):



    • dp[i] = dp[i-1] + dp[i-2],其中 dp[i] 表示第 i 个斐波那契数。




  2. 爬楼梯问题(Climbing Stairs):



    • dp[i] = dp[i-1] + dp[i-2],其中 dp[i] 表示爬到第 i 级楼梯的方法数。




  3. 背包问题(Knapsack Problem):



    • dp[i][j] = max(dp[i-1][j], dp[i-1][j-weight[i]] + value[i]),其中 dp[i][j] 表示在前 i 个物品中选择总重量不超过 j 的最大价值,weight[i] 表示第 i 个物品的重量,value[i] 表示第 i 个物品的价值。




  4. 最长递增子序列(Longest Increasing Subsequence):



    • dp[i] = max(dp[j] + 1, dp[i]),其中 dp[i] 表示以第 i 个元素结尾的最长递增子序列的长度,j0i-1 的索引,且 nums[i] > nums[j]




  5. 最大子数组和(Maximum Subarray Sum):



    • dp[i] = max(nums[i], nums[i] + dp[i-1]),其中 dp[i] 表示以第 i 个元素结尾的最大子数组和。




  6. 最长公共子序列(Longest Common Subsequence):




    • 如果 str1[i] 等于 str2[j],则 dp[i][j] = dp[i-1][j-1] + 1




    • 否则,dp[i][j] = max(dp[i-1][j], dp[i][j-1]),其中 dp[i][j] 表示 str1 的前 i 个字符和 str2 的前 j 个字符的最长公共子序列的长度。






  7. 编辑距离(Edit Distance):




    • 如果 word1[i] 等于 word2[j],则 dp[i][j] = dp[i-1][j-1]




    • 否则,dp[i][j] = min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) + 1,其中 dp[i][j] 表示将 word1 的前 i 个字符转换为 word2 的前 j 个字符所需的最少操作次数。






  8. 打家劫舍(House Robber):



    • dp[i] = max(dp[i-1], dp[i-2] + nums[i]),其中 dp[i] 表示前 i 个房屋能够获得的最大金额,nums[i] 表示第 i 个房屋中的金额。




  9. 最大正方形(Maximal Square):




    • 如果 matrix[i][j] 等于 1,则 dp[i][j] = min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) + 1




    • 否则,dp[i][j] = 0,其中 dp[i][j] 表示以 matrix[i][j] 为右下角的最大正方形的边长。






7. 手写EventBus


当需要手动实现一个简单的 EventBus 时,你可以创建一个全局的事件总线对象,并在该对象上定义事件的订阅和发布方法。


class EventBus {
constructor() {
this.events = {}; // 存储事件及其对应的回调函数列表
}

// 订阅事件
subscribe(eventName, callback) {
this.events[eventName] = this.events[eventName] || []; // 如果事件不存在,创建一个空的回调函数列表
this.events[eventName].push(callback); // 将回调函数添加到事件的回调函数列表中
}

// 发布事件
publish(eventName, data) {
if (this.events[eventName]) {
this.events[eventName].forEach(callback => {
callback(data); // 执行回调函数,并传递数据作为参数
});
}
}

// 取消订阅事件
unsubscribe(eventName, callback) {
if (this.events[eventName]) {
this.events[eventName] = this.events[eventName].filter(cb => cb !== callback); // 过滤掉要取消的回调函数
}
}
}

使用上述 EventBus 类,你可以执行以下操作:


// 创建全局事件总线对象
const eventBus = new EventBus();

const callback1 = data => {
console.log('Callback 1:', data);
};

const callback2 = data => {
console.log('Callback 2:', data);
};

// 订阅事件
eventBus.subscribe('event1', callback1);
eventBus.subscribe('event1', callback2);

// 发布事件
eventBus.publish('event1', 'Hello, world!');

// 输出:
// Callback 1: Hello, world!
// Callback 2: Hello, world!

// 取消订阅事件
eventBus.unsubscribe('event1', callback1);

// 发布事件
eventBus.publish('event1', 'Goodbye!');

// 输出:
// Callback 2: Goodbye!

在上述示例中,我们创建了一个 EventBus 类,该类具有 subscribepublishunsubscribe 方法。subscribe 方法用于订阅事件,publish 方法用于发布事件并触发相关的回调函数,unsubscribe 方法用于取消订阅事件。我们使用全局的 eventBus 对象来执行订阅和发布操作。


这个简单的 EventBus 实现允许你在不同的组件或模块之间发布和订阅事件,以实现跨组件的事件通信和数据传递。你可以根据需要对 EventBus 类进行扩展,添加更多的功能,如命名空间、一次订阅多个事件等。



当问到EventBus时,得预防面试官问到EvnetEmitter,不过当我在网上查找相关的资料时,发现很多人似乎都搞混了这两个概念,虽然我在这里的手写原理似乎也差不多,但在实际使用中,两者可能在细节上有所不同。因此,在具体场景中,你仍然需要根据需求和所选用的实现来查看相关文档或源码,以了解它们的具体实现和用法。



下面是一个简单的 EventEmitter 类实现的基本示例:


class EventEmitter {
constructor() {
this.events = {}; // 用于存储事件及其对应的回调函数列表
}

// 订阅事件
on(eventName, callback) {
this.events[eventName] = this.events[eventName] || []; // 如果事件不存在,创建一个空的回调函数列表
this.events[eventName].push(callback); // 将回调函数添加到事件的回调函数列表中
}

// 发布事件
emit(eventName, data) {
if (this.events[eventName]) {
this.events[eventName].forEach(callback => {
callback(data); // 执行回调函数,并传递数据作为参数
});
}
}

// 取消订阅事件
off(eventName, callback) {
if (this.events[eventName]) {
this.events[eventName] = this.events[eventName].filter(cb => cb !== callback); // 过滤掉要取消的回调函数
}
}

// 添加一次性的事件监听器
once(eventName, callback) {
const onceCallback = data => {
callback(data); // 执行回调函数
this.off(eventName, onceCallback); // 在执行后取消订阅该事件
};
this.on(eventName, onceCallback);
}
}

使用上述 EventEmitter 类,你可以执行以下操作:


    const emitter = new EventEmitter();

const callback1 = data => {
console.log('Callback 1:', data);
};

const callback2 = data => {
console.log('Callback 2:', data);
};

// 添加一次性事件监听器
const onceCallback = data => {
console.log('Once Callback:', data);
};

// 订阅事件
emitter.on('event1', callback1);
emitter.on('event1', callback2);
emitter.once('event1', onceCallback);

// 发布事件
emitter.emit('event1', 'Hello, world!');

// 输出:
// Callback 1: Hello, world!
// Callback 2: Hello, world!
// Once Callback: Hello, world!

// 取消订阅事件
emitter.off('event1', callback1);

// 发布事件
emitter.emit('event1', 'Goodbye!');

// 输出:
// Callback 2: Goodbye!

在上述示例中,EventEmitter 类具有 onemitoffonce 方法。on 方法用于订阅事件,emit 方法用于发布事件并触发相关的回调函数,off 方法用于取消订阅事件,once方法用于添加一次性的事件监听器。你可以根据需求对 EventEmitter 类进行扩展,添加更多的功能,比如一次订阅多个事件、取消所有事件订阅等。


eventBus,eventEmitter的区别


EventBusEventEmitter 都是用于实现事件发布-订阅模式的工具,但它们在实现和使用上有一些区别。




  1. 实现方式:



    • EventBusEventBus 是一个全局的事件总线,通常是作为一个单例对象存在,用于在不同组件或模块之间传递事件和数据。在 Vue.js 中,Vue 实例可以充当 EventBus 的角色。

    • EventEmitterEventEmitter 是一个基于类的模块,通常是作为一个实例对象存在,用于在单个组件或模块内部实现事件的发布和订阅。




  2. 使用范围:



    • EventBusEventBus 的作用范围更广泛,可以跨越不同组件、模块或文件进行事件的发布和订阅。它可以实现多个组件之间的通信和数据传递。

    • EventEmitterEventEmitter 主要用于单个组件或模块内部,用于实现内部事件的处理和通信。




  3. 依赖关系:



    • EventBusEventBus 通常需要一个中央管理的实例,因此需要在应用程序的某个地方进行创建和管理。在 Vue.js 中,Vue 实例可以用作全局的 EventBus

    • EventEmitterEventEmitter 可以在需要的地方创建实例对象,并将其用于内部事件的发布和订阅。




  4. 命名空间:



    • EventBusEventBus 可以使用不同的事件名称来进行事件的区分和分类,可以使用命名空间来标识不同类型的事件。

    • EventEmitterEventEmitter 通常使用字符串作为事件的名称,没有直接支持命名空间的概念。





总结起来,EventBus 主要用于实现跨组件或模块的事件通信和数据传递,适用于大型应用程序;而 EventEmitter 主要用于组件或模块内部的事件处理和通信,适用于小型应用程序或组件级别的事件管理。选择使用哪种工具取决于你的具体需求和应用场景。



8. (场景题)在浏览器中一天只能弹出一个弹窗,如何实现,说一下你的思路?


要在浏览器中实现一天只能弹出一个弹窗的功能,可以使用本地存储(localStorage)来记录弹窗状态。下面是一种实现方案:



  1. 当页面加载时,检查本地存储中是否已存在弹窗状态的标记。

  2. 如果标记不存在或者标记表示上一次弹窗是在前一天,则显示弹窗并更新本地存储中的标记为当前日期。

  3. 如果标记存在且表示上一次弹窗是在当天,则不显示弹窗。


以下是示例代码:


    // 检查弹窗状态的函数
function checkPopupStatus() {
// 获取当前日期
const currentDate = new Date().toDateString();

// 从本地存储中获取弹窗状态标记
const popupStatus = localStorage.getItem('popupStatus');

// 如果标记不存在或者标记表示上一次弹窗是在前一天
if (!popupStatus || popupStatus !== currentDate) {
// 显示弹窗
displayPopup();

// 更新本地存储中的标记为当前日期
localStorage.setItem('popupStatus', currentDate);
}
}

// 显示弹窗的函数
function displayPopup() {
// 在这里编写显示弹窗的逻辑,可以是通过修改 DOM 元素显示弹窗,或者调用自定义的弹窗组件等
console.log('弹出弹窗');
}

// 在页面加载时调用检查弹窗状态的函数
checkPopupStatus();

在这个实现中,checkPopupStatus 函数会在页面加载时被调用。它首先获取当前日期,并从本地存储中获取弹窗状态的标记。如果标记不存在或者表示上一次弹窗是在前一天,就会调用 displayPopup 函数显示弹窗,并更新本地存储中的标记为当前日期。


通过这种方式,就可以确保在同一天只能弹出一个弹窗,而在后续的页面加载中不会重复弹窗。


9. 项目中的性能优化?




  1. 对组件和图片进行懒加载对暂时未使用的组件和图片使用懒加载可以显著地减少页面加载时间,比如在我的项目中路由配置中除了需要频繁切换的页面组件外,其他的组件都使用箭头函数引入组件进行懒加载,以及一些没有展现在界面的图片也进行了一个VueLazy的懒加载。




  2. 减少HTTP请求数量由于频繁的请求会对后端服务器造成极大的负担,所以应该减少不必要的请求,比如在我的项目中的搜索界面,对于搜索按钮增加了防抖功能




  3. 使用缓存使用浏览器缓存可以减少资源请求,从而提高页面加载速度。项目中我会把用户的一些需要持久化的信息存入本地存储。




  4. 异步请求使用Promise.all:异步请求可以在后台加载资源,从而避免阻塞页面加载。在请求数据时,我会使用Promise.all一次性并行的请求类似的数据,而不需要一个一个的请求,较少了请求时间。




  5. 图片优化使用适当的图片格式和大小可以减少页面的资源请求和加载时间,项目中我会把图片转化成base64的格式和webp格式,这样可以使图片大小更小




  6. 使用CDN加速:使用CDN可以提高资源的访问速度,从而加快页面加载速度。我项目中的一些第三方资源有时需要请求,因此我会使用CDN内容分发网络来提高访问速度。




  7. 骨架屏(Skeleton Screen):它可以提升用户感知的加载速度和用户体验。虽然骨架屏本身并不直接影响代码性能,但它可以改善用户对应用程序的感知,提供更好的用户体验。




10. 项目中遇到的难点,如何解决


1. 数据状态管理


前端登录状态管理



  • 我在一个练手的项目中做前端登录功能的时候, 碰到了购物车需要登录判断的功能,比如用isLogin来判断有没有登录,当时由于没有深入了解vuex,所以我一开始想着把这个isLogin通过组件与组件的传值方法,把这个值传给相应的组件,然后在需要登录组件中进行判断,但后来发现这个方法太麻烦了

  • 后来通过学习了解,使用了vuex这个全局状态管理的方法, 通过使用createStore这个vuex中的API创建了一个全局的登录状态,再通过actions mutations实现登录判断和登录状态共享


组件数据状态管理



  • 我项目中一开始首页、详情页等其他页面越来越多的状态放在同一个store上,虽然感觉有点乱,但实现了数据流和组件开发的分离,使得我更能够专注于数据的管理

  • 但随着数据的增多,感觉实在太乱了,然后得知vuex中可以使用 modules 来进行分模块,相应的页面放入相应的模块状态中,之后再用actions,mutations,state,getters这四件套, 更好的模块化管理数据,能够知道哪些状态是全局共享的(登录), 哪些状态是模块共享的

  • 然后在新的项目中,也就是现在简历上的项目里,尝试使用pinia来管理,因为我发现它更简单(没有mutations),模块化更好,让我对组件状态管理的更加得心应手,学习起来也更加的方便。


node的错误处理



  • 一开始用node写后端的时候,一堆错误,比如路由没配置,数据库报错。使得后面的代码都无法运行,写着写着就感觉写不下去,经常一个错误就需要反复的在脑海中想最后依靠那一丝的灵光一闪才解决

  • 之后我就在app.js这个后端入口文件的最后,添加一个统一的错误处理的中间件,向前端返回状态码和相应的信息后,直接使用next()向后继续执行,这样虽然服务器报了错,但仍然可以执行后续的代码。


跨域问题



  • 在我写完前端项目的时候,想要提升一下自己,就转去学习了Koa,在搭建了大致的服务器,写了一个简单的接口并运行服务器后,我想当然的就在前端直接请求后端的端口,结果报了一个跨域的错误,由于当时初学后端,不怎么了解跨域,所以找了很多的解答并逐个在项目中进行尝试,比如跨域中的scriptpostMessagehtml本身的Websocket

  • 但发现最实用的还是在服务器中配置Access-Control-Allow-Origin来控制跨域请求的url地址,以及其他一些Access-Control-Allow头来控制跨域请求方法等,然后跨域请求url的白名单我放入了.env这个全局环境变量中。


axios响应拦截



  • 在后端返回数据的时候,我返回数据有一个状态码以及添加到data这个需要返回的数据(代码如下),这导致我在获取接口里的数据时需要多.data(引用一层data),当时我没意识到,结果一直获取不到数据。之后输出获取的数据才发现在数据外面包了一层,虽然这个时候解决了服务器那边数据返回的问题,但后面每次获取数据时都需要在往里再获取,非常的麻烦。

  • 最后在学习了并在项目中使用axios进行请求和响应后,就在响应的时候设置一个拦截器,对响应进行一番处理之后就可以直接拿到后端接口返回的值,而不会导致接口返回的值不会有太多的嵌套了。


11. 如何学习前端的,学了几年?



这个就看个人情况了,但其中,你得展现出你的学习积极性和对前端的热爱,让面试官能够欣赏你



我大致说说我回答的,仅作参考


我从大二开始就对前端很感兴趣,当时正好学校也分了Web前端的方向,于是就跟着学校的课程开始学习基本的html,css,js三剑客,但之后感觉到老师教的很慢,就自己到B站上学习了,之后由于参加过一次蓝桥杯,就看到了蓝桥云课上有相关的基于html,css,js比较基础项目,接着我还学习了一些行内大牛写的一些博客文章,比如阮一峰,张鑫旭,廖雪峰等这些老师。之后又学习了vue并且在GitHub上学习相关的设计理念,根据GitHub上项目中不懂的东西又逐渐学习了各种UI组件库和数据请求方式,最后又学习了Nodejs中的Koa,用Vue和Koa仿写了一个全栈型项目,目前正在学习一些typescript的基本用法并尝试着运用到项目中,并在学习Vue的一些底层源码。


结语及吐槽


大厂的面试终归到底还是和我之前面的公司不一样,它们更加看重的是代码底层的实现和你的算法基础,终归到底,这次面试只是一次小尝试,想要知道自己的水平到底在哪里,并且能够借此完善自己的能力,努力的提升自己,希望能够给

作者:吃腻的奶油
来源:juejin.cn/post/7240751116701728805
大家带来一些正能量。

收起阅读 »

优化图片和视频的加载过程,提升用户体验

web
展示效果 (因为掘金不能上传视频,所以转成动图之后分辨率比较低,还望多包涵) 展示都是基于 Slow 3G 弱网下的效果。 优化前 这种体验交较差,在图片下载完之前,本应该展示图片的区域会长时间空白。 优化后 图片下载过程中显示模糊的图片占位符,直到图片下...
继续阅读 »

展示效果


(因为掘金不能上传视频,所以转成动图之后分辨率比较低,还望多包涵)


展示都是基于 Slow 3G 弱网下的效果。


优化前


before.gif


这种体验交较差,在图片下载完之前,本应该展示图片的区域会长时间空白。


优化后


eeeee.gif


图片下载过程中显示模糊的图片占位符,直到图片下载完成再切换展示。


原理


首先先贴出页面的代码 index.html:


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<style>
html,
body {
margin: 0;
padding: 0;
}

@keyframes pulse {
0% {
opacity: 0;
}
50% {
opacity: 0.1;
}
100% {
opacity: 0;
}
}

.container {
width: 50vw;
background-repeat: no-repeat;
background-size: cover;
}

.container.loaded::before {
animation: none;
content: none;
}

.container::before {
content: '';
position: absolute;
inset: 0;
opacity: 0;
animation: pulse 2.5s infinite;
background-color: var(--text-color);
}

.container img,
.container video {
opacity: 0;
transition: opacity 250ms ease-in-out;
}

.container.loaded img,
.container.loaded video {
opacity: 1;
}
</style>
<body>
<!-- container容器加载一个体积非常小的低分辨率图片 -->
<div class="container" style="background-image: url(http://localhost:3000/uploads/10007/fox-small.jpeg);">
<!-- 图片延时加载 loading: lazy -->
<img
src="http://localhost:3000/uploads/10007/fox.jpeg"
loading="lazy"
style="width: 50vw"
/>

</div>

<br/>

<video
id="video"
autoplay
controls="controls"
style="width: 50vw"
poster="http://localhost:3000/uploads/10007/big_buck_bunny-small.png"
src="http://localhost:3000/uploads/10007/big_buck_bunny.mp4"
>
</video>
</body>
<script>
const blurredImageDiv = document.querySelector('.container');
const img = blurredImageDiv.querySelector('img');
function loaded() {
// 图片下载完之后 再展示
blurredImageDiv.classList.add('loaded');
}

if (img.complete) {
loaded();
} else {
img.addEventListener('load', loaded);
}

var poster = new Image();
poster.onload = function () {
// 加载完之后替换 poster url 不会重复请求
const video = document.querySelector('#video');
video.poster = 'http://localhost:3000/uploads/10007/big_buck_bunny.png';
};
poster.src = 'http://localhost:3000/uploads/10007/big_buck_bunny.png';
</script>
</html>

其实原理就是基于原图片生成出一个低分辨率体积非常小的图片(因为体积小,下载会很快),然后作为占位符显示,直到原图片完全下载之后再替换展示原图片。


那么如何生成一个超低分辨率的占位图片呢,可以使用 ffmpeg,需要本地提前安装,我是用的MacOS系统,所以直接通过 brew install ffmpeg 安装了。


如果是服务使用 Docker 部署的话,可参考:


FROM node:16 AS deps
WORKDIR /app
COPY . .
RUN wget https://www.johnvansickle.com/ffmpeg/old-releases/ffmpeg-4.4.1-arm64-static.tar.xz &&\
tar xvf ffmpeg-4.4.1-arm64-static.tar.xz &&\
mv ffmpeg-4.4.1-arm64-static/ffmpeg /usr/bin/ &&\
mv ffmpeg-4.4.1-arm64-static/ffprobe /usr/bin/
#RUN apt install -y ffmpeg
RUN yarn install
RUN yarn build
EXPOSE 3000
ENV PORT 3000
CMD [ "node", "dist/index.js" ]

ffmpeg -i sourcePath.jpg -vf scale=width:height outputPath.jpg
// 约束比例压缩
// width/height 为压缩之后图片的宽高 当其中一个值为 -1 的时候将保持原来的尺寸比例压缩

那么我们可以有如下命令:


ffmpeg -i sourcePath.jpg -vf scale=20:-1 outputPath.jpg
// 压缩之后生成 20 像素宽的图片用于做占位符展示

我们可以写个文件上传的服务,上传图片之后,服务端自动生成一个低分辨率的图片版本,然后将两者的地址url都返回过来。比如 Node 中我们可以使用 fluent-ffmpeg,那么以上命令就对应成代码:


import * as ffmpeg from 'fluent-ffmpeg';
import { FfmpegCommand } from 'fluent-ffmpeg';

export const runFfmpegCmd = (command: FfmpegCommand) =>
new Promise<void>((resolve, reject) => {
command
.on('error', (error) => {
reject(error);
})
.on('end', () => {
resolve();
})
.run();
});


public async uploadImage(user: User.UserInfo, file: Express.Multer.File) {
console.log(file);
const path = join(this.uploadPath, `${user.id}`, '/');

await ensureDir(path);

const { originalname, path: filePath } = file;
const finalPath = path + originalname;
const name = originalname.split('.');
const smallPath = path + name[0] + '-small.' + name[1];
console.log(smallPath);
await rename(filePath, finalPath);

// size 对应 scale=20:-1
await runFfmpegCmd(ffmpeg(finalPath).size('20x?').output(smallPath));

return {
statusCode: HttpStatus.OK,
data: {
path: finalPath,
smallPath,
},
};
}

public async uploadVideo(user: User.UserInfo, file: Express.Multer.File) {
console.log(file);
const path = join(this.uploadPath, `${user.id}`, '/');

await ensureDir(path);

const { originalname, path: filePath } = file;
const finalPath = path + originalname;
const name = originalname.split('.');
const shotName = name[0] + '.png';
const smallName = name[0] + '-small.png';

await rename(filePath, finalPath);

// 生成两个不同分辨率的缩略图
await Promise.all([
runScreenShotCmd(
ffmpeg(finalPath).screenshot({
count: 1,
filename: shotName,
folder: path,
}),
),
runScreenShotCmd(
ffmpeg(finalPath).screenshot({
count: 1,
filename: smallName,
folder: path,
size: '20x?',
}),
),
]);

return {
statusCode: HttpStatus.OK,
data: {
path: finalPath,
shotPath: path + shotName,
smallPath: path + smallName,
},
};
}

代码在自己的github上:im_server


自己本地的 swagger 界面的上传截图:


图片
image.png


视频
image.png


那么我们就可以得到一个超低分辨率的图片了,由于体积非常小,所以下载很快(特别是弱网情况下)。


补充


关于 img 标签的 lazy load 可参考:浏览器IMG图片原生懒加载loading=”lazy”实践指南


使用 imgsrcset 属性可实现根据不同屏幕分辨率加载不同尺寸的图片,进一步提升用户体验,而且没必要在小屏幕中加载超大分辨率的图片:响应式图片


结论


通过使用超低分辨率的占位符图片可以优化用户体验,特别是一些图片素材网站,再结合 img 标签的 loading="lazy"

作者:梦想很大很大
来源:juejin.cn/post/7244352006814679100
code> 懒加载。

收起阅读 »

优雅的时钟翻页效果,让你的网页时钟与众不同!

web
你有没有想过给你的网页时钟添加翻页效果,让它更引人注目,更酷炫吗?如果是的话,你来对地方了! 这篇文章将教你如何通过简单的步骤,为你的网页时钟添加翻页效果。 无论你是 web 开发初学者或是有一定经验的开发者,这篇文章都将为你提供有用的实现技巧和原理解释。 ...
继续阅读 »

你有没有想过给你的网页时钟添加翻页效果,让它更引人注目,更酷炫吗?如果是的话,你来对地方了!


13.gif


这篇文章将教你如何通过简单的步骤,为你的网页时钟添加翻页效果。


无论你是 web 开发初学者或是有一定经验的开发者,这篇文章都将为你提供有用的实现技巧和原理解释。


来,跟着子辰一起开始吧!


思考


01.gif


通过上图可以看到,由 3 翻到 4,其实是 3 的上半部分,与 4 的下半部分,一起翻下来的。


为了便于理解将翻页的过程通过侧面角度展示,解析成下图中所示的样子。


02.png


我们先来看一下 3 是如何呈现的。


03.png


那么由 3 的呈现我们可以知道,4 其实一开始是对折的,然后 4B 翻下来后形成完整的 4。


04.png


那么现在我们将 3 与 4 结合在一起看看。


05.png


由上可知,下一个数字都是对折的,在呈现时,都是有由前一个数字的上半部与下一个数字的上半部,翻转得到新的数字。


既然数字翻页的秘密我们知道了,接下来就是实现了。


06.png


实现翻页


容器背景


首先我们要实现一个承载数字的容器,中间使用伪元素做分割线,这就是时钟的底盘。


<div class="card-container"></div>

.card-container {
background: #2c292c;
width: 200px;
height: 200px;
position: relative;
perspective: 500px;
}

.card-container::before {
z-index: 99;
content: " ";
position: absolute;
left: 0;
top: 50%;
background: #120f12;
width: 100%;
height: 6px;
margin-top: -3px;
}

07.png


下层数字上半部分


接下来我们先来实现背后的下一层的 4,因为 4 分为上下两部分,我们先实现上半部分。


<div class="card-container">
<div class="card1 card-item">4</div>
</div>

/* 因为所有的数字都有公共部分,我们提取出来 */
.card-item {
position: absolute;
width: 100%;
/* 因为每个卡片只有半个数字,所以高度只有百分之50 */
height: 50%;
left: 0;
top: 0;
overflow: hidden;
background: #2c292c;
}

.card1 {
line-height: 200px
}

08.png


下层数字下半部分


<div class="card-container">
<div class="card1 card-item">4</div>
<div class="card2 card-item">4</div>
</div>

.card2 {
top: 50%;
}

首先我们写出来是这样的。


09.png


但是我们要求的是 4 的下半部分向上对折覆盖在 4 的上半部分之上。
所以我们看到的应该是 4 下半部分的背面,通过中线向上对折,并且因为是背面,所以我们不应该看到他。


.card2 {
z-index: 2;
top: 50%;
line-height: 0;
/* 变换原点为上边的中部 */
transform-origin: center top;
/* 对折 180 度 */
transform: rotateX(180deg);
/* 通过这个属性让元素的背面隐藏 */
backface-visibility: hidden;
}

08.png


现在看上去好像和只有上半部分没什么区别,所以我们给他加个 hover 加个过渡效果让它翻转下来看看。


11.gif


这样就看出来了。


上层数字


上层数字的原理就比较简单了,我们参考下层数字的逻辑写。


<div class="card-container">
<div class="card1 card-item">4</div>
<div class="card2 card-item">4</div>
<div class="card3 card-item">3</div>
<div class="card4 card-item">3</div>
</div>

.card3 {
line-height: 200px;
transform-origin: center bottom;
backface-visibility: hidden;
z-index: 2
}

.card4 {
top: 50%;
line-height: 0
}

12.png


现在就是这样效果,同样的,我们给它加个 hover,3 的翻页过渡要与 4 的保持同步。


01-13.gif


现在我们就实现了单个数字的过渡。


翻页时钟


时钟无非就是三个翻页效果加上自动翻页,我们去实现一下。


<!-- data-number 用于存储上一次的时间,来和即将改变的时间对比 -->
<div class="card-container flip" id="card-h" data-number="00">
<div class="card1 card-item">00</div>
<div class="card2 card-item">00</div>
<div class="card3 card-item">00</div>
<div class="card4 card-item">00</div>
</div>
<div class="card-container flip" id="card-m" data-number="00">
<div class="card1 card-item">00</div>
<div class="card2 card-item">00</div>
<div class="card3 card-item">00</div>
<div class="card4 card-item">00</div>
</div>
<div class="card-container flip" id="card-s" data-number="00">
<div class="card1 card-item">00</div>
<div class="card2 card-item">00</div>
<div class="card3 card-item">00</div>
<div class="card4 card-item">00</div>
</div>

/* etc... */

.flip .card2 {
transform: rotateX(0);
}

.flip .card3 {
transform: rotateX(-180deg);
}

// 获取 dom
const hour = document.getElementById("card-h");
const minute = document.getElementById("card-m");
const second = document.getElementById("card-s");

function setHTML(dom, time) {
// 下一次要显示的时间
const nextValue = time.toString().padStart(2, "0");
// 上一次的时间
const curValue = dom.dataset.number;
// 如果下次要显示的时间和上一次的一样,直接退出。比如在同一分钟或同一小时内。
if (nextValue === curValue) {
return;
}
// 重置时分秒的 dom
dom.innerHTML = `<div class="card1 card-item">${nextValue}</div>
<div class="card2 card-item">${nextValue}</div>
<div class="card3 card-item">${curValue}</div>
<div class="card4 card-item">${curValue}</div>`
;
// 移除 flip 属性再次添加以触发过渡再次执行
dom.classList.remove("flip");
dom.clientHeight;
dom.classList.add("flip");
// 时间不同时重置 dataset.number
dom.dataset.number = nextValue;
}

// 获取时分秒并分别设置
function setNumbers() {
var now = new Date();
var h = now.getHours();
var m = now.getMinutes();
var s = now.getSeconds();
setHTML(hour, h);
setHTML(minute, m);
setHTML(second, s);
}

setNumbers();

setInterval(setNumbers, 1000);

13.gif


至此我们就完成了时钟翻页的效果了,你学会了吗?


总结


子辰详细介绍了如何通过简单的步骤,为网页时钟添加翻页效果。


文章从思考开始,通过分析数字翻页的秘密来解决问题。


接着,详细讲解了实现翻页的具体方法和原理,并给出了相应的代码实现。


最后,通过组合多个翻页效果,实现了完整的时钟翻页效果。


如果你认真读完了这篇文章,那么以下这几点都是你所学到的:



  1. 提高对 CSS3 属性的理解和掌握,例如 perspective、transform、backface-visibility 等。

  2. 掌握实现元素翻转动画的基本方法和技巧,包括旋转轴心、变换原点、背面可见性等。

  3. 了解如何通过数据属性(data-*)存储和比较数据,避免不必要的重复操作。

  4. 学会如何通过 JavaScript 操作 DOM 元素,实现网页中的动态效果。


其实实现的效果并不难,代码也都是基础的代码,难的是思考翻页的过程,好的思维方法,才是前端进阶的基础,更是关键。


多看好的文章,多看好的思考过程,都是

作者:子辰Web草庐
来源:juejin.cn/post/7244351125448458296
提升思维的一种方式。

收起阅读 »

移动端网页开发有感

web
前段时间参与了一个移动端页面开发的需求,开发时明显感觉与 pc 端开发相比,移动端页面的限制会更多😭 需求结束后思考了一下究竟是哪些方面感觉不舒服?有没有对应的解决方法?以便下次开发移动端页面时能提升开发效率和体验。 移动端网页容易出现布局问题 🤦‍♂️ 因为...
继续阅读 »

前段时间参与了一个移动端页面开发的需求,开发时明显感觉与 pc 端开发相比,移动端页面的限制会更多😭


需求结束后思考了一下究竟是哪些方面感觉不舒服?有没有对应的解决方法?以便下次开发移动端页面时能提升开发效率和体验。


移动端网页容易出现布局问题 🤦‍♂️


因为页面空间小,容易出现元素重叠、挤压、换行等样式问题,怎么在不同尺寸的设备上合适地展示页面?


解决办法:




  1. 使用 <meta name="viewport"> 标签 ✨


    这个标签想必做过移动端页面开发的同学都不陌生吧?它就是专门为移动端展示优化而增加的标签。


    先来看看它的作用是什么?


    它可以设置移动端页面宽度、缩放比例、是否允许用户缩放网页等


    它的基本属性有哪些?


    属性名含义取值范围
    width控制视口大小具体数值或 'device-width'
    initial-scale页面初始缩放比例0.0 ~ 10
    minimum-scale控制页面允许被缩小的倍数0.0 ~ 10
    maximum-scale控制页面允许被大的倍数0.0 ~ 10
    user-scalable控制是否允许放大和缩小页面yes 或 no

    需要注意的是在移动设备上默认页面宽度为 980px:




Luban_16853778753692bc31648-c60e-4ebc-845c-3bac272f7393.jpg


假如我们希望页面的视口宽度与设备宽度相同,同时初始缩放比例为 1,可以在 <head> 里增加这个的 meta 标签
<meta name="viewport" content="width=device-width,initial-scale=1">


Luban_1685377875383f6d72d0e-c360-43aa-807a-739399af01fe.jpg


这样页面的展示就符合我们的预期了




  1. 使用 vw、vh 视口单位
    vw、vh 都是视口单位


    简而言之:100vw = 视口宽度,100vh = 视口高度 (够简单吧 😅




  2. 使用 rem 相对单位




rem 在移动端开发中很重要,因为不同移动设备有着不同的尺寸和分辨率,如果我们用固定的 px 作为元素大小单位会发现不够适用


而 rem 是相对单位大小,它相对的是根元素 html 的字体大小,比如:


<html>
<head>
<style>
html {
font-size: 14px; // 这里将 html 的字体大小设为 14px
}
.content {
font-size: 2rem; // 在页面展示时将会被计算成 14 * 2 = 28px
}
</style>
</head>
<body>
<div class="content">rem</div>
</body>
</html>

所以我们可以根据设备大小动态设置根元素大小,从而成比例地更改页面里其它元素的大小


    const BASE_PAGE_WIDTH = 370
const BASE_SIZE = 16

function setRem() {
const scale = document.documentElement.clientWidth / BASE_PAGE_WIDTH
document.documentElement.style.fontSize = `${scale * BASE_SIZE}px`
}

setRem()

真机调试比较麻烦 😌


尽管可以在电脑浏览器上模拟移动设备的展示情况,但并不够真实,在开发和测试阶段仍然需要在真机上调试;


同时可能我们的测试环境需要连接 vpn 或添加特定的请求头才能访问到,所以在手机上需要通过电脑代理才能访问测试环境,从而进行开发测试;


最后,即使能在手机上访问到本地开发和测试环境的页面,你会发现当页面报错的时候你压根就看不到 log 日志输出或网络请求,这种干看着页面有问题却不能进一步排查的感觉就很难受 😖


还好有一些工具可以帮我们化解这些难题 🥳


解决办法:



  1. 首先我们可以使用 whistlecharles 来连接电脑代理,这里以 whistle 为例:

    • 电脑安装并启动 whistle

    • 手机和电脑在同一局域网下

    • 手机设置网络代理

    • 手机安装 https 证书





具体操作可以访问官方文档的详细步骤哈



Done!现在手机会通过电脑作为代理来访问网络,可以直接访问开发地址或测试环境地址啦~



  1. 然后我们可以使用 VConsole 在移动设备上进行调试,它相当于在页面上加了一个控制台,从而让我们可以查看页面上的日志输出、网络请求等,它的用法也很简单:


// 使用 npm 安装
npm install vconsole

import VConsole from 'vconsole'

new VConsole()

// 使用 CDN 安装
<script src="https://unpkg.com/vconsole@latest/dist/vconsole.min.js"></script>
<script>
// VConsole will be exported to `window.VConsole` by default.
const vConsole = new window.VConsole();
</script>


然后你会发现页面右下角多了一个 vConsole 的按钮:


Screenshot_2023-06-12-23-46-33-090_mark.via.jpg


我们可以测试一下打印日志:


    document.addEventListener('click', handlePageClick)

function handlePageClick() {
console.log('Daniel Yang')
}

在点击页面后再点击 vConsole 按钮会发现在展开的面板里 log 一栏已经显示出 log 的内容:


Screenshot_2023-06-12-23-48-35-778_mark.via.jpg


同时我们也可以在 VConsole 面板上查看页面元素结构、客户端存储、网络请求等,总之非常的 nice 🤗


以上就是自己对一次移动端网页开发过程中遇到的问题小小的总结,如果你在移动端开发中有遇到其它印象深刻的坑,欢迎一起留言讨论哦


006APoFYly8hesgm67dwpj30hs0hbmxv.jpeg

作者:卡布奇诺有点苦
来源:juejin.cn/post/7243757233666195515
th="50%"/>

收起阅读 »

百分百空手接大锅

web
背景 愉快的双休周末刚过完,早上来忽然被运营通知线上业务挂了,用户无法下单。卧槽,赶紧进入debug模式,一查原来是服务端返回的数据有问题,赶紧问了服务端,大佬回复说是业务部门配置套餐错误。好在主责不在我们,不过赶紧写了复盘文档,主动找自己的责任,扛起这口大锅...
继续阅读 »

背景


愉快的双休周末刚过完,早上来忽然被运营通知线上业务挂了,用户无法下单。卧槽,赶紧进入debug模式,一查原来是服务端返回的数据有问题,赶紧问了服务端,大佬回复说是业务部门配置套餐错误。好在主责不在我们,不过赶紧写了复盘文档,主动找自己的责任,扛起这口大锅,都怪我们前端,没有做好前端监控,导致线上问题持续两天才发现。原本以为运营会把推辞一下说不,锅是她们的,可惜人家不太懂人情世故,这锅就扣在了技术部头上。虽然但是,我还是静下心来把前端异常监控搞了出来,下次一定不要主动接锅,希望看到本文的朋友们也不要随便心软接锅^_^


监控


因为之前基于sentry做了埋点处理,基础已经打好,支持全自动埋点、手动埋点和数据上报。相关的原理可以参考之前的一篇文章如何从0-1构建数据平台(2)- 前端埋点。本次监控的数据上报也基于sentry.js。那么如何设计整个流程呢。具体步骤如下:




  1. 监控数据分类




  2. 监控数据定义




  3. 监控数据收集




  4. 监控数据上报




  5. 监控数据输出




  6. 监控数据预警




数据分类


我们主要是前端的数据错误,一般的异常大类分为逻辑异常和代码异常。基于我们的项目,由于涉及营收,我们就将逻辑错误专注于支付异常,其他的代码导致的错误分为一大类。然后再将两大异常进行细分,如下:




  1. 支付异常


    1.1 支付成功


    1.2 支付失败




  2. 代码异常


    2.1 bindexception


     2.1.1  js_error

    2.1.2 img_error

    2.1.3 audio_error

    2.1.4 script_error

    2.1.5 video_error



  3. unhandleRejection


    3.1 promise_unhandledrejection_error


    3.2 ajax_error




  4. vueException




  5. peformanceInfo




数据定义


基于sentry的上报数据,一般都包括事件与属性。在此我们定义支付异常事件为“page_h5_pay_monitor”,定义代码异常事件为“page_monitor”。然后支付异常的属性大概为:



pay_time,

pay_orderid,

pay_result,

pay_amount,

pay_type,

pay_use_coupon,

pay_use_coupon_id,

pay_use_coupon_name,

pay_use_discount_amount,

pay_fail_reason,

pay_platment


代码异常不同的错误类型可能属性会有所区别:



// js_error

monitor_type,

monitor_message,

monitor_lineno,

monitor_colno,

monitor_error,

monitor_stack,

monitor_url

// src_error

monitor_type,

monitor_target_src,

monitor_url

// promise_error

monitor_type,

monitor_message,

monitor_stack,

monitor_url

// ajax_error

monitor_type,

monitor_ajax_method,

monitor_ajax_data,

monitor_ajax_params,

monitor_ajax_url,

monitor_ajax_headers,

monitor_url,

monitor_message,

monitor_ajax_code

// vue_error

monitor_type,

monitor_message,

monitor_stack,

monitor_hook,

monitor_url

// peformanceInfo 为数据添加 loading_time 属性,该属性通过entryTypes获取

try {

const observer = new PerformanceObserver((list) => {

for (const entry of list.getEntries()) {

if (entry.entryType === 'paint') {

sa.store.set('loading_time', entry.startTime)

}
}

})

observer.observe({ entryTypes: ['paint'] })

} catch (err) {

console.log(err)

}


数据收集


数据收集通过事件绑定进行收集,具体绑定如下:


import {

BindErrorReporter,

VueErrorReporter,

UnhandledRejectionReporter

} from './report'

const Vue = require('vue')


// binderror绑定

const MonitorBinderror = () => {

window.addEventListener(

'error',

function(error) {

BindErrorReporter(error)

},true )

}

// unhandleRejection绑定 这里由于使用了axios,因此ajax_error也属于promise_error

const MonitorUnhandledRejection = () => {

window.addEventListener('unhandledrejection', function(error) {

if (error && error.reason) {

const { message, code, stack, isAxios, config } = error.reason

if (isAxios && config) {

// console.log(config)

const { data, params, headers, url, method } = config

UnhandledRejectionReporter({

isAjax: true,

data: JSON.stringify(data),

params: JSON.stringify(params),

headers: JSON.stringify(headers),

url,

method,

message: message || error.message,

code

})

} else {

UnhandledRejectionReporter({

isAjax: false,

message,

stack

})

}

}

})

}

// vueException绑定

const MonitorVueError = () => {

Vue.config.errorHandler = function(error, vm, info) {

const { message, stack } = error

VueErrorReporter({

message,

stack,

vuehook: info

})

}

}

// 输出绑定方法

export const MonitorException = () => {

try {

MonitorBinderror()

MonitorUnhandledRejection()

MonitorVueError()

} catch (error) {

console.log('monitor exception init error', error)

}

}


数据上报


数据上报都是基于sentry进行上报,具体如下:



/*

* 异常监控库 基于sentry jssdk

* 监控类别:

* 1、window onerror 监控未定义属性使用 js资源加载失败问题

* 2、window addListener error 监控未定义属性使用 图片资源加载失败问题

* 3、unhandledrejection 监听promise对象未catch的错误

* 4、vue.errorHandler 监听vue脚本错误

* 5、自定义错误 包括接口错误 或其他diy错误

* 上报事件: page_monitor

*/


// 错误类别常量

const ERROR_TYPE = {

JS_ERROR: 'js_error',

IMG_ERROR: 'img_error',

AUDIO_ERROR: 'audio_error',

SCRIPT_ERROR: 'script_error',

VIDEO_ERROR: 'video_error',

VUE_ERROR: 'vue_error',

PROMISE_ERROR: 'promise_unhandledrejection_error',

AJAX_ERROR: 'ajax_error'

}

const MONITOR_NAME = 'page_monitor'

const PAY_MONITOR_NAME = 'page_h5_pay_monitor'

const MEMBER_PAY_MONITOR_NAME = 'page_member_pay_monitor'

export const BindErrorReporter = function(error) {

if (error) {

if (error.error) {

const { colno, lineno } = error

const { message, stack } = error.error

// 过滤

// 客户端会有调用calljs的场景 可能有一些未知的calljs

if (message && message.toLowerCase().indexOf('calljs') !== -1) {

return

}

sa.track(MONITOR_NAME, {

//属性

})

} else if (error.target) {

const type = error.target.nodeName.toLowerCase()

const monitorType = type + '_error'

const src = error.target.src

sa.track(MONITOR_NAME, {

//属性

})

}

}

}

export const UnhandledRejectionReporter = function({

isAjax = false,

method,

data,

params,

url,

headers,

message,

stack,

code

}
) {

if (!isAjax) {

// 过滤一些特殊的场景

// 1、自动播放触发问题

if (message && message.toLowerCase().indexOf('user gesture') !== -1) {

return

}

sa.track(MONITOR_NAME, {

//属性

})

} else {

sa.track(MONITOR_NAME, {

//属性

})

}

}

export const VueErrorReporter = function({ message, stack, vuehook }) {

sa.track(MONITOR_NAME, {

//属性

})

}

export const H5PayErrorReport = ({

isSuccess = true,

amount = 0,

type = -1,

couponId = -1,

couponName = '',

discountAmount = 0,

reason = '',

orderid = 0,

}
) => {

// 事件名:page_member_pay_monitor

sa.track(PAY_MONITOR_NAME, {

//属性

})

}


以上,通过sentry的sa.track进行上报,具体不作展开


输出与预警


数据被上报到大数据平台,被存储到hdfs中,然后我们直接做定时任务读取hdfs进行一定的过滤通过钉钉webhook输出到钉钉群,另外如果有需要做数据备份可以通过hdfs到数据仓库再到kylin进行存储。


总结


数据监控对于大的,特别是涉及营收的平台是必要的,我们在设计项目的时候一定要考虑到,最好能说服服务端,让他们服务端也提供相应的代码监控。ngnix层或者云端最好也来一层。严重的异常可以直接给你打电话,目前云平台都有相应支持。这样有异常及时发现,锅嘛,接到手里

作者:CodePlayer
来源:juejin.cn/post/7244363578429030459
就可以精准扔出去了。

收起阅读 »

最近遇到的奇葩进度条

web
前言 本文将介绍几个我最近遇到的奇葩进度条,需求看似简单,但是我们可以用平常巧妙的属性来解决,再也不用复杂的html结构和颜色渐变算法。 “奇葩”的环形渐变进度条 需求描述:需要环形渐变的进度条让人快速理解进度实现程度,10-20%是青绿色,20%到30%是黄...
继续阅读 »

前言


本文将介绍几个我最近遇到的奇葩进度条,需求看似简单,但是我们可以用平常巧妙的属性来解决,再也不用复杂的html结构和颜色渐变算法。


“奇葩”的环形渐变进度条


需求描述:需要环形渐变的进度条让人快速理解进度实现程度,10-20%是青绿色,20%到30%是黄色.....


乍一看是不是很容易,但是我思来想去用了echarts的svg渲染,但是只要到了90%,一定会渐变到青绿色,从红色渐变到青绿色,做实让我心一凉。


image.png


思路一:径向渐变分割


网上思路很多,稍微复杂的比如分割区域做大量的颜色的径向渐变。原理是将rgba转为16进制计算颜色插值。这样我们通过计算step步长就可以根据细分做渐变了。但是好像无法很好满足我们的指定区域10%-20%是某种颜色,虽然可以但是也太麻烦了。


  function gradientColor(startRGB, endRGB, step) {
let startR = startRGB[0]
let startG = startRGB[1]
let startB = startRGB[2]
let endR = endRGB[0]
let endG = endRGB[1]
let endB = endRGB[2]
let sR = (endR - startR) / step // 总差值
let sG = (endG - startG) / step
let sB = (endB - startB) / step
var colorArr = []
for (var i = 0; i < step; i++) {
let color = 'rgb(' + parseInt((sR * i + startR)) + ',' + parseInt((sG * i + startG)) + ',' + parseInt((sB * i + startB)) + ')'
colorArr.push(color)
}
return colorArr
}

思路二:CSS结合svg


我们可以用css的background: conic-gradient


background: conic-gradient(#179067, #62e317, #d7f10f, #ffc403, #fcc202, #ff7327, #ff7327, #FF5800, #ff5900, #f64302, #ff0000, #ff0000);

image.png


看着好像不错,那么接下来只要我们做个遮罩,然后用svg的strokeDashoffset来形成我们的环状进度条就可以了。至于百分之几到百分之几我们可以将conic-gradient内部属性做个百分比的拆分就可以了


image.png


<!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>Document</title>
</head>
<style>
.circle {
width: 300px;
height: 300px;
background: conic-gradient(#179067, #62e317, #d7f10f, #ffc403, #fcc202, #ff7327, #ff7327, #FF5800, #ff5900, #f64302, #ff0000, #ff0000);
border-radius: 50%;
position: relative;
}

#progress-circle circle {
stroke-dasharray: 880;
stroke: #f2f2f2;
}

#progress-circle {
transform: rotate(-90deg);
}

.circle-mask {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 2;
width: 260px;
height: 260px;
background: #fff;
border-radius: 50%;
}
</style>

<body>
<div class="circle">
<svg id="progress-circle" width="300" height="300">
<circle r="140" cx="150" cy="150" stroke-width="21" fill="transparent" />
</svg>
<div class="circle-mask"></div>
</div>
</body>
<script>
const circle = document.querySelector('#progress-circle circle');
const radius = circle.r.baseVal.value;
const circumference = radius * 2 * Math.PI;
function setProgress(percent) {
const progress = circumference - (percent / 100) * circumference;
circle.style.strokeDashoffset = -progress;
}
let prog = 40
let val = 100 - prog
setProgress(val); //设置初始进度

</script>

</html>

这里简单讲下逻辑,我们通过计算环的周长,总长其实就是stroke-dasharray,通过strokeDashoffset来偏移我们的虚线线段,那么开始的就是我们的实线线段。其实就是一个蚂蚁线。让这个线长度等于我们的环长度,通过api让实线在开始的位置。


最终效果


image.png


"奇葩"的横向进度条


在我们平常需求用用组件库实现进度条很容易,但是我们看看这个需求的进度条的场景,文字要能被裁剪成黑白两色。


image.png


image.png


思路一: overflow:hidden


具体就不演示了,内部通过两个副本的文案,一套白色一套黑色,通过定位层级的不同,overflow:hidden来隐藏,缺点是相对繁琐的dom结构。


思路二: background-clip 裁剪


<!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>Document</title>
</head>
<style>
:root {
--d: 20%
}

.inverted {
padding: 0 8px;
display: flex;
justify-content: space-between;
background: linear-gradient(-90deg, #000 var(--d), #fff 0) no-repeat, linear-gradient(-90deg, #0000 var(--d), rgb(192, 23, 23) 0) no-repeat;
-webkit-background-clip: text, padding-box;
background-clip: text, padding-box;
color: #0000;
font-weight: bold;
cursor: pointer;
}

.box {
background: #ebebeb;
width: 300px;
border-radius: 24px;
overflow: hidden;
}
</style>

<body>
<div class="box">
<div class="inverted">
<div class="inverted-item">888w/12</div>
<div class="inverted-item">100%/10s</div>
</div>
</div>
</body>

<script>
function modifyProg(prog) {
let val = 100 - prog
document.documentElement.style.setProperty('--d', val + '%')
}
modifyProg(6)
</script>

</html>

这里我们主要用了background-clip的text和padding-box两个裁剪,一个裁剪文本,一个裁剪背景延伸至内边距padding外沿。不会绘制到边框处。在js中我们通过setProperty修改css变量即可。


最终效果


image.png


附录



  1. 卷出新高度,几百行的Canvas游戏秘籍 - 掘金 (juejin.cn)

  2. 为什么我的WebGL开发这么丝滑 🌊

  3. Echarts无法实现这个曲线图😭,那我手写一个 - 掘金 (juejin.cn)

  4. Redux已死,那就用更爽的Zustand - 掘金 (juejin.cn)<
    作者:谦宇
    来源:juejin.cn/post/7244172094547492923
    /a>

收起阅读 »

IntelOne Mono 英特尔编程字体

web
一、简介 IntelOne Mono 是一个被设计为富有表现力的字体,在设计之初就充分的考虑到 开发者的需求:清晰度/易读性,以解决视觉的疲劳, 减少编码错误等问题,并且 IntelOne Mono 已经得到 低视力 等人群的反馈,证明确实得到一些正面反馈效果...
继续阅读 »

IntelOne Mono.png


一、简介


IntelOne Mono 是一个被设计为富有表现力的字体,在设计之初就充分的考虑到 开发者的需求:清晰度/易读性,以解决视觉的疲劳, 减少编码错误等问题,并且 IntelOne Mono 已经得到 低视力 等人群的反馈,证明确实得到一些正面反馈效果。



开源协议:SIL Open Font License 1.1 商用需要注意协议内容。



二、字体粗细


Untitled.png


IntelOne Mono 按照字体粗细可以分为四种字体:细体/常规字体/中等粗细/粗体 以及斜体效果


三、字体格式支持


font-images.png


官方仓库 中,给出四种格式的样式字体,就安装体验:



  • Windows/MacOS 桌面端使用 ttf/otf,这两种字体都具有跨平台性。

  • Web 端使用 woff/woff2 字体。


四、下载并使用字体


1. Git clone 下载


cd <dir> & git clone https://github.com/intel/intel-one-mono.git

2. Github Release 下载



intel/intel-one-mono 根据需要下载即可。



3、在 VS Code 中配置


位置设置示例
1、配置Settings(设置) -> User(用户) -> Text Editor (文本编辑器) -> Font Family (字体家族) -> IntelOne Monovs-code-setting-font.png
2、集成终端Settings(设置) -> User(用户) -> Features(特性) -> Terminal (终端) -> Intergrated Font Family (字集成字体家族) -> IntelOne Monointel-one-inter.png

4、在 WebStrome 中配置


编辑器配置设置位置示例
1、配置编辑器Settings(设置) -> Editor(编辑器) -> Font -> IntelOne Monowebstrom-use-intel-one-mono-font.png

5、在 Sublime Text 中配置


覆盖 json 数据 font_face 属性值:


编辑器配置设置位置示例
font_face"font_face": "IntelOne Mono"sublime-intel-one-mono-screen-cut.png


其他环境字体,例如:终端中配置也比较简单,就不再复述。



五、字体显示效果


不同大小字体展示效果.png


在渲染字体方面,IntelOne Mono 字体推荐使用大号字体,ttf 文件格式字体,已经对大号字体进行了优化,尤其是在 Windows 平台。



注意:在 VSCode 中,当字体大小发生变化的时候,字体/建议窗口/终端的行高最好一起配置。



六、好编程字体的特点


要素说明
1、清晰易读避免过度装饰,准确还原字体,易读易懂。
2、等宽字体编程对排版的整齐度有较高的要求,排版整齐的代码更加容易阅读和理解。
3、字符与符号层次封面字符中,字母,数字等都具有应该具有不同的展示高度,凸显不同的层级的内容,使得编码更具有层次感。便于快速理解代码字符。
4、特殊字符逗号、句号、括号等等编程中常用的字符,应该突出、便于识别。

七、不同编辑器显示效果


编辑器展示
VS Codeshow-vscode.png
WebStromewebstrom-intelOne-mono-show.png
Sublimeshow-sublime-intel-one-mono-screen-cut.png

八、社区反馈


IntelOne Mono 字体自 4 月 22 发布第一个版本,到今天 Github 社区 Star 数目目前 已经达到 5.5K+ star 数目,足以证明字体的受欢迎程度。


九、与其他字体对比



对比是为了找到更加 适合自己 的字体。



字体名称效果展示
IntelOne Monocompare-intelone-mono.png
JetBrainsMono Nerd FontJetBrainsMono Nerd Font.png
Input Monocompare-Input Mono.png
InconsolataGo Nerd Font Monocompare-InconsolataGo Nerd Font Mono.png
Cascadia Monocompare-cascadiamono.png

十、从 IntelOne Mono 看字体设计


1. UFO


logo.svg

  • 全名:(The Unified Font Object) 统一字体对象

  • UFO 3 (unifiedfontobject.org)
    一种跨平台、跨应用程序、人类可读、面向未来的格式,用于存储字体数据。

  • UFO3 文件的目录结构是这样的。


2. glif


全名:(glyph interchage format) 描述字型文件格式,glif 存储了单个字形的 (TrueType、PostScript)轮廓、标记点、参考线,使用 xml 语法。


3. plist


属性列表格式包含一系列以 XML 编码的键值对。


4. fea


.fea文件名扩展名主要与 Adobe OpenType Feature File ( .fea)文件类型相关联,该文件类型与 OpenType 一起使用,OpenType 是一个开放的标准规范,用于可扩展的排版字体,最初由微软和 Adobe Systems 开发。


5. 软件


RoboFont


robotfont.png


仅限 MacOS 支持,推荐使用。


FontForge


fontforge.png


适用于 Windows/Mac。


6. PostScript 字体


是由 Adobe Systems 为专业数字排版桌面排版开发的轮廓字体计算机字体规范编码的字体文件。


7. TrueType 字体


TrueType 是由美国苹果公司和微软公司共同开发的一种电脑轮廓字体(曲线描边字)类型标准。这种类型字体文件的扩展名是.ttf,类型代码是 tfil。


十一、小结


本文主要介绍 IntelOne Mono 字体在当前主流编辑器中使用方法和展示效果,了解好的编程字体的优秀特带你。并且探索其背后实现的直奔知识点涉及字体的设计方法和工具软件,如果感兴趣,可以自己设计一套字体。IntelOne Mono 字体在 GitHub 上 Star 数量反映了其被人快速关注且喜欢的特点。但也有不足,没有提供 Nerd Font 字体,对于喜欢用终端的小伙伴

作者:进二开物
来源:juejin.cn/post/7244174500785373241
,暂时可能会受欢迎。

收起阅读 »

还有多少公司在使用H5?不怕被破解吗?

web
H5还有人在用吗 近几天,老板让我调查一下现在市面上H5的使用现状。粗略地调查了一下,发现现在使用H5的真不多了,但是还是有人在用H5的,原因无非一是成本低,相比户外广告,H5制作费用根本不值一提;二是效果好,转化率高,好一点的H5大概有10W浏览量,这也反映...
继续阅读 »

H5还有人在用吗


近几天,老板让我调查一下现在市面上H5的使用现状。粗略地调查了一下,发现现在使用H5的真不多了,但是还是有人在用H5的,原因无非一是成本低,相比户外广告,H5制作费用根本不值一提;二是效果好,转化率高,好一点的H5大概有10W浏览量,这也反映H5数据统计精准,企业知道钱花在哪个地方,心里就踏实。


但是,H5的安全性会让企业非常头疼,不知道大家还记不记得几年前某App H5 页面被植入色情内容广告在安全圈引起了轰动。后来排查才基本确定为用户当地运营商http劫持导致H5页面被插入广告,给该App 造成极大影响。


为什么H5是黑灰产高发区?


从顶象多年的防控经验来看,H5面临的风险相对较多是有其原因的。


1、JavaScript代码特性。


H5平台开发语言是JavaScript,所有的业务逻辑代码都是直接在客户端以某种“明文”方式执行。代码的安全防护主要依靠混淆,混淆效果直接决定了客户端的安全程度。不过对于技术能力较强的黑产,仍然可以通过调试还原出核心业务处理流程。




2、企业营销推广需求追求简单快捷。


首先,相比其他平台,很多公司在H5平台的开放业务往往会追求简单,快捷。比如在营销推广场景,很多企业的H5页面只需从微信点击链接直接跳转到一个H5页面,点击页面按钮即可完成活动,获取积分或者小红包。


一方面确实提升了用户体验,有助于拉新推广;但另一方面简便的前端业务逻辑,往往也会对应简单的代码,这也给黑灰产提供了便利,相比去破解App,H5或者小程序的破解难度要低一些。


数据显示,如果企业在营销时不做风险控制,黑产比例一般在20%以上,甚至有一些高达50%。这就意味着品牌主在营销中相当一部分费用被浪费了。


3、H5平台自动化工具众多。


核心流程被逆向后,攻击者则可以实现“脱机”,即不再依赖浏览器来执行前端代码。攻击者可以自行构造参数,使用脚本提交请求,即实现完全自动化,如selenium,autojs,Puppeteer等。这些工具可以在不逆向JS代码的情况下有效实现页面自动化,完成爬虫或者薅羊毛的目的。


4、防护能力相对薄弱。


从客观层面来看,H5平台无论是代码保护强度还是风险识别能力,都要弱于App。这是现阶段的框架导致,并不是技术能力问题。JavaScript数据获取能力受限于浏览器,出于隐私保护,浏览器限制了很多数据获取,这种限制从某种程度上也削弱了JavaScript在业务安全层面的能力。


以电商App为例,出于安全考虑,很多核心业务只在App上支持。如果H5和App完全是一样的参数逻辑和加密防护,对于攻击者,破解了H5也就等于破解了App。


5、用户对H5缺乏系统认识。


最后,大部分用户对H5的安全缺乏系统性的认识,线上业务追求短平快,没有在H5渠道构建完善的防护体系情况下就上线涉及资金的营销业务。


H5代码混淆


基于上面这些问题,我们可以采取H5代码混淆的方式来稍微解一下困境。


一、产品简介



  • H5代码混淆产品,通过多层加密体系,对H5文件进行加密、混淆、压缩,可以有效防止H5源代码被黑灰产复制、破解。


二、混淆原理



  • 对代码中的数字,正则表达式、对象属性访问等统一转为字符串的表示形式

  • 对字符串进行随机加密(多种加密方式,倒序/随机密钥进行轮函数加密等)

  • 对字符串进行拆分,并分散到不同作用域

  • 打乱函数顺序

  • 提取常量为数组引用的方式


举个简单的例子来说明一下流程
(1)变量和函数重命名:


// 混淆前
function calculateSum(a, b) {
var result = a + b;
return result;
}

// 混淆后
function a1xG2b(c, d) {
var e = c + d;
return e;
}


(2)代码拆分和重新组合:


// 混淆前
function foo() {
console.log('Hello');
console.log('World');
}

// 混淆后
function foo() {
console.log('Hello');
}

function bar() {
console.log('World');
}


(3)控制流转换:


// 混淆前
if (condition) {
console.log('Condition is true');
} else {
console.log('Condition is false');
}

// 混淆后
var x = condition ? 'Condition is true' : 'Condition is false';
console.log(x);

(4)添加无用代码:


// 混淆前
function foo() {
console.log('Hello');
}

// 混淆后
function foo() {
console.log('Hello');
var unusedVariable = 10;
for (var i = 0; i < 5; i++) {
unusedVariable += i;
}
}

结语


当然,实际的代码混淆技术可能更加复杂。而且,代码混淆并不能完全阻止源代码的泄露或逆向工程,但可以增加攻击者分析和理解代码的难度。


H5现在的使用场景其实更多可能偏向日常的投票场景、活动场景以及游戏营销等等,其实使用场景很少了,但是一旦被攻击,尤其是对于运营商这种大厂来说,危害性还是很大的,企业或者说公司还是需

作者:昀和
来源:juejin.cn/post/7244004118222078010
要注意这方面的安全。

收起阅读 »

AnyScript:前端开发的最佳良药!

web
不以繁琐为名,更以简洁为声! 作为一名Api调用工程师,深知在前端开发中的各种挑战和痛点。在我们开发过程中,代码的可维护性和可扩展性是至关重要的因素。TypeScript(简称TS)作为JavaScript的超集,为我们带来了更好的开发体验和更高的代码质量。 ...
继续阅读 »

cover.png


不以繁琐为名,更以简洁为声!


作为一名Api调用工程师,深知在前端开发中的各种挑战和痛点。在我们开发过程中,代码的可维护性和可扩展性是至关重要的因素。TypeScript(简称TS)作为JavaScript的超集,为我们带来了更好的开发体验和更高的代码质量。


1. 类型系统:保驾护航


1.1 强大的类型检查


TypeScript引入了静态类型检查,这是它最吸引人的特点之一。通过在代码中定义变量的类型,TypeScript可以在编译时发现潜在的错误,大大减少了在运行时遇到的意外错误。例如,在JavaScript中,我们可以将一个字符串类型的变量传递给一个预期接收数字类型的函数,这将导致运行时错误。而在TypeScript中,编译器将会提示我们这个潜在的类型不匹配错误,使得我们可以在开发过程中及早发现并修复问题。


举个例子,假设我们有以下的TypeScript代码:


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

const result = add(3, '5');
console.log(result);

在这个例子中,我们本应该传递两个数字给add函数,但是错误地传递了一个字符串。当我们尝试编译这段代码时,TypeScript编译器会提示错误信息:


Argument of type 'string' is not assignable to parameter of type 'number'.

通过这种类型检查,我们可以在开发过程中发现并解决类型相关的问题,避免了一些常见的错误。


1.2 类型推断的魅力


在TypeScript中,我们不仅可以显式地定义变量的类型,还可以利用类型推断的功能。当我们没有明确指定类型时,TypeScript会根据上下文和赋值语句自动推断变量的类型。这个特性不仅减少了我们编写代码时的工作量,还提供了代码的简洁性。


举个例子,考虑以下的代码:


const name = 'John';

在这个例子中,我们没有显式地指定name的类型,但TypeScript会自动推断它为字符串类型。这种类型推断让我们在编写代码时更加灵活,减少了类型注解的需求。


2. 更好的代码编辑体验


2.1 智能的代码补全和提示


使用TypeScript可以带来更好的代码编辑体验。由于TypeScript具备了静态类型信息,编辑器可以提供智能的代码补全和提示功能,减少我们编写代码时的出错几率。当我们输入一个变量名或者函数名时,编辑器会根据类型信息推断可能的属性和方法,并展示给我们。


例如,当我们有一个对象,并想获取它的属性时,编辑器会给出属性列表供我们选择。这在大型项目中尤其有用,因为我们可以快速了解某个对象的可用属性,而不必查阅文档或者浏览源代码。


2.2 重构的艺术


在大型项目中进行代码重构是一项棘手的任务。TypeScript提供了强大的重构能力,使得我们能够更轻松地重构代码而不担心破坏现有功能。在进行重构操作时,TypeScript会自动更新相关的类型注解,帮助我们在整个重构过程中保持代码的一致性。


举个例子,假设我们有一个函数:


function multiply(x: number, y: number): number {
return x * y;
}

现在我们决定将multiply函数的参数顺序调换一下。在传统的JavaScript中,我们需要手动修改所有调用multiply函数的地方。而在TypeScript中,我们只需要修改函数本身的定义,TypeScript会自动检测到这个变化,并指示我们需要更新的地方。


3. 生态系统的繁荣


3.1 类型定义文件


TypeScript支持类型定义文件(.d.ts),这些文件用于描述 JavaScript 库的类型信息。通过使用类型定义文件,我们可以在TypeScript项目中获得第三方库的类型检查和智能提示功能。这为我们在开发过程中提供了极大的便利,使得我们能够更好地利用现有的JavaScript生态系统。


例如,假设我们使用著名的React库进行开发。React有一个官方提供的类型定义文件,我们只需要将其安装到项目中,就能够获得对React的类型支持。这使得我们可以在编写React组件时,获得相关属性和方法的智能提示,大大提高了开发效率。


3.2 社区的支持


TypeScript拥有庞大而活跃的社区,开发者们不断地分享自己的经验和资源。这意味着我们可以轻松地找到许多优秀的库、工具和教程,帮助我们更好地开发和维护我们的前端项目。无论是遇到问题还是寻找最佳实践,社区都会给予我们及时的支持和建议。


4. 面向未来的技术栈


4.1 ECMAScript的最新特性支持


ECMAScript是JavaScript的标准化版本,不断更新以提供更多的语言特性和功能。TypeScript紧密跟随ECMAScript标准的发展,支持最新的语法和特性。这意味着我们可以在TypeScript项目中使用最新的JavaScript语言功能,而不必等待浏览器的支持。


例如,当ECMAScript引入了Promiseasync/await等异步编程的特性时,TypeScript已经提供了对它们的完整支持。我们可以在TypeScript项目中使用这些特性,而无需担心兼容性问题。


4.2 渐进式采用


对于已有的JavaScript项目,我们可以渐进式地引入TypeScript,而无需一次性对整个项目进行重写。TypeScript与JavaScript是高度兼容的,我们可以逐步将JavaScript文件改写为TypeScript文件,并为每个文件逐渐添加类型注解。这种渐进式的采用方式可以降低迁移的风险和成本,并让我们享受到TypeScript带来的好处。


结语


推荐使用TypeScript来提升我们的开发体验和代码质量。它强大的类型系统、智能的代码编辑体验、丰富的生态系统以及面向未来的技术栈,都使得TypeScript成为当今前端开发的首选语言之一。


但是,我们也需要明确TypeScript并非万能的解决方案。在某些特定场景下,纯粹的JavaScript可能更加合适。我们需要根据具体项目的需求和团队的情况,权衡利弊并做出适当的选择。



示例代码仅用于说明概念,可能不符合最佳实践。在实际开发中,请根据具体情况进行调整。


作者:ShihHsing
来源:juejin.cn/post/7243413799347798072

收起阅读 »

如果让你设计一个弹幕组件,你会怎么做???

web
大家好,我是前端小张同学,这次给大家更新一个开发中常见的需求,接下来我会将我的弹幕实现以及设计思路一步一步描述出来,并分享给大家,希望大家喜欢。 今天我们的主题是 ,用vue手写一个弹幕 1:关于弹幕设计思想 1.1 : 业务层 | 视图层(全局组件) 1.1...
继续阅读 »

大家好,我是前端小张同学,这次给大家更新一个开发中常见的需求,接下来我会将我的弹幕实现以及设计思路一步一步描述出来,并分享给大家,希望大家喜欢。


今天我们的主题是 ,用vue手写一个弹幕


1:关于弹幕设计思想


1.1 : 业务层 | 视图层(全局组件)


1.1.1 : 从业务角度来说,如果你设计的是全局弹幕组件,你要考虑以下几点。



  1. 容器的高度?

  2. 容器层次结构划分?

  3. 渲染弹幕的方式,使用组件的人应该传递什么数据?

  4. 是否支持全屏弹幕?

  5. 是否支持弹幕关闭和开启?

  6. 是否需要重置弹幕?

  7. 是否支持暂停弹幕?

  8. 是否需要集成发送功能?


设计方案考虑完整了以后,你将可以开始考虑 数据层的设计


1.2 数据层


1.2.1 : 从数据角度来说每一条弹幕无非是一个element,然后把弹幕内容放到这个element元素中,并且给 element 添加动画,那接下来,你应该这样考虑。




  1. 弹幕是JS对象?它的属性有哪些?




  2. 谁去管理这些弹幕?如何让他能够支持暂停和关闭?




  3. 你如何把后台的数据,与你前台的一些静态数据进行合并,创造出一个完整对象?




  4. 你怎么去渲染这些弹幕?




  5. 你想要几秒创建一次弹幕并在容器内显示和运行?




  6. 弹幕具备哪些灵活的属性?

    运行动画时间 , 用户自己发布的弹幕样式定制?
    又或者,弹幕需要几条弹道内运行等等这些你都需要考虑。




数据设计方案考虑完整了以后,你将可以开始考虑 数据管理层的设计


1.3 数据管理层


1.3.1 从管理的角度来说,外界调用某些方法,你即可快速的响应操作,例如外界调用 open 方法,你就播放弹幕,调用Stop方法,你就关闭弹幕 接下来,你应该考虑以下几点。



  1. 面向对象设计,应该提供哪些方法,具备哪些功能?

  2. 调用了指定的方法,应该怎么对数据进行操作。

  3. 如何对弹幕做性能优化?


到这里 , 我们设计方案基本完成,接下来我们可以开始编写代码。


2: 代码实现


2.1 : 数据层设计方案实现


我们需要构建一个 Barrage 类 ,我们每次去创建一个弹幕的时候都会 new Barrage,让他帮助我们生成一些弹幕属性。


export class Barrage {
constructor(obj) {
// 每次 new Barrage() 传入一个 后台返回的数据对象 obj
const { barrageId, speed, level, top, jumpUrl, barrageContent, animationPlayState, ...args } = obj
this.barrageId = barrageId; // id : 每条弹幕的唯一id
this.speed = speed; // speed : 弹幕运行的速度,由外界控制
this.level = level; // level : 弹幕的层级 --> 弹幕可分为设计可分为 上下 1 , 1 两个层级 ,可决定弹幕的显示是全屏还是半屏显示
this.top = top; // top :弹幕生成的位置相对于 level 的层级 决定 ,相对于 Level 层级 盒子距离顶部的位置
this.jumpUrl = jumpUrl; // jumpUrl :点击弹幕需要跳转的链接
this.barrageContent = barrageContent; // barrageContent : 弹幕的内容
this.animationPlayState = ''; // 设计弹幕 是否可 点击暂停功能
this.color = '#FFF' // 弹幕颜色
this.args = args // 除去Barrage类之外的一些数据属性全部丢到这里,例如后台返回的数据
}
}

2.1 : 数据管理层设计方案实现


2.1.1 :我们在这里实现了 , 弹幕的 增加删除初始化重置关闭开启功能


1. 实现弹幕开启功能.


BarrageManager.js


export class BarrageManager {

constructor(barrageVue) {
this.barrages = []; // 填弹幕的数组
this.barragesIds = [] // 批量删除弹幕的数组id
this.sourceBarrages = [] // 源弹幕数据
this.timer = null //控制弹幕的开启和关
this.barrageVue = barrageVue // 弹幕组件实例
this.deleteCount = 0, // 销毁弹幕的总数
this.lastDeleteCount = 0, // 最后可销毁的数量
this.row = 0,
this.count = 0
}
init(barrages) {
this.sourceBarrages = barrages
this.deleteCount = parseInt(this.sourceBarrages.length / deleteQuantity.FIFTY) // 计算可删除数量
this.lastDeleteCount = this.sourceBarrages.length % deleteQuantity.FIFTY // 计算 最后一次可删除数量
}
/**
*
* @param {*} barrages 接收一个弹幕数组数据
* @description 循环创建 弹幕对象 ,将后台数据与 创建弹幕的属性结合 存入弹幕数组
*/

loopCreateBarrage(barrages) {
const { rows, createTime, crearteBarrageObject } = this.barrageVue
let maxRows = rows / 2 // 最大的弹幕行数
this.timer = setInterval(() => {
for (let i = 0; i < 1; i++) {
let barrageItem = barrages[this.count]
if (this.row >= maxRows) { this.row = 0 } // 如果当前已经到了 最大的弹幕行数临界点则 回到第0 行弹道继续 创建
if (!barrageItem) return clearInterval(this.timer) // 如果取不到了则证明没数据了 , 结束弹幕展示
const item = crearteBarrageObject({ row: this.row, ...barrageItem }) // 添加对象到 弹幕数组中
this.addBarrage(item)
this.count++ // 用于取值 ,取了多少条
this.row++ // 用于弹道
}
}, createTime * 1000);
}
/**
* @param {*} barrages 传入一个弹幕数组数据
* @returns 无返回值
* @description 调用 该方法 开始播放弹幕
*/

open(barrages) {
if (barrages.length === 0) return
this.init(barrages)
this.loopCreateBarrage(this.sourceBarrages)
}
}

在这里我们初始化了一个 open 方法,并接收一个数组 ,并调用了 init 方法 去做初始化操作,并调用了 循环创建的方法,没 createTime 秒创建一条弹幕,加入到弹幕数组中。



  1. 连接视图层


2.1 : 视图层 | 业务层设计方案实现


index.vue


<template>
<div class="barrage">
<div class="barrage-container" ref="barrageContainer">
<div class="barrage-half-screen" ref="halfScreenContainer">
<template v-for="item in barrageFiltering.level1">
<barrage-item
:item="item" :class="{pausedAnimation : paused }"
:options='barrageTypeCallback(item)'
@destory="destoryBraageItem" :key="item.barrageId">

</barrage-item>
</template>
</div>
<div class="barrage-full-screen" v-if="fullScreen">
<template v-for="item in barrageFiltering.level2">
<barrage-item
:item="item" :class="{pausedAnimation : paused }"
:options='barrageTypeCallback(item)'
@destory="destoryBraageItem" :key="item.barrageId">

</barrage-item>
</template>
</div>
</div>
<user-input ref="publishBarrage" v-if="openPublishBarrage" @onBlur="handleBlur">
<template #user-operatio-right>
<!-- 处理兼容性问题 ios 和 安卓 触发点击事件 -->
<div class="send" @click="sendBarrage($event)" v-if="IOS">
<slot name="rightRegion"></slot>
</div>
<div class="send" @mousedown="sendBarrage($event)" v-else>
<slot name="rightRegion"></slot>
</div>
</template>
</user-input>
</div>

</template>
export default {
created () {
this.barrageManager = new BarrageManager(this)
},
mounted() {
// 初始化弹幕渲染数据
this.initBarrageRenderData();
},
data() {
return {
barrageManager : null,
isClickSend: false,
paused : false
};
},
methods : {
initBarrageRenderData() {
this.barrageManager.open(this.barrages);
},
},
computed : {
barrageFiltering() {
return {
level1:
this.barrageManager.barrages.filter(
item => item.level === barrageLevel.LEVEL1
) || [],
level2:
this.barrageManager.barrages.filter(
item => item.level === barrageLevel.LEVEL2
) || []
};
},
}
}

视图层知识点回顾


在这里我们在弹幕组件创建的时候去创建了一个 弹幕管理对象,并且在挂载的时候去初始化了以下 弹幕渲染的数据,于是我们调用了 弹幕管理类open方法,这样当组件挂载时,就会去渲染 barrageFiltering 数据,这里我们是在管理类中拿到了管理类中循环创建的数据。


open 方法实现


到这里我们的弹幕的开启基本上已经完成了,可以看得出,如果你是这样设计的,你只需要在组件中调用管理类的一些方法,它就能帮你完成一些功能。


3: 实现弹幕关闭功能


barrageManager.js


 class BarrageManager {
constructor(barrageVue) {
this.barrages = []; // 填弹幕的数组
this.barragesIds = [] // 批量删除弹幕的数组id
this.sourceBarrages = [] // 源弹幕数据
this.timer = null //控制弹幕的开启和关
this.barrageVue = barrageVue // 弹幕组件实例
this.deleteCount = 0, // 销毁弹幕的总数
this.lastDeleteCount = 0, // 最后可销毁的数量
this.row = 0,
this.count = 0
}

/**
* @return 无返回值
* @description 调用close 方法 关闭弹幕
*/

close() {
clearInterval(this.timer)
this.removeAllBarrage()
}
/**
* @description 删除全部的弹幕数据
*/

removeAllBarrage() {
this.barrages = []
}
}


关闭功能知识点回顾


在这里我们可以看到,关闭弹幕的功能其实很简单,你只需要把开启弹幕时的定时器关闭,并且把弹幕数组数据清空就可以了


4: 实现弹幕添加功能


index.vue



addBarrage(barrageContent) {
// 获取当前 定时器正在创建的 一行
let currentRow = this.barrageManager.getRow();
let row = currentRow === this.rows / 2 ? 0 : currentRow + 1;
if (row === this.rows / 2) {
row = 0;
}
let myBarrage = {
row,
barrageId: '1686292223004',
barrageContent,
style: this.style,
type: "mySelf", // 用户自己发布的弹幕类型
barrageCategory: this.userBarrageType
};

const item = this.crearteBarrageObject(myBarrage);

this.barrageManager.addBarrage(item); // 数据准备好了 调用添加方法

console.info("发送成功")

this.barrageManager.setRow(row + 1);
},

barrageManager.js


 class BarrageManager {
constructor(barrageVue) {
this.barrages = []; // 填弹幕的数组
this.barragesIds = [] // 批量删除弹幕的数组id
this.sourceBarrages = [] // 源弹幕数据
this.timer = null //控制弹幕的开启和关
this.barrageVue = barrageVue // 弹幕组件实例
this.deleteCount = 0, // 销毁弹幕的总数
this.lastDeleteCount = 0, // 最后可销毁的数量
this.row = 0,
this.count = 0
}
/**
*
* @param {*} obj 合并完整的的弹幕对象
* @param {...any} args 开发者以后可能需要传递的剩余参数
*/

addBarrage(obj, ...args) {
const barrage = new Barrage(obj, ...args)
this.barrages.push(barrage)
}
}

添加功能知识点回顾


在这里我们可以看到,添加的时候,我们 组件 只需要去调用 addBarrage 方法进行弹幕添加,并且在调用的过程中我们去 new Barrage 这个类 , 也就是我们之前准备好的 弹幕数据类 | 数据层设计


5: 实现弹幕删除功能


class BarrageManager {
constructor(barrageVue) {
this.barrages = []; // 填弹幕的数组
this.barragesIds = [] // 批量删除弹幕的数组id
this.sourceBarrages = [] // 源弹幕数据
this.timer = null //控制弹幕的开启和关
this.barrageVue = barrageVue // 弹幕组件实例
this.deleteCount = 0, // 销毁弹幕的总数
this.lastDeleteCount = 0, // 最后可销毁的数量
this.row = 0,
this.count = 0
}

/**
*
* @param {*} barrageId // 入参 弹幕id
* @returns 无返回值
* @description 添加需要批量删除的 id 到 批量删除的栈中 barragesIds
*/

addBatchRemoveId(barrageId) {
this.barragesIds.push(barrageId)
this.batchRemoveHandle()
}
/**
*
* @param {*} start 你需要从第几位开始删除
* @param {*} deleteCount // 删除的总数是多少个
* @returns 无返回值
*/

batchRemoveBarrage(start, deleteCount) {
if (this.barrages.length === 0) return
this.barrages.splice(start, deleteCount)
}
batchRemoveId(start, deleteCount) {
if (this.barragesIds.length === 0) return
this.barragesIds.splice(start, deleteCount)
}
/**
* @param {*} barrageId 弹幕 id 针对单个删除弹幕时 使用
*/

removeBarrage(barrageId) {
let index = this.barrages.findIndex(item => item.barrageId === barrageId)
this.barrages.splice(index, 1)
}
/**
* @description 删除全部的弹幕数据
*/

removeAllBarrage() {
this.barrages = []
}
// 批量移除逻辑处理
batchRemoveHandle() {
if (this.deleteCount === 0 || this.deleteCount === 0) {
if (this.barragesIds.length === this.lastDeleteCount) {
this.batchRemoveBarrage(0, this.lastDeleteCount)
this.batchRemoveId(0, this.lastDeleteCount)
}
} else {
if (this.barragesIds.length === deleteQuantity.FIFTY) {
this.batchRemoveBarrage(0, deleteQuantity.FIFTY)
this.batchRemoveId(0, deleteQuantity.FIFTY)
this.deleteCount--
}
}
}
}

删除功能知识点回顾


在这里我们可以看到,删除的时候我们把每一个弹幕id加入到了一个数组中 , 当 弹幕id数组长度达到我想要删除的数量的时候, 调用 splice 方法 执行批量删除操作,当数据发生更新,视图也会更新,这样我们只需要执行一次dom操作,不需要每一次删除弹幕更新dom,造成不必要的性能消耗。


5: 实现弹幕重置功能


到这里,我相信你已经明白了我的设计,如果现在让你实现一个 重置弹幕方法 你会怎么做 ? 是不是只需要,调用一下 close 方法 , 然后再去 调用 open方法就可以了,ok 接下来我会将完整版代码 放入我的github仓库,小伙伴们可以去拉取 仓库链接,具体代码还需要小伙伴们自己从头阅读一次,这里只是说明了部分内容 , 阅读完成后 , 你就会彻底理解。


关于 barrageTypeCallback 函数


这个方法主要是可以解决弹幕样式定制的问题,你可以根据每个弹幕的类型 做不同的样式对象返回,我们会自动帮你渲染。


barrageTypeCallback ( {args} ) {

const { barrageCategary } = args

if(barrageCategary === 'type1'){

retun {
className : 'classOne',
children : {
show : false
i : {
showIcon : false,
style : {
color : 'red'
}
}
}
}
}
else{

return { className : 'default' }
}
}




结束语


前面的所有代码只是想告诉大家这个设计思想,当你的思维模型出来以后,其实很轻松。


我是 前端小张同学

作者:前端小张同学
来源:juejin.cn/post/7243680440694980668
期待你的关注,谢谢。

收起阅读 »

为什么推荐用svg而不用icon?

web
为什么要用svg而没有用icon? 使用背景: 1.因为svg图标在任何设备下都可以高清显示,不会模糊。而icon会在显卡比较低的电脑上有显示模糊的情况 2.svg图标在页面render时 速度会比icon稍微快一点 3.实现小程序换肤功能 ;方案见:ht...
继续阅读 »

为什么要用svg而没有用icon?


使用背景:


图片.png



1.因为svg图标在任何设备下都可以高清显示,不会模糊。而icon会在显卡比较低的电脑上有显示模糊的情况


2.svg图标在页面render时 速度会比icon稍微快一点
3.实现小程序换肤功能 ;方案见:http://www.yuque.com/lufeilizhix…



// svg在html里的使用示例01
<div>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<title>home</title>
<path d="M32 18.451l-16-12.42-16 12.42v-5.064l16-12.42 16 12.42zM28 18v12h-8v-8h-8v8h-8v-12l12-9z"></path>
</svg>
</div>


SVG基础可参考:http://www.yuque.com/lufeilizhix…


Svg-inline的使用


//示例02

import iconShop from '../assets/menuIcon/shop.svg?inline'
import iconCustomer from '../assets/menuIcon/customer.svg?inline'
import iconCustomerService from '../assets/menuIcon/customerService.svg?inline'
import iconNuCoin from '../assets/menuIcon/nuCoin.svg?inline'
import iconBanner from '../assets/menuIcon/banner.svg?inline'
import iconAccount from '../assets/menuIcon/account.svg?inline'
import iconDataReport from '../assets/menuIcon/dataReport.svg?inline'
import iconVera from '../assets/menuIcon/banner_01.svg?inline'

inline svg是目前前端图标解决方案的最优解(当然不仅限于图标),而且使用方式也及其简单,只要将svg图标代码当成普通的html元素来使用即可,如:


<!-- 绘制右箭头 -->
<svg viewBox="0 0 1024 1024" height="1em" width="1em" fill="currentColor">
<path d="M665.6 512L419.84 768l-61.44-64 184.32-192L358.4 320l61.44-64 184.32 192 61.44 64z" />
</svg>

<!-- 绘制边框 -->
<svg viewBox="0 0 20 2" preserveAspectRatio="none" width="100%" height="2px">
<path d="M0 1L20 1" stroke="#000" stoke-width="2px"></path>
</svg>

注意: 新版chrome不支持 # , 需要改成%23 ;stroke="%23000"

作为图片或背景使用时


 icon: https://www.baidu.com+ '/icons/icon_01.svg' 
<image class="headIcon" src="data:image/svg+xml,{{icon}}"></image>
**特别注意 需要把img标签换成image标签**

将上面的代码插入html文档即可以很简单地绘制出一些图标。
正常情况下会将svg保存在本地,具体的页面中导入,参考示例02 作为组件使用;目的是可复用
一般来说,使用inline svg作为图标使用时,想要保留svg的纵横比,可以只指定width属性,但是一般为了清晰都同时指定height属性。但如果是像上面绘制边框这种不需要保留纵横比的情形,可将preserveAspectRatio设置为none


优势与使用方式


从示例01可以看到,将svg直接作为普通html元素插入文档中,其本质和渲染出一个div、span等元素无异,天生具有渲染快、不会造成额外的http请求等优势,除此之外还有以下优势之处:


样式控制更加方便;
inline svg顶层的元素会设置以下几个属性:


height=“1em” width=“1em” 可以方便地通过设置父元素的font-size属性控制尺寸


fill=“currentColor” 可以方便地根据父元素或自身的color属性控制颜色


但是我们也可以为其内部的子元素单独设置样式 参考


注意事项


如需对svg中各部分分别应用样式,则在设计svg时最好不要将各部分都编于一组,可以将应用相同样式的部分进行分别编组,其他不需要设置样式的部分编为一组,这样我们在应用样式时,只需为对应的标签设置class属性即可。


一般在拿到svg文件后,推荐使用svgo优化svg代码,节省体积,但是如果我们需要针对性设置样式时则需要谨慎使用,因为优化代码会进行路径合并等操作,可能我们想要设置的子元素已经不是独立的了。


inline svg的复用及组件化


同一个inline svg必须能够进行复用,将需要复用inline svg封装成组件


// 使用inline svg组件
import AnySvgIcon from './inline-svg-component'
<AnySvgIcon width="16px" height="16px" />

参考:


inline svg和字体图标的对比


字体图标

的使用与设计

收起阅读 »

这几个让代码清新的魔法,让领导追着给我涨薪

web
清晰易维护的代码对于任何软件项目的长期成功和可扩展性至关重要。它提升团队成员之间的协作效率,减少错误的可能性,并使代码更易于理解、测试和维护。在本博文中,我们将探讨一些编写清晰易维护的 JavaScript 代码的最佳实践,并提供代码示例以阐明每个实践方法。 ...
继续阅读 »

清晰易维护的代码对于任何软件项目的长期成功和可扩展性至关重要。它提升团队成员之间的协作效率,减少错误的可能性,并使代码更易于理解、测试和维护。在本博文中,我们将探讨一些编写清晰易维护的 JavaScript 代码的最佳实践,并提供代码示例以阐明每个实践方法。


1. 一致的代码格式:


一致的代码格式对于可读性非常重要。它有助于开发人员更快地理解代码,提升协作效果。使用一致且被广泛接受的代码风格指南,比如 ESLint 提供的指南,并配置你的编辑器或 IDE 以自动格式化代码。
示例:


// 错误的格式化
function calculateSum(a,b){return a+b; }

// 正确的格式化
function calculateSum(a, b) {
return a + b;
}

2. 有意义的变量和函数命名:


为变量、函数和类使用有意义且描述性的名称。避免使用单个字母或容易引起他人困惑的缩写。这种做法提高了代码的可读性,并减少了对注释的需求。
示例:


// 错误的命名
const x = 5;

// 正确的命名
const numberOfStudents = 5;

3. 模块化和单一职责原则:


遵循单一职责原则,为函数和类设定单一、明确的职责。这种做法提高了代码的可重用性,并使其更易于测试、调试和维护。
示例:


// 错误的做法
function calculateSumAndAverage(numbers) {
let sum = 0;
for (let i = 0; i < numbers.length; i++) {
sum += numbers[i];
}
const average = sum / numbers.length;
return [sum, average];
}

// 正确的做法
function calculateSum(numbers) {
let sum = 0;
for (let i = 0; i < numbers.length; i++) {
sum += numbers[i];
}
return sum;
}

function calculateAverage(numbers) {
const sum = calculateSum(numbers);
const average = sum / numbers.length;
return average;
}

4. 避免全局变量:


尽量减少使用全局变量,因为它们可能导致命名冲突,并使代码更难以理解。相反,封装你的代码到函数或模块中,并尽可能使用局部变量。
示例:


// 错误的做法
let count = 0;

function incrementCount() {
count++;
}

// 正确的做法
function createCounter() {
let count = 0;

function incrementCount() {


count++;
}

return {
incrementCount,
getCount() {
return count;
}
};
}

const counter = createCounter();
counter.incrementCount();

5. 错误处理和鲁棒性:


优雅地处理错误,并提供有意义的错误信息或适当地记录它们。验证输入,处理边界情况,并使用正确的异常处理技术,如 try-catch 块。
示例:


// 错误的做法
function divide(a, b) {
return a / b;
}

// 正确的做法
function divide(a, b) {
if (b === 0) {
throw new Error('Cannot divide by zero');
}
return a / b;
}

try {
const result = divide(10, 0);
console.log(result);
} catch (error) {
console.error(error.message);
}

6. 避免重复代码:


代码重复不仅会导致冗余代码,还会增加维护和修复错误的难度。将可重用的代码封装到函数或类中,并努力遵循 DRY(Don't Repeat Yourself)原则。如果发现自己在复制粘贴代码,请考虑将其重构为可重用的函数或模块。
示例:


// 错误的做法
function calculateAreaOfRectangle(length, width) {
return length * width;
}

function calculatePerimeterOfRectangle(length, width) {
return 2 * (length + width);
}

// 正确的做法
function calculateArea(length, width) {
return length * width;
}

function calculatePerimeter(length, width) {
return 2 * (length + width);
}

7. 明智地使用注释:


干净的代码应该自解释,但有些情况下需要注释来提供额外的上下文或澄清复杂的逻辑。谨慎使用注释,并使其简洁而有意义。注重解释“为什么”而不是“如何”。
示例:


// 错误的做法
function calculateTotalPrice(products) {
// 遍历产品
let totalPrice = 0;
for (let i = 0; i < products.length; i++) {
totalPrice += products[i].
price;
}
return totalPrice;
}

// 正确的做法
function calculateTotalPrice(products) {
let totalPrice = 0;
for (let i = 0; i < products.length; i++) {
totalPrice += products[i].
price;
}
return totalPrice;
// 总价格通过将数组中所有产品的价格相加来计算。
}

8. 优化性能:


高效的代码提升了应用程序的整体性能。注意不必要的计算、过度的内存使用和潜在的瓶颈。使用适当的数据结构和算法来优化性能。使用类似 Chrome DevTools 的工具对代码进行性能分析和测量,以识别并相应地解决性能问题。


示例:


// 错误的做法
function findItemIndex(array, target) {
for (let i = 0; i < array.length; i++) {
if (array[i] === target) {
return i;
}
}
return -1;
}

// 正确的做法
function findItemIndex(array, target) {
let left = 0;
let right = array.length - 1;

while (left <= right) {
const mid = Math.floor((left + right) / 2);

if (array[mid] === target) {
return mid;
}

if (array[mid] < target) {
left = mid +
1;
}
else {
right = mid -
1;
}
}

return -1;
}

9. 编写单元测试:


单元测试对于确保代码的正确性和可维护性非常重要。编写自动化测试以覆盖不同的场景和边界情况。这有助于尽早发现错误,便于代码重构,并对修改现有代码充满信心。使用像 Jest 或 Mocha 这样的测试框架来编写和运行测试。
示例(使用 Jest):


// 代码
function sum(a, b) {
return a + b;
}

// 测试
test('sum 函数正确地相加两个数字', () => {
expect(sum(2, 3)).toBe(5);
expect(sum(-1, 5)).toBe(4);
expect(sum(0, 0)).toBe(0);
});

10. 使用函数式编程概念:


函数式编程概念,如不可变性和纯函数,可以使代码更可预测且更易于理解。拥抱不可变数据结构,并尽量避免对对象或数组进行突变。编写无副作用且对于相同的输入产生相同输出的纯函数,这样更容易进行测试和调试。
示例:


// 错误的做法
let total = 0;

function addToTotal(value) {
total += value;
}

// 正确的做法
function addToTotal(total, value) {
return total + value;
}

11. 使用 JSDoc 文档化代码:


使用 JSDoc 来为函数、类和模块编写文档。这有助于其他开发人员理解你的代码,并使其更易于维护。


/**
* 将两个数字相加。
* @param {number} a - 第一个数字。
* @param {number} b - 第二个数字。
* @returns {number} 两个数字的和。
*/

function add(a, b) {
return a + b;
}

12. 使用代码检查工具和格式化工具:


使用 ESLint 和 Prettier 等工具来强制执行一致的代码风格,并在问题出现之前捕获潜在问题。


// .eslintrc.json
{
"extends": ["eslint:recommended", "prettier"],
"

plugins"
: ["prettier"],
"rules": {
"prettier/prettier": "error"
}
}

// .prettierrc.json
{
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "all"
}

结论:


编写清晰且易于维护的代码不仅仅是个人偏好的问题,而是一项专业责任。通过遵循本博文中概述的最佳实践,您可以提高 JavaScript 代码的质量,使其更易于理解、维护和协作,并确保软件项目的长期成功。在追求清晰且易于维护的代码时,请牢记一致性、可读性、模块化和错误处理这些关键原则。祝你编码愉快!

原文:dev.to/wizdomtek/b…
翻译 / 润色:ssh

作者:ssh_晨曦时梦见兮
来源:juejin.cn/post/7243680592192782393

收起阅读 »

大专前端,三轮面试,终与阿里无缘

web
因为一些缘故,最近一直在找工作,再加上这个互联网寒冬的大环境,从三月找到六月了,一直没有合适的机会 先说一下背景,目前三年半年经验,base 杭州,大专学历+自考本科 就在前几天,Boss 上收到了阿里某个团队的投递邀请(具体部门就不透露了),因为学历问题...
继续阅读 »

因为一些缘故,最近一直在找工作,再加上这个互联网寒冬的大环境,从三月找到六月了,一直没有合适的机会



先说一下背景,目前三年半年经验,base 杭州,大专学历+自考本科



就在前几天,Boss 上收到了阿里某个团队的投递邀请(具体部门就不透露了),因为学历问题,基本上大厂简历都不会通过初筛,但还是抱着破罐子破摔的心态投递给了对方,出乎意料的是简历评估通过了,可能是因为有两个开源项目和一个协同文档加分吧。


进入到面试环节,首先是两道笔试题,算是前置面试:


第一道题目是算法题:


提供了一个数组结构的 data,要求实现一个 query 方法,返回一个新的数组,query 方法内部有 过滤排序分组 等操作,并且支持链式调用,调用最终的 execute 方法返回结果:


const result = query(list)
.where(item => item.age > 18)
.sortBy('id')
.groupBy('name')
.execute();

console.log(result);

具体实现这里就不贴了,过滤用原生的数组 filter 方法,排序用原生的数组 sort 方法,分组需要手写一下,类似 lodash/groupBy 方法。


过滤和排序实现都比较顺利,在实现分组方法的时候不是很顺利,有点忘记思路了,不过最后还是写出来了,关于链式调用,核心是只需要在每一步的操作最后返回 this 即可。


第二道题目是场景题:


要求用 vue 或者 react 实现一个倒计时抢券组件,页面加载时从 10s 开始倒计时,倒计时结束之后点击按钮请求接口进行抢券,同时更新文案等等功能。因为我对 react 比较熟悉一点,所以这里就选择了 react。


涉及到的知识点有 hook 中对 setTimeout 的封装、异步请求处理、状态更新CSS基本功 的考察等等……


具体实现这里也不贴了,写了一堆自定义 hook,因为平时也在参与 ahooks 的维护工作,ahooks 源码背的滚瓜烂熟,所以直接搬过来了,这道题整体感觉没啥难度,算是比较顺利的。


笔试题整个过程中唯一不顺利的是在线编辑器没有类似 vscode 这样的 自动补全 功能,不管是变量还是保留字,很多单词想不起来怎么拼写,就很尴尬,英文太差是硬伤 :(


笔试过程最后中出现了一点小插曲,因为笔试有时间限制,需要在规定的时间内完成,但是倒计时还没结束,不知道为什么就自动交卷了,不过那个时候已经写的差不多了,功能全部实现了,还剩下卡片的样式没完成,css 还需要完善一下,于是就在 Boss 上跟对方解释了一下,说明了情况。


过了几分钟,对面直接回复笔试过了,然后约了面试。


一面:




  • 自我介绍


    这里大概说了两分钟,介绍了过往工作经历,做过的业务以及技术栈。




  • 七层网络模型、和 DNS 啥的


    计网这方面属于知识盲区了,听到这个问题两眼一黑,思索了一会儿,直接说回答不上来。




  • 然后问了一些 host 相关的东西



    • 很遗憾也没回答上来,尴尬。对方问我是不是计算机专业的,我坦诚的告诉对方是建筑工程。




  • React 代码层的优化可以说一下么?



    • 大概说了 class 组件和 function 组件两种情况,核心是通过减少渲染次数达到优化目的,具体的优化手段有 PureComponentshouldComponentUpdateReact.memoReact.useMemoReact.useCallbackReact.useRef 等等。




  • 说一下 useMemouseCallback 有什么区别



    • 很基础的问题,这里就不展开说了。




  • 说一下 useEffectuseLayoutEffect 有什么区别



    • 很基础的问题,这里就不展开说了。




  • 问了一下 useEffect 对应在 class 中都生命周期怎么写?



    • 很基础的问题,这里就不展开说了。




  • 如果在 if 里面写 useEffect 会有什么表现?



    • 开始没听清楚,误解对方的意思了,以为他说的是在 useEffect 里面写 if 语句,所以胡扯了一堆,后面对方纠正了一下,我才意识到对方在问什么,然后回答了在条件语句里面写 useEffect 控制台会出现报错,因为 hook 的规则就是不能在条件语句或者循环语句里面写,这点在 react 官方文档里面也有提到。




  • 说一下 React 的 Fiber 架构是什么




    • 这里说了一下 Fiber 本质上就是一个对象,是 React 16.8 出现的东西,主要有三层含义:




      1. 作为架构来说,在旧的架构中,Reconciler(协调器)采用递归的方式执行,无法中断,节点数据保存在递归的调用栈中,被称为 Stack Reconciler,stack 就是调用栈;在新的架构中,Reconciler(协调器)是基于 fiber 实现的,节点数据保存在 fiber 中,所以被称为 fiber Reconciler。




      2. 作为静态数据结构来说,每个 fiber 对应一个组件,保存了这个组件的类型对应的 dom 节点信息,这个时候,fiber 节点就是我们所说的虚拟 DOM。




      3. 作为动态工作单元来说,fiber 节点保存了该节点需要更新的状态,以及需要执行的副作用。




      (这里可以参考卡颂老师的《自顶向下学 React 源码》课程)






  • 前面提到,在 if 语句里面写 hook 会报错,你可以用 fiber 架构来解释一下吗?



    • 这里说了一下,因为 fiber 是一个对象,多个 fiber 之间是用链表连接起来的,有一个固定的顺序…… 其实后面还有一些没说完,然后对方听到这里直接打断了,告诉我 OK,这个问题直接过了。




  • 个人方面有什么规划吗?



    • 主要有两个方面,一个是计算机基础需要补补,前面也提到,我不是科班毕业的,计算机底层这方面比起其他人还是比较欠缺的,尤其是计网,另一方面就是英文水平有待提高,也会在将来持续学习。




  • 对未来的技术上有什么规划呢?



    • 主要从业务转型工程化,比如做一些工具链什么的,构建、打包、部署、监控几个大的方向,node 相关的,这些都是我感兴趣的方向,未来都可以去探索,当然了现在也慢慢的在做这些事情,这里顺便提了一嘴,antd 的 script 文件夹里面的文件是我迁移到 esm + ts 的,其中一些逻辑也有重构过,比如收集 css token、生成 contributors 列表、预发布前的一些检查等等…… 所以对 node 这块也有一些了解。




  • 能不能从技术的角度讲一下你工作中负责业务的复杂度?




    • 因为前两份工作中做的是传统的 B 端项目和 C 端项目,并没有什么可以深挖的技术难点,所以这里只说了第三份工作负责的项目,这是一个协同文档,既不算 B 端,也不算 C 端,这是一款企业级的多人协作数据平台,竞品有腾讯文档、飞书文档、语雀、WPS、维卡表格等等。


      协同文档在前端的难点主要有两个方面:




      1. 实时协同编辑的处理:当两个人同时进入一个单元格编辑内容,如果保证两个人看到的视图是同步的?那么这个时候就要提到冲突处理了,冲突处理的解决方案其实已经相对成熟,包括:




        • 编辑锁:当有人在编辑某个文档时,系统会将这个单元格锁定,避免其他人同时编辑,这种方法实现方式最简单,但也会直接影响用户体验。




        • diff-patch:基于 Git 等版本管理类似的思想,对内容进行差异对比、合并等操作,也可以像 Git 那样,在冲突出现时交给用户处理。




        • 最终一致性实现:包括 Operational Transformation(OT)、 Conflict-free replicated data type(CRDT,称为无冲突可复制数据类型)。






      2. 性能问题




        • 众所周知,互联网一线大厂的协同文档工具都是基于 canvas 实现,并且有一套自己的 canvas 渲染引擎,但是我们没有,毕竟团队规模没法跟大厂比,这个项目前端就 2 个人,所以只能用 dom 堆起来(另一个同事已经跑路,现在就剩下我一个人了)。这导致页面卡顿问题非常严重,即使做了虚拟滚动,但是也没有达到很好的优化效果。老板的要求是做到十万量级的数据,但是实际上几千行就非常卡了,根本原因是数据量太大(相当于一张很大的 Excel 表格,里面的每一个单元格都是一个富文本编辑器),渲染任务多,导致内存开销太大。目前没有很好的解决方案,如果需要彻底解决性能问题,那么就需要考虑用 canvas 重写,但是这个基本上不太现实。




        • 因为卡顿的问题,暴露出来另一个问题,状态更新时,视图同步缓慢,所以这时候不得不提到另一个优化策略:乐观更新。乐观更新的思想是,当用户进行交互的时候,先更新视图,然后再向服务端发送请求,如果请求成功,那么什么都不用管,如果请求失败,那么就回滚视图。这样做的好处是,用户体验会好很多,在一些强交互的场景,不会阻塞用户操作,比如抖音的点赞就是这样做的。但是也会带来一些问题,比如:如果用户在编辑某个单元格时,另一个用户也在编辑这个单元格,那么就会出现冲突,这个时候就需要用到前面提到的冲突处理方案了。










  • 可以讲一下你在工作中技术上的建设吗?



    • 这里讲了一下对 hooks 仓库的建设,封装了 100 多个功能 hook业务 hook,把不变的部分隐藏起来,把变化的部分暴露出去,在业务中无脑传参即可,让业务开发更加简单,同时也提高了代码的复用性。然后讲了一下数据流重构之类的 balabala……




  • 你有什么想问我的吗?



    • 问了一下面试结果大概多久能反馈给我,对方说两三天左右,然后就结束了。





结束之后不到 20 分钟,对方就在 Boss 上回复我说面试过了,然后约了二面。



二面:




  • 自我介绍



    • 跟上一轮一样,大概说了两分钟,介绍了过往工作经历,做过的业务以及技术栈。




  • 在 js 中原型链是一个很重要的概念,你能介绍一下它吗?



    • 要介绍原型链,首先要介绍一下原型,原型是什么…… 这块是纯八股,懒得打字了,直接省略吧。




  • object 的原型指向谁?



    • 回答了 null。(我也不知道对不对,瞎说的)




  • 能说一下原型链的查找过程吗?



    • 磕磕绊绊背了一段八股文,这里省略吧。




  • node 的内存管理跟垃圾回收机制有了解过吗?




    • 暗暗窃喜,这个问题问到点子上了,因为两年前被问到过,所以当时专门写了一篇文章,虽然已经过去两年了,但还是背的滚瓜烂熟:




    • 首先分两种情况:V8 将内存分成 新生代空间老生代空间




      • 新生代空间: 用于存活较短的对象




        • 又分成两个空间: from 空间 与 to 空间




        • Scavenge GC 算法: 当 from 空间被占满时,启动 GC 算法



          • 存活的对象从 from space 转移到 to space

          • 清空 from space

          • from space 与 to space 互换

          • 完成一次新生代 GC






      • 老生代空间: 用于存活时间较长的对象




        • 新生代空间 转移到 老生代空间 的条件(这个过程称为对象晋升



          • 经历过一次以上 Scavenge GC 的对象

          • 当 to space 体积超过 25%




        • 标记清除算法:标记存活的对象,未被标记的则被释放



          • 增量标记:小模块标记,在代码执行间隙执,GC 会影响性能

          • 并发标记:不阻塞 js 执行










  • js 中的基础类型和对象类型有什么不一样?



    • 基础类型存储在栈中,对象类型存储在堆中。




  • 看你简历上是用 React,你能简单的介绍一下 hooks 吗?



    • 本质上就是一个纯函数,大概介绍了一下 hooks 的优点,以及 hooks 的使用规则等等。




  • 简单说一下 useEffect 的用法:



    • useEffect 可以代替 class 中的一些生命周期,讲了一下大概用法,然后讲了一下 useEffect 的执行时机,以及 deps 的作用。




  • 说一下 useEffect 的返回值用来做什么?



    • 返回一个函数,用来做清除副作用的工作,比如:清除定时器清除事件监听等等。




  • 你知道 useEffect 第二个参数内部是怎么比较的吗?



    • 说了一下内部是浅比较,源码中用 for 循环配合 Object.is 实现。(感觉这个问题就是在考察有没有读过 React 源码)




  • 前端的话可能跟网络打交道比较多,网络你了解多少呢?



    • 这里直接坦诚的说了一下,网络是我的弱项,前面一面也问到了网络七层模型,没回答出来。




  • 那你回去了解过七层模型吗?我现在再问你一遍,你能回答出来吗?



    • 磕磕绊绊回答出来了。




  • 追问:http 是在哪一层实现的?



    • 应用层。




  • 说一下 getpost 有什么区别?



    • 两眼一黑,脑子一片空白,突然不知道说什么了,挤了半天挤出来一句:get 大多数情况下用来查询,post 大多数情况下用来提交数据。get 的入参拼在 url 上,post 请求的入参在 body 里面。面试官问我还有其它吗?我说想不起来了……




  • 说一下浏览器输入 url 到页面加载的过程:




    • 输入网址发生以下步骤:



      1. 通过 DNS 解析域名的实际 IP 地址

      2. 检查浏览器是否有缓存,命中则直接取本地磁盘的 html,如果没有命中强缓存,则会向服务器发起请求(先进行下一步的 TCP 连接)

      3. 强缓存协商缓存都没有命中,则返回请求结果

      4. 然后与 WEB 服务器通过三次握手建立 TCP 连接。期间会判断一下,若协议是 https 则会做加密,如果不是,则会跳过这一步

      5. 加密完成之后,浏览器发送请求获取页面 html,服务器响应 html,这里的服务器可能是 server、也可能是 cdn

      6. 接下来是浏览器解析 HTML,开始渲染页面




    • 顺便说了渲染页面的过程:



      1. 浏览器会将 HTML 解析成一个 DOM 树,DOM 树的构建过程是一个深度遍历过程:当前节点的所有子节点都构建好后才会去构建当前节点的下一个兄弟节点。

      2. 将 CSS 解析成 CSS Rule Tree(css 规则树)。

      3. 解析完成后,浏览器引擎会根据 DOM 树CSS 规则树来构造 Render Tree。(注意:Render Tree 渲染树并不等同于 DOM 树,因为一些像 Headerdisplay:none 的东西就没必要放在渲染树中了。)

      4. 有了 Render Tree,浏览器已经能知道网页中有哪些节点、各个节点的 CSS 定义以及他们的从属关系。下一步进行 layout,进入布局处理阶段,即计算出每个节点在屏幕中的位置。

      5. 最后一个步骤就是绘制,即遍历 RenderTree,层绘制每个节点。根据计算好的信息绘制整个页面。




    • 渲染完成之后,开始执行其它任务:



      1. dom 操作

      2. ajax 发起的 http 网络请求等等……

      3. 浏览器处理事件循环等异步逻辑等等……






  • 菜单左中右布局,两边定宽,中间自适应,说一下有几种实现方式



    • 比较经典的面试题,说了 flexfloat 两种方式。




  • 项目难点



    • 和一面一样,说了协同文档的两大难点,这里就不重复了。




  • 你有什么想问我的吗?



    • 和一面一样,问了一下面试结果大概多久能反馈给我,对方说两三天左右,然后就结束了。




  • 最后问了期望薪资什么的,然后就结束了。




二面结束之后,大概过了几个小时,在 Boss 上跟对方说了一声,如果没过的话也麻烦跟我说一下,然后这时候,对方在 Boss 上问我,第一学历是不是专科?我说是的,感觉到不太妙的样子,


然后又过了一会儿,对方说定级应该不会高,他后续看一下面试官的反馈如何……


然后又追问我,换工作的核心诉求是涨薪还是能力的提升,这里我回答的比较委婉,其实两个都想要 QAQ


今天已经是第二天了,目前没有下文,看起来二面是过了,但是因为学历不够,中止了三面的流程,基本上是失败了,我也不会报有什么希望了,所以写个面经记录一下。


最后,给自己打个广告!求职求职求职!!!


社交信息:



个人优势:



  • antd 团队成员、ahooks 团队成员,活跃于 github 开源社区,给众多知名大型开源项目提交过 PR,拥有丰富的 React + TS 实战经验

  • 熟悉前端性能优化的实现,例如代码优化、打包优化、资源优化,能结合实际业务场景进行优化

  • 熟悉 webpack / vite 等打包工具的基本配置, 能够对以上工具进行二次封装、基于以上工具搭建通用的开发环境

  • 熟悉 prettier / eslint 基本配置,有良好且严格的编码习惯,唯客户论,实用主义者

  • 熟悉代码开发到上线全流程,对协同开发分支管理项目配置等都有较深刻的最佳实践



可内推的大佬们麻烦联系我!在 github 主页有联系方式,或者直接在掘金私聊我也可,谢谢!!


作者:三年没洗澡
来源:juejin.cn/post/7239715208792342584

收起阅读 »

vue打包脚本:打包前判定某个npm link依赖库是否是指定分支

web
1. 需求场景 有一个项目A,它依赖项目B 项目B是本地开发的,也在本地维护 项目A通过npm link链接到了项目B 随着项目A的不断迭代,功能升级较大创建了新的分支项目A-dev 项目A-dev分支也要求项目B也要创建出项目B-dev分支与之对应 项目A...
继续阅读 »

1. 需求场景



  • 有一个项目A,它依赖项目B

  • 项目B是本地开发的,也在本地维护

  • 项目A通过npm link链接到了项目B

  • 随着项目A的不断迭代,功能升级较大创建了新的分支项目A-dev

  • 项目A-dev分支也要求项目B也要创建出项目B-dev分支与之对应

  • 项目A项目A-dev都在产品同时运行,遇到问题都要在各自分支同步修复缺陷


在启动或者打包的时候需要特别小心项目B处在什么分支,错配分支就会导致项目启动失败或报错,于是需要有一个脚本帮我在项目启动时,检查依赖脚本是否在正确的分支上,如果不在,就自动将依赖分支切换到对应需要的分支上。


2. 脚本编写


2.1 脚本思路:



  • 先去指定项目B的文件夹中查看该项目处于哪一个分支

  • 通过动态参数获得本地启动或打包的是哪一个分支,和当前项目分支进行比对

  • 如果不是当前分支,就切换到要求的分支, 切到对应分支后,再执行install操作

  • 如果是当前分支则直接跳过分支切换操作,直接往下走


2.2 脚本创建


在vue项目根目录创建一个脚本文件check-dependency.js


下面脚本中分支名@tiamaes/t4-framework就对应项目B


const t4FrameworkFilePath = "D:/leehoo/t4-framework"; //本地@tiamaes/t4-framework目录地址

const { spawnSync } = require("child_process");

const branchName = process.argv[2];//获取参数分支名,打包时需要传递进来
if (!branchName) {
console.error("Branch name is not specified.");
process.exit(1);
}

// 获取当前分支
const result = spawnSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], { cwd: t4FrameworkFilePath });
if (result.status !== 0) {
console.error("Failed to get current branch:", result.stderr.toString());
process.exit(1);
}
const currentBranch = result.stdout.toString().trim();

// 判断分支是否需要切换
if (currentBranch !== branchName) {
console.log(`@tiamaes/t4-framework is not in ${branchName} branch. Switching to ${branchName} branch...`);
const checkoutResult = spawnSync("git", ["checkout", branchName], { cwd: t4FrameworkFilePath });
if (checkoutResult.status !== 0) {
console.error(`Failed to switch to ${branchName} branch:`, checkoutResult.stderr.toString());
process.exit(1);
}

// 安装依赖包
console.log("Installing dependencies...");
const installResult = spawnSync(process.platform === "win32" ? "npm.cmd" : "npm", ["install"], { cwd: t4FrameworkFilePath });
if (installResult.status !== 0) {
console.error("Failed to install dependencies:", installResult.stderr.toString());
process.exit(1);
}
console.log("Dependencies installed successfully.");
}

console.log(`@tiamaes/t4-framework is in ${branchName} branch. Proceeding to build...`);

process.exit(0);


3. package.json引用


在该脚本的script中增加引用方式,在项目启动或打包的时候都要执行一次node check-dependency.js,其后跟随的是项目B的分支名,我这里是erp-dev和erp-m1两个分支


"scripts": {
"serve:erp": "node check-dependency.js erp-dev && npm run link:local && vue-cli-service serve --mode development.erp",
"serve:m1": "node check-dependency.js erp-m1 && npm run link:local && vue-cli-service serve --mode development.m1",
"build:erp": "node check-dependency.js erp-dev && npm run link:local && vue-cli-service build --report --mode production.erp",
"build:m1": "node check-dependency.js erp-m1 && npm run link:local && vue-cli-service build --mode production.m1",
"link:local": "npm link @tiamaes/t4-framework",
},

下面是执行效果


Video_2023-06-09_202602.gif


image.png

收起阅读 »

Vue和React权限控制的那些事

web
自我介绍 看官们好,我叫JetTsang,之前都是在掘金潜水来着,现在偶尔做一些内容输出吧。 前言 无论是后台管理系统,还是面向C端的产品,权限控制都是日常工作中常见的需求。在此梳理一下权限控制的那些逻辑,以及在Vue/React框架下是有什么样的解决方案。 ...
继续阅读 »

自我介绍


看官们好,我叫JetTsang,之前都是在掘金潜水来着,现在偶尔做一些内容输出吧。


前言


无论是后台管理系统,还是面向C端的产品,权限控制都是日常工作中常见的需求。在此梳理一下权限控制的那些逻辑,以及在Vue/React框架下是有什么样的解决方案。


什么是权限控制?


现在基本上都是基于RBAC权限模型来做权限控制


一般来说权限控制就是三种




  • 页面权限:说白了部分页面是具备权限的,没权限的无法访问




  • 操作权限:增删改查的操作会有权限的控制




  • 数据权限:不同用户看到的、数据是不一样的,比如一个列表,不同权限的查看这部分数据,可能有些字段是**脱敏的,有些条目无法查看详情,甚至部分条目是无法查看




那么对应到前端的维度,常见的就4种




  • 权限失效(无效)(token过期/尚未登录)




  • 页面路由控制,以路由为控制单位




  • 页面上的操作按钮、组件等的权限控制,以组件/按钮为最小控制单位




  • 动态权限控制,比如1个列表,部分数据可以编辑,部分部分不可编辑




image.png


⚠️注意: 本文一些方案 基于 React18 React-Router V6 以及 Vue3 Vue-Router V4


⚠️Umi Max 这种具备权限控制系统的框架暂时不在讨论范围内~~


前置条件


由于市面上各家实现细节不一样,这里只讨论核心逻辑思路,不考虑细节实现


无论框架如何,后端根据RABC角色权限这套逻辑下来的,会有如下类似的权限标识信息,可以通过专门的接口获取,或者跟登录接口放在一起。


image.png


然后根据这些数据,去跟路由,按钮/组件等,比对产生真正的权限


像这种权限标识一般都存在内存当中(即便存在本地存储也需要加密,不过其实真正的权限控制还是需要后端来控),一般都是全局维护的状态,配合全局状态管理库使用。


权限失效(无效)


image.png


这种场景一般是在发送某些请求,返回过期状态


或者跟后端约定一个过期时间(这种比较不靠谱)


通常是在 全局请求拦截 下做,整理一下逻辑


路由级别权限控制


通常前端配好的路由可以分为 2 种:


一种是静态路由:即无论什么权限都会有的,比如登录页、404页这些


另一种是动态路由:虽然叫动态路由,其实也是在前端当中定义好了的。说它是动态的原因是根据后端的权限列表,要去做动态控制的


vue实现


在vue体系下,可以通过路由守卫以及动态添加路由来实现


动态路由


先配置静态路由表 , 不在路由表内的路由重定向到指定页(比如404)


在异步获取到权限列表之后,对动态部分的路由进行过滤之后得到有权限的那部分路由,再通过router.addRoute()添加到路由实例当中。


流程为:


(初始化时) 添加静态路由 --> 校验登录态(比如是否有token之类的) --> 获取权限列表(存到vuex / pinia) --> 动态添加路由(在路由守卫处添加)



rightsRoutesList // 来自后端的当前用户的权限列表,可以考虑存在全局状态库
dynamicRoutes // 动态部分路由,在前端已经定义好, 直接引入

// 对动态路由进行过滤,这里仅用path来比较
// 目的是添加有权限的那部分路由,具体实现方案自定。
const generateRoute = (rightsRoutesList)=>{
//ps: 这里需要注意下(如果有)嵌套路由的处理
return dynamicRoutes.filter(i=>
rightsRoutesList.some(path=>path === i.path)
)
}

// 拿到后端返回的权限列表
const getRightsRoutesList = ()=>{
return new Promise(resolve=>{
const store = userStore()
if(store.rightsRoutesList){
resolve(store.rightsRoutesList)
}else{
// 这里用 pinia 封装的函数去获取 后端权限列表
const rightsRoutesList = await store.fetchRightsRoutesList()
resolve(rightsRoutesList)
}
}
}

let hasAddedDynamicRoute = false
router.beforeEach(async (to, from) => {
if(hasAddedDynamicRoute){
// 获取
const rightsRoutesList = await getRightsRoutesList()

// 添加到路由示例当中
const routes = generateRoute(rightsRoutesList)
routes.forEach(route=>router.addRoute(route))
// 对于部分嵌套路由的子路由才是动态路由的,可以
router.addRoute('fatherName',route)
hasAddedDynamicRoute = true
}
// 其他逻辑。。。略


next({...to})
}


踩坑

通过动态addRoute去添加的路由,如果你F5刷新进入这部分路由,会有白屏现象。


image.png


因为刷新进入的过程经历了 异步获取权限列表 --> addRoute注册 的过程,此时跳转的目标路由就和你新增的路由相匹配了,需要去手动导航。


因此你需要在路由守卫那边next放行,等下次再进去匹配到当前路由


你可以这么写


router.beforeEach( (to,from,next) => {
// ...其他逻辑

// 关键代码
next({...to})
})


路由守卫


一次性添加所有的路由,包括静态和动态。每次导航的时候,去对那些即将进入的路由,如果即将进入的路由是在动态路由里,进行权限匹配。


可以利用全局的路由守卫


router.beforeEach( (to,from,next) => {
// 没有访问权限,则重定向到404
if(!hasAuthorization(to)){
// 重定向
return '/404'
}
})

也可以使用路由独享守卫,给 权限路由 添加


    // 路由表
const routes = [
//其他路由。。。略

// 权限路由
{
path: '/users/:id',
component: UserDetails,
// 定义独享路由守卫
beforeEnter: (to, from) => {
// 如果没有许可,则
if(!hasAuthorization(to)){
// 重定向到其他位置
return '/404'
}
},
},
]


react实现


在react当中,一般先将所有路由添加好,再通过路由守卫来做权限校验


局部守卫loader


React-router 当中没有路由守卫功能,可以利用v6版本的新特性loader来做,给权限路由都加上对应的控制loader


import { redirect, createBrowserRouter, RouterProvider } from 'react-router-dom'


const router = createBrowserRouter([
{
// it renders this element
element: <Team />,

// when the URL matches this segment
path: "teams/:teamId",

// with this data loaded before rendering
loader: async ({ request, params }) => {
// 拿到权限
const permission = await getPermission("teams/:teamId")
// 没有权限则跳到404
if(!permission){
return redirect('/404')
}
return null
},

// and renders this element in case something went wrong
errorElement: <ErrorBoundary />,
},
]);

// 使用
function RouterView (){
return (
<RouterProvider router={router}/>
)
}



包装路由(相当于路由守卫)


配置路由组件的时候,先渲染包装的路由组件


image.png


在包装的组件里做权限判断


function RouteElementWrapper({children, path, ...props }: any) {
const [isLoading, setIsLoading] = useState<boolean>(false);
useEffect(()=>{
// 判断登录态之类的逻辑

// 如果要获取权限,则需要setIsLoading,保持加载状态

// 这里判断权限
if(!hasAccess(path)){
navigate('/404')
}
},[])
// 渲染routes里定义好的路由
return isLoading ? <Locading/> : children
}

按钮(组件)级别权限控制


组件级别的权限控制,核心思路就是 将判断权限的逻辑抽离出来,方便复用。


vue 实现思路


在vue当中可以利用指令系统,以及hook来实现


自定义指令


指令可以这么去使用


<template>
<button v-auth='/site/config.btn'> 编辑 </button>
</template>

指令内部可以操作该组件dom和vNode,因此可以控制显隐、样式等。


hook


同样的利用hook 配合v-if 等指令 也可以实现组件级颗粒度的权限控制


<template>
<button v-if='editAuth'> 权限编辑 </button>
<div v-else>
无权限时做些什么
</div>

<button v-if='saveAuth'> 权限保存 </button>
<button v-if='removeAuth'> 权限删除 </button>
</template>
<script setup>
import useAuth from '@/hooks/useAuth'
// 传入权限
const [editAuth,saveAuth,removeAuth] = useAuth(['edit','save','remove'])
</script>


hook里的实现思路: 从pinia获取权限列表,hook里监听这个列表,并且匹配对应的权限,同时修改响应式数据。


react 实现思路


在React当中可以用高阶组件和hook的方式来实现


hook


定义一个useAuth的hook


主要逻辑是: 取出权限,然后通过关联响应式,暴露出以及authKeys ,hasAuth函数


export function useAuth(){
// 取出权限 ps: 这里从redux当中取
const authData = useSelector((state:any)=>state.login)
// 取出权限keys
const authKeys = useMemo(()=>authData.auth.components ?? [],[authData])
// 是否拥有权限
const hasAuth = useCallback(
(auths:string[]|string)=>(
turnIntoList(auths).every(auth=>authKeys.includes(auth))
),
[authKeys]
)
const ret:[typeof authKeys,typeof hasAuth] = [authKeys,hasAuth]
return ret
}

使用


const ProductList: React.FC = () => {
// 引入
const [, hasAuth] = useAuth();
// 计算是否有权限
const authorized = useMemo(() => hasAuth("edit"), [hasAuth]);

// ...略
return (
<>
{ authorized ? <button> 编辑按钮(权限)</button> : null}
</>

)
};


权限包裹组件


可以跟进一步,依据这个权限hook,封装一层包裹组件


const AuthWrapper:React.FC<{auth:string|string[],children:JSX.Element}> = ({auth, children})=>{
const [, hasAuth] = useAuth();
// 计算是否有权限
const authorized = useMemo(() => hasAuth(auth), [hasAuth]);
// 控制显隐
return authorized ? children : null
}

使用


<AuthWrapper auth='edit'>
<button> 编辑按钮(AuthWrapper) </button>
</AuthWrapper>

还可以利用renderProps特性


const AuthWrapper:React.FC<{auth:string|string[],children:JSX.Element}> = ({auth, children})=>{
const [, hasAuth] = useAuth();
// 计算是否有权限
const authorized = useMemo(() => hasAuth(auth), [hasAuth]);
+ if(typeof children === 'function'){
+ return children(authorized)
+ }
// 控制显隐
return authorized ? children : null
}

<AuthWrapper auth='edit'>
{
(authorized:boolean)=> authorized ? <button> 编辑按钮(rederProps) </button> : null
}
</AuthWrapper>

动态权限控制


这种主要是通过动态获取到的权限标识,来控制显隐、样式等。可以根据特定场景做特定的封装优化。主要逻辑其实是在后端处理。


结尾


可以看到在两大框架下实现权限控制时,思路和细节上还是稍稍有点不一样的,React给人的感觉是手上的积木更加零碎的一点,有些功能需要自己搭起来。相反Vue给人的感觉是面面俱到,用起来下限会更高。


最后


如果大家有什么想法和思考,欢迎在评论区留言~~。


另外:本人经验有限,

作者:JetTsang
来源:juejin.cn/post/7242677017034915899
如果有错误欢迎指正。

收起阅读 »

Vue3项目实现图片实现响应式布局和懒加载

web
Element实现响应式布局 分享一下,在Vue3项目中实现响应式布局(一行显示7列)。在这个例子中,我参考了Element官方的Layout布局,使用el-card来放置图片。 利用分栏布局,el-row行上设置每列的间隔gutter,el-col上设置响应...
继续阅读 »

Element实现响应式布局


分享一下,在Vue3项目中实现响应式布局(一行显示7列)。在这个例子中,我参考了Element官方的Layout布局,使用el-card来放置图片。
利用分栏布局,el-row行上设置每列的间隔gutter,el-col上设置响应式的栅格布局,Element官方预设了5个响应式尺寸,官方给出了详细的属性解释。这个例子中我设置了4个尺寸。
在这里插入图片描述
栅格默认的占据的列数是24,设置24就是一列,设置12就显示两列,设置8就显示3列,设置6就显示4列,设置4显示6列......可以根据自己的场景需求来进行布局。这个例子中我设置的响应式布局如下:



:xs="12" 当浏览器宽度<768px时,一行展示2列

:sm="8" 当浏览器宽度>=768px时,一行展示3列

:md="6" 当浏览器宽度>=992px时,一行展示4列

:lg="{ span: '7' }" 当浏览器宽度>=1200px时,一行展示7列 这个需要在css样式中设置一下。



这里例子中的图片都是放在el-card中的,并且图片都是一样大小的。修改图片可以利用图片处理工具,分享一个自己常用的工具:轻量级图片处理工具photopea
Element的Card组件由 header 和 body 组成。 header 是可选的,卡片可以只有内容区域。可以配置 body-style 属性来自定义body部分的style。
:body-style="{padding:10px}" ,这个其实是对el-card头部自动生成的横线下方的body进行边距设置。也就是除了el-card的header部分,其余都是body部分了。
这里例子中没有头部,就是给卡片的body部分设置内边距。
在这里插入图片描述
具体代码如下所示:
在这里插入图片描述
在这里插入图片描述


在这里插入图片描述
图片效果如下所示:
当浏览器宽度>=1200px时,一行展示7列:
图片14.png


当浏览器宽度>=992px时,一行展示4列:


图片15.png
当浏览器宽度>=768px时,一行展示3列:


图片16.png
当浏览器宽度<768px时,一行展示2列:


图片17.png
接下来,优化一下页面,对图片进行懒加载处理。


图片懒加载


看下上面没有用于懒加载方式的情况,F12---NetWork---Img可以看到页面加载就会显示这个页面用到的所有图片。


图片18.png
可以利用vue-lazyload,它是一个Vue.js 图片懒加载插件,可以在页面滚动至图片加载区域内时按需加载图片,进一步优化页面加载速度和性能。采用懒加载技术,可以仅在需要加载的情况下再进行加载,从而减少资源的消耗。也就是在页面加载时仅加载可视区域内的图片,而对于网页下方的图片,我们滑动到该图片时才会进行加载。


下载、引入vue-lazyload


npm install vue-lazyload --save


在package.json中查看:


图片19.png
在main.ts中引入:


图片20.png


使用vue-lazyload


在需要使用懒加载的图片中使用v-lazy指令替换src属性。


图片21.png
也可以是下面的写法:


图片22.png
这样,就实现图片的懒加载了。
验证一下懒加载是否生效,F12---NetWork---Img,可以看到图片的加载情况。


一开始没有滑动到图片区域,就不会加载图片,可以在Img中看到loding占位图片在显示。


图片23.png
滑动到了对应的照片才会显示对应的图片信息。


图片24.png


图片25.png


图片26.png


作者:YM13140912
来源:juejin.cn/post/7242516121769033787
>这就实现了懒加载。

收起阅读 »

一个大龄小前端的年终悔恨

web
今年都做什么了? 刷视频 打王者 空余时间维护了一个项目 就这样吧 仔细想了想今年也没有做什么呀! 真是年纪越大时间越快 为什么有大有小啊? 95的够大了吧 步入前端也才不到3年 So一个大龄的小前端 技术有长进么? 一个PC端项目 用了 react a...
继续阅读 »

image.png




今年都做什么了? 刷视频 打王者 空余时间维护了一个项目 就这样吧



仔细想了想今年也没有做什么呀! 真是年纪越大时间越快




为什么有大有小啊?


95的够大了吧


步入前端也才不到3年


So一个大龄的小前端


技术有长进么?


一个PC端项目 用了 react antd redux-toolkit react-router ahooks axios 也就这样吧,就一点简单的项目,react熟练了么?有点会用了,可是我工作快3年了,写项目还是要来回查文档,antd用的熟练的时候倒是可以不用去查文档,可是过了就忘了,今天写项目就有点想不起来怎么用了,查了文档才可以继续写下去


有长进么?




  1. react熟练了一些,可以自己看源码了




  2. 自己解决问题的能力有了一点提升




  3. 技术的广度认识有了(23年目标是深度)




  4. 数据结构了解一点了 二叉树 队列 链表 队列 (还学了一点算法,不过忘了🤣)




  5. 写代码喜欢封装组件了




  6. node学了一点又忘了




  7. ts会的多了一点




  8. antd也好一点了,以前在群里问一些小白问题,还好有个大哥经常帮我




  9. css 还是不咋地 不过我刚买了一个掘金小册 s.juejin.cn/ds/hjUap4V[…




生活上有什么说的呢?


生活很好 吃喝不愁


就是太久没有回家了 老家黑龙江 爷爷奶奶年纪大了 有时候想不在杭州了 回哈尔滨吧 这样可以多陪陪他们 可是回哈尔滨基本就是躺平了 回去我能做什么? 继续做前端? 好好补补基础去做一个培训讲师?


回去的好处是房子压力小 可以买一个车 每天正常上班 下班陪家人 到家有饭吃 想想也挺好


不过女朋友想在杭州,所以我还会在杭州闯一下的,毕竟我们在杭州买房子也是可以努力一下的


女朋友对我很好 我们在一起也快3年了 我刚步入前端的时候我们刚在一起 2020-05-20 她把我照顾的很好 她很喜欢我我感觉的到 我平时不太会表达 其实我是想跟她结婚的我也喜欢她 我对她耐心少了一点 这一点我会改的 以后我想多跟她分享我每天发生的事 我想这样她会更开心一点吧


今年她给我做了好多的饭,有段时间上班都是她晚上下班回来做的(她下班的早 离家近) 第二天我们好带去(偶尔我们吃一段时间的轻食) 可是我还是胖了




image.png


2023要怎么做?


我想成为大佬 我想自律一些 还有工资也要多一点吧



  • 开年主要大任务 两个字 搞钱 咱们不多来 15万可以吧 嗯 目标攒15W

  • 紧接上条 要是买 20W-30W的车 那你可以少攒点 8万到10万 (买车尽量贷款10W)

  • MD 减肥可以吧 你不看看你多胖了呀 175的身高 快170斤了减到140斤 (总觉得不胖,壮)

  • 技术一定要提升 你不能再这样下去了 要被清除地~





技术我们来好好的捋一下,该怎么提升




  1. 现有项目自己codeReview(改改你的垃圾代码吧)

  2. css多学点

    1. css in js

    2. Tailwindcss

    3. css Module less 写法好好研究一下

    4. css 相关配置要会



  3. react源码要搞一下

    1. fiber

    2. hooks

    3. diff

    4. 一些相关的库的源码 (router,redux等)



  4. webpack vite (要能写出来插件)

  5. node 这个一定要学会 (最起码能自己写接口和工具)

  6. 文章要搞起来 (最起码要写20篇,前5篇要一周一篇文章)


2023 搞一个 pc端 H5 小程序 后台接口 要齐全 必须搞出来一个 加

作者:奈斯啊小刘超奈斯_
来源:juejin.cn/post/7174789490580389925
油💪🏻

收起阅读 »

变“鼠”为“鸭”——为SVG Path制作FIFO路径变换动画,效果丝滑

web
一个月前我曾撰文《使用batik在kotlin中将TTF字体转换为SVG图像》,介绍了如何将汉字转为SVG Path路径进行展示和变换,以此为基础不妨畅想一下,用动画将一个汉字变为另一个汉字,听上去是不是很简单呢?下面动手实践一下: 我随便找了一个字体Aa剑豪...
继续阅读 »

一个月前我曾撰文《使用batik在kotlin中将TTF字体转换为SVG图像》,介绍了如何将汉字转为SVG Path路径进行展示和变换,以此为基础不妨畅想一下,用动画将一个汉字变为另一个汉字,听上去是不是很简单呢?下面动手实践一下:


我随便找了一个字体Aa剑豪体,然后随机选取了两个汉字:,再用上文提到的文章介绍的提取整体字形区块方法取出了SVG:


image.png


image.png


可以看到很简单就提取出了两个字整体的字形,下面用D3做一个简单的变换动画展示:


初始变换


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>鼠鼠我鸭</title>
</head>
<body style="text-align: center"></body>
<script src="https://d3js.org/d3.v7.min.js"></script>
<script type="module">
const _svg = d3
.select("body")
.append("svg")
.attr("width", "1000")
.attr("height", "1000")
.style("zoom", "0.3")
.style("transform", "rotateX(180deg)");
_svg.append("g")
.attr("transform", "translate(0, 160)")
.append("path")
.attr("fill", "#3fd")
.attr("stroke", "black")
.attr("stroke-width", "4")
.attr("d", 上面提到的鼠的SVG_Path...)
.transition().delay(1000).duration(1200)
.attr("fill", "#ef2")
.attr("d", 上面提到的鸭的SVG_Path...);
</script>
</html>

这里调整了svg尺寸以及zoomtransform等属性更好的适应画面,还做了个g标签套住并将定位交给它,动画效果如下图所示:


Animation.gif


很明显的看到,效果非常奇怪,怎么一开始就突然变得很乱?一开始乱这一下显得很突兀,这是因为两段图像的path长度就相差很多,绘进方式也完全不一样,很难真正的渐变过去,我试了一个有优化此过程的库d3-interpolate-path,用上去效果也没有什么差别,而且它用的还是d3@v5版本的,不知道怎么path中还会穿插一些NaN的值,很怪异,看来只能自己做了。


想真正的点对点的渐移过去,可能还是有些难的,所以我想出了一个较为简单的方案,实现一种队列式的效果,的笔画慢慢消失,而则跟随在后面逐步画出,实现一种像队列中常说的FIFO(先进先出)的效果


首先就是拆解,做一个while拆分两个字所有的节点,然后再一步步绘上拆出来的节点以验证拆的是否完整,再才能进行后面的处理。


事先要将“鸭鼠”各自的path定义为常量sourceresult,将二者开头的M与结尾的Z都去掉(中间的M不要去掉),因为动画中字形是流动的,起止点不应提前定义。


拆分路径点


const source = 鼠的SVG_Path(没有MZ)...
const result = 鸭的SVG_Path(没有MZ)...
const actionReg = new RegExp(/[a-z]/, "gi");
const data = new Array();
let match;
let lastIndex;
while ((match = actionReg.exec(result))) {
data.push(result.substring(lastIndex, match.index));
lastIndex = match.index;
}
data.push(result.substring(lastIndex));

就这样就能把的部分拆开了,先直接累加到试验一下是否成功:


叠加试验


let tran = g
.append("path")
.attr("fill", "red")
.attr("stroke", "black")
.attr("stroke-width", "4")
.attr("d", "M" + source + "Z")
.transition()
.delay(800);

let step = "L";
data.map(item => {
step += item + " ";
tran = tran.transition().attr("d", "M" + source + step.trimEnd() + "Z").duration(20);
});

首先是把上面path独立出来改一改,变成红色的利于观看,然后下面慢慢的拼合上每个节点,效果如下:


Animation.gif


是理想中的效果,那么下一步就是加速FIFO先进先出的变换了:


FIFO先进先出


这一步是不能用SVG动画的,要用setInterval定时器进行动画调节,SVG始终还是只能处理很简单的path变化,效果不如直接变来的好,这里设计成把每一帧的动画存进一个方法数组然后交给setInterval计时器循环执行(写起来比较方便),先是改一下tran的定义,因为不是动画了,所以现在改叫path就好了,border也不需要了:


let path = g
.append("path")
.attr("fill", "red")
.attr("d", "M" + source + "Z");

就这样简单的初始化一下就好了,然后就是最核心的一个过程,path的绘制循序就像一个FIFO队列:


let step = "";
let pre = source;
const funs = new Array();
data.map(async function (item, i) {
step += item + " ";
match = pre && actionReg.exec(source);
if (!match) {
pre = "";
} else if (~["M", "L", "T"].indexOf(match[0])) {
pre = source.substring(match.index + 1);
}
const d = "M" + pre + (pre ? "L" : "") + step.trimEnd() + "Z";
funs.push(() => path.attr("d", d));
});

首先是pre负责的字形,这个字形是要慢慢消失的前部,这个前部不是所有的节点都能用的,而是"M", "L", "T"这种明确有点位的动作才行,毕竟这是动画的起始点。然后step就是代表,要一步一步累加。循环结束funs数组也就累计好了所有的帧(方法),然后用定时器执行这些带参方法即可:


const animation = setInterval(() => {
if (!funs.length) {
clearInterval(animation);
return;
}
funs.shift()();
}, 20);

这种方式虽然非常少见,不过这个定时器流程还是很好理解的过程,效果如下:


Animation.gif


是想象中的效果,但稍微有些单调,可以加上一段摇摆的动画配合变换:


摇摆动画


let pathTran = path;
Array(8)
.fill(0)
.map(function () {
pathTran = pathTran
.transition()
.attr("transform", "skewX(10)")
.duration(300)
.transition()
.attr("transform", "skewX(-10)")
.duration(300);
});
pathTran.transition().attr("transform", "").duration(600);

这段动画要不断赋值才能形成连贯动画,所以直接用path处理动画是不行的,因为上面计时器也是用到这个path对象,所以要额外定义一个pathTran专门用于动画,这段摇摆动画效果如下:


Animation.gif


时间掐的刚刚好,那边计时器停掉,这边摇摆动画也缓停了。


写的十分简便,一点小创

作者:lyrieek
来源:juejin.cn/post/7241826575951200293
意,供大家参考观赏。

收起阅读 »

什么是 HTTP 长轮询?

web
什么是 HTTP 长轮询? Web 应用程序最初是围绕客户端/服务器模型开发的,其中 Web 客户端始终是事务的发起者,向服务器请求数据。因此,没有任何机制可以让服务器在没有客户端先发出请求的情况下独立地向客户端发送或推送数据。 为了克服这个缺陷,Web 应用...
继续阅读 »

什么是 HTTP 长轮询?


Web 应用程序最初是围绕客户端/服务器模型开发的,其中 Web 客户端始终是事务的发起者,向服务器请求数据。因此,没有任何机制可以让服务器在没有客户端先发出请求的情况下独立地向客户端发送或推送数据。


为了克服这个缺陷,Web 应用程序开发人员可以实施一种称为 HTTP长轮询的技术,其中客户端轮询服务器以请求新信息。服务器保持请求打开,直到有新数据可用。一旦可用,服务器就会响应并发送新信息。客户端收到新信息后,立即发送另一个请求,重复上述操作。


什么是 HTTP 长轮询?


那么,什么是长轮询?HTTP 长轮询是标准轮询的一种变体,它模拟服务器有效地将消息推送到客户端(或浏览器)。


长轮询是最早开发的允许服务器将数据“推送”到客户端的技术之一,并且由于其寿命长,它在所有浏览器和 Web 技术中几乎无处不在。即使在一个专门为持久双向通信设计的协议(例如 WebSockets)的时代,长轮询的能力仍然作为一种无处不在的回退机制占有一席之地。


HTTP 长轮询如何工作?


要了解长轮询,首先要考虑使用 HTTP 的标准轮询。


“标准”HTTP 轮询


HTTP 轮询由客户端(例如 Web 浏览器)组成,不断向服务器请求更新。


一个用例是想要关注快速发展的新闻报道的用户。在用户的浏览器中,他们已经加载了网页,并希望该网页随着新闻报道的展开而更新。实现这一点的一种方法是浏览器反复询问新闻服务器“内容是否有任何更新”,然后服务器将以更新作为响应,或者如果没有更新则给出空响应。浏览器请求更新的速率决定了新闻页面更新的频率——更新之间的时间过长意味着重要的更新被延迟。更新之间的时间太短意味着会有很多“无更新”响应,从而导致资源浪费和效率低下。


HTTP 轮询


上图:Web 浏览器和服务器之间的 HTTP 轮询。服务器向立即响应的服务器发出重复请求。


这种“标准”HTTP 轮询有缺点:



  • 更新请求之间没有完美的时间间隔。请求总是要么太频繁(效率低下)要么太慢(更新时间比要求的要长)。

  • 随着规模的扩大和客户端数量的增加,对服务器的请求数量也会增加。由于资源被无目的使用,这可能会变得低效和浪费。


HTTP 长轮询解决了使用 HTTP 进行轮询的缺点



  1. 请求从浏览器发送到服务器,就像以前一样

  2. 服务器不会关闭连接,而是保持连接打开,直到有数据供服务器发送

  3. 客户端等待服务器的响应。

  4. 当数据可用时,服务器将其发送给客户端

  5. 客户端立即向服务器发出另一个 HTTP 长轮询请求


HTTP 长轮询


上图:客户端和服务器之间的 HTTP 长轮询。请注意,请求和响应之间有很长的时间,因为服务器会等待直到有数据要发送。


这比常规轮询更有效率。



  • 浏览器将始终在可用时接收最新更新

  • 服务器不会被永远无法满足的请求所搞垮。


长轮询有多长时间?


在现实世界中,任何与服务器的客户端连接最终都会超时。服务器在响应之前保持连接打开的时间取决于几个因素:服务器协议实现、服务器体系结构、客户端标头和实现(特别是 HTTP Keep-Alive 标头)以及用于启动的任何库并保持连接。


当然,许多外部因素也会影响连接,例如,移动浏览器在 WiFi 和蜂窝连接之间切换时更有可能暂时断开连接。


通常,除非您可以控制整个架构堆栈,否则没有单一的轮询持续时间。


使用长轮询时的注意事项


在您的应用程序中使用 HTTP 长轮询构建实时交互时,需要考虑几件事情,无论是在开发方面还是在操作/扩展方面。



  • 随着使用量的增长,您将如何编排实时后端?

  • 当移动设备在WiFi和蜂窝网络之间快速切换或失去连接,IP地址发生变化时,长轮询会自动重新建立连接吗?

  • 通过长轮询,您能否管理消息队列并如何处理丢失的消息?

  • 长轮询是否提供跨多个服务器的负载平衡或故障转移支持?


在为服务器推送构建具有 HTTP 长轮询的实时应用程序时,您必须开发自己的通信管理系统。这意味着您将负责更新、维护和扩展您的后端基础设施。


服务器性能和扩展


使用您的解决方案的每个客户端将至少每 5 分钟启动一次与您的服务器的连接,并且您的服务器将需要分配资源来管理该连接,直到它准备好满足客户端的请求。一旦完成,客户端将立即重新启动连接,这意味着实际上,服务器将需要能够永久分配其资源的一部分来为该客户端提供服务。当您的解决方案超出单个服务器的能力并且引入负载平衡时,您需要考虑会话状态——如何在服务器之间共享客户端状态?您如何应对连接不同 IP 地址的移动客户端?您如何处理潜在的拒绝服务攻击?


这些扩展挑战都不是 HTTP 长轮询独有的,但协议的设计可能会加剧这些挑战——例如,您如何区分多个客户端发出多个真正的连续请求和拒绝服务攻击?


消息排序和排队


在服务器向客户端发送数据和客户端发起轮询请求之间总会有一小段时间,数据可能会丢失。


服务器在此期间要发送给客户端的任何数据都需要缓存起来,并在下一次请求时传递给客户端。


HTTP 长轮询 MQ


然后出现几个明显的问题:



  • 服务器应该将数据缓存或排队多长时间?

  • 应该如何处理失败的客户端连接?

  • 服务器如何知道同一个客户端正在重新连接,而不是新客户端?

  • 如果重新连接花费了很长时间,客户端如何请求落在缓存窗口之外的数据?


所有这些问题都需要 HTTP 长轮询解决方案来回答。


设备和网络支持


如前所述,由于 HTTP 长轮询已经存在了很长时间,它在浏览器、服务器和其他网络基础设施(交换机、路由器、代理、防火墙)中几乎得到了无处不在的支持。这种级别的支持意味着长轮询是一种很好的后备机制,即使对于依赖更现代协议(如 WebSockets )的解决方案也是如此。


众所周知,WebSocket 实现,尤其是早期实现,在双重 NAT 和某些 HTTP 长轮询运行良

作者:demo007x
来源:juejin.cn/post/7240111396869161020
好的代理环境中挣扎。

收起阅读 »

10个让你爱不释手的一行Javascript代码

web
在这篇博客中,我们将分享 10+ 个实用的一行 JavaScript 代码,这些代码可以帮助你提高编码效率和代码简洁度。这些代码片段将涵盖各种用途,从操作数组和字符串,到更高级的概念,如异步编程和面向对象编程。 获取数组中的随机元素 使用 Math.rand...
继续阅读 »

freysteinn-g-jonsson-s94zCnADcUs-unsplash.jpg
在这篇博客中,我们将分享 10+ 个实用的一行 JavaScript 代码,这些代码可以帮助你提高编码效率和代码简洁度。这些代码片段将涵盖各种用途,从操作数组和字符串,到更高级的概念,如异步编程和面向对象编程。


获取数组中的随机元素


使用 Math.random() 函数和数组长度可以轻松获取数组中的随机元素:


const arr = [1, 2, 3, 4, 5];
const randomElement = arr[Math.floor(Math.random() * arr.length)];
console.log(randomElement);

数组扁平化


使用 reduce() 函数和 concat() 函数可以轻松实现数组扁平化:


const arr = [[1, 2], [3, 4], [5, 6]];
const flattenedArr = arr.reduce((acc, cur) => acc.concat(cur), []);
console.log(flattenedArr); // [1, 2, 3, 4, 5, 6]

对象数组根据某个属性值进行排序


const sortedArray = array.sort((a, b) => (a.property > b.property ? 1 : -1));

从数组中删除特定元素


const removedArray = array.filter((item) => item !== elementToRemove);

检查数组中是否存在重复项


const hasDuplicates = (array) => new Set(array).size !== array.length;

判断数组是否包含某个值


const hasValue = arr.includes(value);

首字母大写


const capitalized = str.charAt(0).toUpperCase() + str.slice(1);

获取随机整数


const randomInt = Math.floor(Math.random() * (max - min + 1)) + min;

获取随机字符串


const randomStr = Math.random().toString(36).substring(2, length);

使用解构和 rest 运算符交换变量的值:


let a = 1, b = 2
[b, a] = [a, b]
console.log(a, b) // 2, 1

将字符串转换为小驼峰式命名:


const str = 'hello world'
const camelCase = str.replace(/\s(.)/g, ($1) => $1.toUpperCase()).replace(/\s/g, '').replace(/^(.)/, ($1) => $1.toLowerCase())
console.log(camelCase) // "helloWorld"

计算两个日期之间的间隔


const diffInDays = (dateA, dateB) => Math.floor((dateB - dateA) / (1000 * 60 * 60 * 24));

查找日期位于一年中的第几天


const dayOfYear = (date) => Math.floor((date - new Date(date.getFullYear(), 0, 0)) / 1000 / 60 / 60 / 24);

复制内容到剪切板


const copyToClipboard = (text) => navigator.clipboard.writeText(text);

copyToClipboard("Hello World");

获取变量的类型


const getType = (variable) => Object.prototype.toString.call(variable).slice(8, -1).toLowerCase();

getType(''); // string
getType(0); // number
getType(); // undefined
getType(null); // null
getType({}); // object
getType([]); // array
getType(0); // number
getType(() => {}); // function

检测对象是否为空


const isEmptyObject = (obj) => Object.keys(obj).length === 0 && obj.constructor === Object;



作者:shichuan

来源:juejin.cn/post/7230810119122190397

收起阅读 »

低代码的那些事

web
在当今数字化的时代,前端开发已成为构建出色用户体验的重要领域。然而,传统的前端开发过程往往需要耗费大量时间和精力,对于那些没有技术背景或时间有限的人来说,这无疑是一个巨大的挑战。然而,随着技术的不断进步,低代码开发正迅速崛起,为我们提供了一种简化开发流程的全新...
继续阅读 »

在当今数字化的时代,前端开发已成为构建出色用户体验的重要领域。然而,传统的前端开发过程往往需要耗费大量时间和精力,对于那些没有技术背景或时间有限的人来说,这无疑是一个巨大的挑战。然而,随着技术的不断进步,低代码开发正迅速崛起,为我们提供了一种简化开发流程的全新方法。


终端概念


终端 = 前端 + 客户端


在讲低代码之前,我们先来聊一聊终端概念,这个终端不是指敲命令行的小窗口,而是 终端 = 前端 + 客户端。乍一听,这个不是和大前端的概念类似吗?为什么又提出一个重复的名词,实际上它俩还是有很大区别的,在大前端里面,岗位是有不同区分的,比如前端开发工程师、客户端开发工程师,每个岗位的分工是不一样的,但是你可以把终端看成一个岗位。


image.png


下面是阿里巴巴终端开发工程师招聘的 JD,因为内容较长,我将他分成了三张图片,我们从上到下依次看。


第一张图片:
2024届实习生的招聘,招聘岗位为终端开发工程师





第二张图片:
这是他们对终端开发工程师的描述,大家主要看标了特殊颜色的字体就行



它包括原有的“前端工程师”和“移动端工程师” 相较过去,我们强调面向最终的用户进行交付,不局限于“前端〞、“移动端〞,这将显著拓宽工程师的职责、 能力边界。






第三张图片:
这是他们对终端开发工程师的岗位要求,可以从要求的第1、2、3项看到,这个岗位更侧重于基础技术、终端界面,而不是在于要求你会使用某某框架。





大家对终端概念有了一定了解之后,那么这个终端概念是谁提出的呢?没错,就是阿里巴巴。

阿里巴巴公众号改名史


这个公众号可能会有一些朋友之前关注过,它会发布前端和客户端相关的文章,但是之前的名字并不叫阿里巴巴终端技术。


image.png


我们来看看他的改名史:



  • 2019年05月10日注册 "Alibaba FED"(FED:Front-end Developer 前端开发者)

  • 2019年06月12日 "Alibaba FED" 认证 Alibaba F2E"(F2E:Front-end Engineer 前端工程师)

  • 2022年07月08日 "Alibaba F2E" 帐号迁移改名"阿里巴巴终端技术"


所以是从此又多了一个终端开发工程师的岗位吗,显然不是的,终端开发工程师最终是要取代前端开发工程师和客户端开发工程师的,最终的目的是达到降本增效。


那如何让前端开发工程师和客户端开发工程师过渡成为终端开发工程师。


终端走向荧幕


在阿里 2022 年举办的第 17 届 D2 终端技术大会上,当然他们是这一届将大会名字改成了终端,其中有一篇卓越工程的主题简介如下:


image.png



在过去十年,不管是前端的工具链还是客户端的版本交付效能等都在快速演进,面向未来,我们升级工程体系走向终端工程一体化,覆盖前端及客户端工程师从研发交付到运维的全生命周期,利用低代码、极速构建、全链路运维、Serverless 等新型的工程技术,在卓越工程标准推动下引领终端工程师走向卓越。



可以看到,低代码是可以作为实践终端的一种技术方案,并且将其放在了第一位,那么什么是低代码,低代码能做什么事情,为什么使用低代码可以让终端开发工程师变的更加卓越?低代码对我们普通的一线开发又能带来什么改变或者赋能?
好,接下来,我们就来聊一聊低代码。


什么是低代码


Low-Code


低代码来源于英语翻译——Low-Code,当然,此“Low”非彼“Low”,它意指一种快速开发的方式,使用最少的代码、以最快的速度来交付应用程序。


低代码的定义是什么


虽然低代码已经是个老生常谈的话题了,但关于它的定义我觉得还是有必要描述一遍(来自ChatGPT):


低代码是一种软件开发方法,旨在通过最小化手动编码的工作量,利用可视化工具和组件来快速构建应用程序。它提供了一个图形化的界面,使开发者能够以图形化方式设计和创建应用程序的用户界面、业务逻辑和数据模型,而无需编写大量的传统代码。


低代码它作为一种软件的开发方法,他不仅仅可以作为终端的解决方案,也可以在后端、IOT、大数据等领域上进行使用,并且这些领域也都有现成的低代码开源工具或者平台。


传统前端开发 VS 低代码开发


传统的前端开发方式,我们需要使用 HTML + CSS 描绘出页面的骨架,再使用 JAVASCRIPT 完成页面的功能逻辑。


image.png


可以看到图片上,我们定义了三个 div 元素,并且给每个 div 元素加上了背景颜色,并且加上了点击事件。每一块的元素,都需要我们使用相应的代码去描述。页面越复杂,描述的代码量就会越多。页面的代码量越多,相应的维护的成本就会越高。


我们在来看下如何使用低代码进行开发:


Untitled2.png


左侧物料区,中间画布区,右侧物料配置区,这种区域划分也是比较常见的低代码平台布局。选择物料以后,将物料拖进画布区,接下来我们就可以对物料进行属性配置。


相较于故枯燥难懂的代码,直观的拖拉拽显得更加简单,也更加容易理解。


背后的原理是什么


通过简单的托拉拽后我们可以看到一份表格页面,那么页面实际上是如何生成的呢?


背后实际对应的一份 Schema 描述,主要由物料描述和布局描述组成。


10921685966317_.pic.jpg


我们从左到右依次看,componentsMap 记录了我们当前页面使用的组件,可以看到我们使用了Form.Item、Input、Table,以及这些组件来自的 package 包信息。


componentsTree 里面记录了整个页面布局的信息,最外层由Page组件包裹,然后依次是 Form.Item组件,label 标签为姓名,里面还有一个 input 作为子元素,后面还有两个 Form.Item,为年龄和地址,最后的元素是 Table 组件,通过这些信息,我们就可以布局出一份简单的表格页面。


componentsMap 描述了我们当前页面所需的物料,componentsTree 描述了我们当前页面的布局顺序,将这两份数据组合,通过特定的解析器,就可以得到一份页面。低代码的页面渲染是通过我们事先约定好的数据结构进行生成的。


Schema ⇒ 页面,会不会使我的页面加载变慢


可能会有一些同学心中会有疑问,通过 Schema 生成页面,应该是需要一层 runtime 层吧,通过一段运行时的代码,将 Schema 转换为页面。


那在将 Schema 的内容转换为页面的时候,难免会产生性能上的损耗吧?


性能 VS 可维护性


这里就涉及到了性能 和 可维护性的取舍了,平台的意义在于为你掩盖底层代码的操作。让你用更直观的方式来描述你的目的,其实这里可以牵扯出另外一个相似的话题。


真实DOM VS 虚拟DOM


10931685966489_.pic.jpg


现代化的前端框架都会使用虚拟 DOM,那大家觉得真实DOM更快还是虚拟DOM更快?


框架的意义在于为你掩盖底层的 DOM 操作,让你用更声明式的方式来描述你的目的,从而让你的代码更容易维护。


没有任何框架可以比纯手动的优化 DOM 操作更快,因为框架的 DOM 操作层需要应对任何上层 API 可能产生的操作,它的实现必须是普适的。


针对任何一处基准,我都可以写出比任何框架更快的手动优化,但是那有什么意义呢?在构建一个实际应用的时候,你难道为每一个地方都去做手动优化吗?出于可维护性的考虑,这显然不可能。框架给你的保证是,你在不需要手动优化的情况下,我依然可以给你提供过得去的性能。


你会发现低代码的 Schema 和 虚拟 DOM 是比较相似的,都是通过对象的形式去描述 DOM 节点,
虚拟 DOM 的另外一个优势,是在于实现跨端,将底层对 DOM 操作的 API 更换为对 Android 或者 IOS 的 UI 操作。同理低代码的 Schema 也可以比较好的实现跨端,思路基本是一致的,Schema 只是一份组件 + 页面的描述,你可以根据不同的场景,进行不同平台的组件的渲染。


有没有办法可以优化页面渲染的性能吗?


10941685966667_.pic.jpg


那么有没有解决方案呢?


是有的,我们可以通过 Schema 的方式对页面进行出码,出码后是一套完整的应用工程,基于应用工程在去对应用进行一次构建,将这部分负担转移到编译时去完成。


生成的应用工程和我们平常开发的应用工程基本相似:


10951685966768_.pic.jpg


什么是低代码?


如果之前你没有听过低代码,到这里你还是没有明白低代码是什么。没关系,你可以暂时的把他理解一个可视化编辑器,通过拖拽元素就能生成页面,页面背后实际对应的是一串 Schema JSON 数据。到最后,我们会重新对低代码进行一个定义。


低代码发展趋势


低代码发展时间线


image.png


我们来看下低代码发展的时间线:



  • 1980年代:出现了第四代编程语言(ABAP, Unix Shell, SQL, PL/SQL, Oracle Reports, R)第四代编程语言指的是非过程的高级规范语言,包括支持数据库管理、报告生成、数学优化、图形用户界面(GUI)开发和 web 开发。

  • 2000年:出现了 VPL 语言(即 visual programming language 可视化变成语言)

  • 2014年:提出了低代码 / 零代码概念

  • 2016年:国内独立的低代码开发平台开始相继发布

  • 2021年:中国市场逐渐形成完整的低代码、无代码生态体系


发展趋势


image.png


这是 OSS Insight 上关于低代码的一组统计数据,他们分析了50亿的 Github event数据,得出了这些报告,我从里面摘选了和低代码相关的部分。


首先,在2022年热门话题的开源存储库活跃度,LowCode 以76.3%活跃度位居第一,其次是Web3、Github Actions、Database、AI,可见大部分低代码开源仓库都处于一个开发或者维护的状态。


image.png


我们在来看下,低代码发展的趋势图,从2016年到2020年,低代码整体处于上升趋势,并且新增仓库在2020年达到最高点,新增低代码相关的仓库高达了300%以上。


在2020年野蛮生长后,2021年的新增仓库趋近于0,但是在2021低代码相关仓库的start数量将近增长了200%,2022年的数据开始趋于平缓,并且新增仓库数量减少,标志着低代码技术沉淀趋于平稳,百花齐放的时代已经过去。


没有规矩,不成方圆


百花齐放下其实是关于低代码标准的缺失,每套低代码平台都有自己的行为标准,各个平台之间的物料是有隔阂的,无法通用的。就有点像现在百花齐放的小程序,比如微信小程序、支付宝小程序、百度小程序等等,从一定程度上来讲,标准的缺失,会给用户和开发者带来了一定的困扰。


如果有行业组织或者技术社区可以积极推动低代码标准化的倡议,制定统一的行为标准和规范,标准物料的
定义,那么对于低代码的未来发展是一件非常有利的事情。


低代码产品矩阵


我们看来下国外的低代码产品矩阵,种类和平台还是非常多的。
10981685967331_.pic.jpg


可以看到关于低代码的落地场景其实有非常多,并且已经有了大量成熟应用流入市场,所以低代码作为一种开发软件的方法,可以将其灵活运用在各个场景。


而且每个场景的低代码,在其下还可以细分领域,比如 Web 应用程序开发,可以细分为中后台管理搭建、活动推广页面、图表大盘页面等。


一款低代码平台,通常都会有它自己的定位,是为了解决某种特定领域下的特定业务而生。所以一个公司内部有十几个低代码平台是很正常的,他们在细分下的业务场景有不同的分工。


我们正在做什么


这一章节分为三个小块来讲,为什么要做低代码、怎么去做低代码、现在做的怎么样了


为什么要做低代码


我们为什么要做低代码,低代码其实能解决的问题和场景有非常多,那么低代码能帮我们研发团队解决哪些问题?


1.由繁去简


image.png


通常一个需求下来,



  1. 产品会先对需求进行规划,产出一份原型图交付给 UI 设计

  2. UI 通过产品提供的原型图,对原型进行美化,产出一份设计稿

  3. 前端对照设计稿进行页面开发,交付高保真的页面

  4. 最后进行接口联调,将静态的数据更改为接口获取的数据


做程序本质上也是在做交流,假设我们现在是一位前端或者客户端的开发工程师,



  1. 我们需要先和产品 battle 原型图

  2. 和 UI 讨论设计稿

  3. 交付高保真的页面

  4. 和后端进行接口联调


可以看到绝大部分的时间都花在了如何去做页面上,在加上关于各个环节对页面的讨论和修改,这中间会产生大量的浸没成本。


如果说,现在有一个工具,可以做到产品交付的就是高保真页面,你会选择用还是不用?


image.png


这个是使用低代码后的开发流程,由产品直接生成高保真页面交付给前端,极大提高了开发生产力。那么,这个时候前端可以更加聚焦于业务逻辑,聚焦于工程体系,而不是页面本身。


2. 我不想在去改样式了


好像所有的产品经理都喜欢在项目即将上线前,对页面的样式进行调整,没错,既不是测试阶段,也不是预发阶段,而是即将发布前,改完一个,在改一个,好像总是也改不完。


而使用低代码平台后,将页面生成的权利递到产品经理手中,他们可以随心所欲的修改,尽情的展示自己的创造力,我们也不在需要反复的修改样式,反复的重新构建应用发布,你可以专心的去做其它事情。


3. 真正的所见即所得


真正的所见即所得,相比于黑盒子的代码,低代码平台显得更加直观,操作的是页面,而不是代码,你可以在平台上尽情的组装,就像是搭积木一样。


怎么去做低代码


image.png


在能够协调足够多的资源的情况下,选择自研是比较好的一条路,因为一切都是可控的。


但是在资源有限的情况下,选择开源或许是一种最快最便捷的方法了。我们在低代码发展趋势中,可以发现低代码平台和开源技术已经趋于稳定。


使用稳定的开源框架可以更快的帮助我们创建低代码平台,并且有足够多懂低代码底层的人,去帮助我们维护基础设施,站在巨人的肩膀上出发,往往会事半功倍。


我们选择的是阿里开源的 lowcode-engine,在其基础上进行二次开发,选择它的理由有很多:


10991685967710_.pic.jpg


现在做的怎么样了


下面是平台的真实演示,目前已经支持开发以及发布预览了。
_d.gif


低代码架构图:
image.png


平台使用流程的步骤图:


image.png



  • 第一步是创建应用

  • 第二步是创建页面,当然一个应用下面可能会有多个页面,每个页面都会是相互独立的,

  • 第三步是布局调整,可以在选中的页面进行拖拽物料进行布局

  • 第四步是属性配置,配置物料的属性,配置物料的事件或者样式,提供自定义样式和逻辑的功能支持

  • 第五步是保存发布,将当前各个页面的schema进行保存上传,存储到数据库

  • 第六步是页面渲染,可以直接通过平台生成的页面地址,打开页面


被误解的低代码


我相信是会有一大部分的程序员从内心抵制低代码的,一方面,作为一个技术工种,对自己的技术是有底气的,有傲骨的,人为写的代码都不怎么样,还指望低代码平台上的代码吗,另一方面,在低代码平台的代码上维护,那简直就是在屎山上维护,维护的成本会更大吧


出码 VS 不出码


这里的痛点是在于需不需要维护低代码产出的代码,前面我们讲到过出码,出码可以用于产物构建。但构建这一块,是平台去做的,用户并不会感知到背后实际的应用工程。


出码同时也可以用于用户的自定义需求,如果平台的物料完全覆盖了你的业务场景,你是不需要去出码的。但是,如果平台的物料无法满足你的业务场景,你需要的组件又具备足够的特殊性,这个时候你可能需要使用出码功能,在出码的应用工程基础下,添加自己的业务代码,那么这个时候,你是需要维护整个应用工程的。


对低代码的分歧往往是这个时候产生的,每个人心中都有自己的标准代码,都会本能的去抵触和自己标准不同的代码。


不出码如何自定义开发需求?


那有没有既不出码,又可以满足自定义开发的需求呢?


因为我们更多的是希望平台去维护工程,而不是通过人为方式去维护。像我们平时开发页面,本质上就是在写组件,通过拼装组件,形成页面。


我们的思想上可以做个转变,既然 80%~90% 的工作平台可以承接,剩余的平台无法实现,可以自己实现自定义组件进行发布,发布的组件可以沉淀到市场,你的其它项目可以使用自己的业务组件,其他同事也可以使用你的组件。


低代码会不会导致前端岗位变少?


其实完全可以将低代码看成提升工作效率的一个工具,低代码解决页面视图,页面逻辑和数据联调需要开发者在平台上进行完成,开发者的关注点更多的聚焦于业务逻辑,聚焦于如何去做好工程化体系。


AI Code 不是更好吗?


那好像 AI Code 也可以做到低代码能做的地步?


在今年3月份 GPT-4 的发布会上,只需要在草稿本上用纸笔画出一个非常粗糙的草图,再拍照告诉GPT-4需要一个这样的网站,AI 就可以在10秒钟左右根据草图,生成一个网站完整的前端 HTML 代码。


GPT-4发布:一张草图,一秒生成网站


image.png


这简直就是低代码 plus,回归我第一次使用 GPT 的时候,我确实是被惊讶到,特别是他能衔接上下文关系,给出你想要的答案。


我们开发应用,其实本身就是一个庞大的上下文,版本一直迭代,需求一直新增,通过人本身去记住业务上下文,在一个足够复杂的应用下,他的上下文会足够大,足够的冗余,我们去抽离组件,抽离函数,使用数据结构优化代码,实际上就是在优化上下文,写代码并不难,难的是如何梳理页面的组件和那些难以理解的业务以及那些人与人的沟通。


至少现在看来,GPT 无法做到承接复杂应用的上下文,现在的他只能帮助你快速产出一个 demo 应用,前提你需要做到甄别代码的能力,以及还需要面临后续版本迭代更新的窘境问题。


或者说,如果 AI 真的能做到独立开发复杂应用,程序员就真的会被取代吗,做程序本身就是一个相对复杂的活,需要持续学习,持续精进。如果AI真的能做到独立开发这一步,那我觉得离真正的无人驾驶也不远了,出租车司机全部都得失业,因为做一个程序相比于驾驶车辆,会难上不少,当然还包括其它行业,80% 以上的职业都极有可能面临下岗危机。


这个是政客、政府不允许的,虽然科技进步是好事,但是 AI 并没有带来实际的增量工作岗位,反而导致失业率变高,失业率若变高,整体社会的稳定性结构就会变差。
所以,我们更多的将 AI 看成工具,关注点在于,如何用 AI 去做成更多的事情。


什么是低代码?


讲到这里,基本上今天的分享就已经进入尾声了,最后我们在来确定下什么是低代码?



低代码是一种软件开发方法,旨在通过最小化手动编码的工作量,利用可视化工具和组件来快速构建应用程序。它提供了一个图形化的界面,使开发者能够以图形化方式设计和创建应用程序的用户界面、业务逻辑和数据模型,而无需编写大量的传统代码。



一千个人眼中,有一千个哈姆雷特,每个人对低代码的理解都会有些许不同,首先低代码是一种软件开发的方法,这套方法可以用在很多场景。如果一个平台提供了可视化的工具和组件并且又提供部分手动编码的能力,它就可以是一个低代码平台。


在前端低代码的方案中,并不是不再使用 HTML、CSS、JAVASCRIPT 进行开发,而是大大减少他们的使用频率,通过少量的代码,就可以完成一个页面的开发。


参考


收起阅读 »

让弹窗更易于使用~

web
标题又名:简单弹窗、多弹窗、复杂弹窗怎么做代码和状态的解耦。 关键字:react / modal 问题 实际业务中,不乏弹窗组件中包含大量复杂的业务逻辑。如: function Order() { // 省略上百行方法状态    const [vis...
继续阅读 »

标题又名:简单弹窗、多弹窗、复杂弹窗怎么做代码和状态的解耦。


关键字:react / modal


问题


实际业务中,不乏弹窗组件中包含大量复杂的业务逻辑。如:


function Order() {
// 省略上百行方法状态
   const [visible,setVisible] = useState(false)
   const withModalState = useState<any>()
   
   return (
  <Modal>
      <Input/>
           <Input/>
<Select/>
<Checkbox/>
       </Modal>

  )
}

甚至还有多弹窗的情形。如:


function Order() {
// 省略上百行方法状态
   const [visible1,setVisible1] = useState(false)
   const [visible2,setVisible2] = useState(false)
   const [visible3,setVisible3] = useState(false)
   
   const withModalState1 = useState<any>()
   const withModalState2 = useState<any>()
   const withModalState3 = useState<any>()
   
   // 省略 不懂多少 handlexx
   return (
       <main>
        ...
           <Modal1></* 省略很多代码 */></Modal1>

           <Modal2></* 省略很多代码 */></Modal3>
           <Modal3></* 省略很多代码 */></Modal3>
       </main>
  )
}


非常的痛:



  1. 如果弹窗在处理完内部流程后,又还有返回值,这有又会有一大通的处理函数。

  2. 如果这些代码都写在一个文件中还是不太容易维护的。


而且随着业务的断增长,每次都这么写还是很烦的。


期望的使用方式


因此有没有一种更贴近业务实际的方案。


让弹窗只专注于自己的内部事务,同时又能将控制权交给调用方呢。


或者说我就是不喜欢 Modal 代码堆在一起……


如下:


// Modal1.tsx
export function Modal1(props) return <Modal></* ... */></Modal>

// 甚至可以将MOdal中的逻辑做的更聚合。通过2次封再导出给各方使用
export const function Check_XXX_Window() {/* */ return open(Modal1)}
export const function Check_XXX_By_Id_Window() { return open(Modal1)}
export const function Check_XXX_Of_Ohter_Window() { return open(Modal1)}

// Order.tsx
function Order() {

function xxHndanle {
const expect = Check_XXX_Window(Modal1,args) // 调用方法,传入参数,获得预期值 ,完成1个流程
}
return (
<main>
...
// 不实际挂载 Modal 组件
</main>

)
}

像这样分离两者的代码,这样处理起来肯定是清爽很多的。


实现思路


实现的思路都是一致的。



  1. 创建占位符组件,放到应用的某处。

  2. 将相关状态集管理,并与Modal做好关联。

  3. 暴露控制权。


因此基于不同的状态管理方案,实现是多种多样的。相信很多大佬都自己内部封装过不少了。


但是在此基础上,我还想要3点。



  1. API使用简单,但也允许一定配置。

  2. 返回值类型推导,除了帮我管理 close 和 open 还要让我用起来带提示的。

  3. 无入侵性,不依赖框架以外的东西。


ez-modal-react


emm......苦于市面上找不到这类产品(感觉同类并不多……讨论的也不多?)


于是我自己开源了一个。回应上文实现思路,可以点击看代码,几百行而已。



并不是突发奇想而来,其实相关特性早就在企业内部是使用多年了。我也是受前辈和社区启发。



基本特性



  1. 基于Promise封装

  2. 返回值类型推导

  3. 没有入侵性,体积小。


使用画面


import EasyModal, { InnerModalProps } from 'ez-modal-react';

+ interface IProps extends InnerModalProps<'fybe?'> /*传入返回值类型*/ {
+ age: number;
+ name: string;
+ }

export const InfoModal = EasyModal.create(
+ (props: Props) => {
return (
<Modal
title="Hello"
open={props.visible}
onOk={() => {
+ props.hide(); // warn 应有 1 个参数,但获得 0 个。 (property) hide: (result: "fybe?") => void ts(2554)
}}
onCancel={() => {
props.hide(null); //safe hide 接受 null 作为参数。它兼具 hide resolve 两种功能。
}}
>
<h1>{props.age}</h1>
</Modal>
);
});

+ // warn 类型 "{ name: string; }" 中缺少属性 "age",但类型 "ModalProps<Props, "fybe?">" 中需要该属性。
EasyModal.show(InfoModal, { name: 'foo' }).then((resolve) => {
console.log(resolve);
+ //输出 "fybe?"
});

也支持用 hook


import EasyModal, { useModal, InnerModalProps } from 'ez-modal-react';

interface IProps extends InnerModalProps<'苏振华'>/* 指定返回值类型 */ {
age: number;
name: string;
}

export const Info = EasyModal.create((props: Props) => {
const modal = useModal<Props>();

function handleOk(){
modal.hide(); // ts(2554) (property) hide: (result: "苏振华") => void ts(2554)
}

return <Modal open={modal.visible} onOk={handleOk} onCancel={modal.hide}></Modal>
});


EasyModal.show(Info,{age:18,}) // 缺少属性 "age"

还有一些特性如支持配置 hide 弹窗时的默认行为。(我认为大多数情况下可能用不上)



export const Info = EasyModal.create((props: Props) => {
const modal = useModal<Props>();

function handleOk(){
modal.hide();
+ modal.resolve('苏振华') // 需要手动抛出成功
+ modal.remove() // 需要手动注销组件。可用于反复打开弹窗,但是不希望状态被清除的场景。
}

return <Modal open={modal.visible} onOk={handleOk} onCancel={modal.hide}></Modal>

// Ohter.tsx
EasyModal.open(Info, {},
+ config:{
+ resolveOnHide:false, // 默认为 true
+ removeOnHide:false, // 默认为 true
+ }
);


当然以上是针对弹窗内有复杂业务场景的状况。


大部分场景都是调用方只在乎 open或close ,仅仅解耦代码这一项好处,就可以让代码变得清爽。


如常用的有展示类,设置类的弹窗,TS类型都用不上。


仓库


其他就不一一介绍了,主要的已经说完了,想了解更多,可以看看仓库。github


🎮 Codesandbox Demo


Codesandbox是一个线上集成环境,可以直接打开玩玩。点击 Demo Link 前往


初衷


让弹窗使用更轻松。



包名 ez 开头,是因为我DOTA在东南亚打多了,觉得该词特别贴切。



授之于鱼叉


GitHub仓库地址
ez-modal-react


觉得有用帮我点个星~~~ 感恩,有啥问题可以提出,看到会回复。如果有需要会持续维护该项目。


下列诸神,望您不吝赐教

作者:胖东蓝
来源:juejin.cn/post/7238917620849246263

收起阅读 »

为什么需要PNPM ?

web
PNPM是什么? 在日常工作里面,总是有同事让我将npm替换成pnpm,理由是这玩意速度更快,效率更高。那到底pnpm是什么呢?他为什么比npm/yarn有着更大的优势?首先,毫不疑问,pnpm是作为一个前端包管理器工具被提出的,作者的初衷是为了能够开发出一款...
继续阅读 »


PNPM是什么?


在日常工作里面,总是有同事让我将npm替换成pnpm,理由是这玩意速度更快,效率更高。那到底pnpm是什么呢?他为什么比npm/yarn有着更大的优势?
首先,毫不疑问,pnpm是作为一个前端包管理器工具被提出的,作者的初衷是为了能够开发出一款能够有效节省磁盘空间,提高安装速度的包管理工具。
其次,npm和yarn存在的诸多问题,也让开发者诟病已久,pnpm也是在这样的背景下被开发出来并广受欢迎的。
image.png
image.png
image.png


PNPM解决了什么样的问题?


在讨论pnpm解决了什么样的问题之前,我们可以先看看npm和yarn这些传统包管理工具到底存在什么样的问题。


NPM 2


在Npm2.x里面,当你观察node_modules时,你会发现对于不同包之间的依赖关系,npm2.x采用的是层层嵌套的方式去管理这些依赖包。
image.png
对于依赖关系来说,嵌套的管理方式虽然让不同包之间的依赖关系一目了然,但是却存在着诸多的问题。



  • 公共的依赖无法重复利用。不同的包里难免会存在相同的依赖,但是在npm2.x里面,这些公共依赖会被复制很多次,存在于不同的包的嵌套node_modules里,这样会导致下载速度的下降以及浪费了许多磁盘空间。

  • **嵌套关系过深时,路径名过长。**在window下,有很多程序无法处理超过260个字符的文件路径名,如果嵌套关系过深时,就有可能会超出这个限制,导致windows下存在无法解析的现象。



NPM 3 & YARN


针对上述Npm 2存在的两个典型的问题,Yarn和Npm3采用了扁平化的依赖管理方式,所有的依赖不再层层嵌套,而是全部都在同一层,这样就同时解决了重复依赖多次复制和路径名过长的问题了。
image.png
可以看到使用npm3安装的express使,大部分的包都不会有二层的node_modules的,当然,如果同时存在多个版本的包时,则还是会出现部分嵌套node_modules的情况。
另外,Yarn和Npm 3都采用了lock文件,借此来保证每次拉取同一个项目的依赖时,使用的是同一个版本,避免不同版本之间的差异性导致的项目Bug。
但是,问题又来了,难道使用了扁平化的依赖管理方式就是完美的吗,这种管理方式会不会产生新的问题?答案否定的,扁平化的管理方式会导致两个最主要的问题



  • 幽灵依赖

  • 磁盘空间问题没有完全解决


幽灵依赖


幽灵依赖是指你的项目明明没有在package.json文件里声明的依赖,但是在代码里面却可以使用到。导致这个问题的最主要的原因就是,依赖扁平化了,当你的项目寻找依赖的时候,可以找到所有node_modules里的最外层依赖。
显然,幽灵依赖是会带来隐患的,当你依赖的包A有一天不再需要包B了, 你对B的幽灵依赖就会导致错误,从而发生问题。


磁盘问题


当同一个项目依赖了某个包的多个版本时,Npm3和Yarn只会提升其中的一个,而其余版本的包还是不能避免复制多次的问题 。


PNPM是如何解决这些问题的?


在讨论Pnpm的机制之前,我们需要先学习下一些操作系统相关的知识。


链接


链接实际上是一种文件共享的方式。在Linux的文件系统中,除了文件名和文件内容,还有一个很重要的概念,就是inode,inode类似于C语言的指针,它指向了物理硬盘的一个区块,只有有文件指向这个区块,他就不会从硬盘中消失。而硬链接和软连接最大的区别,则是inode的与原文件的关系。


硬链接


一般来说,inode与文件名、文件数据是一对一的关系,但我们可以通过shell命令让多个文件名指向同一个inode,这种就是硬链接(hard link)。由于硬链接文件和源文件使用同一个inode,并指向同一块文件数据,除文件名之外的所有信息都是一样的。所以这两个文件是等价的,可以说是互为硬链接文件。修改任意一个文件,可以看到另外一个文件的内容也会同步变化。
也正是因为有多个相同的inode指向同一个区块,所以硬链接的源文件即使被删除,也不会对链接文件有任何影响。


软链接


软连接又称符号链接,与硬链接共用一个inode不同的是,软链接会创建一个新的inode,存放着源文件的绝对路径信息,并指向源文件。当用户访问软链接时,系统会自动将其替换为该软链接所指向的源文件的文件路径,然后访问源文件。
而PNPM则是利用了以上链接的机制,来解决Npm和Yarn存在的问题。
当你使用Pnpm安装依赖时,Pnpm会在全局的仓库里保存一份npm包的内容,然后再利用链接的方式,从全局仓库里链接到你项目里的虚拟仓库,也就是node_modules里的是.pnpm。


image.png


你所安装的依赖,都会单独存放在node_modules下,而依赖所依赖的npm包都会通过软链接的方式,链接.pnpm里的虚拟仓库里的包,而.pnpm里的包,则是通过硬链接从全局Store里链接过来的。
image.pngimage.png
通过软硬链接结合的方式,Pnpm可以很有效的解决了Npm和yarn遗留的问题:
1、抛弃了扁平化的管理方式,避免了幽灵依赖。
2、Npm包的存储方式都是放在全局,使用到的时候只会建立一个硬链接,硬链接和源文件共享同一份内存空间,不会造成重复依赖的空间浪费。另外,当依赖了同一个包的不同版本时,只对变更的文件进行更新,不需要重复下载没有变更的部分。
3、下载速度,当存在已经使用过的npm包时,只会建立链接,而不会重新下载,大大提升包安装的速度。


PNPM天生支持Monorepo?


monorepo 是在一个项目中管理多个包的项目组织形式。
它能解决很多问题:工程化配置重复、link 麻烦、执行命令麻烦、版本更新麻烦等。
而利用Pnpm的workSpace配合changesets,就可以很简单的完成一个Monorepo项目的搭建,所以说Pnpm天生就是Mo

作者:Gamble_
来源:juejin.cn/post/7240662396020916282
norepo的利器。

收起阅读 »

前端自动部署:从手动到自动的进化

web
在现代 Web 开发中,前端自动化已经成为了必不可少的一部分。随着项目规模的增加和开发人员的增多,手动部署已经无法满足需求,因为手动部署容易出错,而且需要大量的时间和精力。因此,自动化部署已经成为了前端开发的趋势。在本文中,我们将介绍前端自动化部署的基本原理和...
继续阅读 »

在现代 Web 开发中,前端自动化已经成为了必不可少的一部分。随着项目规模的增加和开发人员的增多,手动部署已经无法满足需求,因为手动部署容易出错,而且需要大量的时间和精力。因此,自动化部署已经成为了前端开发的趋势。在本文中,我们将介绍前端自动化部署的基本原理和实现方式,并提供一些示例代码来说明。


前端自动化部署的基本原理


前端自动化部署的基本原理是将人工操作转换为自动化脚本。这些脚本可以执行一系列操作,例如构建、测试和部署应用程序。自动化部署可以帮助开发人员节省时间和精力,并提高应用程序的质量和可靠性。


自动化部署通常包括以下步骤:



  1. 构建应用程序:使用构建工具(例如 webpack、gulp 或 grunt)构建应用程序的代码和资源文件。

  2. 运行测试:使用测试工具(例如 Jest、Mocha 或 Karma)运行应用程序的单元测试、集成测试和端到端测试。

  3. 部署应用程序:使用部署工具(例如 Jenkins、Travis CI 或 CircleCI)将应用程序部署到生产服务器或云平台上。


实现前端自动化部署的方式


实现前端自动化部署的方式有很多种,以下是其中的一些:


1. 使用自动化部署工具


自动化部署工具可以帮助我们自动化构建、测试和部署应用程序。这些工具通常具有以下功能:



  • 与版本控制系统集成,例如 Git 或 SVN。

  • 与构建工具集成,例如 webpack、gulp 或 grunt。

  • 与测试工具集成,例如 Jest、Mocha 或 Karma。

  • 与部署平台集成,例如 AWS、Azure 或 Heroku。


自动化部署工具可以帮助我们节省时间和精力,并提高应用程序的质量和可靠性。


以下是一个使用 Jenkins 自动化部署前端应用程序的示例:


pipeline {
agent any
stages {
stage('Build') {
steps {
sh 'npm install'
sh 'npm run build'
}
}
stage('Test') {
steps {
sh 'npm run test'
}
}
stage('Deploy') {
steps {
sh 'npm run deploy'
}
}
}
}

在这个示例中,我们使用 Jenkins 构建、测试和部署前端应用程序。我们将应用程序的代码和资源文件打包到一个 Docker 容器中,并将容器部署到生产服务器上。


2. 使用自动化构建工具


自动化构建工具可以帮助我们自动化构建应用程序的代码和资源文件。这些工具通常具有以下功能:



  • 支持多种语言和框架,例如 JavaScript、React 和 Vue。

  • 支持多种模块化方案,例如 CommonJS 和 ES6 模块。

  • 支持多种打包方式,例如单文件和多文件打包。

  • 支持多种优化方式,例如代码压缩和文件合并。


以下是一个使用 webpack 自动化构建前端应用程序的示例:


const path = require('path');

module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
},
module: {
rules: [
{
test: /.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
},
},
{
test: /.css$/,
use: ['style-loader', 'css-loader'],
},
],
},
};

在这个示例中,我们使用 webpack 构建前端应用程序的代码和资源文件。我们将应用程序的入口文件设置为 src/index.js,将输出文件设置为 dist/bundle.js,并使用 Babel 转换 JavaScript 代码和使用 CSS Loader 加载 CSS 文件。


3. 使用自动化测试工具


自动化测试工具可以帮助我们自动化运行应用程序的单元测试、集成测试和端到端测试。这些工具通常具有以下功能:



  • 支持多种测试框架,例如 Jest、Mocha 和 Jasmine。

  • 支持多种测试类型,例如单元测试、集成测试和端到端测试。

  • 支持多种测试覆盖率工具,例如 Istanbul 和 nyc。

  • 支持多种测试报告工具,例如 JUnit 和 HTML。


以下是一个使用 Jest 自动化测试前端应用程序的示例:


test('adds 1 + 2 to equal 3', () => {
expect(sum(1, 2)).toBe(3);
});

在这个示例中,我们使用 Jest 运行一个简单的单元测试。我们将 sum 函数的输入设置为 1 和 2,并将期望输出设置为 3。如果测试通过,Jest 将输出 PASS,否则将输出 FAIL


结论


前端自动化部署已经成为了现代 Web 开发的趋势。通过使用自动化部署工具、自动化构建工具和自动化测试工具,我们可以节省时间和精力,并提高应用程序的质量和可靠性。在未来,前端自动化部署将会变得更加普遍和重要,因此我们需要不断学习和掌

作者:_大脑斧
来源:juejin.cn/post/7240636320761921594
握相关的技术和工具。

收起阅读 »

vue3项目打包时We're sorry but XXX doesn't work properly without JavaScript

vue
题引: 这周末公司突然分配了一个任务,让我搞一个混合代码的平板项目:vue3+安卓原生 来配合实现。看了一眼,问题不大,那边只要求把做好的页面打包成 dist 文件发给组长即可。开干。 正文: 当界面做完之后且打包完成,就打开了 dist 文件夹里的 inde...
继续阅读 »

题引:


这周末公司突然分配了一个任务,让我搞一个混合代码的平板项目:vue3+安卓原生 来配合实现。看了一眼,问题不大,那边只要求把做好的页面打包成 dist 文件发给组长即可。开干。


正文:


当界面做完之后且打包完成,就打开了 dist 文件夹里的 index.html 。突然发现页面是空白的,打开调试器之后突然发现了一个报错:

<strong>We’re sorry but XXX doesn’t work properly without JavaScript enabled</strong>



看了一下vue-router、pinia没有什么问题,调用顺序也没错。于是就往打包的文件夹查看,才发现了引用的路径是以 / 绝对路径开头的,以至于资源无法加载而导致页面空白。


发现了这个问题,直接定位到 vue.config.js 文件,如果是vite创建的话应该是 vite.config.js

//vue.config.js
export default = {
publicPath: './', //打包文件的路径
... // 其他配置
}

//vite.config.js
import {defineConfig} from 'vite';
import vue from '@vitejs/plugin-vue';
export default defineConfig({
publicPath: './', //打包文件的路径
...
})

当然,上网查了一下前端的路由也会导致这个问题的出现。可以把mode值从 history 改成 hash

import {createRouter,createWebHashHistory} from 'vue-router';

const routes = [];
const router = createRouter({
router,
history:createWebHashHistory()
})

结尾:


以上就是处理打包上线时遇到 项目在没有启用JavaScript的情况下无法正常工作 的情况。


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

箭头函数函数是否有原型

web
问题:箭头函数是否有原型 今天在博客重构之余,看到某个前端群有群友这样问: 面试被问到了一个题,箭头函数是否有原型 大家觉得有吗? 首先不说它是不是,我们来回顾一下 箭头函数 是什么,原型 又是什么。 箭头函数是什么 箭头函数表达式的语法比函数表达式更简...
继续阅读 »

问题:箭头函数是否有原型


今天在博客重构之余,看到某个前端群有群友这样问:



面试被问到了一个题,箭头函数是否有原型
大家觉得有吗?



首先不说它是不是,我们来回顾一下 箭头函数 是什么,原型 又是什么。


箭头函数是什么



箭头函数表达式的语法比函数表达式更简洁,并且没有自己的this,arguments,super或new.target。箭头函数表达式更适用于那些本来需要匿名函数的地方,并且它不能用作构造函数。



developer.mozilla.org/zh-CN/docs/…



引入箭头函数有两个方面的作用:更简短的函数并且不绑定this。



一般来说如果问题是函数是否有原型时,那么可以好不犹豫的回答说是,但因为箭头函数的特殊性导致了答案的不确定性。


原型是什么



当谈到继承时,JavaScript 只有一种结构:对象。
每个对象(object)都有一个私有属性指向另一个名为原型(prototype)的对象。
原型对象也有一个自己的原型,层层向上直到一个对象的原型为 null。
根据定义,null 没有原型,并作为这个原型链(prototype chain)中的最后一个环节。



developer.mozilla.org/zh-CN/docs/…



一个 Function 对象在使用 new 运算符来作为构造函数时,会用到它的 prototype 属性。它将成为新对象的原型。



developer.mozilla.org/zh-CN/docs/…


什么是原型在 mdn 中已经讲得很清楚了,也就是对象的prototype


试验


所以按理来说对象都有原型,那么试试就知道了。


首先看看箭头函数的原型:


const a = () => {};

console.log(a.prototype); // undefined

在浏览器控制台可以把上述代码执行一遍,可以发现结果是 undefined。


那么是否就说明箭头函数没有原型了呢?别急继续往下看。


const a = () => {};

console.log(a.prototype); // undefined

console.log(a.__proto__); // ƒ () { [native code] }

我们可以看到 a.__proto__ 是一个 native function


那么 __proto__ 又是什么呢?


__proto__是什么



Object.prototype (en-US) 的 __proto__ 属性是一个访问器属性(一个 getter 函数和一个 setter 函数), 暴露了通过它访问的对象的内部[[Prototype]] (一个对象或 null)。




__proto__ 的读取器 (getter) 暴露了一个对象的内部 [[Prototype]] 。对于使用对象字面量创建的对象,这个值是 Object.prototype (en-US)。对于使用数组字面量创建的对象,这个值是 Array.prototype。对于 functions,这个值是 Function.prototype。对于使用 new fun 创建的对象,其中 fun 是由 js 提供的内建构造器函数之一 (Array, Boolean, Date, Number, Object, String 等等),这个值总是 fun.prototype。对于用 JS 定义的其他 JS 构造器函数创建的对象,这个值就是该构造器函数的 prototype 属性。




__proto__ 属性是 Object.prototype (en-US) 一个简单的访问器属性,其中包含了 get(获取)和 set(设置)的方法,任何一个 __proto__ 的存取属性都继承于 Object.prototype (en-US),但一个访问属性如果不是来源于 Object.prototype (en-US) 就不拥有 __proto__ 属性,譬如一个元素设置了其他的 __proto__ 属性在 Object.prototype (en-US) 之前,将会覆盖原有的 Object.prototype (en-US)。



developer.mozilla.org/zh-CN/docs/…


看解释可能有人不大理解,举个例子 🌰:


function F() {}
const f = new F();

console.log(f.prototype); // undefined
console.log(f.__proto__ === F.prototype); // true
console.log(f.__proto__.constructor); // F(){}

new F 也即是 F 的实例 ff__proto__ 属性指向 Fprototype


由此可以得出实例与原型的关系:new function得到实例,实例的 __proto__ 又指向function的原型,原型的构造器指向原函数。


结论


好了,理解了什么是 __proto__ 后,我们回到原来的问题上:箭头函数函数是否有原型?


通过上述的代码我们可以知道箭头函数就是Function的实例,如果你觉得不是,那么请看下面的例子:


const a = () => {};

console.log(a instanceof Function); // true
console.log(a.__proto__ === Function.prototype); // true
console.log(a.prototype); // undefined

所以最终得出两种结论:如果按中文语意那么箭头函数是Function的实例,而依据实例与原型的关系,它是有原型的;如果原型仅仅只说的是prototype,那么结论就是没有



注:以上代码结果都是在chrome113下运行得出



如有错误,欢迎指正~


参考


箭头函数
developer.mozilla.org/zh-CN/docs/…


构造器
developer.mozilla.org/zh-CN/docs/…


原型链
developer.mozilla.org/zh-CN/docs/…


Function.prototype
developer.mozilla.org/zh-CN/docs/…


__proto__
developer.mozilla.org/zh-CN/

docs/…

收起阅读 »

不用递归也能实现深拷贝

web
前言 在现代化的 Web 开发中,深拷贝是一个常见的数据处理需求,它允许我们复制并操作数据,而不影响原始数据。然而,使用递归实现深拷贝的方法可能对性能产生负面影响,特别是在处理大规模数据时。因此,越来越多的前端开发者开始关注另一种不用递归的方式实现深拷贝。 深...
继续阅读 »

前言


在现代化的 Web 开发中,深拷贝是一个常见的数据处理需求,它允许我们复制并操作数据,而不影响原始数据。然而,使用递归实现深拷贝的方法可能对性能产生负面影响,特别是在处理大规模数据时。因此,越来越多的前端开发者开始关注另一种不用递归的方式实现深拷贝。


深拷贝的实现方式


我们先来看看常用的深拷贝的实现方式


JSON.parse(JSON.stringify())


利用 JSON.stringify 将对象转成 JSON 字符串,再用 JSON.parse 把字符串解析成新的对象实现深拷贝。


这种方式代码简单,常用于深拷贝简单类型的对象。


在复杂类型的对象上会有问题:



  1. undefined、function、symbol 会被忽略或者转为 null(数组中)

  2. 时间对象变成了字符串

  3. RegExp、Error 对象序列化的结果将只得到空对象

  4. NaN、Infinity 和-Infinity,则序列化的结果会变成 null

  5. 对象中存在循环引用的情况也无法正确实现深拷贝


函数库 lodash 的 cloneDeep 方法


这种方式使用简单,而且 cloneDeep 内部是使用递归方式实现深拷贝,因此不会有 JSON 转换方式的问题;但是需要引入函数库 js,为了一个函数而引入一个库总感觉不划算。


递归方法


声明一个函数,函数中变量对象或数组,值为基本数据类型赋值到新对象中,值为对象或数组就调用自身函数。


// 手写深拷贝
function deepCopy(data) {
const map = {
"[object Number]": "number",
"[object Boolean]": "boolean",
"[object String]": "string",
"[object Function]": "function",
"[object Array]": "array",
"[object Object]": "object",
"[object Null]": "null",
"[object Undefined]": "undefined",
"[object Date]": "date",
"[object RegExp]": "regexp",
};
var copyData;
var type = map[Object.prototype.toString.call(data)];
if (type === "array") {
copyData = [];
data.forEach((item) => copyData.push(deepCopy(item)));
} else if (type === "object") {
copyData = {};
for (var key in data) {
copyData[key] = deepCopy(data[key]);
}
} else {
copyData = data;
}
return copyData;
}

递归方式结构清晰将任务拆分成多个简单的小任务执行,可读性强,但是效率低,调用栈可能会溢出,函数每次调用都会在内存栈中分配空间,而每个进程的容量是有限的,当调用的层次太多时,就会超出栈的容量,从而导致溢出。


深拷贝其实是对树的遍历过程


嵌套对象很像下面图中的树。


Untitled.png


递归的思路是遍历 1 对象的属性判断是否是对象,发现属性 2 是一个对象在调用函数本身来遍历 2 对象的属性是否是对象如此反复知道变量 9 对象。


Untitled 1.png


9 对象的属性中没有对象然后返回 5 对象去遍历其他属性是否是对象,没有再返回 2 对象,最后返回到 1 对象发现其 3 属性是一个对象。


Untitled 2.png


Untitled 3.png


最后在找 4 对象。


Untitled 4.png


可以看到递归其实是对树的深度优先遍历。


那么不用递归可以实现树的深度优先遍历么?


答案是肯定的。


不用递归实现深度优先遍历深拷贝


观察递归算法可以发现实现深度优先遍历主要是两个点



  1. 利用栈来实现深度优先遍历的节点顺序

  2. 记录哪些节点已经走过了


第一点可以用数组来实现栈


const stack = [source]
while (stack.length) {
const data = stack.pop()
for (let key in data) {
if (typeof source[key] === "object") {
stack.push(data[key])
}
}
}

这样就能把所有的嵌套对象都放入栈中,就可以遍历所有的嵌套子对象。


第二点因为发现对象属性值是对象时会中断当前对象的属性遍历改去遍历子对象,因此要记录对象的遍历的状态。由于 for in 的遍历是无序的即使用一个变量存 key 也没办法知道哪些 key 已经遍历过了,需要一个数组记录所有遍历过的属性。


这里还有另一种简单的方法就是用 Object.keys 来获取对象的 key 数组放到 stack 栈中。


const stack = [...Object.keys(source).map(key => ({ key, source: source }))]
while (stack.length) {
const { key, data } = stack.pop()
if (typeof data[key] === "object") {
stack.push(...Object.keys(data[key]).map(k => ({ key: k, data: data[key] })))
}
}

这样 stack 中深度优先遍历的遍历的对象顺序也记录其中。


这里将代码优化下, 把 Object.keys 换成 Object.entries 更为精简


const stack = [...Object.entries(source)]
while (stack.length) {
const [ key, value ] = stack.pop()
if (typeof value === "object") {
stack.push(...Object.entries(value))
}
}

遍历完成下一步就是创建一个新的对象进行赋值。


const stack = [...Object.entries(source)]
const result = {}
const cacheMap = {}
let id = 0
let cache
while (stack.length) {
const [key, value, id] = stack.pop()
if (id != undefined && cacheMap[id]) {
cache = cacheMap[id]
} else {
cache = result
}
if (typeof value === "object") {
cacheMap[id] = cache[key] = {}
stack.push(...Object.entries(value).map(item => [...item, id++]))
} else {
cache[key] = value
}
}
return result

因为对象时引用类型,因此可以通过 cacheMap[id] 来快速访问 result 的嵌套对象。


代码还可以优化:


cacheMap 可以用 WeakMap 来声明减少 id 的声明:


const stack = [...Object.entries(source)]
const result = {}
const cacheMap = new WeakMap()
let cache
while (stack.length) {
const [key, value, parent] = stack.pop()
if (cacheMap.has(parent)) {
cache = cacheMap.get(parent)
} else {
cache = result
}
if (typeof value === "object") {
cache[key] = {}
cacheMap.set(value, cache[key])
stack.push(...Object.entries(value).map(item => [...item, value]))
} else {
cache[key] = value
}
}
return result

stack 中的数组项中的 parent 可以换成目标对象:


const result = {}
const stack = [...Object.entries(source).map(item => [...item, result])]
while (stack.length) {
const [key, value, target] = stack.pop()
if (typeof value === "object") {
target[key] = {}
stack.push(...Object.entries(value).map(item => [...item, target[key]]))
} else {
target[key] = value
}
}
return result

加上数组的判断最终代码为:


function cloneDeep(source) {
const map = {
"[object Number]": "number",
"[object Boolean]": "boolean",
"[object String]": "string",
"[object Function]": "function",
"[object Array]": "array",
"[object Object]": "object",
"[object Null]": "null",
"[object Undefined]": "undefined",
"[object Date]": "date",
"[object RegExp]": "regexp"
}
const result = Array.isArray(source) ? [] : {}
const stack = [...Object.entries(source).map(item => [...item, result])]
const toString = Object.prototype.toString
while (stack.length) {
const [key, value, target] = stack.pop()
if (map[toString.call(value)] === 'object' || map[toString.call(value)] === 'array') {
target[key] = Array.isArray(value) ? [] : {}
stack.push(...Object.entries(value).map(item => [...item, target[key]]))
} else {
target[key] = value
}
}
return result
}

console.log(cloneDeep({ a: 1, b: '12' }))
//{ a: 1, b: '12' }
console.log(cloneDeep([{ a: 1, b: '12' }, { a: 2, b: '12' }, { a: 3, b: '12' }]))
//[{ a: 1, b: '12' }, { a: 2, b: '12' }, { a: 3, b: '12' }]

广度优先遍历实现深拷贝


同样的思路,实现深拷贝的最终代码为:


function cloneDeep(source) {
const map = {
"[object Number]": "number",
"[object Boolean]": "boolean",
"[object String]": "string",
"[object Function]": "function",
"[object Array]": "array",
"[object Object]": "object",
"[object Null]": "null",
"[object Undefined]": "undefined",
"[object Date]": "date",
"[object RegExp]": "regexp"
}
const result = {}
const stack = [{ data: source, target: result }]
const toString = Object.prototype.toString
while (stack.length) {
let { target, data } = stack.unshift()
for (let key in data) {
if (map[toString.call(data[key])] === 'object' || map[toString.call(data[key])] === 'array') {
target[key] = Array.isArray(data[key]) ? [] : {}
stack.push({ data: data[key], target: target[key] })
} else {
target[key] = data[key]
}
}
}
return result
}

作者:千空
来源:juejin.cn/post/7238978371689136185
收起阅读 »

Vue 为什么要禁用 undefined?

web
Halo Word!大家好,我是大家的林语冰(挨踢版)~ 今天我们来伪科普一下——Vue 等开源项目为什么要禁用/限用 undefined? 敏感话题 我们会讨论几个敏感话题,包括但不限于—— 测不准的 undefined 如何引发复合 BUG? 薛定谔的...
继续阅读 »

Halo Word!大家好,我是大家的林语冰(挨踢版)~


今天我们来伪科普一下——Vue 等开源项目为什么要禁用/限用 undefined




敏感话题


我们会讨论几个敏感话题,包括但不限于——



  1. 测不准的 undefined 如何引发复合 BUG?

  2. 薛定谔的 undefined 如何造成二义性?

  3. 未定义的 undefined 为何语义不明?


懂得都懂,不懂关注,日后再说~




1. 测不准的 undefined 如何引发复合 BUG?


一般而言,开源项目对 undefined 的使用有两种保守方案:



  • 禁欲系——能且仅能节制地使用 undefined

  • 绝育系——禁用 undefined


举个粒子,Vue 源码就选择用魔法打败魔法——安排黑科技 void 0 重构 undefined


vue-void.png


事实上,直接使用 undefined 也问题不大,毕竟 undefined 表面上还是比较有安全感的。


readonly-desc.gif


猫眼可见,undefined 是一个鲁棒只读的属性,表面上相当靠谱。


虽然 undefined 自己问题不大,但最大的问题在于使用不慎可能会出 BUG。undefined 到底可能整出什么幺蛾子呢?


你知道的,不同于 null 字面量,undefined 并不恒等于 undefined 原始值,比如说祂可以被“作用域链截胡”。


举个粒子,当 undefined 变身成为 bilibili,同事的内心是崩溃的。


bilibili.png


猫眼可见,写做 undefined 变量,读做 'bilbili' 字符串,这样的代码十分反人类。


这里稍微有点违和感。机智如你可能会灵魂拷问,我们前面不是已经证明了 undefined 是不可赋值的只读属性吗?怎么祂喵地一言不合说变就变,又可以赋值了呢?来骗,来偷袭,不讲码德!


这种灵异现象主要跟变量查找的作用域链机制有关。读写变量会遵循“就近原则”优先匹配,先找到谁就匹配谁,就跟同城约会一样,和樱花妹异地恋的优先级肯定不会太高,所以当前局部作用域的优先级高于全局作用域,于是乎 JS 会优先使用当前非全局同名变量 undefined


换而言之,局部的同名变量 undefined 屏蔽(shadow,AKA“遮蔽”)了全局变量 globalThis.undefined


关于作用域链这种“远亲不如近邻”的机制,吾愿赐名为“作用域链截胡”。倘若你不会搓麻将,你也可以命名为“作用域链抢断”。倘若你不会打篮球,那就叫“作用域链拦截”吧。


globalThis.undefined 确实是只读属性。虽然但是,你们重写非全局的 undefined,跟我 globalThis.undefined 有什么关系?


周树人.gif


我们总以为 undefined 短小精悍,但其实 globalThis.undefined 才能扬长避短。


当我们重新定义了 undefinedundefined 就名不副实——名为 undefined,值为任意值。这可能会在团队协作中引发复合 BUG。


所谓“复合 BUG”指的是,单独的代码可以正常工作,但是多人代码集成就出现问题。


举个粒子,常见的复合 BUG 包括但不限于:



  • 命名冲突,比如说 Vue2 的 Mixin 就有这个瑕疵,所以 Vue3 就引入更加灵活的组合式 API

  • 作用域污染,ESM 模块之前也有全局作用域污染的老毛病,所以社区有 CJS 等模块化的轮子,也有 IIFE 等最佳实践

  • 团队协作,Git 等代码版本管理工具的开发冲突


举个粒子,undefined 也可能造成类似的问题。


complex-bug.png


猫眼可见,双方的代码都问题不大,但放在一起就像水遇见钠一般干柴烈火瞬间爆炸。


这里分享一个小众的冷知识,这样的代码被称为“Jenga Code”(积木代码)。


Jenga 是一种派对益智积木玩具,它的规则是,先把那些小木条堆成一个规则的塔,玩家轮流从下面抽出一块来放在最上面,谁放上之后木塔垮掉了,谁就 GG 了。


jenga.gif


积木代码指的是一点点的代码带来了亿点点的 BUG,一行代码搞崩整个项目,码农一句,可怜焦土。


换而言之,这样的代码对于 JS 运行时是“程序正义”的,对于开发者却并非“结果正义”,违和感拉满,可读性和可为维护性十分“赶人”,同事读完欲哭无泪。


所谓“程序正义”指的是——JS 运行时没有“阳”,不会抛出异常,直接挂掉,浏览器承认你的代码 Bug free,问题不大。


祂敢报错吗?祂不敢。虽然但是,无症状感染也是感染。你敢这么写吗?你不敢。除非忍不住,或者想跑路。


举个粒子,“离离原上谱”的“饭圈倒牛奶”事件——



  • 有人鞠躬尽瘁粮食安全

  • 有人精神饥荒疯狂倒奶


这种行为未必违法,但是背德,每次看到只能无视,毕竟语冰有“傻叉恐惧症”。


“程序正义”不代表“结果正义”,代码能 run 不代表符合“甲方肝虚”,不讲码德可能造成业务上的技术负债,将来要重构优化来还债。所谓“前猫拉屎,后人铲屎”大抵也是如此。


综上所述,要警惕测不准的 undefined 在团队开发中造成复合 BUG。




2. 薛定谔的 undefined 如何造成二义性?


除了复合 BUG,undefined 还可能让代码产生二义性。


代码二义性指的是,同一行代码,可能有不同的语义。


举个粒子,JS 的一些代码解读就可能有歧义。


mistake.png


undefined 也可能造成代码二义性,除了上文的变量名不副实之外,还很可能产生精神分裂的割裂感。


举个粒子,代码中存在两个一龙一猪的 undefined


default.png


猫眼可见,undefined 的值并不相同,我只觉得祂们双标。


undefined 变量之所以是 'bilibili' 字符串,是因为作用域链就近屏蔽,cat 变量之所以是 undefined 原始值,是因为已声明未赋值的变量默认使用 undefined 原始值作为缺省值,所以没有使用局部的 undefined 变量。


倘若上述二义性强度还不够,那我们还可以写出可读性更加逆天的代码。


destruct.png


猫眼可见,undefined 有没有精神分裂我不知道,但我快精神分裂了。


代码二义性还可能与代码的执行环境有关,譬如说一猫一样的代码,在不同的运行时,可能有一龙一猪的结果。


strict-mode.png


猫眼可见,我写你猜,谁都不爱。


大家大约会理直气壮地反驳,我们必不可能写出这样不当人的代码,var 是不可能 var 的,这辈子都不可能 var


问题在于,墨菲定律告诉我们,只要可能有 BUG,就有可能有 BUG。说不定你的猪队友下一秒就给你来个神助攻,毕竟不是每个人都像你如此好学,既关注了我,还给我打 call。


语冰以前也不相信倒牛奶这么“离离原上谱”的事件,但是写做“impossible”,读做“I M possible”。


事实上,大多数教程一般不会刻意教你去写错误的代码,这其实恰恰剥夺了我们犯错的权利。不犯错我们就不会去探究为什么,而对知识点的掌握只停留在表面是什么,很多人知错就改,下次还敢就是因为缺少了试错的成就感和多巴胺,不知道 BUG 的 G 点在哪里,没有形成稳固的情绪记忆。


请相信我,永远写正确的代码本身就是一件不正确的事情,你会看到这期内容就是因为语冰被坑了气不过,才给祂载入日记。


语冰很喜欢的一部神作《七龙珠》里的赛亚人,每次从濒死体验中绝处逢生战斗力就会增量更新,这个设定其实蛮科学的,譬如说我们身边一些“量变到质变”的粒子,包括但不限于:



  • 骨折之后骨头更加坚硬了

  • 健身也是肌肉轻度撕裂后增生

  • 记忆也是不断复习巩固


语冰并不是让大家在物理层面去骨折,而是鼓励大家从 BUG 中学习。私以为大神从来不是没有 BUG,而是 fix 了足够多的 BUG。正如爱迪生所说,我没有失败 999 次,而是成功了 999 次,我成功证明了那些方法完全达咩。


综上所述,undefined 的二义性在于可能产生局部的副作用,一猫一样的代码在不同运行时也可以有一龙一猪的结果,最终导致一千个麻瓜眼中有一千个哈利波特,读码人集体精神分裂。




3. 未定义的 undefined 为何语义不明?


除了可维护性感人的复合 BUG 和可读性感人的代码二义性,undefined 自身的语义也很难把握。


举个粒子,因为太麻烦就全写 undefined 了。


init.png


猫眼可见,原则上允许我们可以无脑地使用 undefined 初始化任何变量,万物皆可 undefined


虽然但是,绝对的光明等于绝对的黑暗,绝对的权力导致绝对的腐败。undefined 的无能恰恰在于祂无所不能,语冰有幸百度了一本书叫《选择的悖论》,这大约也是 undefined 的悖论。


代码是写给人看的,代码的信息越具体明确越好,偏偏 undefined 既模糊又抽象。你知道的,我们接触的大多数资料会告诉我们 undefined 的意义是“未定义/无值”。


虽然但是,准确而无用的观念,终究还是无用的。undefined 的正确打开方式就是无为,使用 undefined 的最佳方式是不使用祂。




免责声明



本文示例代码默认均为 ESM(ECMAScript Module)筑基测评,因为现代化前端开发相对推荐集成 ESM,其他开发环境下的示例会额外注释说明,edge cases 的解释权归大家所有。



今天的《ES6 混合理论》就讲到这里啦,我们将在本合集中深度学习若干奇奇怪怪的前端面试题/冷知识,感兴趣的前端爱好者可以关注订阅,也欢迎大家自由言论和留言许愿,共享 BUG,共同内卷。


吾乃前端的虔信徒,传播 BUG 的福音。


我是大家的林语冰,我们一期一会,不散不见,掰掰~


作者:大家的林语冰
来源:juejin.cn/post/7240483867123220540
收起阅读 »

Vue3 除了keep-alive,还有哪些页面缓存的实现方案

web
引言 有这么一个需求:列表页进入详情页后,切换回列表页,需要对列表页进行缓存,如果从首页进入列表页,就要重新加载列表页。 对于这个需求,我的第一个想法就是使用keep-alive来缓存列表页,列表和详情页切换时,列表页会被缓存;从首页进入列表页时,就重置列表页...
继续阅读 »

引言


有这么一个需求:列表页进入详情页后,切换回列表页,需要对列表页进行缓存,如果从首页进入列表页,就要重新加载列表页。


对于这个需求,我的第一个想法就是使用keep-alive来缓存列表页,列表和详情页切换时,列表页会被缓存;从首页进入列表页时,就重置列表页数据并重新获取新数据来达到列表页重新加载的效果。


但是,这个方案有个很不好的地方就是:如果列表页足够复杂,有下拉刷新、下拉加载、有弹窗、有轮播等,在清除缓存时,就需要重置很多数据和状态,而且还可能要手动去销毁和重新加载某些组件,这样做既增加了复杂度,也容易出bug。


接下来说说我的想到的新实现方案(代码基于Vue3)。


省流


demo: xiaocheng555.github.io/page-cache/…


代码: github.com/xiaocheng55…


keep-alive 缓存和清除



keep-alive 缓存原理:进入页面时,页面组件渲染完成,keep-alive 会缓存页面组件的实例;离开页面后,组件实例由于已经缓存就不会进行销毁;当再次进入页面时,就会将缓存的组件实例拿出来渲染,因为组件实例保存着原来页面的数据和Dom的状态,那么直接渲染组件实例就能得到原来的页面。



keep-alive 最大的难题就是缓存的清理,如果能有简单的缓存清理方法,那么keep-alive 组件用起来就很爽。


但是,keep-alive 组件没有提供清除缓存的API,那有没有其他清除缓存的办法呢?答案是有的。我们先看看 keep-alive 组件的props:


include - string | RegExp | Array。只有名称匹配的组件会被缓存。
exclude - string | RegExp | Array。任何名称匹配的组件都不会被缓存。
max - number | string。最多可以缓存多少组件实例。

从include描述来看,我发现include是可以用来清除缓存,做法是:将组件名称添加到include里,组件会被缓存;移除组件名称,组件缓存会被清除。根据这个原理,用hook简单封装一下代码:


import { ref, nextTick } from 'vue'

const caches = ref<string[]>([])

export default function useRouteCache () {
// 添加缓存的路由组件
function addCache (componentName: string | string []) {
if (Array.isArray(componentName)) {
componentName.forEach(addCache)
return
}

if (!componentName || caches.value.includes(componentName)) return

caches.value.push(componentName)
}

// 移除缓存的路由组件
function removeCache (componentName: string) {
const index = caches.value.indexOf(componentName)
if (index > -1) {
return caches.value.splice(index, 1)
}
}

// 移除缓存的路由组件的实例
async function removeCacheEntry (componentName: string) {
if (removeCache(componentName)) {
await nextTick()
addCache(componentName)
}
}

return {
caches,
addCache,
removeCache,
removeCacheEntry
}
}

hook的用法如下:


<router-view v-slot="{ Component }">
<keep-alive :include="caches">
<component :is="Component" />
</keep-alive>
</router-view>

<script setup lang="ts">
import useRouteCache from './hooks/useRouteCache'
const { caches, addCache } = useRouteCache()

<!-- 将列表页组件名称添加到需要缓存名单中 -->
addCache(['List'])
</script>

清除列表页缓存如下:


import useRouteCache from '@/hooks/useRouteCache'

const { removeCacheEntry } = useRouteCache()
removeCacheEntry('List')


此处removeCacheEntry方法清除的是列表组件的实例,'List' 值仍然在 组件的include里,下次重新进入列表页会重新加载列表组件,并且之后会继续列表组件进行缓存。



列表页清除缓存的时机


进入列表页后清除缓存


在列表页路由组件的beforeRouteEnter勾子中判断是否是从其他页面(Home)进入的,是则清除缓存,不是则使用缓存。


defineOptions({
name: 'List1',
beforeRouteEnter (to: RouteRecordNormalized, from: RouteRecordNormalized) {
if (from.name === 'Home') {
const { removeCacheEntry } = useRouteCache()
removeCacheEntry('List1')
}
}
})

这种缓存方式有个不太友好的地方:当从首页进入列表页,列表页和详情页来回切换,列表页是缓存的;但是在首页和列表页间用浏览器的前进后退来切换时,我们更多的是希望列表页能保留缓存,就像在多页面中浏览器前进后退会缓存原页面一样的效果。但实际上,列表页重新刷新了,这就需要使用另一种解决办法,点击链接时清除缓存清除缓存


点击链接跳转前清除缓存


在首页点击跳转列表页前,在点击事件的时候去清除列表页缓存,这样的话在首页和列表页用浏览器的前进后退来回切换,列表页都是缓存状态,只要当重新点击跳转链接的时候,才重新加载列表页,满足预期。


// 首页 Home.vue

<li>
<router-link to="/list" @click="removeCacheBeforeEnter">列表页</router-link>
</li>


<script setup lang="ts">
import useRouteCache from '@/hooks/useRouteCache'

defineOptions({
name: 'Home'
})

const { removeCacheEntry } = useRouteCache()

// 进入页面前,先清除缓存实例
function removeCacheBeforeEnter () {
removeCacheEntry('List')
}
</script>

状态管理实现缓存


通过状态管理库存储页面的状态和数据也能实现页面缓存。此处状态管理使用的是pinia。


首先使用pinia创建列表页store:


import { defineStore } from 'pinia'

interface Item {
id?: number,
content?: string
}

const useListStore = defineStore('list', {
// 推荐使用 完整类型推断的箭头函数
state: () => {
return {
isRefresh: true,
pageSize: 30,
currentPage: 1,
list: [] as Item[],
curRow: null as Item | null
}
},
actions: {
setList (data: Item []) {
this.list = data
},
setCurRow (data: Item) {
this.curRow = data
},
setIsRefresh (data: boolean) {
this.isRefresh = data
}
}
})

export default useListStore

然后在列表页中使用store:


<div>
<el-page-header @back="goBack">
<template #content>状态管理实现列表页缓存</template>
</el-page-header>
<el-table v-loading="loading" :data="tableData" border style="width: 100%; margin-top: 30px;">
<el-table-column prop="id" label="id" />
<el-table-column prop="content" label="内容"/>
<el-table-column label="操作">
<template v-slot="{ row }">
<el-link type="primary" @click="gotoDetail(row)">进入详情</el-link>
<el-tag type="success" v-if="row.id === listStore.curRow?.id">刚点击</el-tag>
</template>
</el-table-column>
</el-table>
<el-pagination
v-model:currentPage="listStore.currentPage"
:page-size="listStore.pageSize"
layout="total, prev, pager, next"
:total="listStore.list.length"
/>
</div>

<script setup lang="ts">
import useListStore from '@/store/listStore'
const listStore = useListStore()

...
</script>

通过beforeRouteEnter钩子判断是否从首页进来,是则通过 listStore.$reset() 来重置数据,否则使用缓存的数据状态;之后根据 listStore.isRefresh 标示判断是否重新获取列表数据。


defineOptions({
beforeRouteEnter (to: RouteLocationNormalized, from: RouteLocationNormalized) {
if (from.name === 'Home') {
const listStore = useListStore()
listStore.$reset()
}
}
})

onBeforeMount(() => {
if (!listStore.useCache) {
loading.value = true
setTimeout(() => {
listStore.setList(getData())
loading.value = false
}, 1000)
listStore.useCache = true
}
})

缺点


通过状态管理去做缓存的话,需要将状态数据都存在stroe里,状态多起来的话,会有点繁琐,而且状态写在store里肯定没有写在列表组件里来的直观;状态管理由于只做列表页数据的缓存,对于一些非受控组件来说,组件内部状态改变是缓存不了的,这就导致页面渲染后跟原来有差别,需要额外代码操作。


页面弹窗实现缓存


将详情页做成全屏弹窗,那么从列表页进入详情页,就只是简单地打开详情页弹窗,将列表页覆盖,从而达到列表页 “缓存”的效果,而非真正的缓存。


这里还有一个问题,打开详情页之后,如果点后退,会返回到首页,实际上我们希望是返回列表页,这就需要给详情弹窗加个历史记录,如列表页地址为 '/list',打开详情页变为 '/list?id=1'。


弹窗组件实现:


// PopupPage.vue

<template>
<div class="popup-page" :class="[!dialogVisible && 'hidden']">
<slot v-if="dialogVisible"></slot>
</div>
</template>

<script setup lang="ts">
import { useLockscreen } from 'element-plus'
import { computed, defineProps, defineEmits } from 'vue'
import useHistoryPopup from './useHistoryPopup'

const props = defineProps({
modelValue: {
type: Boolean,
default: false
},
// 路由记录
history: {
type: Object
},
// 配置了history后,初次渲染时,如果有url上有history参数,则自动打开弹窗
auto: {
type: Boolean,
default: true
},
size: {
type: String,
default: '50%'
},
full: {
type: Boolean,
default: false
}
})
const emit = defineEmits(
['update:modelValue', 'autoOpen', 'autoClose']
)

const dialogVisible = computed<boolean>({ // 控制弹窗显示
get () {
return props.modelValue
},
set (val) {
emit('update:modelValue', val)
}
})

useLockscreen(dialogVisible)

useHistoryPopup({
history: computed(() => props.history),
auto: props.auto,
dialogVisible: dialogVisible,
onAutoOpen: () => emit('autoOpen'),
onAutoClose: () => emit('autoClose')
})
</script>

<style lang='less'>
.popup-page {
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
z-index: 100;
overflow: auto;
padding: 10px;
background: #fff;

&.hidden {
display: none;
}
}
</style>

弹窗组件调用:


<popup-page 
v-model="visible"
full
:history="{ id: id }">
<Detail></Detail>
</popup-page>


hook:useHistoryPopup 参考文章:juejin.cn/post/713994…



缺点


弹窗实现页面缓存,局限比较大,只能在列表页和详情页中才有效,离开列表页之后,缓存就会失效,比较合适一些简单缓存的场景。


父子路由实现缓存


该方案原理其实就是页面弹窗,列表页为父路由,详情页为子路由,从列表页跳转到详情页时,显示详情页字路由,且详情页全屏显示,覆盖住列表页。


声明父子路由:


{
path: '/list',
name: 'list',
component: () => import('./views/List.vue'),
children: [
{
path: '/detail',
name: 'detail',
component: () => import('./views/Detail.vue'),
}
]
}

列表页代码:


// 列表页
<template>
<el-table v-loading="loading" :data="tableData" border style="width: 100%; margin-top: 30px;">
<el-table-column prop="id" label="id" />
<el-table-column prop="content" label="内容"/>
<el-table-column label="操作">
<template v-slot="{ row }">
<el-link type="primary" @click="gotoDetail(row)">进入详情</el-link>
<el-tag type="success" v-if="row.id === curRow?.id">刚点击</el-tag>
</template>
</el-table-column>
</el-table>
<el-pagination
v-model:currentPage="currentPage"
:page-size="pageSize"
layout="total, prev, pager, next"
:total="list.length"
/>


<!-- 详情页 -->
<router-view class="popyp-page"></router-view>
</template>

<style lang='less' scoped>
.popyp-page {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
z-index: 100;
background: #fff;
overflow: auto;
}
</style>

结尾


地址:


demo: xiaocheng555.github.io/page-cache/…


代码: github.com/xiaoch

eng55…

收起阅读 »

用proxy改造你的console

web
前言 在前端平常的开发中,最长使用的调试手段应该就是console大法。console很好用,但有时候打印变量多了,看起来就比较懵。 let name1 = 'kk'; name2 = 'kkk'; name3 = 'kk1k'; name4= 'k1kk';...
继续阅读 »

前言


在前端平常的开发中,最长使用的调试手段应该就是console大法。console很好用,但有时候打印变量多了,看起来就比较懵。


let name1 = 'kk'; name2 = 'kkk'; name3 = 'kk1k'; name4= 'k1kk';
console.log(name1)
console.log(name2)
console.log(name3)
console.log(name4)

打印如下
image.png


那个变量 对应那个 就比较难分辨。我又不想在写代码来分辨(懒😀),那个打印对应的变量是多少。


解决方案


方案一,通过ast解析console 将变量名放在console后面,奈何esbuild不支持ast操作(不是我不会 哈哈哈哈), 故放弃。


方案二,既然vue能代理对象,那么console是不是也能被代理。


实践


第一步代理console,将原始的console,用全局变量originConsole保存起来,以便后续使用
withLogging 函数拦截console.log 重写log参数


const originConsole = window.console; 
var console = new Proxy(window.console, {
get(target, property) {
if(property === 'log') {
return withLogging(target[property])
}
return target[property] },
})

遇到问题,js中 无法获取获取变量的名称的字符串。就是说无法打印变量名。


解决方案,通过vite中的钩子函数transform,将console.log(name.x) 转化成 console.log(name.x, ['isPlugin', 'name.x'])


      transform(src, id) {
if(id.includes('src')) { // 只解析src 下的console
const matchs = src.matchAll(/console.log\((.*)\);?/g);
[...matchs].forEach((item) => {
const [matchStr, args] = item;
let replaceMatch = ''
const haveSemicolon = matchStr.endsWith(";");
const sliceIndex = haveSemicolon ? -2 : -1;
const temp = matchStr.slice(0,sliceIndex);
const tempArgs = args.split(",").map(item => {
if(item.endsWith('"')) {
return item
}
return `"${item}"`
}).join(",")
replaceMatch = `${temp},['isPlugin',${tempArgs}]);`
src = src.replace(matchStr, replaceMatch)
});
}
return {
code: src,
id,
}
},

这样最终就实现了类型于这样的输出代码


  originConsole.log('name.x=', name.x)

这样也就最终实现了通过变量输出变量名跟变量值的一一对应


最后


我将其写成了一个vite插件,vite-plugin-consoles 感兴趣的可以试试,有bug记得跟我说(●'◡'●)


源码地址:
github.com/ALiangTech/…


作者:平平无奇的阿良
来源:juejin.cn/post/7238508573667344441
收起阅读 »

前端小白的几个坏习惯

web
最近在教授前端小白学员编写一些简单的网页。在这个过程中发现了小白们比较容易遇到的一些问题或者坏习惯,在这里对它们进行一一解释。 文件名命名 有些学员的文件命名是这样的: 除了网页的内容外,所有的东西都应该用英文,而不是拼音。 原因有如下几点: 编程不是一个...
继续阅读 »

最近在教授前端小白学员编写一些简单的网页。在这个过程中发现了小白们比较容易遇到的一些问题或者坏习惯,在这里对它们进行一一解释。


文件名命名


有些学员的文件命名是这样的:



除了网页的内容外,所有的东西都应该用英文,而不是拼音。


原因有如下几点:



  1. 编程不是一个人的活动,是群体活动。我们使用的编程语言、框架和库,几乎全部都是英文。使用中文,你的协作者会难以理解你的代码。而且中英混搭会让代码阅读困难。

  2. 使用拼音和使用汉字基本上没有什么区别,甚至还不如汉字直观。

  3. 拼音很难加音标,而且即使能加音标,也很难表达真正的意思,因为同音词太多,它存在多义性,比如 heshui,你不知道它到底是在表达喝水还是河水。

  4. 使用拼音会让你显得非常不专业。

  5. 坚持使用英文编程,有利于提高英语水平。


如果英语不好,刚开始可能会难以忍受,但是一旦熬过去开始这段时间,坚持下来,将会是长期的回报。


如果你英语实在是非常差劲,可以借助一些翻译软件。比如世界上最好的翻译网站:translate.google.com/,虽然是 Google 的域名,但是大陆并没有墙。


不止是文件名,变量、函数等事物都应该使用英文命名。


使用英语,越早越好。


文件类型命名


有些同学的文件命名是这样的:



文件命名的问题上面已经解释了,这里主要来看文件后缀名的问题。


应该使用小写 .htm/.html 结尾。


原因有如下几点:



  1. 不同的操作系统处理大小写是不一样的。Windows/Mac 系统大小写不敏感,Linux 系统大小写敏感。统一命名方式会具有更好的移植性。


比如我们有如下目录结构:


├── cat.html
├── dog.html

下面的代码在 Mac/Windows 系统上正常。


<a href="./Dog.html">跳转到狗的页面</a>

但是在 Linux 系统上会出现 404。


我们开发时通常是在 Mac/Windows 系统,这时问题很难暴露,但是部署时通常是在 Linux 系统,就容易导致开发时正常,部署时异常的不一致性。



  1. 易读性,事实证明小写的单词更易于阅读。

  2. 便捷性,文件名和后缀名都保持小写,不需要额外按下 Shift 键了。

  3. htm 和 html 的区别是,在老的 DOS 系统上,文件后缀名最多只支持 3 位。所以很多语言都会把文件后缀名限制成 3 位以内。现在的操作系统已经没有这个问题了,所以 htm 和 html 的作用是完全一致的。如果你追求简洁一点,那么使用 htm 时完全没问题的。


代码格式化


有些同学的代码是这样的:



VSCode 提供了 prettier 插件,我们可以使用它对代码格式化。


代码格式化有以下优点:



  1. 代码格式化后更易于阅读和修改。比如它会自动帮你添加空格、对齐、换行等。

  2. 不需要去刻意学习代码样式了,代码格式化工具会帮你做好,并且在这个过程中你会潜移默化的学会怎么样调整代码样式。

  3. 使用统一的代码格式化,可以帮助大家在协作时保持一致,不会有比必要的争议。

  4. 新人加入项目时也可以更容易地融入到项目,不会看到风格迥异的代码。

  5. 在代码合并的时候也可以减少冲突的发生。


建议一定要开启代码格式化。


补充说明


这部分和文章内容无关,是针对评论区进行补充。


掘金没有评论置顶功能,我没办法逐一回复评论区。所以只能在文末进行统一解释。


很多人在评论区说本文水,或者在拿拼音的事情抬杠。本来我不想解释,但是负面评论的人确实不少。


我说两点。


第一,文章标题开头四字明确表明目标群体是前端小白,小白是什么概念能明白吗?一定是「xxx源码解读」才是干货硬货?


第二,关于中文好还是英文好,我不想继续争论。我从业多年,看过无数项目源码,从后端 Java JDBC、Spring、JVM、Go 到前端 React、Redux、Webpack、Babel,无一例外全是英文。或者你随便找个初具规模的互联网中大厂或者外企的程序员,看看他们公司是不是有不让用拼音和汉字的规范。


程序员群体普遍的毛病就是固执己见。永远只是站在自己的视角去观察世界,看到的永远都是自己想看到的。然后去贸然指责,这样真的会显得自己很没有修养,而且很无知。


哪怕做不到感同身受,也应该给予应有的尊重,哪怕难以理解,也不要随意贬低。这是做人的基本修养。


掘金是技术分享平台,不是贴吧/知乎。我写文章的本心只是分享内容,没有收各位一分钱。


文章内容对你有价值的话,非常感谢你的点赞支持。


文章内容对你无用的话,退出去就好了。


言尽于此。


能看懂就看,再看不懂就直接屏蔽我吧,谢谢配合。


最后希望掘金能推出文章评论置顶功能,或者文章禁止评论功能。这对创作者来说绝对是刚需。


作者:代码与野兽
来源:juejin.cn/post/7142368724144619556
收起阅读 »

卸下if-else 侠的皮衣!

web
🤭当我是if-else侠的时候 😶怕出错 给我一个功能,我总是要写很多if-else,虽然能跑,但是维护起来确实很难受,每次都要在一个方法里面增加逻辑,生怕搞错,要是涉及到支付功能,分分钟炸锅 😑难调试 我总是不知道之前写的逻辑在哪里,一个方法几百行逻辑,而且...
继续阅读 »

🤭当我是if-else侠的时候


😶怕出错


给我一个功能,我总是要写很多if-else,虽然能跑,但是维护起来确实很难受,每次都要在一个方法里面增加逻辑,生怕搞错,要是涉及到支付功能,分分钟炸锅


😑难调试


我总是不知道之前写的逻辑在哪里,一个方法几百行逻辑,而且是不同功能点冗余在一起!这可能让我牺牲大量时间在这查找调试中


🤨交接容易挨打


当你交接给新同事的时候,这个要做好新同事的白眼和嘲讽,这代码简直是坨翔!这代码简直是个易碎的玻璃,一碰就碎!这代码简直是个世界十大奇迹!


🤔脱下if-else侠的皮衣


先学习下开发的设计原则


单一职责原则(SRP)



就一个类而言,应该仅有一个引起他变化的原因



开放封闭原则(ASD)



类、模块、函数等等应该是可以扩展的,但是不可以修改的



里氏替换原则(LSP)



所有引用基类的地方必须透明地使用其子类的对象



依赖倒置原则(DIP)



高层模块不应该依赖底层模块



迪米特原则(LOD)



一个软件实体应当尽可能的少与其他实体发生相互作用



接口隔离原则(ISP)



一个类对另一个类的依赖应该建立在最小的接口上



在学习下设计模式


大致可以分三大类:创建型结构型行为型

创建型:工厂模式 ,单例模式,原型模式

结构型:装饰器模式,适配器模式,代理模式

行为型:策略模式,状态模式,观察者模式


为了尽快脱掉这个if-else的皮衣,我们就先学习一种比较容易接受的设计模型:策略模式


策略模式


举个例子



  • 当购物类型为“苹果”时,满 100 - 20,不满 100 打 9 折

  • 当购物类型为“香蕉”时,满 100 - 30,不满 100 打 8 折

  • 当购物类型为“葡萄”时,满 200 - 50,不叠加

  • 当购物类型为“梨”时,直接打 5 折

    然后你根据传入的类型和金额,写一个通用逻辑出来,
    当我是if-else侠的时候,我估计会这样写:


funcion getPrice(type,money)
//处理苹果
if(type == 'apple'){
if(money >= 100){
return money - 20
}
return money * 0.9
}
//处理香蕉
if(type == 'banana'){
if(money >= 100){
return money - 30
}
return money * 0.8
}
//处理葡萄
if(type == 'grape'){
if(money >= 200){
return money - 50
}
return money
}
//处理梨
if(type == 'pear'){
return money * 0.5
}
}

然后我们开始来分析问题:\



  1. 违反了单一职责原则(SRP)

    一个方法里面处理了四个逻辑,要是里面的哪个代码块出事了,调试起来也麻烦

  2. 违反了开放封闭原则(ASD)

    假如我们要增加多一种水果的逻辑,就又要在这个方法中修改,然后你修改完这个方法,就跟测试说,我在这个方法增加了多一个种水果,可能要重新回归这个方法,那你看测试增加了多少工作量



改造考虑:消灭if-else, 支持扩展但是不影响原本功能!



const fruitsPrice = {
apple(money){
if(money >= 100){
return money - 20
}
return money * 0.9
},
banana(money){
if(money >= 100){
return money - 30
}
return money * 0.8
},
grape(money){
if(money >= 200){
return money - 50
}
return money
},
pear(money){
return money * 0.5
}
}

首先定义一个fruitPrice对象,里面都是各种水果价格的映射关系

然后我们调用的时候可以这样


function getPrice(type,money){
return fruitsPrice[type](money)
}

当我们要扩展新水果的时候


fruitsPrice.orange = function(money){
return money*0.4
}

综上所述:
用策略模式重构这个原本的逻辑,方便扩展,调试,清晰简明,当然这只是一个模式重构的情况,可能还有更优的情况,靠大家摸索


结尾


遵守设计规则,脱掉if-else的皮衣,善用设计模式,加油,骚年们!

作者:向乾看
来源:juejin.cn/post/7239267216805871671
给我点点赞,关注下!

收起阅读 »

前端路由访问权限控制方案

web
本篇所讲的路由控制方案是由前端实现的,根据具体的业务做的设计,可能不具备一般性,仅供参考! 项目背景及路由初步设计 目前在做的一个项目,目标是为了解决互联网行业里面关于资金清分业务的一些痛点。虽然目前只成功对接并上线了一个第三方企业,随着产品功能的不断完善,...
继续阅读 »

本篇所讲的路由控制方案是由前端实现的,根据具体的业务做的设计,可能不具备一般性,仅供参考!



项目背景及路由初步设计


目前在做的一个项目,目标是为了解决互联网行业里面关于资金清分业务的一些痛点。虽然目前只成功对接并上线了一个第三方企业,随着产品功能的不断完善,相信后续还会有更多的第三方企业对接,以及更多的业务场景。


这就需要提前对系统的菜单权限进行规划,由于后期开发的不确定性以及人力资源有限,路由权限控制没有采用跟后端强耦合的方式实现,由前端自行配置并处理。


一开始由于意向企业业务上的强相关性,并没有规划太多的模块(主业务全都写在了src/views/main目录下),也没有对用户行为进行规划分类,路由控制方面也是根据平台标识手动配置的路由表


const xxx = [
'/main/nopage',
'/main/checkFace',
// ...
]

缺点


最近又做了一个B端的项目,发现上面的实现方法并不是很好,原因有以下几点:




  1. 对接平台多的话,就会出现一堆路由配置,不优雅、不美观




  2. 业务上的不一致性带来扩展的不灵活




  3. 暂时没有想起来 : )




经过一番考量,我决定这样做(其实也是常见的方法)


改进方案


首先业务实现上需要我新建一个文件目录(src/views/xxx,不能再往main目录下放了),在里面写路由组件。


期间我想过以对接的平台标识创建路由组件目录,进行业务上的隔离,没有做出实际尝试就被我舍弃了,原因是:以平台标识作为业务的根目录,跟原先的做法本质上是一致的,只是改进,相当于是补丁,而我要做的是寻找另一种解决办法。


根据Linux系统一切皆文件的思想,类似的,我还是采用了老套的办法,给每一个路由菜单赋予一个访问权限的配置。


这样做,后面维护起来也简单(有了平台标识和用户行为的划分)


{
path: "/test",
name: "Test",
meta: {
belong: ["xxx", "xxx"] // 所属平台信息,操作行为信息...
},
component: () => import("@/views/test")
},

后端同事配合规划用户平台和行为,在用户访问的时候,后端返回用户信息的同时,返回平台标识和行为标识。


同样的,在全局路由钩子里验证访问权限。



router.beforeEach((to, from, next) => {
try {
const { belong = [] } = to.meta
const authInfo = ["platform", "action"]
if (accessTokenStr) {
// 已登录, 做点什么
// belong <--> authInfo
// arrayInclude(belong, authInfo)
} else {
next()
}
} catch (err) {
console.log(err);
}
})

/**
* 判断数组元素的包含关系,不要求顺序一致
* 数组中不存在重复元素
* 用于验证路由权限
* arrA包含arrB,返回true, 否则返回false
*/

export const arrayInclude = (arrA, arrB) => {
let flag = true
for (let i = 0; i < arrB.length; i++) {
if (!arrA.includes(arrB[i])) {
flag = false
break
}
}
return flag
}


👉👉以上方案写于2021-11-06





维护总结


2023-04-03


近期业务扩展,发现上面的菜单权限控制有点不合理


这种配置不直观,有点混乱!!!


还是采用json的方式分配路由, 比如:


const menus = {
[platformId]: [
"/a"
"/b"
],
[platformId]: [
"/a"
"/b"
],
}

这样可以更加直观的显示出来某个业务包含哪些菜单,而不是像之前那样把菜单的权限配置在路由上!


总结: 路由设计要中电考虑可读性、易维护性




我是 甜点cc,个人网站(国外站点): blog.i-xiao.space/


公众号

作者:甜点cc
来源:juejin.cn/post/7239173692228255802
:【看见另一种可能】

收起阅读 »

一文搞清楚Node.js的本质

web
学习Node.js已有很长的时间了,但一直学的懵懵懂懂,不得要领,现决定跟网上的大佬从头开始理一下其中的底层逻辑,为早日成为全栈工程师打下基础。 Node.js 是什么? Node.js 是一个基于 V8 引擎 的 JS 运行时,它由 Ryan Dahl 在 ...
继续阅读 »

学习Node.js已有很长的时间了,但一直学的懵懵懂懂,不得要领,现决定跟网上的大佬从头开始理一下其中的底层逻辑,为早日成为全栈工程师打下基础。


Node.js 是什么?


Node.js 是一个基于 V8 引擎JS 运行时,它由 Ryan Dahl 在 2009 年创建。


这里有两个关键词,一是 JS 引擎,二是 JS 的运行时


那什么叫 JS 引擎呢?


image.png


JS 引擎就是把一些 JS 的代码进行解析执行,最后得到一个结果。


比如,上图的左边是用 JS 的语法定义一个变量 a 等于 1,b 等于 1,然后把 a 加 b 的值赋值给新的变量 c,接着把这个字符串传入 JS 引擎里,JS 引擎就会进行解析执行,执行完之后就可以得到对应的结果。


那么 JS 运行时又是什么呢?它和 JS 本身有什么区别 ?


要搞清楚上面的问题,可以看下面这张图:


image.png


从下往上看:




  1. 最下面一层是脚本语言规范ECMAScript,也就是常说的ES5、ES6语法。




  2. 往上一层就是对于该规范的实现了,如JavaScriptJScript等都属于对 ECMAScript语言规范的实现。




  3. 再往上一层就是执行引擎JavaScript 常见的引擎有 V8QuickJS等,用来解释执行代码。




  4. 最上面就是运行时环境了,比如基于 V8 封装的运行时环境有 ChromiumNode.jsDeno 等等。




可以看到,JavaScript 在第二层,Node.js 则在第四层,两个根本不是一个东西。


所以,Node.js 并不是语言,而是一个 JavaScript 运行时环境,它的语言是 JavaScript。这就跟 PHP、Python、Ruby 这类不一样,它们既代表语言,也可代表执行它们的运行时环境(或解释器)。


JS 作为一门语言,有独立的语法规范,提供一些内置对象和 API(如数组、对象、函数等)。但和其他语言(C、C++等)不一样的是,JS 不提供网络、文件、进程等功能,这些额外的功能是由运行时环境实现和提供的,比如浏览器或 Node.js


所以,JS 运行时可以理解为 JS 本身 + 一些拓展的能力所组成的一个运行环境,如下面这张图:


image.png


可以看到,这些运行时都不同程度地拓展了 JS 本身的功能。JS 运行时封装底层复杂的逻辑,对上层暴露 JS API,开发者只需要了解 JS 的语法,就可以使用这些 JS 运行时做很多 JS 本身无法做到的事情。


Node.js 的组成


搞清楚了什么是Node.js后,再来看看 Node.js 的组成。


Node.js 主要是由 V8Libuv 和一些第三方库组成的。


V8引擎


V8 是一个 JS 引擎,它不仅实现了 JS 解析和执行,还支持自定义拓展。


这有什么用处呢?


比如说,在下面这张图中我们直接使用了 A 函数,但 JS 本身并没有提供 A 这个函数。这个时候,我们给 V8 引擎提供的 API 里注入一个全局变量 A ,就可以直接在 JS 中使用这个 A 函数了。正是因为 V8 支持这个自定义的拓展,才有了 Node.js 等 JS 运行时


image.png


Libuv


Libuv 是一个跨平台的异步 IO 库,它主要是封装各个操作系统的一些 API,提供网络还有文件进程这些功能


我们知道在 JS 里面是没有网络文件这些功能的,前端是由浏览器提供,而 Node.js 里则是由 Libuv 提供


image.png




  1. 左侧部分是 JS 本身的功能,也就是 V8 实现的功能。




  2. 中间部分是一些C++ 胶水代码。




  3. 右侧部分是 Libuv 的代码。




V8Libuv 通过第二部分的胶水代码粘合在一起,最后就形成了整一个 Node.js


因此,在 Node.js 里面不仅可以使用 JS 本身给我们提供的一些变量,如数组、函数,还能使用 JS 本身没有提供的 TCP、文件操作和定时器功能


这些扩展出来的能力都是扩展到V8上,然后提供给开发者使用,不过,Node.js 并不是通过全局变量的方式实现扩展的,它是通过模块加载来实现的。


第三方库工具库


有了 V8 引擎和拓展 JS 能力的 Libuv,理论上就可以写一个 JS 运行时了,但是随着 JS 运行时功能的不断增加,Libuv 已经不能满足需求,比如实现加密解密、压缩解压缩。


这时候就需要使用一些经过业界验证的第三方库,比如异步 DNS 解析 c-ares 库、HTTP 解析器 llhttp、HTTP2 解析器 nghttp2、解压压缩库 zlib、加密解密库 openssl 等等。


Node.js 代码组成


了解了 Node.js 的核心组成后,再来简单看一下 Node.js 代码的组成。


image.png


Node.js 代码主要是分为三个部分,分别是 CC++JavaScript


JS


JS 代码就是我们平时使用的那些 JS 模块,像 http 和 fs 这些模块


Node.js 之所以流行,有很大一部分原因在于选择了 JS 语言。


Node.js 内核通过 CC++ 实现了核心的功能,然后通过 JS API 暴露给用户使用,这样用户只需要了解 JS 语法就可以进行开发。相比其他的语言,这个门槛降低了很多。


C++


C++代码主要分为三个部分:




  1. 第一部分主要是封装 Libuv 和第三方库的 C++ 代码,比如 netfs 这些模块都会对应一个 C++ 模块,它主要是对底层 Libuv 的一些封装。




  2. 第二部分是不依赖 Libuv 和第三方库的 C++ 代码,比方像 Buffer 模块的实现,主要依赖于 V8




  3. 第三部分 C++ 代码是 V8 本身的代码。




C++ 代码中最重要的是了解如何通过 V8 API 把 C、C++ 的能力暴露给 JS 层使用,正如前面讲到的通过拓展一个全局变量 A,然后就可以在 JS层使用 A。


C 语言层


C 语言代码主要是包括 Libuv 和第三方库的代码,它们大多数是纯 C 语言实现的代码。


Libuv 等库提供了一系列 C API,然后在 Node.jsC++ 层对其进行封装使用,最终暴露 JS APIJS 层使用。


总结


文章第一部分介绍了Node.js的本质,它实际上是一个JS运行时,提供了网络、文件、进程等功能,类似于浏览器,提供了JS的运行环境。


第二部分介绍了Node.js的组成,它由V8引擎、Libuv及第三方库构成,Node.js核心功能大都是Libuv提供的,它封装底层各个操作系统的一些 API,因此,Node.js是跨平台的。


第三部分从代码的角度描述了Node.js的组成,包括JavaScriptC++C三部分,中间的C++部分通过对C部分的封装提供给JavaScript部分使用。


作者:小p
来源:juejin.cn/post/7238814783598297144
收起阅读 »

如何使用localStorage判断设置值是否过期

web
简介:本文介绍了使用 localStorage 判断设置值是否过期的方法。通过设置过期时间,我们可以使用 setItemWithExpiration 函数将数据存储到 localStorage 中,并使用 getItemWithExpiration 函数获取数...
继续阅读 »

简介:本文介绍了使用 localStorage 判断设置值是否过期的方法。通过设置过期时间,我们可以使用 setItemWithExpiration 函数将数据存储到 localStorage 中,并使用 getItemWithExpiration 函数获取数据并检查是否过期。localStorage 提供简单的 API 方法来存储、检索和删除数据,并具有持久性和域隔离的特点。通过本文的方法,你可以方便地管理数据,并灵活设置过期时间,实现更多的数据存储和管理功能。



目标:在网站中实现定期弹窗功能,以提示用户。


选择实现方式:为了实现持久化存储和检测功能,我们选择使用localStorage作为首选方案。


解决方案:



  1. 存储设置值:使用localStorage将设置值存储在浏览器中,以便在用户访问网站时保持数据的持久性。

  2. 设置过期时间:根据需求,为设置值设定一个过期时间,表示多长时间后需要再次弹窗提示用户。

  3. 检测过期状态:每次用户访问网站时,检测存储的设置值是否已过期。若过期,则触发弹窗功能,提醒用户。

  4. 更新设置值:在弹窗提示后,可以根据用户的操作进行相应的更新操作,例如延长过期时间或更新设置内容。


优势:



  1. 持久化存储:使用localStorage可以将设置值保存在浏览器中,即使用户关闭了浏览器或重新启动设备,设置值仍然有效。

  2. 简单易用:localStorage提供了简单的API,方便存储和读取数据,实现起来相对简单。

  3. 跨浏览器支持:localStorage是HTML5的标准特性,几乎所有现代浏览器都支持它,因此具有良好的跨浏览器兼容性。


注意事项:



  1. 需要根据业务需求合理设置过期时间,避免频繁弹窗对用户体验造成困扰。

  2. 在使用localStorage时,要考虑浏览器的隐私设置,以确保能够正常存储和读取数据。


说一下locaStorage


localStorage 是 Web 浏览器提供的一种存储数据的机制,它允许在浏览器中存储和检索键值对数据。


以下是关于 localStorage 的一些重要特点和使用方法:




  1. 持久性:与会话存储(session storage)相比,localStorage 是一种持久性的存储机制。存储在 localStorage 中的数据在用户关闭浏览器后仍然保留,下次打开网页时仍然可用。




  2. 容量限制:每个域名下的 localStorage 存储空间通常为 5MB。这个限制是针对整个域名的,不是针对单个页面或单个 localStorage 对象的。




  3. 键值对数据存储:localStorage 使用键值对的方式存储数据。每个键和对应的值都是字符串类型。如果要存储其他数据类型(如对象或数组),需要进行序列化和反序列化操作。




  4. API 方法:


    属性方法
    localStorage.setItem(key, value)将键值对存储到 localStorage 中。如果键已存在,则更新对应的值。
    localStorage.getItem(key)根据键获取存储在 localStorage 中的值
    localStorage.removeItem(key)根据键从 localStorage 中删除对应的键值对
    localStorage.clear()清除所有存储在 localStorage 中的键值对。
    localStorage.key(index)根据索引获取对应位置的键名。



  5. 域限制:localStorage 存储的数据与特定的域名相关。不同的域名之间的 localStorage 是相互隔离的,一个网站无法访问另一个网站的 localStorage 数据。




  6. 安全性:localStorage 中的数据存储在用户的本地浏览器中,因此可以被用户修改或删除。敏感数据应该避免存储在 localStorage 中,并使用其他更安全的机制来处理。




以下是一个示例,展示如何使用 localStorage 存储和检索数据:


// 存储数据到 localStorage
localStorage.setItem('name', 'localStorage');
localStorage.setItem('size', '5mb');

// 从 localStorage 中获取数据
const name = localStorage.getItem('name');
const age = localStorage.getItem('size');

console.log(name); // 输出: localStorage
console.log(size); // 输出: 5mb

// 从 localStorage 中删除数据
localStorage.removeItem('size');

// 清除所有的 localStorage 数据
localStorage.clear();

总结来说,localStorage 是一种用于在 Web 浏览器中持久存储键值对数据的机制。它提供简单的 API 方法来存储、检索和删除数据,并具有一定的容量限制和域隔离。


判断本地存储时间的实现


存储时间与值


使用以下代码将值与过期时间存储到localStorage中:


const expiration = 24 * 60 * 60 * 1000 * 7; // 设置过期时间为七天
// const expiration = 2 * 60 * 1000; // 设置过期时间为2分钟
setItemWithExpiration("read_rule", true, expiration);

下面是相应的函数实现:


// 存储数据到LocalStorage,并设置过期时间(单位:毫秒)
function setItemWithExpiration(key, value, expiration) {
   const item = {
       value: value,
       expiration: Date.now() + expiration
  };
   localStorage.setItem(key, JSON.stringify(item));
}

获取值并判断是否过期


使用以下代码从localStorage中获取值,并判断其是否过期:


const retrievedData = getItemWithExpiration("read_rule");

下面是相应的函数实现:


// 从LocalStorage中获取数据,并检查是否过期
function getItemWithExpiration(key) {
   const item = JSON.parse(localStorage.getItem(key));
   if (item && Date.now() < item.expiration) {
       return item.value;
  }
   // 如果数据已过期或不存在,则返回 null 或其他默认值
   return null;
}

通过以上实现,可以方便地存储带有过期时间的值,并根据需要获取和判断其有效性。如果存储的数据已过期或不存在,函数getItemWithExpiration将返回null

作者:猫头_
来源:juejin.cn/post/7238794430966677564
其他您设定的默认值。

收起阅读 »

CSS样式穿透?你准备好了吗!

web
你是否遇到过这样的情况:想要修改子元素的样式却发现使用父元素选择器无法生效。这时,你就需要了解一下CSS样式穿透的概念。 简单介绍 一般来说,我们可以通过父级选择器来选中它下面的子元素。例如: .parent .child { color: red; } ...
继续阅读 »

cover.png


你是否遇到过这样的情况:想要修改子元素的样式却发现使用父元素选择器无法生效。这时,你就需要了解一下CSS样式穿透的概念。


简单介绍


一般来说,我们可以通过父级选择器来选中它下面的子元素。例如:


.parent .child {
color: red;
}

但是,有些时候我们需要给子元素中特定的元素修改样式,而不是所有的子元素都修改。这时,我们就需要了解CSS样式穿透这个概念。


CSS样式穿透


在CSS中,我们可以使用“/deep/”、“::v-deep”、“::shadow”等方式实现CSS样式的穿透。


使用/deep/


通过使用/deep/关键字,可以达到子组件穿透自身样式的目的。例如:


.parent /deep/ .child {
color: red;
}

这种方式相比于上述普通方法,能够选中更深层次的子元素(即使用多个空格连接的子元素)。但是,由于浏览器对“/deep/”选择器支持并不友好,因此尽量避免使用。


使用::v-deep


在Vue框架中,如果需要穿透组件样式,可以使用::v-deep或者>>>选择器。例如:


.parent ::v-deep .child {
color: red;
}

这种方式只对Vue组件可用,且与/deep/的作用类似。


使用::shadow


在Web Components规范中,定义了Shadow DOM的概念,它能够使得元素的样式隔离开来,不会影响到其它元素。如果我们需要在Shadow DOM中修改样式,可以使用::shadow伪类。


parent::shadow .child {
color: red;
}

这种方式相比较于上述两种方法,更加安全和规范,但需要先了解Shadow DOM的概念。


补充说明


尽管CSS样式穿透能够方便地修改子元素样式,但是在实际开发中还是应当尽可能地避免使用它们。


CSS一直致力于封装样式,降低代码耦合度,而使用CSS样式穿透会将样式的层级深度加深,增加样式的维护成本。


此外,在跨浏览器、跨框架的情况下,CSS样式穿透的表现都不尽相同,因此建议在项目中谨慎使用。


结语


CSS样式穿透虽然能够带来方便,却也需要谨慎使用,遵循代码封装的原则,保持样式的简洁、规范和易维护。


作者:𝑺𝒉𝒊𝒉𝑯𝒔𝒊𝒏𝒈
来源:juejin.cn/post/7238999952553771066
收起阅读 »

为什么面试官这么爱问性能优化?

web
笔者是一个六年前端,没有大厂经历,也没有什么出彩的项目,所以今年以来,前端现在这种行情下并没有收到多少面试,但是为数不多的面试中,百分之九十都问到了性能优化的问题,而且问题都出奇的一致: 平时的工作中你有做过什么性能优化? 对于这个问题其实我的内心os是(...
继续阅读 »

笔者是一个六年前端,没有大厂经历,也没有什么出彩的项目,所以今年以来,前端现在这种行情下并没有收到多少面试,但是为数不多的面试中,百分之九十都问到了性能优化的问题,而且问题都出奇的一致:



平时的工作中你有做过什么性能优化?



对于这个问题其实我的内心os是(各位轻喷~):



你们怎么都这么爱问性能优化的问题?我的简历中也没有写到这个啊。


你们的业务都这么复杂吗?怎么动不动就要性能优化?


你们的代码写的这么拉吗?不优化都不能使用吗?


性能优化是一个高级前端的必要技能吗?



首先客观现实是笔者平时工作中的业务并不复杂,需要性能优化的地方确实不多,一些存在性能瓶颈的大多是使用了其他团队开发的东西,比如播放直播视频的SDK、3D地图引擎等,也找过他们进行优化,但是没用,他们也优化不动。


所以每次被问到这个问题我就很尴尬,说工作中没有遇到过性能问题,估计面试官也不信,直接说没有做过性能优化,那又显得我这个六年经验的前端太水了,连这个都不做,所以每次我只能硬说。


没吃过猪肉,还没见过猪跑吗?其实性能优化的文章我也看过很多,各种名词我还是知道一点的,比如:



  • 性能问题排查:



1.数据埋点上报


2.使用控制台的NetWork、Performance等工具


3.webpack-bundle-analyzer插件分析打包产物




  • http相关:



1.gzip压缩


2.强缓存、协商缓存




  • 图片相关:



1.图片压缩


2.图片懒加载


3.雪碧图、使用字体图标、svg




  • webpack相关:



1.优化文件搜索


2.多进程打包


3.分包


4.代码压缩


5.使用CDN




  • 框架相关:



1.vue性能优化、react性能优化


2.异步组件


3.tree shaking


4.服务端渲染




  • 代码实现



1.按需加载,逻辑后移,优先保证首屏内容渲染


2.复杂计算使用web worker


3.接口缓存、计算结果缓存


4.预加载


5.骨架屏


6.虚拟滚动



等等。


但这些绝大部分我并没有实践过,所以我都说不出口,说我没有机会实践也行,说我没有好奇心不好学不爱思考不主动发现问题也行,总之结果就是没有经验。


所以通常我硬着头皮只能说出以下这些:


1.开发前会花点时间梳理业务,全局视角过一遍交互和视觉,思考组件划分,找出项目中相似的部分,提取为公共组件和通用逻辑。


2.代码开发中尽量保证写出的代码清晰、可维护,比如:清晰的目录和文件结构、添加必要的注释、提取公共函数公共组件、组件单向数据流、组件功能尽量单一等。


3.时刻关注可能会存在性能问题的部分,比如:



路由组件异步加载


动态加载一些初始不需要用到的资源


频繁切换的组件使用KeepAlive进行缓存


缓存复杂或常用的计算结果


对实时性不高的接口进行缓存


同一个接口多次请求时取消上一次没有完成的请求


页面中存在很多接口时进行优先级排序,优先请求页面重要信息的接口,并关注同一时刻请求的接口数量,如果过多进行分批请求


对于一些确实比较慢的接口使用loading或骨架屏


懒加载列表,懒加载图片,对移出可视区的图片和dom进行销毁


关注页面中使用到的图片大小,推动后端进行图片压缩


地图撒点时使用聚合减少地图引擎渲染压力


对于一些频繁的操作使用防抖或节流


使用三方库或组件库尽量采用按需加载,减少打包体积


组件卸载时取消事件的监听、取消组件中的定时器、销毁一些三方库的实例



我工作中的实践也就以上这些,其实就是写代码的基本要求,另外我觉得如果业务复杂,以上这些也并不能阻止性能问题的出现,更多的还是当出现了问题,去思考如何解决。


比如我开源的一个思维导图项目mind-map,当节点数量多了会非常卡,调试分析思考后发现原因是做法不合理,每次画布上有操作后都是清空画布上的所有元素,然后重新创建所有元素,数据驱动视图,原理非常简单,但是因为是通过svg实现,所以就是DOM节点,这玩意我们都知道,当节点数量非常多以后,删除节点和创建节点都是非常耗时的,所以数据驱动视图的框架,比如Vue会通过虚拟DOM的diff算法对比来找出最小的更新部分,但是我没有做。。。所以。。。那么我就自然的做了一些优化,比如:



思维导图场景,大部分情况下操作的其实就是其中一个或部分节点,所以不需要重新删除创建所有元素,那么就可以通过节点复用的方式来优化,将真实节点缓存起来,渲染时通过数据唯一的id来检查是否存在可复用节点,如果没有,那么代表是新增节点,那么创建新节点即可;如果有,那么就判断节点数据是否发生改变,没有改变直接复用,如果发生了改变那么判断是否可以进行更新,如果更新成本高那么直接重新创建;另外也需要和上一次的缓存进行对比,找出本次渲染不需要的节点进行删除;当然,为了避免缓存节点数量无限膨胀,也通过LRU缓存算法来管理


对于不影响其他节点的操作只更新被操作的节点


通过setTimeout异步渲染节点,留一些中间时间来响应页面其他操作


将触发渲染的任务放到队列中,在下一帧进行处理,合并掉一些中间状态


对于鼠标移动和滚动的场景,通过节流来优化


进行一些取舍,早期节点激活时可以修改节点的所有样式,导致激活操作需要重新计算节点大小,更新节点样式,在多选和全选操作下非常耗时,所以后期改为只允许修改不改变节点大小的样式属性


其他一些细节优化:对于数据没有改变的操作不触发赋值或函数调用,一些不起眼的操作可能也是需要耗费时间的;改变了不涉及节点大小的属性不触发节点大小重新计算等



经过以上这些修改后,性能确实有了很大程度的提升,不过有些项目可以通过不断的优化来提升性能,但是有些可能就是设计缺陷,比如我开源的另一个白板项目,更好的方式其实是重做它。


写到这里其实并没有解决本文标题提出的问题:



为什么面试官这么爱问性能优化?



因为我没有怎么做过面试官,甚至面试经验其实都不太多,写这篇文章目的主要有两个:


1.想听听有面试官经验的各位的想法或建议


2.想看看和我有类似情况的面试者面对这个问题,或者说类似的问题是如何回答的


最后再牢骚几句:



有时会感慨时间过的真快,一转眼,作为一个前端已经工作了六年,即将三十而立却立不起来,这么多年的工作,更多的只是收获了六年的经历,但是并没有六年的能力,回过头看,当初的有些选择确实是错误的,也许这就是人生把。


作为一个普通的前端,在如今的行情下面试确实很艰难,尤其是我这种不擅长面试的人,不过话说回来,改变哪有不痛苦的,除了面对也没有其他办法。



作者:街角小林
来源:juejin.cn/post/7239267216805838903
收起阅读 »

项目很大,得忍一下

web
背景 常和我们的客户端厮混,也经常陪他们发版,每次发版编译打包都可以在那边玩一局游戏了。一边幸灾乐祸,一边庆幸h5编译还好挺快的,直到我们的项目也发展成了*山,巨石项目。由于线上要给用户查看历史的推广活动,所以很多老的业务项目都还是留在项目中,导致我们的rou...
继续阅读 »

背景


常和我们的客户端厮混,也经常陪他们发版,每次发版编译打包都可以在那边玩一局游戏了。一边幸灾乐祸,一边庆幸h5编译还好挺快的,直到我们的项目也发展成了*山,巨石项目。由于线上要给用户查看历史的推广活动,所以很多老的业务项目都还是留在项目中,导致我们的router层爆炸,打包速度直线下降,开发过程中,开了hmr稍微有点改动也要等个几秒钟,恨不得立刻重启一个新项目。但是现实告诉你,忍住,别吐,后面还有更多的业务活动加进来。那么怎么解决这个问题呢,这个时候mp的思路是个不错的选择。


关键点


打包慢,本质原因是依赖庞大,组件过多。开发过程中,我们开新的业务组件时,往往和其他业务组件是隔离的,那么我们打包的时候是不是可以把那些不相干的业务组件隔离出去,当然可以。打包工具,从入口开始进行扫描,单页面的模块引入基本都是借助router,所以,关键的是如果我们能够控制router的数量,其实就能够控制编译和打包规模了。


问题


router在vue项目中我们常用的是全家桶的库vue-router,vue-router最多提供了懒加载,动态引入功能并不支持。有小伙伴说router的引入路径可不可以动态传入,我只能说小伙子你很机智,但是vue-router并不支持动态的引入路径。因此我们换个思路,就是在入口的位置控制router的规模,通过不同规模的router实例来实现router的动态引入。当然这需要我们对router库进行一定改造,使其变的灵活易用


一般的router


通常的router如下:



// router.js

/*global require*/

const Vue = require('vue')

const Router = require('vue-router')

Vue.use(Router)

const routes = [

{

path: '/routermap',

component: (resolve) => require(['../containers/map.vue'], resolve),

name: 'routermap',

desc: '路由列表'

},

{

path: '/',

component: (resolve) => require(['../containers/index.vue'], resolve)

},

{

path: '*',

component: (resolve) => require(['../containers/nofound.vue'], resolve),

name: 'defaultPage',

desc: '默认页'

}

]

const router = new Router({

mode: 'history',

routes

})

router.afterEach((to, from) => {

///

})

export default router

// 引入 entry.js

import router from './router.js'

router.beforeEach((to, from, next) => {

///

next()

})

router.afterEach(function(to, from) {

///

})

new Vue({

el: '#app',

template: '<App/>',

router,

})


我们可以不断的往routes数组中添加新的router item来添加新的业务组件,这也是我们的项目不断变大的根本,这样既不好维护,也会导致后面的编译效率


易于维护和管理的router


其实好的管理和维护本质就是分门别类,把类似功能的放在一起,而不是一锅粥都放在一起,这样基本就能解决追踪维护的功能,对应router管理其实也不是很复杂,多建几个文件夹就行如下:


router.png


对应routes/index.js代码如下:



import testRouter from './test.js'

const routes = [

{

path: '/map',

component: (resolve) => require(['../containers/map.vue'], resolve),

name: 'map',

desc: '路由列表'

},

{

path: '/',

component: (resolve) => require(['../containers/index.vue'], resolve)

},

...testRouter,

// 可以扩展其他router

{

path: '*',

component: (resolve) => require(['../containers/nofound.vue'], resolve),

name: 'defaultPage',

desc: '默认页'

}

]

// test.js

/**

* 测试相关页面路由映射

*/


/*global require*/

export default [

{

path: '/test/tools',

name: 'testTools',

component: resolve => require(['@test/tools/index.vue'], resolve),

desc: '测试工具'

}

]


我们通过把router分为几个类别的js,然后在通过router item的数组展开合并,就做到了分门别类,虽然看似简单,但是可以把管理和维护效果提升几个数量级。


支持mp的router


虽然上面支持了易于管理和维护,但是实际上我们如果只是单一入口的话,导出的还是一个巨大的router。那么如何支持多入口呢,其实也不用想的过于复杂,我们让类似test.js的router文件既支持router item的数组导出,也支持类似routes/index.js一样的router实例导出即可。所谓既能分也能合才是最灵活的,这里我们可以利用工厂模式做一个factory.js,如下:



/**

* app 内的页面路由映射

*/


/*global require*/

const Vue = require('vue')

const Router = require('vue-router')

Vue.use(Router)

const RouterFactory = (routes) => {

return new Router({

mode: 'history',

routes: [

{

path: '/map',

component: (resolve) => require(['../containers/map.vue'], resolve),

name: 'map',

desc: '路由列表'

},

{

path: '/',

component: (resolve) => require(['../containers/index.vue'], resolve)

},

...routes,

{

path: '*',

component: (resolve) => require(['../containers/nofound.vue'], resolve),

name: 'defaultPage',

desc: '默认页'

}

]

})

}

export default RouterFactory


这个factory.js产出的router实例和routes/index.js一模一样所以我们只需组装一下test.js即可,如下:



/*global require*/

import RouterFactory from './factory'

export const testRouter = [

{

path: '/test/tools',

name: 'testTools',

component: resolve => require(['@test/tools/index.vue'], resolve),

desc: '测试工具'

}

]

export default RouterFactory(developRouter)

// routes/index.js的引入变化一下即可

import testRouter from './test.js'

// 修改为=》

import { testRouter } from './test.js'


那么我们的入口该如何修改呢?也很简单:



// testEntry.js

import router from './routes/test.js'

router.beforeEach((to, from, next) => {

///

next()

})

router.afterEach(function(to, from) {

///

})

new Vue({

el: '#app',

template: '<App/>',

router,

})


我们建立了一个新的入口文件 testEntry.js 这个入口只引入了test相关的模块组件


如何灵活的和编译命令做配合呢


根据上面,我们进行mp改造的基础已经做好,关于如何多入口编译webpack或者其他打包里面都是基础知识,这里就不多赘述。这里主要聊一下如何灵活的配合命令做编译和部署。


既然router我们都可以分为不同的文件,编译文件我们同样可以拆分为不同的文件,这也使得我们的命令可以灵活多变,这里我们以webpack做为示例:


config.png


config1.png


config2.png


config3.png


根据上图示例 我们的webpack的配置文件仅仅改动了entry,我们稍微改造一下build.js,使其能够接受不同的编译命令:



// build.js

let page = 'all'

if (process.argv[2]) {

page = process.argv[2]

}

let configMap = {

'all': require('./webpack.prod.conf'),

'app': require('./webpack.app.conf')

}

let webpackConfig = configMap[page]

// dev-server.js

let page = 'all'

if (process.argv[2]) {

page = process.argv[2]

}

let configMap = {

'all': require('./webpack.dev.conf'),

'app': require('./webpack.app.dev.conf')

}

let webpackConfig = configMap[page]


对应的脚本配置:



// package.json

"scripts": {

"dev": "node build/dev-server.js",

"build": "node build/build.js",

"build:app": "node build/build.js app",

"dev:app": "node build/dev-server.js app"

},


以上app对应test。最后,我们只需要在命令行执行相应命令,即可实现我们可控的router规模的开发,基本随便来新的需求,咱都可以夜夜做新郎,怎么搞都是飞速。当然部署的话我们也可以单独执行一部分页面的部署命令到单独的域名,换个思路也可以作为一种预发测试的部署方法。



#
整体项目的开发编译

npm run dev

#
单独的app,也即test项目的开发编译

npm run dev:app

#
整体项目的部署

npm run build

#
单独的app,也即test项目的部署

npm run build:app


结语


以上,即如何利用mp思路,提高我们的编译开发效率。时常有人会在提高网页性能的时候说到mp,但mp本质上并不能提高页面的性能,比如白屏优化。而路由中使用懒加载其实才是提高部分网页性能的出力者,关于白屏优化,本篇文章不作展开讨论。


作者:CodePlayer
来源:juejin.cn/post/7218866717739696183
收起阅读 »

你还在凭感觉来优化性能?

web
前言 大家好,我是 simple ,我的理想是利用科技手段来解决生活中遇到的各种问题。 众所周知,感觉是最不靠谱的东西,这完全取决于主观意识。我们做性能优化一定要取决于一些指标,而Performance API向我们提供了访问和测量浏览器性能相关信息的方式。通...
继续阅读 »

前言


大家好,我是 simple ,我的理想是利用科技手段来解决生活中遇到的各种问题


众所周知,感觉是最不靠谱的东西,这完全取决于主观意识。我们做性能优化一定要取决于一些指标,而Performance API向我们提供了访问和测量浏览器性能相关信息的方式。通过它,我们可以获取有关页面加载时间、资源加载性能、用户交互延迟等详细信息,用于性能分析和优化,而不是完全靠自己的感官意识,感觉更快了或者更慢了。


指标收集


1. Navigation Timing API - 页面加载时间


// 获取页面加载时间相关的性能指标
const navigationTiming = performance.timing;
console.log('页面开始加载时间: ', navigationTiming.navigationStart);
console.log('DOMContentLoaded 事件发生时间: ', navigationTiming.domContentLoadedEventEnd);
console.log('页面加载完成时间: ', navigationTiming.loadEventEnd);
console.log('页面从加载到结束所需时间',navigationTiming.loadEventEnd - navigationTiming.navigationStart)

2. Resource Timing API - 资源加载性能


// 获取资源加载性能数据
const resources = performance.getEntriesByType('resource');
resources.forEach(resource => {
console.log('资源 URL: ', resource.name);
console.log('资源开始加载时间: ', resource.startTime);
console.log('资源加载结束时间: ', resource.responseEnd);
console.log('资源加载持续时间: ', resource.duration);
});

3. User Timing API - 自定义时间点


// 标记自定义时间点
performance.mark('startOperation');
// 执行需要测量的操作

for(let i = 0;i < 10000;i++) {}

performance.mark('endOperation');
// 测量时间差
performance.measure('operationDuration', 'startOperation', 'endOperation');
const measurement = performance.getEntriesByName('operationDuration')[0];
console.log('操作执行时间: ', measurement.duration);
// 和console.time,console.timeEnd比较相似

4. Long Tasks API - 长任务性能


// 获取长任务性能数据
const longTasks = performance.getEntriesByType('longtask');
longTasks.forEach(task => {
console.log('任务开始时间: ', task.startTime);
console.log('任务持续时间: ', task.duration);
});

5. Navigation Observer API - 导航事件监测


// 创建 PerformanceObserver 对象并监听导航事件
const observer = new PerformanceObserver(list => {
const entries = list.getEntries();
entries.forEach(entry => {
console.log('导航类型: ', entry.type);
// navigate 表示页面的初始导航,即浏览器打开新的网页或重新加载当前网页。
// reload 表示页面的重新加载,即浏览器刷新当前网页。
// back_forward 表示通过浏览器的前进或后退按钮导航到页面。
console.log('导航开始时间: ', entry.startTime);
console.log('导航持续时间: ', entry.duration);
});
});
// 监听 navigation 类型的事件
observer.observe({ type: 'navigation', buffered: true });

6. LCP的采集


LCP(Largest Contentful Paint)表示最大内容绘制,指的是页面上最大的可见内容元素(例如图片、视频等)绘制完成的时间点。LCP反映了用户感知到的页面加载速度,因为它代表了用户最关注的内容何时变得可见。LCP 应在页面首次开始加载后的2.5 秒内发生。


new PerformanceObserver((entryList) => {
  for (const entry of entryList.getEntries()) {
    console.log('Largest Contentful Paint:', entry.startTime);
  }
}).observe({type'largest-contentful-paint'bufferedtrue});


浏览器会多次报告 LCP ,而真正的 LCP 是用户交互前最近一次报告的 LCP。



7. FID的收集


FID(First Input Delay)表示首次输入延迟,衡量了用户首次与页面进行交互(例如点击按钮、链接等)时,响应所需的时间。较低的FID值表示页面对用户输入更敏感,用户可以更快地与页面进行交互,页面的 FID 应为100 毫秒或更短。


new PerformanceObserver(function(list, obs) {  
  const firstInput = list.getEntries()[0];
  const firstInputDelay = firstInput.processingStart - firstInput.startTime;
  const firstInputDuration = firstInput.duration;
  console.log('First Input Delay', firstInputDuration);
  obs.disconnect();
}).observe({type'first-input'bufferedtrue});

8. CLS的收集


CLS(Cumulative Layout Shift)表示累积布局偏移,衡量了页面在加载过程中发生的意外布局变化程度。当页面上的元素在加载过程中发生位置偏移,导致用户正在交互时意外点击链接或按钮,就会产生布局偏移。页面的 CLS 应保持在 0.1  或更少,这里的0.1表示10%。请注意,CLS 的计算可能涉及复杂的算法和权重计算,下列代码示例仅演示了基本的计算过程。


const observer = new PerformanceObserver((entryList) => {
const entries = entryList.getEntries();
let clsScore = 0;
entries.forEach(entry => {
// 计算每个布局变化的分数
clsScore += entry.value;
});
console.log('CLS 值: ', clsScore);
});

// 监听 Layout Shift 类型的条目
observer.observe({ type: 'layout-shift', buffered: true });

小结



  • 测量页面加载时间:性能 API 允许我们测量和分析网页加载所需的时间。通过使用性能计时指标,如 navigationStart、domContentLoadedEventEnd 和 loadEventEnd,我们可以准确测量页面加载过程中各个阶段的持续时间。

  • 分析资源加载性能:利用性能 API,我们可以检查网页上正在加载的各个资源(如图像、脚本、样式表)的性能。这包括跟踪资源加载时间、大小和状态码,有助于识别影响整体性能的瓶颈或问题。

  • 监测用户交互延迟:性能 API 使我们能够测量用户交互和浏览器响应之间的延迟。通过跟踪类似于 firstInputDelay(FID)和 firstInputTime 的指标,我们可以评估网页对用户操作(如点击或触摸)的响应速度,并确定改进的方向。

  • 基准测试和比较分析:性能 API 允许我们对网页的不同版本或不同网页的性能进行基准测试和比较分析。通过收集性能数据和指标,我们可以评估代码更改、优化或第三方资源对页面性能的影响,并做出明智的决策。

  • 性能优化和报告:利用性能 API 获得的洞察力,我们可以确定性能瓶颈和改进的方向。然后,可以使用这些信息来实施优化,例如减小文件大小、降低服务器响应时间、优化缓存策略和提高渲染
    作者:simple_lau
    来源:juejin.cn/post/7238779568478552122
    效率。

收起阅读 »

能把队友气死的8种屎山代码(React版)

web
前几天在前端技术群里聊起Code Review的事,大伙儿似乎都憋了一肚子气: 我觉得这份难言之隐应该要让更多人看到,就跟Henry约了个稿: 于是Henry赶在周末,一边带娃,一边给我抹眼泪整理(脱敏)出了这篇小小的屎山合集,供大家品鉴。 以下是正文。...
继续阅读 »

前几天在前端技术群里聊起Code Review的事,大伙儿似乎都憋了一肚子气:


图片


图片


我觉得这份难言之隐应该要让更多人看到,就跟Henry约了个稿:


图片


于是Henry赶在周末,一边带娃,一边给我抹眼泪整理(脱敏)出了这篇小小的屎山合集,供大家品鉴。


以下是正文。


(文字大部分是Henry所写,沐洒进行了一些精简和调整)




1. 直接操作DOM


const a = document.querySelector('.a');

const scrollListener = throttle(() => {
  const currentY = window.scrollY;

  if (currentY > 100) {
    a.classList.add('show');
  } else {
    a.classList.remove('show');
  }
}, 300);

window.addEventListener('scroll', scrollListener);
return () => {
  window.removeEventListener('scroll', scrollListener);
};

上面的代码在监听scroll方法的回调函数中,直接上手修改DOM的类名。众所周知,React属于响应式编程,大部份情况都不需要直接操作DOM,具体原因参考官方文档(react.dev/learn/manip…


优化方法也很简单,充分发挥响应式编程的优点,用变量代替即可:


const [refreshStatus, setRefreshStatus] = useState('');

const scrollListener = throttle(() => {
  if (tab.getBoundingClientRect().top < topH) {
    setRefreshStatus('show');
  } else {
    setRefreshStatus('');
  }
}, 300);

return <div className={['page_refresh', refreshStatus].join(' ')}/>;

2. useEffect不指定依赖


依赖参数缺失。


useEffect(() => {
    console.log('no deps=====')
    // code...
});

这样的话,每次页面有重渲染,该useEffect都会执行,带来严重的性能问题。例如我们项目中,这个useEffect内部执行的是第一点中的内容,即每次都会绑定一个scroll事件的回调,而且页面中有实时轮询接口每隔5s刷新一次列表,用户在该页面稍加停留,就会有卡顿问题出现。解决方案很简单,根据useEffect的回调函数内容可知,如果需要在第一次渲染之后挂载一个scroll的回调函数,那么就给useEffect第二个参数传入空数组即可,参考官方文档(react.dev/reference/r…


useEffect(() => {
    // code...
}, []);

3. 硬编码


硬编码,即一些数据信息或配置信息直接写死在逻辑代码中,例如


图片


这两行代码本意是从url上拿到指定的参数的值,如果没有,会用一个固定的配置做兜底。


乍一看代码逻辑很清晰,但再想深一层,兜底值具体的含义是什么?为什么要用这两个值来兜底?写这行代码的同学可能很快可以解答,但是一段时间之后,写代码的人和提需求的人都找不到了呢?


这个示例代码还比较简单,拿对应的值去后台可以找到对应的含义,如果是写死的是枚举值,而且还没有类型定义,那代码就很难维护了。


图片


解决此类问题,要么将这些内容配置化,即写到一个config文件中,使用清晰的语义化命名变量;要么,至少在硬编码的地方写上注释,交代清楚这里需要硬编码的前因后果。


4. 放任文件长度,只着眼于当下的需求


很多同学做需求、写代码都比较少从全局考虑,只关注到当前需求如何完成。从“战术”上来说没有问题,快速完成产品的需求、快速迭代产品也是大家希望看到的。


可一旦只关注“战术实现”而忽略“战略设计”,除非做的产品是月抛型的,否则一定会遇到旧逻辑难以修改的情况。


如果再加上一个文件被多达10余人修改过的情况,那么每改一行代码都会是一场灾难,例如最近接手的一个页面:


图片


单文件高达1600多行!哪怕去除300多行的注释,和300多行的模板,剩下的逻辑代码也有1000行左右,这种代码可读性就极其糟糕,必须进行拆分。


而很常见的是,由于每一任经手人都疏于考虑全局,导致大量代码毫无模块化可言,甚至出现多个useEffect的依赖是完全相同的:


图片


这里明显还有另一个问题:滥用hooks。


从行号可以看出来确实是相同的依赖写了多个useEffect,很明显是多个同学各写各的的需求引入的这些hooks。

这代码跑肯定是能跑的,但是很可能会出现多个hooks中修改同一个变量,导致其他地方在使用的时候需要搞一些很tricky的操作来修Bug。


5.变量无初始值


在typescript的加持下,对变量的类型定义可以说是日益严格了。可是在一些变量的类型定义比较复杂的情况下,可能一个变量的字段很多、层级很复杂,此时有些同学就可能想偷个懒了,例如:


const [variable, setVariable] = useState();

// some code...
const queryData = function({
    // some logic
    setVariable({ showtrue });
};

useEffect(() => {
    queryData();
}, []);

return variable.show ?  : null;

这里的问题很明显,如果queryData耗时比较长,在第一次渲染的时候,最后一行的variable.show就会报错了,因为variable的初始值是undefined。所以声明变量时,一定要根据变量的类型设置好有效默认值。


6. 三元选择符嵌套使用


网上很多人会推荐说用三元选择符代替简单的if-else,但几乎没有见过有人提到嵌套使用三元选择符的事情,如果看到如下代码,不知道各位读者会作何感想?


{condition1 === 1
    ? "数据加载中"
    : condition2
    ? "没有更多了"
    : condition3
    ? "当前没有可用房间"
    : "数据加载中"}

真的很难理解,明明只是一个简单的提示语句的判断,却需要拿出分析性能的精力去理解,多少有点得不偿失了。


这还只是一种比较简单的三元选择符的嵌套,因为当各个条件分支都为true时,就直接返回了,没有做更多的判断,如果再多做一层,都会直接把人的cpu的干爆炸了。 


替代方案: 



  1. 直接用if-else,可读性更高,以后如果要加逻辑也很方便。

  2. Early Return,也叫卫语句,这种写法能有效简化逻辑,增加可读性。


if (condition1 === 1return "数据加载中";
if (condition2) return "没有更多了";
if (condition3) return "当前没有可用房间";
return "数据加载中";

虽然不嵌套的三元选择符很简单,但是在例如jsx的模版中,仍然不建议大量使用三元选择符,因为可能会出现如下代码:


return (
    condition1 ? (
        <div className={condition2 ? cls1 : cls2}>
            {condition3 ? "111" : "222"}
            {condition4 ? (
                a : b} />
            ) : null
        

    ) : (
        
            {condition6 ? children1 : children2}
        

    )
)

类似的代码在我们的项目中频繁出现,模版中大量的三元选择符导致文件内容拉得很长,很容易看着看着就不记得自己在哪个逻辑分支上了。


像这种简单的三元选择符,做成一个简单的memo变量,哪怕是在组件内直接写变量定义(例如:const clsName = condition2 ? cls1 : cls2),最终到模板的可读性也会比上述代码高。


7. 逻辑不拆分


React hooks可以很方便地帮助开发者聚合逻辑抽离成自定义hooks,千万不要把一个页面所有的useState、useEffect等全都放在一个文件中:


图片


其实从功能上可以对页面进行拆分,拆分之后这些变量的定义也就可以拆出去了。其中有一个很简单的原则就是,如果一个逻辑同时涉及到了useState和useEffect,那么就可以一并抽离出去成为一个自定义hooks。例如接口请求大家一般都是直接在业务逻辑中做:


const Comp = () => {
    const [data, setData] = useState({});
    const [loading, setLoading] = useState(false);
    
    useEffect(() => {
        setLoading(true);
        queryData()
            .then((response) => {
                setData(response);
            })
            .catch((error) => {
                console.error(error);
            })
            .finally(() => {
                setLoading(false);
            });
    });
    
    if (loading) return "loading...";
    
    return <div>{data.text}div>;
}

根据上面的原则,和数据拉取相关的内容涉及到了useState和useEffect,这整块逻辑就可以拆出去,那么最终就只剩下:


const Comp = () => {
    const { data, loading } = useQueryData();
    
    if (loading) return "loading...";
    
    return 
{data.text}
;
};

这样下来,Comp组件就变得身份清爽了。大家可以参考阿里的ahooks库,里面收集了很多前端常用的hooks,可以极大提升开发效率和减少重复代码。


8. 随意读取window对象的值


作为大型项目,很容易需要依赖别的模板挂载到window对象的内容,读取的时候需要考虑到是否有可能拿不到window对象上的内容,从而导致js报错?例如:


window.tmeXXX.a.func();

如果这个tmeXXX所在的js加载失败了,或者是某个版本中没有a这个属性或者func这个函数,那么页面就会白屏。


好啦,最近CR常出现的8种屎山代码都讲完了,你写过哪几种?你们团队的代码中又有哪些让你一口老血喷出来的不良代码呢?欢迎评论区告诉我。


作者:沐洒
来源:juejin.cn/post/7235663093748138021
收起阅读 »

关于“凌晨服务器告警!我被动把性能优化了2000%”这件事~

web
前言 大家好,我是林三心,用最通俗易懂的话讲最难的知识点是我的座右铭,基础是进阶的前提是我的初心。 大早上被叫醒! 前几天周末,大早上的时候,太阳才刚出来,我突然被老大电话叫醒了,并通知我速速进入飞书会议,说是服务器发生了警报,出现了严重事故。 进到会议才...
继续阅读 »

前言


大家好,我是林三心,用最通俗易懂的话讲最难的知识点是我的座右铭,基础是进阶的前提是我的初心。



大早上被叫醒!


前几天周末,大早上的时候,太阳才刚出来,我突然被老大电话叫醒了,并通知我速速进入飞书会议,说是服务器发生了警报,出现了严重事故。



进到会议才发现是我们的后端有一个接口,让监控直接红色了。由于这一块代码我比较熟,所以老大让我紧急定位并处理一下这个严重的问题~


定位问题


所以本全干工程师就开始了后端接口的问题定位~



初步定位


首先说说这个接口的背景,这个接口是提供给用户用作导入用的,用户在做导入的时候,有可能导入的数据超级大,所以会不会是因为导入的数据量太大了,所以导致此接口直接崩掉呢?


但是老大查了日志后跟我说,这个用户在以前也导入过这么大的数据量,但是那时候都没啥问题的啊~


所以我觉得这应该不是用户行为造成的,而是什么新功能造成的这个BUG~于是我看了涉及到此接口的最近的提交,发现确实是有一个新功能在最近上线了,是涉及到 json-schema 的


简单认识 json-schema


什么是 json-schema 呢?给大家举个小例子,让大家简单认识一下吧,比如下方的三个属性



  • name

  • age

  • cars


他们都有对应的数据类型,他们需要通过 json-schema 来寻找到自己对应的类型


// jsonschema
const schema = {
name: {
$ref: 1
},
age: {
$ref: 2
},
cars: {
$ref: 3
}
}

// jsonschema合集
const schemaDefination = {
1: {
type: 'string'
},
2: {
type: 'number'
},
3: {
$ref: 4
},
4: {
type: 'array'
}
}

// 得出结果
const result = build(schema, schemaDefination)
console.log(result)
// {
// name: 'string',
// age: 'number',
// cars: 'array'
// }

继续定位问题


回到这个 BUG 上,我继续定位。其实刚刚上面的例子是很简单的例子,但是其实在这个功能里,数据量和负责度远远没这么简单,我们刚刚看到的 schemaDefination 其实就是所有 jsonschema 的引用的结合


// 实际上可能有几百个
const schema1 = {
$ref: 1
}
const schema2 = {
$ref: 2
}
const schema3 = {
$ref: 3
}

const schemaDefination = gen(schema1, schema2, schema3)
console.log(schemaDefination)
// 实际上可能有几百个
// {
// 1: {
// type: 'string'
// },
// 2: {
// type: 'number'
// },
// 3: {
// type: 'array'
// }
// }

也就是一开始会先根据所有 schema 去生成一个引用的集合 schemaDefination,而这个集合可能有几百个,数据量挺大


最终定位


然后到最终的时候 schema 结合 schemaDefination 去生成结果,我感觉就是在这一步导致了 BUG


// 得出结果
// 可能要 build 几百次
const result1 = build(schema1, schemaDefination)
const result2 = build(schema2, schemaDefination)
const result3 = build(schema3, schemaDefination)

为什么我觉得是这一步出问题呢?我们刚刚说了 schemaDefination 是所有 schema 的引用集合,数据量很大,你每次 build 的时候 schema 传的是一个 schema,但是你 schemaDefination 传的是集合!!!


正常来说应该是传 schema 时只需要传对应的 schemaDefination 即可,比如


// 合理的
const result1 = build({
$ref: 1
}, {
1: {
type: 'string'
}
})

// 不合理的
const result1 = build({
$ref: 1
}, {
1: {
type: 'string'
},
2: {
type: 'number'
},
3: {
type: 'array'
}
})

而我们现在就是处于不合理的情况,于是我特地看了 build 这个函数的内部实现,发现有 对象序列化处理 的代码,想一下下面的模拟代码


const obj = { 几百个数据 }
while(i < 300) {
JSON.stringfy(obj)
i++
}

这样的代码会给服务器造成非常大的压力,甚至把接口给搞崩!!!


解决问题,性能提升几百倍!


上面其实我已经分析出问题所在了:传 schema 的时候不要传整个 Defination集合!,所以我们只需要传入所需的 defination, 那么性能是不是可以优化几百倍!!!


解决手段


所以我们只需要写一个函数,过滤出所需要的 defination 即可,例如


// 找出所有被 ref 的数据模型
const filter = (
schema,
schemaDefinitions,
) => {
// 进行过滤操作
}

// jsonschema
const schema = {
name: {
$ref: 1
}
}

// jsonschema合集
const schemaDefination = {
1: {
type: 'string'
},
2: {
type: 'number'
},
3: {
$ref: 4
},
4: {
type: 'array'
}
}

// 过滤
const defination = filter(schema, schemaDefination)
console.log(defination)
//{
// 1: {
// type: 'string'
// },
//}

所以只需要在 build 的时候传入过滤后的 defination 即可!


const result1 = build(schema1, filter(schema1, schemaDefination))
const result2 = build(schema2, filter(schema2, schemaDefination))
const result3 = build(schema3, filter(schema3, schemaDefination))

测试无误,继续睡觉!


然后拿到一份用户的数据,在测试环境测了一下,没有发生之前那个 BUG 了!合并代码!打包上线!继续睡觉!



结语 & 加学习群 & 摸鱼群


我是林三心



  • 一个待过小型toG型外包公司、大型外包公司、小公司、潜力型创业公司、大公司的作死型前端选手;

  • 一个偏前端的全干工程师;

  • 一个不正经的掘金作者;

  • 一个逗比的B站up主;

  • 一个不帅的小红书博主;

  • 一个喜欢打铁的篮球菜鸟;

  • 一个喜欢历史的乏味少年;

  • 一个喜欢rap的五音不全弱鸡


如果你想一起学习前端,一起摸鱼,一起研究简历优化,一起研究面试进步,一起交流历史音乐篮球rap,可以来俺的摸鱼学习群哈哈,点这个,有7000多名前端小伙伴在等着一起学习哦 --> 摸鱼沸点


image.png


作者:Sunshine_Lin
来源:juejin.cn/post/7238973821801594935
收起阅读 »

让整个网站界面无滚动条

web
界面无滚动条 滚动条的优化也有很多种,比如随便再网上搜索美化浏览器滚动条样式,就会出现些用css去美化滚动条的方案。 那种更好呢? 没有更好只有更合适 像默认的滚动条的话,他能让你方便摁着往下滑动(他比较宽)特别省劲,不用担心美化过后变细摁不到问题。 美化...
继续阅读 »

界面无滚动条


滚动条的优化也有很多种,比如随便再网上搜索美化浏览器滚动条样式,就会出现些用css去美化滚动条的方案。


那种更好呢?


没有更好只有更合适



  • 像默认的滚动条的话,他能让你方便摁着往下滑动(他比较宽)特别省劲,不用担心美化过后变细摁不到问题。

  • 美化后的滚动条样式啊更贴合网站主题,让用户体验更好。

  • 无滚动条(鼠标放上去后出现)这种更适合像一个页面好多个块,每个块的话还很多内容(都有滚动条)。如果像这种都默认都出现滚动条的话,也太不美观了。



那咱们就从无滚动条展开说说!!!



无滚动条设计



比如像element ui组件内像input的自定义模块数据过多的时候出现的下拉框内的滚动条,如下图:



element-ui里面它其实是有内部组件el-scrollbar在的。那么它是怎么实现无滚动条呢?


如下图咱们先把:hover勾选上让滚动条一直处于显示得状态。然后咱们再分析他的实现。


当我把样式稍微修改下,咱们再观察下图:


01.jpg


这么看是不是就很明白了 他其实用margin值把整个容器扩大了点然后溢出隐藏,其实滚动条还在就是给界面上看不到了而已。


然后它自己用dom画了个滚动条,如下图:


02.jpg



经过上面分析,咱们已经很清楚得了解到一个无滚动条是从那个方面实现得了。



  1. 使用margin值把滚动条给溢出隐藏掉。

  2. 使用div自己画了一个滚动条。方便咱们隐藏、显示、更改样式等。



无滚动条实现


那咱们再从细节上拆分下具体实现要考虑那些点:



  1. 需要计算滚动条得宽度用来margin扩大得距离(每个界面上得滚动条得宽度是不一样得)。

  2. 需要计算画的div滚动条得高度(这个内容多少会影响滚动条的高度)。

  3. 需要根据滚动条去transform: translateY(19.3916%);移动咱们自己画的div滚动条的。

  4. 需要根据摁着画的div滚动条,去实际更改需要滚动的高度。

  5. 需要点击滚动轴的柱子支持跳到目标的滚动位置;


一 计算界面原本滚动条的宽度



计算下界面上原本滚动条的宽度如下:



let scrollBarWidth;

export default function() {
if (scrollBarWidth !== undefined) return scrollBarWidth;

const outer = document.createElement('div');
outer.className = 'el-scrollbar__wrap';
outer.style.visibility = 'hidden';
outer.style.width = '100px';
outer.style.position = 'absolute';
outer.style.top = '-9999px';
document.body.appendChild(outer);

const widthNoScroll = outer.offsetWidth;
outer.style.overflow = 'scroll';

const inner = document.createElement('div');
inner.style.width = '100%';
outer.appendChild(inner);

const widthWithScroll = inner.offsetWidth;
outer.parentNode.removeChild(outer);
scrollBarWidth = widthNoScroll - widthWithScroll;

return scrollBarWidth;
};


先创建了一个div, 设置成scroll, 然后再在里面嵌套一个没有滚动条的div设置宽度100%, 获取到两者的offsetWidth, 相减获取到scrollBarWidth赋值给scrollBarWidth 是惰性加载的优化,只需要计算一次就可以了。 具体展现如下图:



03.jpg


二 计算画的滚动条的高度height



计算下画的div滚动条的高度height。是用当前容器的内部高度 / 容器整个滚动条的高度 * 100计算出百分比;



比如:


const warp = this.$refs.wrap; // 或者使用documnet获取容器
const heightPercentage = (wrap.clientHeight * 100 / wrap.scrollHeight); // height
const widthPercentage = (wrap.clientWidth * 100 / wrap.scrollWidth); // width


解析: 如当前容器高30px,内容撑起来总共高100px,那么滚动条的高度就是当前容器的30%;



三 计算滚动条需要移动的值



计算画的div需要滚动条的高度moveY是, 获取当前容器滚动的scrollTop / 当前容器内部高度 * 100



算法一:



解析 使用transform: translateY(0%);是移动的是自己本身的百分比那么(容器滚动的scrollTop / 当前容器内部高度 * 100)算法如下:



const warp = this.$refs.wrap; // 或者使用documnet获取容器
this.moveY = ((wrap.scrollTop * 100) / wrap.clientHeight);
this.moveX = ((wrap.scrollLeft * 100) / wrap.clientWidth);

算法二:



解析:使用定位top值,这个比较好理解滚动条的滚动 / 容器的滚动总高度 * 100得到百分比,如下:



const warp = this.$refs.wrap; // 或者使用documnet获取容器
this.moveY = ((wrap.scrollTop * 100) / wrap.scrollHeight);
this.moveX = ((wrap.scrollLeft * 100) / wrap.scrollWidth);


把计算出来的moveYmoveX的方法 绑定给scroll 滚动事件就可以了。



四 摁着画的div滚动条 经行拖动



滚动条都是支持拖着上下左右移动的,那咱们也要支持下:




  • 获取当前滚动条的高度或者宽度可以使用getBoundingClientRect()如下图:

  • 获取拖着移动的距离 就是再鼠标摁下先计一个当前的x1、y1监听movex2、y2相减就是拖动的距离了。

  • 获取到拖动的距离后转成transform || top值。


一个简单的拖动组件如下:


<template>
<div
ref="draggableRef"
class="draggable"
:style="style"
>
<slot />
</div>
</template>

<script>
export default {
name: 'DraggableComponent',

props: {
initialValue: {
type: Object,
required: false,
default: () => ({ x: 0, y: 0 }),
},
},

data() {
return {
currentValue: { x: 0, y: 0 },
isDragging: false,
startX: 0,
startY: 0,
diffX: 0,
diffY: 0,
};
},

computed: {
style() {
return `left: ${this.currentValue.x + this.diffX}px; top: ${this.currentValue.y + this.diffY}px`;
},
},

watch: {
initialValue: {
handler(val) {
this.currentValue = val;
},
immediate: true,
},
},

mounted() {
this.$nextTick(() => {
const { draggableRef } = this.$refs;
if (draggableRef) {
draggableRef.addEventListener('mousedown', this.startDrag);
document.addEventListener('mousemove', this.moveDrag);
document.addEventListener('mouseup', this.endDrag);
}
});
},

beforeDestroy() {
const { draggableRef } = this.$refs;
draggableRef.removeEventListener('mousedown', this.startDrag);
document.removeEventListener('mousemove', this.moveDrag);
document.removeEventListener('mouseup', this.endDrag);
},

methods: {
startDrag({ clientX: x1, clientY: y1 }) {
this.isDragging = true;
document.onselectstart = () => false;
this.startX = x1;
this.startY = y1;
},

moveDrag({ clientX: x2, clientY: y2 }) {
if (this.isDragging) {
this.diffX = x2 - this.startX;
this.diffY = y2 - this.startY;
}
},

endDrag() {
this.isDragging = false;
document.onselectstart = () => true;
this.currentValue.x += this.diffX;
this.currentValue.y += this.diffY;
this.diffX = 0;
this.diffY = 0;
},
},
};
</script>

<style>
.draggable {
position: fixed;
z-index: 9;
}
</style>


咱们需要获取到画的滚动条的高度,然后根据拖动的距离算出来transform: translateY(0%);或者top值;

如上面拖动组件 拖动部分代码就不在重复了 咱们直接用diffX、diffY、lastX、lastY来用了。



  • diffX、diffY 是拖动差的值

  • lastX、lastY 是上一次也就是未拖动前的值translateY || top



算法一(transform)


const thumb = document.querySelector('el-scrollbar__thumb'); // element ui  el-scrollbar 的滚动条
const { height: thumbHeight } = thumb?.getBoundingClientRect() || {};


const diffY = 10;
const lastY = '300'; // transform: translateY(300%);`
const moveY = (diffY / thumbHeight) + lastY;

算法二(top)


const thumb = document.querySelector('el-scrollbar__thumb'); // element ui  el-scrollbar 的滚动条
const { height: thumbHeight } = thumb?.getBoundingClientRect() || {};


const diffY = 10;
const lastY = 30; // top: 30%`
const moveY = (diffY / wrap.scrollWidth * 100) + lastY;

五 点击滚动轴使滚动条跳转到该位置



  • getBoundingClientRect 的 top 是获取到距离浏览器顶部的距离。
    写一个点击事件如下


function clickTrackHandler(event) {
const wrap = this.$refs.wrap;
// 1. 减去clientX 正好能获取到需要滚动到的位置
const offset = Math.abs(e.target.getBoundingClientRect().top - e.clientX);

// 2. 利用offset 的到画的滚动条的实际位置 两种算法transform || top
const thumb = document.querySelector('el-scrollbar__thumb'); // element ui el-scrollbar 的滚动条
const { height: thumbHeight } = thumb?.getBoundingClientRect() || {};

const translateY = offset / height * 100; // transform
const top = offset / wrap.scrollHeight * 100; // top

// 3、计算实际需要滚动的高度 使界面滚动到该位置。两种算法transform(scrollTop2) || top(scrollTop1)
const scrollTop1 = top * wrap.scrollHeight; // top
const scrollTop2 = translateY * wrap.clientHeight; // transform
}

总结



针对无滚动条如果是vue使用的话,再了解具体实现后可以直接用elementel-scrollbar组件就好,如果在其他框架中, 结合上面的逻辑也会很快封装一个组件。



作者:三原
来源:juejin.cn/post/7227033124856135738
收起阅读 »

跟我一起探索 HTTP-HTTP缓存

web
概览 HTTP 缓存会存储与请求关联的响应,并将存储的响应复用于后续请求。 可复用性有几个优点。首先,由于不需要将请求传递到源服务器,因此客户端和缓存越近,响应速度就越快。最典型的例子是浏览器本身为浏览器请求存储缓存。 此外,当响应可复用时,源服务器不需要处理...
继续阅读 »

概览


HTTP 缓存会存储与请求关联的响应,并将存储的响应复用于后续请求。


可复用性有几个优点。首先,由于不需要将请求传递到源服务器,因此客户端和缓存越近,响应速度就越快。最典型的例子是浏览器本身为浏览器请求存储缓存。


此外,当响应可复用时,源服务器不需要处理请求——因为它不需要解析和路由请求、根据 cookie 恢复会话、查询数据库以获取结果或渲染模板引擎。这减少了服务器上的负载。


缓存的正确操作对系统的稳定运行至关重要。


不同种类的缓存


HTTP Caching 标准中,有两种不同类型的缓存:私有缓存共享缓存


私有缓存


私有缓存是绑定到特定客户端的缓存——通常是浏览器缓存。由于存储的响应不与其他客户端共享,因此私有缓存可以存储该用户的个性化响应。


另一方面,如果个性化内容存储在私有缓存以外的缓存中,那么其他用户可能能够检索到这些内容——这可能会导致无意的信息泄露。


如果响应包含个性化内容并且你只想将响应存储在私有缓存中,则必须指定 private 指令。


Cache-Control: private

个性化内容通常由 cookie 控制,但 cookie 的存在并不能表明它是私有的,因此单独的 cookie 不会使响应成为私有的。


请注意,如果响应具有 Authorization 标头,则不能将其存储在私有缓存(或共享缓存,除非 Cache-Control 指定的是 public)中。


共享缓存


共享缓存位于客户端和服务器之间,可以存储能在用户之间共享的响应。共享缓存可以进一步细分为代理缓存托管缓存


代理缓存


除了访问控制的功能外,一些代理还实现了缓存以减少网络流量。这通常不由服务开发人员管理,因此必须由恰当的 HTTP 标头等控制。然而,在过去,过时的代理缓存实现——例如没有正确理解 HTTP 缓存标准的实现——经常给开发人员带来问题。


Kitchen-sink 标头如下所示,用于尝试解决不理解当前 HTTP 缓存规范指令(如 no-store)的“旧且未更新的代理缓存”的实现。


Cache-Control: no-store, no-cache, max-age=0, must-revalidate, proxy-revalidate

然而,近年来,随着 HTTPS 变得越来越普遍,客户端/服务器通信变得加密,在许多情况下,路径中的代理缓存只能传输响应而不能充当缓存。因此,在这种情况下,无需担心甚至无法看到响应的过时代理缓存的实现。


另一方面,如果 TLS 桥接代理通过在 PC 上安装来自组织管理的 CA 证书,以中间人方式解密所有通信,并执行访问控制等,则可以查看响应的内容并将其缓存。但是,由于证书透明度(certificate transparency)在最近几年变得很普遍,并且一些浏览器只允许使用证书签署时间戳(signed certificate timestamp)颁发的证书,因此这种方法需要应用于企业策略。在这样的受控环境中,无需担心代理缓存“已过时且未更新”。


托管缓存


托管缓存由服务开发人员明确部署,以降低源服务器负载并有效地交付内容。示例包括反向代理、CDN 和 service worker 与缓存 API 的组合。


托管缓存的特性因部署的产品而异。在大多数情况下,你可以通过 Cache-Control 标头和你自己的配置文件或仪表板来控制缓存的行为。


例如,HTTP 缓存规范本质上没有定义显式删除缓存的方法——但是使用托管缓存,可以通过仪表板操作、API 调用、重新启动等实时删除已经存储的响应。这允许更主动的缓存策略。


也可以忽略标准 HTTP 缓存规范协议以支持显式操作。例如,可以指定以下内容以选择退出私有缓存或代理缓存,同时使用你自己的策略仅在托管缓存中进行缓存。


Cache-Control: no-store

例如,Varnish Cache 使用 VCL(Varnish Configuration Language,一种 DSL逻辑来处理缓存存储,而 service worker 结合缓存 API 允许你在 JavaScript 中创建该逻辑。


这意味着如果托管缓存故意忽略 no-store 指令,则无需将其视为“不符合”标准。你应该做的是,避免使用 kitchen-sink 标头,但请仔细阅读你正在使用的任何托管缓存机制的文档,并确保你选择的方式可以正确的控制缓存。


请注意,某些 CDN 提供自己的标头,这些标头仅对该 CDN 有效(例如,Surrogate-Control)。目前,正在努力定义一个 CDN-Cache-Control 标头来标准化这些标头。


缓存的类型


启发式缓存


HTTP 旨在尽可能多地缓存,因此即使没有给出 Cache-Control,如果满足某些条件,响应也会被存储和重用。这称为启发式缓存


例如,采取以下响应。此回复最后一次更新是在 1 年前。


HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1024
Date: Tue, 22 Feb 2022 22:22:22 GMT
Last-Modified: Tue, 22 Feb 2021 22:22:22 GMT

<!doctype html>


试探性地知道,整整一年没有更新的内容在那之后的一段时间内不会更新。因此,客户端存储此响应(尽管缺少 max-age)并重用它一段时间。复用多长时间取决于实现,但规范建议存储后大约 10%(在本例中为 0.1 年)的时间。


启发式缓存是在 Cache-Control 被广泛采用之前出现的一种解决方法,基本上所有响应都应明确指定 Cache-Control 标头。


基于 age 的缓存策略


存储的 HTTP 响应有两种状态:freshstalefresh 状态通常表示响应仍然有效,可以重复使用,而 stale 状态表示缓存的响应已经过期。


确定响应何时是 fresh 的和何时是 stale 的标准是 age。在 HTTP 中,age 是自响应生成以来经过的时间。这类似于其他缓存机制中的 TTL


以下面的示例响应为例(604800 秒是一周):


HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1024
Date: Tue, 22 Feb 2022 22:22:22 GMT
Cache-Control: max-age=604800

<!doctype html>


存储示例响应的缓存会计算响应生成后经过的时间,并将结果用作响应的 age


对于该示例的响应,max-age 的含义如下:



  • 如果响应的 age 小于一周,则响应为 fresh

  • 如果响应的 age 超过一周,则响应为 stale


只要存储的响应保持新鲜(fresh),它将用于兑现客户端请求。


当响应存储在共享缓存中时,有必要通知客户端响应的 age。继续看示例,如果共享缓存将响应存储了一天,则共享缓存将向后续客户端请求发送以下响应。


HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1024
Date: Tue, 22 Feb 2022 22:22:22 GMT
Cache-Control: max-age=604800
Age: 86400

<!doctype html>


收到该响应的客户端会发现它在剩余的 518400 秒内是新鲜(fresh)的,这是响应的 max-ageAge 之间的差异。


Expires 或 max-age


在 HTTP/1.0 中,新鲜度过去由 Expires 标头指定。


Expires 标头使用明确的时间而不是通过指定经过的时间来指定缓存的生命周期。


Expires: Tue, 28 Feb 2022 22:22:22 GMT

但是时间格式难以解析,也发现了很多实现的错误,有可能通过故意偏移系统时钟来诱发问题;因此,在 HTTP/1.1 中,Cache-Control 采用了 max-age——用于指定经过的时间。


如果 ExpiresCache-Control: max-age 都可用,则将 max-age 定义为首选。因此,由于 HTTP/1.1 已被广泛使用,无需特地提供 Expires


Vary 响应


区分响应的方式本质上是基于它们的 URL:


使用 url 作为键


但是响应的内容并不总是相同的,即使它们具有相同的 URL。特别是在执行内容协商时,来自服务器的响应可能取决于 AcceptAccept-LanguageAccept-Encoding 请求标头的值。


例如,对于带有 Accept-Language: en 标头并已缓存的英语内容,不希望再对具有 Accept-Language: ja 请求标头的请求重用该缓存响应。在这种情况下,你可以通过在 Vary 标头的值中添加“Accept-Language”,根据语言单独缓存响应。


Vary: Accept-Language

这会导致缓存基于响应 URLAccept-Language请求标头的组合进行键控——而不是仅仅基于响应 URL。


使用 url 和语言作为键


此外,如果你基于用户代理提供内容优化(例如,响应式设计),你可能会想在 Vary 标头的值中包含“User-Agent”。但是,User-Agent 请求标头通常具有非常多的变体,这大大降低了缓存被重用的机会。因此,如果可能,请考虑一种基于特征检测而不是基于 User-Agent 请求标头来改变行为的方法。


对于使用 cookie 来防止其他人重复使用缓存的个性化内容的应用程序,你应该指定 Cache-Control: private 而不是为 Vary 指定 cookie。


验证响应


过时的响应不会立即被丢弃。HTTP 有一种机制,可以通过询问源服务器将陈旧的响应转换为新的响应。这称为验证,有时也称为重新验证


验证是通过使用包含 If-Modified-SinceIf--Match 请求标头的条件请求完成的。


If-Modified-Since


以下响应在 22:22:22 生成,max-age 为 1 小时,因此你知道它在 23:22:22 之前是新鲜的。


HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1024
Date: Tue, 22 Feb 2022 22:22:22 GMT
Last-Modified: Tue, 22 Feb 2022 22:00:00 GMT
Cache-Control: max-age=3600

<!doctype html>


到 23:22:22 时,响应会过时并且不能重用缓存。因此,下面的请求显示客户端发送带有 If-Modified-Since 请求标头的请求,以询问服务器自指定时间以来是否有任何的改变。


GET /index.html HTTP/1.1
Host: example.com
Accept: text/html
If-Modified-Since: Tue, 22 Feb 2022 22:00:00 GMT

如果内容自指定时间以来没有更改,服务器将响应 304 Not Modified


由于此响应仅表示“没有变化”,因此没有响应主体——只有一个状态码——因此传输大小非常小。


HTTP/1.1 304 Not Modified
Content-Type: text/html
Date: Tue, 22 Feb 2022 23:22:22 GMT
Last-Modified: Tue, 22 Feb 2022 22:00:00 GMT
Cache-Control: max-age=3600

收到该响应后,客户端将存储的陈旧响应恢复为新鲜的,并可以在剩余的 1 小时内重复使用它。


服务器可以从操作系统的文件系统中获取修改时间,这对于提供静态文件的情况来说是比较容易做到的。但是,也存在一些问题;例如,时间格式复杂且难以解析,分布式服务器难以同步文件更新时间。


为了解决这些问题,ETag 响应标头被标准化作为替代方案。


ETag/If--Match


ETag 响应标头的值是服务器生成的任意值。服务器对于生成值没有任何限制,因此服务器可以根据他们选择的任何方式自由设置值——例如主体内容的哈希或版本号。


举个例子,如果 ETag 标头使用了 hash 值,index.html 资源的 hash 值是 deadbeef,响应如下:


HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1024
Date: Tue, 22 Feb 2022 22:22:22 GMT
ETag: "deadbeef"
Cache-Control: max-age=3600

<!doctype html>


如果该响应是陈旧的,则客户端获取缓存响应的 ETag 响应标头的值,并将其放入 If--Match 请求标头中,以询问服务器资源是否已被修改:


GET /index.html HTTP/1.1
Host: example.com
Accept: text/html
If--Match: "deadbeef"

如果服务器为请求的资源确定的 ETag 标头的值与请求中的 If--Match 值相同,则服务器将返回 304 Not Modified


但是,如果服务器确定请求的资源现在应该具有不同的 ETag 值,则服务器将其改为 200 OK 和资源的最新版本进行响应。



备注: 在评估如何使用 ETagLast-Modified 时,请考虑以下几点:在缓存重新验证期间,如果 ETagLast-Modified 都存在,则 ETag 优先。因此,如果你只考虑缓存,你可能会认为 Last-Modified 是不必要的。然而,Last-Modified 不仅仅对缓存有用;相反,它是一个标准的 HTTP 标头,内容管理 (CMS) 系统也使用它来显示上次修改时间,由爬虫调整爬取频率,以及用于其他各种目的。所以考虑到整个 HTTP 生态系统,最好同时提供 ETagLast-Modified



强制重新验证


如果你不希望重复使用响应,而是希望始终从服务器获取最新内容,则可以使用 no-cache 指令强制验证。


通过在响应中添加 Cache-Control: no-cache 以及 Last-ModifiedETag——如下所示——如果请求的资源已更新,客户端将收到 200 OK 响应,否则,如果请求的资源尚未更新,则会收到 304 Not Modified 响应。


HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1024
Date: Tue, 22 Feb 2022 22:22:22 GMT
Last-Modified: Tue, 22 Feb 2022 22:00:00 GMT
ETag: deadbeef
Cache-Control: no-cache

<!doctype html>


max-age=0must-revalidate 的组合与 no-cache 具有相同的含义。


Cache-Control: max-age=0, must-revalidate

max-age=0 意味着响应立即过时,而 must-revalidate 意味着一旦过时就不得在没有重新验证的情况下重用它——因此,结合起来,语义似乎与 no-cache 相同。


然而,max-age=0 的使用是解决 HTTP/1.1 之前的许多实现无法处理 no-cache 这一指令——因此为了解决这个限制,max-age=0 被用作解决方法。


但是现在符合 HTTP/1.1 的服务器已经广泛部署,没有理由使用 max-age=0must-revalidate 组合——你应该只使用 no-cache


不使用缓存


no-cache 指令不会阻止响应的存储,而是阻止在没有重新验证的情况下重用响应。


如果你不希望将响应存储在任何缓存中,请使用 no-store


Cache-Control: no-store

但是,一般来说,实践中“不缓存”的原因满足以下情况:



  • 出于隐私原因,不希望特定客户以外的任何人存储响应。

  • 希望始终提供最新信息。

  • 不知道在过时的实现中会发生什么。


在这种情况下,no-store 并不总是最合适的指令。


以下部分更详细地介绍了这些情况。


不与其他用户共享


如果具有个性化内容的响应意外地对缓存的其他用户可见,那将是有问题的。


在这种情况下,使用 private 指令将导致个性化响应仅与特定客户端一起存储,而不会泄露给缓存的任何其他用户。


Cache-Control: private

在这种情况下,即使设置了 no-store,也必须设置 private


每次都提供最新的内容


no-store 指令阻止存储响应,但不会删除相同 URL 的任何已存储响应。


换句话说,如果已经为特定 URL 存储了旧响应,则返回 no-store 不会阻止旧响应被重用。


但是,no-cache 指令将强制客户端在重用任何存储的响应之前发送验证请求。


Cache-Control: no-cache

如果服务端不支持条件请求,你可以强制客户端每次都访问服务端,总是得到最新的 200 OK 响应。


兼容过时的实现


作为忽略 no-store 的过时实现的解决方法,你可能会看到使用了诸如以下内容的 kitchen-sink 标头:


Cache-Control: no-store, no-cache, max-age=0, must-revalidate, proxy-revalidate

推荐使用 no-cache 作为处理这种过时的实现的替代方案,如果从一开始就设置 no-cache 就没问题,因为服务器总是会收到请求。


如果你关心的是共享缓存,你可以通过添加 private 来防止意外缓存:


Cache-Control: no-cache, private

no-store 丢失了什么


你可能认为添加 no-store 是选择退出缓存的正确方法。


但是,不建议随意授予 no-store,因为你失去了 HTTP 和浏览器所拥有的许多优势,包括浏览器的后退/前进缓存。


因此,要获得 Web 平台的全部功能集的优势,最好将 no-cacheprivate 结合使用。


重新加载和强制重新加载


可以对请求和响应执行验证。


重新加载强制重新加载操作是从浏览器端执行验证的常见示例。


重新加载


为了从页面错误中恢复或更新到最新版本的资源,浏览器为用户提供了重新加载功能。


在浏览器重新加载期间发送的 HTTP 请求的简化视图如下所示:


GET / HTTP/1.1
Host: example.com
Cache-Control: max-age=0
If--Match: "deadbeef"
If-Modified-Since: Tue, 22 Feb 2022 20:20:20 GMT

请求中的 max-age=0 指令指定“重用 age 为 0 或更少的响应”——因此,中间存储的响应不会被重用。


请求通过 If--MatchIf-Modified-Since 进行验证。


该行为也在 Fetch 标准中定义,并且可以通过在缓存模式设置为 no-cache 的情况下,在 JavaScript 中调用 fetch() 来重现(注意 reload 不是这种情况下的正确模式):


// 注意:“reload”不是正常重新加载的正确模式;“no-cache”才是
fetch("/", { cache: "no-cache" });

强制重新加载


出于向后兼容的原因,浏览器在重新加载期间使用 max-age=0——因为在 HTTP/1.1 之前的许多过时的实现中不理解 no-cache。但是在这个用例中,no-cache 已被支持,并且强制重新加载是绕过缓存响应的另一种方法。


浏览器强制重新加载期间的 HTTP 请求如下所示:


GET / HTTP/1.1
Host: example.com
Pragma: no-cache
Cache-Control: no-cache

由于这不是带有 no-cache 的条件请求,因此你可以确定你会从源服务器获得 200 OK


该行为也在 Fetch 标准中定义,并且可以通过在缓存模式设置为 reload 的情况下,在 JavaScript 中调用 fetch() 来重现(注意它不是 force-reload):


// 注意:“reload”——而不是“no-cache”——是“强制重新加载”的正确模式
fetch("/", { cache: "reload" });

避免重新验证


永远不会改变的内容应该被赋予一个较长的 max-age,方法是使用缓存破坏——也就是说,在请求 URL 中包含版本号、哈希值等。


但是,当用户重新加载时,即使服务器知道内容是不可变的,也会发送重新验证请求。


为了防止这种情况,immutable 指令可用于明确指示不需要重新验证,因为内容永远不会改变。


Cache-Control: max-age=31536000, immutable

这可以防止在重新加载期间进行不必要的重新验证。


删除存储的响应


基本上没有办法删除用很长的 max-age 存储的响应。


想象一下,来自 https://example.com/ 的以下响应已被存储。


HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1024
Cache-Control: max-age=31536000

<!doctype html>


一旦响应在服务器上过期,你可能希望覆盖该响应,但是一旦存储响应,服务器就无法执行任何操作——因为由于缓存,不再有请求到达服务器。


规范中提到的方法之一是使用不安全的方法(例如 POST)发送对同一 URL 的请求,但对于许多客户端而言,通常很难故意这样做。


还有一个 Clear-Site-Data: cache 标头和值的规范,但并非所有浏览器都支持它——即使使用它,它也只会影响浏览器缓存,而不会影响中间缓存。


因此,除非用户手动执行重新加载、强制重新加载或清除历史操作,否则应该假设任何存储的响应都将保留其 max-age 期间。


缓存减少了对服务器的访问,这意味着服务器失去了对该 URL 的控制。如果服务器不想失去对 URL 的控制——例如,在资源被频繁更新的情况下——你应该添加 no-cache,以便服务器始终接收请求并发送预期的响应。


请求折叠


共享缓存主要位于源服务器之前,旨在减少到源服务器的流量。


因此,如果多个相同的请求同时到达共享缓存,中间缓存将代表自己将单个请求转发到源,然后源可以将结果重用于所有客户端。这称为请求折叠


当请求同时到达时会发生请求折叠,因此即使响应中给出了 max-age=0no-cache,它也会被重用。


如果响应是针对特定用户个性化的,并且你不希望它在折叠中共享,则应添加 private 指令:


请求折叠


常见的缓存模式


Cache-Control 规范中有很多指令,可能很难全部理解。但是大多数网站都可以通过几种模式的组合来覆盖。


本节介绍设计缓存的常见模式。


默认设置


如上所述,缓存的默认行为(即对于没有 Cache-Control 的响应)不是简单的“不缓存”,而是根据所谓的“启发式缓存”进行隐式缓存。


为了避免这种启发式缓存,最好显式地为所有响应提供一个默认的 Cache-Control 标头。


为确保默认情况下始终传输最新版本的资源,通常的做法是让默认的 Cache-Control 值包含 no-cache


Cache-Control: no-cache

另外,如果服务实现了 cookie 或其他登录方式,并且内容是为每个用户个性化的,那么也必须提供 private,以防止与其他用户共享:


Cache-Control: no-cache, private

缓存破坏


最适合缓存的资源是静态不可变文件,其内容永远不会改变。而对于会变化的资源,通常的最佳实践是每次内容变化时都改变 URL,这样 URL 单元可以被缓存更长的时间。


例如,考虑以下 HTML:


<script src="bundle.js"></script>
<link rel="stylesheet" href="build.css" />
<body>
hello
</body>

在现代 Web 开发中,JavaScript 和 CSS 资源会随着开发的进展而频繁更新。此外,如果客户端使用的 JavaScript 和 CSS 资源的版本不同步,则显示将中断。


所以上面的 HTML 用 max-age 缓存 bundle.jsbuild.css 变得很困难。


因此,你可以使用包含基于版本号或哈希值的更改部分的 URL 来提供 JavaScript 和 CSS。一些方法如下所示。


# version in filename
bundle.v123.js

# version in query
bundle.js?v=123

# hash in filename
bundle.YsAIAAAA-QG4G6kCMAMBAAAAAAAoK.js

# hash in query
bundle.js?v=YsAIAAAA-QG4G6kCMAMBAAAAAAAoK

由于缓存根据它们的 URL 来区分资源,因此如果在更新资源时 URL 发生变化,缓存将不会再次被重用。


<script src="bundle.v123.js"></script>
<link rel="stylesheet" href="build.v123.css" />
<body>
hello
</body>

通过这种设计,JavaScript 和 CSS 资源都可以被缓存很长时间。那么 max-age 应该设置多长时间呢?QPACK 规范提供了该问题的答案。


QPACK 是一种用于压缩 HTTP 标头字段的标准,其中定义了常用字段值表。


一些常用的缓存头值如下所示。


36 cache-control max-age=0
37 cache-control max-age=604800
38 cache-control max-age=2592000
39 cache-control no-cache
40 cache-control no-store
41 cache-control public, max-age=31536000

如果你选择其中一个编号选项,则可以在通过 HTTP3 传输时将值压缩为 1 个字节。


数字“37”、“38”和“41”分别代表一周、一个月和一年。


因为缓存会在保存新条目时删除旧条目,所以一周后存储的响应仍然存在的可能性并不高——即使 max-age 设置为 1 周。因此,在实践中,你选择哪一种并没有太大的区别。


请注意,数字“41”具有最长的 max-age(1 年),但具有 public


public 值具有使响应可存储的效果,即使存在 Authorization 标头。



备注: 只有在设置了 Authorization 标头时需要存储响应时才应使用 public 指令。否则不需要,因为只要给出了 max-age,响应就会存储在共享缓存中。



因此,如果响应是使用基本身份验证进行个性化的,public 的存在可能会导致问题。如果你对此感到担忧,你可以选择第二长的值 38(1 个月)。


# response for bundle.v123.js

# If you never personalize responses via Authorization
Cache-Control: public, max-age=31536000

# If you can't be certain
Cache-Control: max-age=2592000

验证响应


不要忘记设置 Last-ModifiedETag 标头,以便在重新加载时不必重新传输资源。对于预构建的静态文件生成这些标头很容易。


这里的 ETag 值可能是文件的哈希值。


# response for bundle.v123.js
Last-Modified: Tue, 22 Feb 2022 20:20:20 GMT
ETag: YsAIAAAA-QG4G6kCMAMBAAAAAAAoK

此外,可以添加 immutable 以防止重新加载时验证。


组合结果如下所示。


# bundle.v123.js
200 OK HTTP/1.1
Content-Type: application/javascript
Content-Length: 1024
Cache-Control: public, max-age=31536000, immutable
Last-Modified: Tue, 22 Feb 2022 20:20:20 GMT
ETag: YsAIAAAA-QG4G6kCMAMBAAAAAAAoK

缓存破坏是一种通过在内容更改时更改 URL 来使响应在很长一段时间内可缓存的技术。该技术可以应用于所有子资源,例如图像。


备注: 在评估 immutable 和 QPACK 的使用时:如果你担心 immutable 会更改 QPACK 提供的预定义值,请考虑在这种情况下,immutable 部分可以通过将 Cache-Control 值分成两行来单独编码——尽管这取决于特定 QPACK 实现使用的编码算法。


Cache-Control: public, max-age=31536000
Cache-Control: immutable

主要资源


与子资源不同,主资源不能使用缓存破坏,因为它们的 URL 不能像子资源 URL 一样被修饰。


如果存储以下 HTML 本身,即使在服务器端更新内容,也无法显示最新版本。


<script src="bundle.v123.js"></script>
<link rel="stylesheet" href="build.v123.css" />
<body>
hello
</body>

对于这种情况,no-cache 将是合适的——而不是 no-store——因为我们不想存储 HTML,而只是希望它始终是最新的。


此外,添加 Last-ModifiedETag 将允许客户端发送条件请求,如果 HTML 没有更新,则可以返回 304 Not Modified


200 OK HTTP/1.1
Content-Type: text/html
Content-Length: 1024
Cache-Control: no-cache
Last-Modified: Tue, 22 Feb 2022 20:20:20 GMT
ETag: AAPuIbAOdvAGEETbgAAAAAAABAAE

该设置适用于非个性化 HTML,但对于使用 cookie 进行个性化的响应(例如,在登录后),不要忘记同时指定 private


200 OK HTTP/1.1
Content-Type: text/html
Content-Length: 1024
Cache-Control: no-cache, private
Last-Modified: Tue, 22 Feb 2022 20:20:20 GMT
ETag: AAPuIbAOdvAGEETbgAAAAAAABAAE
Set-Cookie: __Host-SID=AHNtAyt3fvJrUL5g5tnGwER; Secure; Path=/; HttpOnly

favicon.icomanifest.json.well-known 和无法使用缓存破坏更改 URL 的 API 端点也是如此。


大多数 Web 内容都可以通过上述两种模式的组合来覆盖。


有关托管缓存的更多信息


使用前面章节描述的方法,子资源可以通过缓存破坏来缓存很长时间,但主资源(通常是 HTML 文档)不能。


缓存主要资源很困难,因为仅使用 HTTP 缓存规范中的标准指令,在服务器上更新内容时无法主动删除缓存内容。


但是,可以通过部署托管缓存(例如 CDN 或 service worker)来实现。


例如,允许通过 API 或仪表板操作清除缓存的 CDN 将通过存储主要资源并仅在服务器上发生更新时显式清除相关缓存来实现更积极的缓存策略。


如果 service worker 可以在服务器上发生更新时删除缓存 API 中的内容,它也可以这样做。


作者:demo007x
来源:juejin.cn/post/7237022394790281271
收起阅读 »

Vue+Element-UI 中 el-table 动态合并单元格 :span-method 方法

web
合并单元格 记录一下工作时遇到的 el-table 合并单元格的需求,超详细😊 el-table官方提供了合并单元格的方法与返回格式 如下: 根据叙述有了如下思路: 因为后端返回的数据非统一, 可能不是按照类别排好的😨, 所以官网的例子满足不了所有的需求...
继续阅读 »
合并单元格


记录一下工作时遇到的 el-table 合并单元格的需求,超详细😊



el-table官方提供了合并单元格的方法与返回格式 如下:

在这里插入图片描述

根据叙述有了如下思路:

因为后端返回的数据非统一, 可能不是按照类别排好的😨, 所以官网的例子满足不了所有的需求所以我们通过遍历table的数据比较前后两个元素是否相等, 来构造一个spanArr用来存放rowspan, 最后通过rowspan的值来判断colspan的值😊.


案例如下, 这是我需要处理的一个表格:

需要根据数据动态的合并

在这里插入图片描述

对应的配置数组为

在这里插入图片描述


处理数据


因为获取的数据的非统一性, 我们首先要将数据根据我们想要合并的字段进行排序分组, 这里我实现了一个简单的方法来处理数据:


// data 为 表格数据 , params 为需要合并的字段
groupBy (data, params) {
const groups = {};
data.forEach(v => {
// 获取data中的传入的params属性对应的属性值
const group = JSON.stringify(v[params]);
// 把group作为groups的key,初始化value,循环时找到相同的v[params]时不变
groups[group] = groups[group] || [];
// 将对应找到的值作为value放入数组中
groups[group].push(v);
})
// 返回处理好的二维数组
return Object.values(groups);
},

此时打印一下我们的数据console.log(this.groupBy(this.tableListData.items, 'FirstIndex'))

在这里插入图片描述

如图, 我们已经将数据分好组并合并在一个数组中啦, FirstIndex相同的在一个数组


构造控制合并的数组spanArr


这里实现了一个方法, 用来构造一个spanArr数组赋予rowspan,即控制行合并



  • 接收重构数组 let arr = []

  • 设置索引 let pos = 0

  • 控制合并的数组 this.spanArr = []


先将groupby()处理好的数据再次用arr进行处理:连接所有数组成员为一个新数组

this.groupBy(this.tableListData.items, 'FirstIndex').map(v => (arr = arr.concat(v)))


现在处理好了数据,需要赋予原数据了:this.tableListData.items = arr


但是因为我是写在getSpanArr(data, params)方法中的,已经通过形参data将 this.tableListData.items传入了这里,如果想方便封装调用的话,不用每次使用都需要再次写入 this.tableListData.items = arr

于是想到一个办法,js数组的shift()和push()是直接修改数组所占内存的方法。

所以有:


arr.map(res => {
// 每次遍历都删除data && this.tableListData.items的第一个元素
data.shift()
// 每次遍历都将arr数组元素对应push进 data && this.tableListData.items
data.push(res)
})

还需要定义一个redata存放arr要合并字段的value

const redata = arr.map(v => v[params])


reduce处理spanArr数组 ⭐⭐


使用reduce方法比较redata前后两个元素是否相等,相等的话spanArr中对应索引的元素的值+1,并且在其后增加一个0占位(防止合并过后表格数据错位),否则的话增加一个1占位,并记录当前索引,往复循环,构造一个给 rowspan 取值判断合并的数组:


  const redata = arr.map(v => v[params])
redata.reduce((old, cur, i) => {
// old 上一个元素 cur 当前元素 i 索引
if (i === 0) {
// 第一次判断先增加一个 1 占位 ,索引为0
this.spanArr.push(1)
pos = 0
} else {
if (cur === old) {
this.spanArr[pos] += 1
this.spanArr.push(0)
} else {
this.spanArr.push(1)
pos = i
}
}
return cur
}, {})

看一下现在的数据spanArr, 这里传的参数为SecondIndex, 即表格的第二列

在这里插入图片描述

数组中大于0的数字就是我们数据中要合并的这组数据的数量, 同时也是这组数据需要合并的列数,而0就是代表这列不合并, 依次遍历,实现合并所选字段这一列的最终目的 如图理解:

在这里插入图片描述


返回最终结果


最后一步啦😊根据官方给的方法把我们处理好的spanArr传给rowspan即可


spanMethod({ row, column, rowIndex, columnIndex }) {
// 第一列
if (columnIndex === 0) {
const _row = this.spanArr[rowIndex];
const _col = _row > 0 ? 1 : 0;
return {
rowspan: _row,
colspan: _col
}
}
}

效果如图!

在这里插入图片描述


完整代码


就很nice, !!最后把完整代码贴上:


// ......
mounted() {
this.getSpanArr(this.tableListData.items, 'FirstIndex');
},
methods: {
groupBy (data, params) {
const groups = {}
data.forEach(v => {
const group = JSON.stringify(v[params])
groups[group] = groups[group] || []
groups[group].push(v)
})
return Object.values(groups)
},
getSpanArr (data, params) {
let arr = []
let pos = 0
this.spanArr = []
this.groupBy(data, params).map(v => (arr = arr.concat(v)))
arr.map(res => {
data.shift()
data.push(res)
})
const redata = arr.map(v => v[params])
redata.reduce((old, cur, i) => {
if (i === 0) {
this.spanArr.push(1)
pos = 0
} else {
if (cur === old) {
this.spanArr[pos] += 1
this.spanArr.push(0)
} else {
this.spanArr.push(1)
pos = i
}
}
return cur
}, {})
},
spanMethod({ row, column, rowIndex, columnIndex }) {
if (columnIndex === 0) {
const _row = this.spanArr[rowIndex];
const _col = _row > 0 ? 1 : 0;
return {
rowspan: _row,
colspan: _col
}
}
}
}

完美! 撒花!!!🎉🎉🎉


作者:小星星__
来源:juejin.cn/post/7238478149049483301
收起阅读 »

改写el-table表格排序, 支持多列排序远程排序!!!

web
改写el-table的默认排序 提示:在el-table封装的表格基础上改写排序方法 前言 我们在做表格的时候经常会遇到表头有一个排序的icon 用来对数据进行, el-table有自己的排序方法, 如下: 在列中设置sortable属性即可实现以该列为基...
继续阅读 »

改写el-table的默认排序


提示:在el-table封装的表格基础上改写排序方法




前言


我们在做表格的时候经常会遇到表头有一个排序的icon 用来对数据进行, el-table有自己的排序方法, 如下:



在列中设置sortable属性即可实现以该列为基准的排序,接受一个Boolean,默认为false。





一、el-table支持调接口排序吗?


el-table默认的排序支持从接口获取排序的数据



sortable: 对应列是否可以排序,如果设置为 custom,则代表用户希望远程排序,需要监听 Table 的 sort-change 事件



二、el-table支持多列排序吗?


默认的排序很简单, 加一个参数就可以了, 而且会自动根据数据进行排序, 但是我们会发现, 默认的排序只支持一列进行排序, 当我们排过一列之后在点击另一列的排序图标, 之前的排序就会消失😨.


三、如何实现多列远程排序?



  1. 自己写一个组件插入到表头的位置实现排序

  2. 根据el-table已有的属性以及抛出的方法实现多列排序


如果手动封装一个组件肯定能实现, 但是比较麻烦, 所以就研究了el-table相关了一些属性和方法, 思路如下:



header-cell-class-name: 表头单元格的 className 的回调方法,也可以使用字符串为所有表头单元格设置一个固定的className



在点击表头的时候排序的列以及是升降序保存到一个数组对象ordersList里, 然后通过header-cell-class-name属性设置选中的样式.


四、核心代码


	data: {
return {
ordersList: [],
}
}
// 点击表头
handleHeaderCLick(column){
if (column.sortable !== 'custom') {
return
}
if (!column.multiOrder) {
column.multiOrder = 'descending'
} else if (column.multiOrder === 'descending') {
column.multiOrder = 'ascending'
} else {
column.multiOrder = ''
}
this.handleOrderChange(column.property, column.multiOrder)

},
handleOrderChange (orderColumn, orderState) {
let result = this.ordersList.find(e => e.orderColumn === orderColumn)
if (result) {
result.orderState = orderState
} else {
this.ordersList.push({
orderColumn: orderColumn,
orderState: orderState,
})
}
// 调接口查询,在传参的时候把ordersList进行处理成后端想要的格式(这里是把数据抛出, 外部调用组件的地方处理)
this.sendInfo(this.ordersList, 'sort-change')
},
// 上面缺点是只能通过点击表头切换排序状态,点击小三角排序不会触发,处理sort-change事件和点击表头一样
sortChange({column}) {
// 有些列不需要排序,提前返回
if (column.sortable !== 'custom') {
return
}
if (!column.multiOrder) {
column.multiOrder = 'descending'
} else if (column.multiOrder === 'descending') {
column.multiOrder = 'ascending'
} else {
column.multiOrder = ''
}
this.handleOrderChange(column.property, column.multiOrder)
},
// 设置列的排序为我们自定义的排序
handleHeaderClass({ column }) {
column.order = column.multiOrder
}

这样外部拿到的就是一个所有排序的数组, 包括prop以及当前列的排序规则(ascending/descending/null), 将其处理成正确的入参格式即可.




在这里插入图片描述

在这里插入图片描述


如此, 就实现了多列远程排序, 欢迎大家一起讨论学习😊~


作者:小星星__
来源:juejin.cn/post/7238479015723089980
收起阅读 »

2023了,该用一下pnpm了

web
前言 大家好,我是 simple ,我的理想是利用科技手段来解决生活中遇到的各种问题。 performant npm ,意味高性能的 npm。pnpm由 npm/yarn 衍生而来,解决了 npm/yarn 内部潜在的bug,极大的优化了性能,扩展了使用场景。...
继续阅读 »

前言


大家好,我是 simple ,我的理想是利用科技手段来解决生活中遇到的各种问题


performant npm ,意味高性能的 npm。pnpm由 npm/yarn 衍生而来,解决了 npm/yarn 内部潜在的bug,极大的优化了性能,扩展了使用场景。被誉为"最先进的包管理工具"。


npm,yarn,pnpm的安装区别


首先我创建了三个文件夹分别是npm,yarn和pnpm用于比较三者之间的区别。首先初始化项目,然后安装了express来观察三个文件夹的区别。


npm和yarn的node_modules都是点开之后一眼看不到尽头。


image.png image.png


pnpm的node_modules略有区别。


image.png


npm/yarn 包结构分析


出现这种情况,是因为yarn和npm安装依赖包,会在node_modules下都平铺出来。现在安装了express,但express中也会有很多不同的依赖。在这些依赖里面有可能又引用了新的依赖,导致node_modules一点开就一望无际了。


初心是好的,因为平铺就可以复用很多依赖。如果说我package Apackage B都用了lodash@3.0.0这个包,那么使用平铺,我只需要下载一次lodash即可,节约了装包时间和存储空间。


但是真实开发情况往往是现在下载了五个依赖,其中A,B依赖引用了lodash@3.0.0,而C,D,E引用了lodash@4.0.0。咋整?


npmyarn目前给出的方案是将其中一个版本,(假设是)lodash@3.0.0的版本放在根目录的node_modules下面,而将需要lodash@4.0.0版本安装到C,D,E的node_modules下。如下所示,A, B可以直接使用lodash@3.0.0,而4,5,6想要使用就只能独自安装lodash@4.0.0


├─── lodash@3.0.0
├─── package-A
├─── package-B
├─── package-C
│ └── lodash@4.0.0
├─── package-D
│ └── lodash@4.0.0
├─── package-E
│ └── lodash@4.0.0

pnpm包结构分析


按照上文的例子,如果pnpm也安装五个包,A,B依赖引用了lodash@3.0.0,而C,D,E引用了lodash@4.0.0


├─ .pnpm
│ └── lodash@3.0.0
│ └── lodash@4.0.0
│ └── package-A@1.0.0
│ └── package-B@1.0.0
│ └── package-C@1.0.0
│ └── package-D@1.0.0
│ └── package-E@1.0.0
├──── package-A 符号链接
├──── package-B 符号链接
├──── package-C 符号链接
├──── package-D 符号链接
├──── package-E 符号链接

pnpm文件夹的node_modules下除了.pnpm文件夹外,就剩下一个package A,B,C,D,E,并且这五个包都是符号链接,它们真正的地址都是在.pnpm下。


也就是说,pnpm通过.pnpm/<name>@<version>/node_modules/<name>找到不同的包,这不仅解决了包重复下载的问题,还顺手解决了幽灵依赖的问题。



幽灵依赖:即开发者并未在package.json中下载相关包,但是在开发过程中却可以直接引用的问题。就是因为npm将依赖直接在node_modules下直接展开,导致开发者可以直接引用。问题就是当开发者升级一些包的时候,那些幽灵依赖可能并不存在于新的版本中,导致项目崩溃。



.pnpm store


pnpm牛皮的地方不只是借用了符号链接解决了包引用的问题,更是借助了硬链接解决了整个直接所有的项目依赖都给整合了,一个包全局只保存一份,并且是通过链接,速度比复制还要快的多。


借一张pnpm官网的图。


image.png


从图可以看出,.pnpm store就是依赖的实际存储位置,Mac/linux在{home dir}>/.pnpm-store/v3,windows在当前盘/.pnpm-store/v3。这样就会有个好处,你在多个项目使用的是同一个依赖时,一个包全局只保存一份,这也太省空间了吧。(只要你下载过一次,如果你没有清理.pnpm store,第二次就算你不联网照样能帮你install。)


pnpm store清理


但是,随着使用时间越长,pnpm store也会越来越大。并且随着项目版本的迭代,可能很多包都不再需要了pnpm store依旧会保留着它。此时我们需要定时清理一下。


未引用的包是系统上的任何项目中都未使用的包。 在大多数安装操作之后,包有可能会变为未引用状态,例如当依赖项变得多余时。


最好的做法是 pnpm store prune 来清理存储,但不要太频繁。 有时,未引用的包会再次被需要。 这可能在切换分支和安装旧的依赖项时发生,在这种情况下,pnpm 需要重新下载所有删除的包,会暂时减慢安装过程。


请注意,当 存储服务器正在运行时,这个命令是禁止使用的。


pnpm store prune

作者:simple_lau
来源:juejin.cn/post/7237856777588670521
收起阅读 »

身为Ikun,我想用console.log输出giegie打球的视频~

web
前言 大家好,我是林三心,用最通俗易懂的话讲最难的知识点是我的座右铭,基础是进阶的前提是我的初心。 事情是这样的,这天我醒来,觉得身为一个 “ikun”,我得向我的giegie看齐,早点把篮球水平练上去,早点追上我的giegie,于是我便开始了我的打球(打铁)...
继续阅读 »

前言


大家好,我是林三心,用最通俗易懂的话讲最难的知识点是我的座右铭,基础是进阶的前提是我的初心。


事情是这样的,这天我醒来,觉得身为一个 “ikun”,我得向我的giegie看齐,早点把篮球水平练上去,早点追上我的giegie,于是我便开始了我的打球(打铁)之旅~


May-28-2023 23-36-14.gif


突发奇想


晚上,当我在分析着我的打球视频时,我又感觉我做这些是远远不够的!我要将这一切融入到前端里,让别人知道,咱们 ”ikun“ 是一个爱giegie,也爱学习的团体!我想要达到以下的效果


May-27-2023 23-47-31.gif


于是我想,我能不能把这个打球(打铁)视频,在控制台里 console.log 出来呢?我在心里演练了一遍,我觉得是可行的,我的思路有两个


直接用 console.log 输出视频?


好吧,目前 console.log 不支持输出视频吧~此路不通啊!


细分成每一帧去输出?


这个方式的思路具体分为以下几步:



  • 捕获视频的每一帧

  • 将每一帧转换成图片

  • 使用console.log输出生图片


如何捕获视频的每一帧?


使用 video 的 requestVideoFrameCallback 方法即可,requestVideoFrameCallback() 是一个新的WEB API,2021 年 1 月 25 日提交的草案。requestVideoFrameCallback() 方法允许WEB开发者注册一个回调方法,回调方法在新视频帧发送到合成器时在渲染步骤中运行。这是为了让开发人员对视频执行高效的每帧视频操作,例如视频处理和绘制到画布上(截屏)、视频分析或与外部音频源同步。


如何将每一帧转换成图片?


这得使用 canvas来完成,主要依赖了两个方法



  • ctx.drawImage:将一帧画面画到 canvas 上

  • canvas.toDataURL('image/png'):将 canvas 画布上的图像转成base64的URL


console.log 能输出图片?


console.log 是可以输出图片了,这一特性很久前就有了~不信你们复制以下代码,去尝试一下~


image.png


console.log(
"%c image",
`background-image: url(https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ca412c9f87fb4df1b5402a5ad64474f1~tplv-k3u1fbpfcp-watermark.image?);
background-size: contain;
background-repeat: no-repeat;
padding: 200px;
`

)

开始实现吧~


那我们开始实现吧,我这边的技术栈是 Vue3哦~


先看看效果


先看看效果是怎么样的


May-28-2023 23-36-26.gif


初始化代码


我们需要 video 和 canvas,这两个标签,前者是视频标签,后者是画布标签


<template>
<video ref="videoRef" width="640" height="360" controls playsinline muted>
<source src="./kunkun.mp4" />
您的浏览器不支持 video 标签。
video>
<canvas ref="canvasRef" width="640" height="360">canvas>
template>

封装 useIKun


封装一个 Vue3 的 hooks,名为 useIKun,用来处理 ikun 的打球视频转换图片输出~




接下来我们开始封装 useIKun


import { onMounted } from 'vue'
import type { Ref } from 'vue'

const CANVAS_WIDTH_BASE = 220
const CANVAS_HEIGHT_BASE = 220

export const useIKun = ({
videoInstance,
canvasInstance,
}: {
videoInstance: Ref
canvasInstance: Ref
}
) => {
onMounted(() => {
// 播放视频
handleVideoPlay()
})

// 获取dom实例
const getInstances = () => {
const video = videoInstance.value
const canvas = canvasInstance.value
return {
video,
canvas,
}
}

// 获取canvas尺寸信息
const getCanvasSize = () => {
const { video } = getInstances()
// videoWidht、videoHeight视频原始宽度、高度(单位:px)
const width = video.videoWidth
const height = video.videoHeight

const rate = height / width

return {
width: CANVAS_WIDTH_BASE,
height: CANVAS_HEIGHT_BASE * rate,
}
}

const handleVideoPlay = () => {
const video = videoInstance.value
const canvas = canvasInstance.value
video.oncanplay = () => {
const { width, height } = getCanvasSize()
canvas.width = width
canvas.height = height
video.play() // 播放视频

// 判断HTMLVideoElement是否支持requestVideoFrameCallback()方法
if ('requestVideoFrameCallback' in video) {
// 下此视频帧呈现时触发回调
video.requestVideoFrameCallback(updateVideo)
}
}
}

/*
* 根据当前视频帧绘制图片
*/

const updateVideo = () => {
const video = videoInstance.value
const canvas = canvasInstance.value
const ctx = canvas.getContext('2d')
if (ctx) {
const { width, height } = getCanvasSize()
ctx.drawImage(video, 0, 0, width, height) // 使用视频帧(当前帧)绘制canvas
const dataURL = canvas.toDataURL('image/png')
console.log(
'%c image',
`background-image: url(${dataURL});
background-size: contain;
background-repeat: no-repeat;
padding: 200px;
`
,
)
video.requestVideoFrameCallback(updateVideo)
}
}
}

console.clear() ?


我们可以看到效果出来了,控制台里确实“播放了视频”,但其实不是视频,其实是以非常快的速度,去打印出一帧一帧的图片出来,由于速度很快,所以给你一种在放视频的假象,不信你看其实控制台里不止一个画面哦~


May-28-2023 23-36-26.gif


所以我们咋办,每次输出图片的时候用 console.clear() 清除一下吗?我们可以试试~


  const updateVideo = () => {
const video = videoInstance.value
const canvas = canvasInstance.value
const ctx = canvas.getContext('2d')
if (ctx) {
const { width, height } = getCanvasSize()
ctx.drawImage(video, 0, 0, width, height) // 使用视频帧(当前帧)绘制canvas
const dataURL = canvas.toDataURL('image/png')
// 清除
+ console.clear()
console.log(
'%c image',
`background-image: url(${dataURL});
background-size: contain;
background-repeat: no-repeat;
padding: 200px;
`
,
)
video.requestVideoFrameCallback(updateVideo)
}
}

再来看看效果~ 显然这样是不行的,console.clear 会导致闪烁问题~


May-28-2023 23-36-37.gif


Gif图?


所以我又有新思路,将每一帧的图像收集起来,然后组成一个 Gif 图,然后输出在控制台,不就行了!!!


gifshot


想要完成这个事情,就要借助这个库——gifshot,他的作用是可以把你传给他的图像数组,组成一个 gif图


重新封装 useIKun


 import { onMounted, ref } from 'vue'
import type { Ref } from 'vue'
import gifshot from 'gifshot'

const CANVAS_WIDTH_BASE = 220
const CANVAS_HEIGHT_BASE = 220
const IMG_SPAN = 5

export const useIKun = ({
videoInstance,
canvasInstance,
}: {
videoInstance: Ref
canvasInstance: Ref
}
) => {
const imgs = ref<string[]>([])

onMounted(() => {
// 播放视频
handleVideoPlay()
// 监听播放结束
onVideoEnded()
})

const getInstances = () => {
const video = videoInstance.value
const canvas = canvasInstance.value
return {
video,
canvas,
}
}

const getCanvasSize = () => {
const { video } = getInstances()
// videoWidht、videoHeight视频原始宽度、高度(单位:px)
const width = video.videoWidth
const height = video.videoHeight

const rate = height / width

return {
width: CANVAS_WIDTH_BASE,
height: CANVAS_HEIGHT_BASE * rate,
}
}

/*
* 控制视频播放
*/

const handleVideoPlay = () => {
const { video, canvas } = getInstances()
video.oncanplay = () => {
const { width, height } = getCanvasSize()
canvas.width = width
canvas.height = height
video.play() // 播放视频

// 判断HTMLVideoElement是否支持requestVideoFrameCallback()方法
if ('requestVideoFrameCallback' in video) {
// 下此视频帧呈现时触发回调
video.requestVideoFrameCallback(updateVideo)
}
}
}

/*
* 根据当前视频帧绘制图片
*/

const updateVideo = () => {
const { video, canvas } = getInstances()
const ctx = canvas.getContext('2d')
if (ctx) {
const { width, height } = getCanvasSize()
ctx.drawImage(video, 0, 0, width, height) // 使用视频帧(当前帧)绘制canvas
const dataURL = canvas.toDataURL('image/png')
imgs.value.push(dataURL)
video.requestVideoFrameCallback(updateVideo)
}
}

/*
* 监听视频停止播放
*/

const onVideoEnded = () => {
const { video } = getInstances()
video.onended = () => {
console.log('播放完了')
console.log(imgs.value.length)
const currentImgs = imgs.value
const resultImgs: string[] = []
currentImgs.forEach((img, index) => {
// 稀释图片数组,怕太大太久~你们也可以选择不走这一步
if (index % IMG_SPAN === 0) {
resultImgs.push(img)
}
})
// gifshot转换gif
gifshot.createGIF(
{
fps: 10,
width: 220,
height: 500,
images: resultImgs,
},
(obj) => {
console.log(obj)
if (!obj.error) {
const url = obj.image
// 输出最终的gif地址
console.log(
'%c image',
`background-image: url(${url});
background-size: contain;
background-repeat: no-repeat;
padding: 200px;
`
,
)
}
},
)
}
}
}


结果


哎,,,这画质,虽然比较糙,但是也算是本 ikun 为 giegie做出的一点点贡献了~


May-27-2023 23-31-48.gif


结语 & 加学习群 & 摸鱼群


我是林三心



  • 一个待过小型toG型外包公司、大型外包公司、小公司、潜力型创业公司、大公司的作死型前端选手;

  • 一个偏前端的全干工程师;

  • 一个不正经的掘金作者;

  • 一个逗比的B站up主;

  • 一个不帅的小红书博主;

  • 一个喜欢打铁的篮球菜鸟;

  • 一个喜欢历史的乏味少年;

  • 一个喜欢rap的五音不全弱鸡

作者:Sunshine_Lin
来源:juejin.cn/post/7238195267286138939
收起阅读 »

根据高德地图,画个心给你对象吧

web
文章来源 因为逛掘金的时候,看到了这篇文章,但是文章写的没头没尾,于是我就按照他给的思路自己实现了一下,感觉效果来不错,就在这里分享给大家一下。 高德地图接入 用这个申请的应用的key可以进行sdk的接入。可以通过高德地图的文档API来查询具体的API如何使...
继续阅读 »

文章来源


因为逛掘金的时候,看到了这篇文章,但是文章写的没头没尾,于是我就按照他给的思路自己实现了一下,感觉效果来不错,就在这里分享给大家一下。


image.png


高德地图接入


用这个申请的应用的key可以进行sdk的接入。可以通过高德地图的文档API来查询具体的API如何使用。


本文中主要使用高德地图的4个API



  • 初始化地图

  • 绘制路径(PathSimplifier)

  • 点标记(Marker)

  • 信息窗体(InfoWindow)


具体步骤



  1. 需要初始化地图,首先你要有一个容器去绘制地图,所以你要在html下有一个div容器,id随便起,然后使用new AMap.Map(#id)的方式去初始化地图,初始化的时候还可以带入参数(中心点、zoom)等。


   this.map = new AMap.Map('container', {
center:[x,y],
zoom: 10
})

这里你可以去查询当前你所在位置的坐标作为中心点传入,zoom我的案例里没有使用,因为使用了zoom,初始化的时候会卡一下,不知道具体原因。



  1. 绑定事件,获取心型坐标。给map绑定点击事件,每点击一次打一个标记点,然后你画一个心型,记录路径点。此处需要画左右两条路径哦


 this.map.on('click', (e) => {
const position = [+e.lnglat.getLng(), +e.lnglat.getLat()]
const marker = new AMap.Marker({
position: [+e.lnglat.getLng(), +e.lnglat.getLat()],
})
if (!window.list) {
window.list = [position]
} else {
window.list.push(position)
}
marker.setMap(this.map)
})

image.png
可以通过window.list来查看所有标记点的坐标,记住这些坐标,后面画线的时候要用哦。(tips:可以先画一条线,然后再控制台把window.list清空,再画另一条线。两条线的坐标点数量尽量一致,防止出现先后抵达的问题。)



  1. 下面你已经找好了两条线的坐标,下面你需要画两条线了


const initPath = () => {
AMapUI.load(['ui/misc/PathSimplifier'], (PathSimplifier) => {
if (!PathSimplifier.supportCanvas) {
alert('当前环境不支持 Canvas!')
return
}
//启动页面
this.pathSimplifierIns = new PathSimplifier({
zIndex: 100,
map: this.map, //所属的地图实例
getPath: function (pathData, pathIndex) {
//返回轨迹数据中的节点坐标信息,[AMap.LngLat, AMap.LngLat...] 或者 [[lng|number,lat|number],...]
return pathData.path
},
})
this.pathSimplifierIns.setData([
{
name: '轨迹1',
path: this.path1,
},
{
name: '轨迹2',
path: this.path2,
},
])
var navg0 = this.pathSimplifierIns.createPathNavigator(
0, //关联第1条轨迹
{
loop: false, //循环播放
speed: 800,
pathNavigatorStyle: {
width: 40,
height: 40,
autoRotate: false, // 禁止调整方向
// 经过路径的样式
pathLinePassedStyle: {
lineWidth: 6,
strokeStyle: 'black',
dirArrowStyle: {
stepSpace: 15,
strokeStyle: 'red',
},
},
//设置头像 不需要可以删除
content: PathSimplifier.Render.Canvas.getImageContent(
aJpg,
onload,
onerror,
),
},
},
)
var navg1 = this.pathSimplifierIns.createPathNavigator(
1, //关联第1条轨迹
{
loop: false, //循环播放
speed: 800,
pathNavigatorStyle: {
width: 40,
height: 40,
autoRotate: false, // 禁止调整方向
// 经过路径的样式
pathLinePassedStyle: {
lineWidth: 6,
strokeStyle: 'blue',
dirArrowStyle: {
stepSpace: 15,
strokeStyle: 'red',
},
},
//设置头像 不需要可以删除
content: PathSimplifier.Render.Canvas.getImageContent(
bJpg,
onload,
onerror,
),
},
},
)
// 设置定时器,方式map加载卡顿时,动画先开始
setTimeout(() => {
navg0.start()
navg1.start()
// 设置途径路上的 说的话
navg1.on('move', (e) => {
const idx = navg1.getCursor().idx // 走到了第几个点
const list = [
'不开门一直敲',
'是一种打扰',
'不回复本身',
'就是一种回复',
'双向奔赴才有意义',
]
let text = ''
if(idx < 3) {
text = list[0]
} else if(idx < 8) {
text = list[1]
} else if(idx < 13) {
text = list[2]
} else if(idx < 17) {
text = list[3]
} else {
text = list[4]
}
const cont = `<div class="toptit">
<p>${text}</p>
</div>`


// 设置气泡
this.infoWindow.setContent(cont)
this.infoWindow.open(this.map, e.target.getPosition())
})
}, 3000)
this.pathSimplifierIns.renderLater()
})
}


  1. 上面代码把信息窗体漏了,信息窗体也需要初始化,在初始化地图的后就行


  mounted() {
this.map = new AMap.Map('container',)
this.infoWindow = new AMap.InfoWindow({
offset: new AMap.Pixel(0, 0),
})
}

成品展示



这个demo里面,没有设置头像和要说的话,因为头像icon无法放上去,后续需要的画 可以自己添加哦。


结语


520已经过去了,这个就等着七夕给你们对象制造点浪漫吧。。


作者:哈库拉马塔塔
来源:juejin.cn/post/7236593783843913787
收起阅读 »