注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

HashMap原理浅析及相关知识

一、初识Hashmap 作为集合Map的主要实现类;线程不安全的,效率高;存储null的key和value。 二、HashMap在Jdk7中实现原理 1、HashMap map = new HashMap() 实例化之后会在底层创建长度是16的一维数组Ent...
继续阅读 »

一、初识Hashmap


作为集合Map的主要实现类;线程不安全的,效率高;存储null的key和value。


image.png


二、HashMap在Jdk7中实现原理


1、HashMap map = new HashMap()


实例化之后会在底层创建长度是16的一维数组Entry[] table。


2、map.put(key1,value1)


调用Key1所在类的hashCode()计算key1哈希值,得到Entry数组中存放的位置                   ---比较存放位置

如果此位置为空,此时key1-value1添加成功 *情况1,添加成功*

此位置不为空(以为此位置存在一个或多个数据(以链表形式存在)),比较key1和已存在的数据的哈希值: --比较哈希值

如果key1的哈希值与存在数据哈希值都不相同,此时key1-value1添加成功 *情况2,添加成功*

如果key1的哈希值与某一存在数据(key2,value2)相同,继续调用key1类的equals(key2)方法 --equals比较

如果equals()返回false,此时key1-value1添加成功 *情况3,添加成功*

如果equals()返回true,此时value1替换value2 *情况4,更新原有key的值*

情况2和情况3状态下,key1-value1和原来的数据以链表方式存储。

添加过程中会涉及扩容,超出临界值(存放位置非空)时扩容。默认扩容方式:扩容为原来容量的2倍,并将原有的数据复制过来。




三、HashMap在Jdk8之后实现原理


1、HashMap map = new HashMap()


底层没创建一个长度为16的数组,而是在首次调用put()方法时,底层创建长度为16的数组。


2、map.put(key1,value1)


final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;//首次put,创建长度为16的数组
if ((p = tab[i = (n - 1) & hash]) == null)// 需要插入数据位置为空。注:[i = (n - 1) & hash]找到当前key应插入的位置
tab[i] = newNode(hash, key, value, null); //*情况1*
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))//*情况4*
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);//红黑树情况
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);//*情况2、3*
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))//*情况4*
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}

3、map.entrySet()


返回一个Set集合


public Set<Map.Entry<K,V>> entrySet() {
Set<Map.Entry<K,V>> es;
return (es = entrySet) == null ? (entrySet = new EntrySet()) : es;
}

4、map.get(ket)


返回key对应的value值。


public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}

5、常见参数:


DEFAULT_INITIAL_CAPACITY : HashMap的默认容量,16


DEFAULT_LOAD_FACTOR:HashMap的默认加载因子:0.75


threshold:扩容的临界值,=容量*填充因子:16 * 0.75 => 12


TREEIFY_THRESHOLD:Bucket中链表长度大于该默认值,转化为红黑树:8


MIN_TREEIFY_CAPACITY:桶中的Node被树化时最小的hash表容量:64


四、涉及的基础知识


位运算符用来对二进制位进行操作,Java中提供了如下表所示的位运算符:位运算符中,除 ~ 以外,其余均为二元运算符。


操作数只能为整型和字符型数据。


C语言中六种位运算符:


<<左移


>>右移


| 按位或


& 按位与


~取反


^ 按位异或


左移符号<<:向左移动若干位,高位丢弃,低位补零,对于左移N位,就等于乘以2^n


带符号右移操作>>:向右移动若干位,低位进行丢弃,高位按照符号位进行填补,对于正数做右移操作时,高位补充0;负数进行右移时,高位补充1


不带符号的右移操作>>>:与右移操作类似,高位补零,低位丢弃,正数符合规律,负数不符合规律


键(key)经过hash函数得到的结果作为地址去存放当前的键值对(key-value)(这个是hashmap的存值方式),但是却发现该地址已经有人先来了,一山不容二虎,就会产生冲突。这个冲突就是hash冲突了。


简单来说:两个不同对象的hashCode相同,这种现象称为hash冲突。


HashMap的Put方法在第2、3情况添加前会产生哈希冲突,HashMap采用的链地址法(将所有哈希地址相同的都链接在同一个链表中 ,因而查找、插入和删除主要在同义词链中进行。链地址法适用于经常进行插入和删除的情况)解决哈希冲突。


五、相关面试问题


1、HashMap原理?


见上


2、HashMap初始化时阈值默认为12(加载因子为0.75),会使HashMap提前进行扩容,那为什么不在HashMap满的时候再进行扩容?


若加载因子越大,填满的元素越多,好处是,空间利用率高了,但冲突的机会加大了.链表长度会越来越长,查找效率降低。
反之,加载因子越小,填满的元素越少,好处是冲突的机会减小了,但空间浪费多了.表中的数据将过于稀疏(很多空间还没用,就开始扩容了)
冲突的机会越大,则查找的成本越高. 因此,必须在 "冲突的机会"与"空间利用率"之间寻找一种平衡与折衷.
这种平衡与折衷本质上是数据结构中有名的"时-空"矛盾的平衡与折衷.
如果机器内存足够,并且想要提高查询速度的话可以将加载因子设置小一点;相反如果机器内存紧张,并且对查询速度没有什么要求的话可以将加载因子设置大一点。不过一般我们都不用去设置它,让它取默认值0.75就好了。


3、什么是哈希冲突?如何解决?


4、并发集合


以下均为java.util.concurrent - Java并发工具包中的同步集合


4.1、ConcurrentHashMap 支持完全并发的检索和更新,所希望的可调整并发的哈希表。此类遵守与 Hashtable 相同的功能规范,并且包括对应于 Hashtable 的每个方法的方法版本。不过,尽管所有操作都是线程安全的,但检索操作不必锁定,并且不支持以某种防止所有访问的方式锁定整个表。此类可以通过程序完全与 Hashtable 进行互操作,这取决于其线程安全,而与其同步细节无关。


4.2、ConcurrentSkipListMap 是基于跳表的实现,也是支持key有序排列的一个key-value数据结构,在并发情况下表现很好,是一种空间换时间的实现,ConcurrentSkipListMap是基于一种乐观锁的方式去实现高并发。


4.3、ConCurrentSkipListSet (在JavaSE 6新增的)提供的功能类似于TreeSet,能够并发的访问有序的set。因为ConcurrentSkipListSet是基于“跳跃列表(skip list)”实现的,只要多个线程没有同时修改集合的同一个部分,那么在正常读、写集合的操作中不会出现竞争现象。


4.4、CopyOnWriteArrayList 是ArrayList 的一个线程安全的变形,其中所有可变操作(添加、设置,等等)都是通过对基础数组进行一次新的复制来实现的。这一般需要很大的开销,但是当遍历操作的数量大大超过可变操作的数量时,这种方法可能比其他替代方法更 有效。在不能或不想进行同步遍历,但又需要从并发线程中排除冲突时,它也很有用。“快照”风格的迭代器方法在创建迭代器时使用了对数组状态的引用。此数组在迭代器的生存期内绝不会更改,因此不可能发生冲突,并且迭代器保证不会抛出 ConcurrentModificationException。自创建迭代器以后,迭代器就不会反映列表的添加、移除或者更改。不支持迭代器上更改元素的操作(移除、设置和添加)。这些方法将抛出 UnsupportedOperationException。


4.5、CopyOnWriteArraySet 线程安全的无序的集合,可以将它理解成线程安全的HashSet。有意思的是,CopyOnWriteArraySet和HashSet虽然都继承于共同的父类AbstractSet;但是,HashSet是通过“散列表(HashMap)”实现的,而CopyOnWriteArraySet则是通过“动态数组(CopyOnWriteArrayList)”实现的,并不是散列表。


4.6、ConcurrentLinkedQueue 是一个基于链接节点的、无界的、线程安全的队列。此队列按照 FIFO(先进先出)原则对元素进行排序,队列的头部 是队列中时间最长的元素。队列的尾部 是队列中时间最短的元素。新的元素插入到队列的尾部,队列检索操作从队列头部获得元素。当许多线程共享访问一个公共 collection 时,ConcurrentLinkedQueue 是一个恰当的选择,此队列不允许 null 元素。


注:ArrayList和HashMap是非并发集合,迭代时不能进行修改和删除操作

注:CopyOnWriteArrayList和CopyOnWriteArraySet,最适合于读操作通常大大超过写操作的情况


5、线程安全集合及实现原理?


5.1 早期线程安全的集合


Vector:作为Collection->List接口的古老实现类;线程安全的,效率低;底层使用Object[] elementData存储


HashTable:作为Map古老的实现类;线程安全的,效率低;不能存储null的key和value(Properties为其子类:常用来处理配置文件。key和value都是String类型)


5.2 Collections包装方法


Vector和HashTable被弃用后,它们被ArrayList和HashMap代替,但它们不是线程安全的,所以Collections工具类中提供了相应的包装方法把它们包装成线程安全的集合


List<E> synArrayList = Collections.synchronizedList(new ArrayList<E>());

Set<E> synHashSet = Collections.synchronizedSet(new HashSet<E>());

Map<K,V> synHashMap = Collections.synchronizedMap(new HashMap<K,V>());

...

5.3 java.util.concurrent包中的集合


ConcurrentHashMap和HashTable都是线程安全的集合,它们的不同主要是加锁粒度上的不同。HashTable的加锁方法是给每个方法加上synchronized关键字,这样锁住的是整个Table对象。而ConcurrentHashMap是更细粒度的加锁
在JDK1.8之前,ConcurrentHashMap加的是分段锁,也就是Segment锁,每个Segment含有整个table的一部分,这样不同分段之间的并发操作就互不影响
JDK1.8对此做了进一步的改进,它取消了Segment字段,直接在table元素上加锁,实现对每一行进行加锁,进一步减小了并发冲突的概率


CopyOnWriteArrayList和CopyOnWriteArraySet
它们是加了写锁的ArrayList和ArraySet,锁住的是整个对象,但读操作可以并发执行


除此之外还有ConcurrentSkipListMap、ConcurrentSkipListSet、ConcurrentLinkedQueue、ConcurrentLinkedDeque等,至于为什么没有ConcurrentArrayList,原因是无法设计一个通用的而且可以规避ArrayList的并发瓶颈的线程安全的集合类,只能锁住整个list,这用Collections里的包装类就能办到


6、HashMap和hashTable的区别?


HashMap:作为Map的主要实现类;线程不安全的,效率高;存储null的key和value


Hashtable:作为古老的实现类;线程安全的,效率低;不能存储null的key和value


7、hashCode的作用?如何重载hashCode方法?


hashCode的存在主要是用于查找的快捷性,如Hashtable,HashMap等,hashCode是用来在散列存储结构中确定对象的存储地址的;如果两个对象相同,就是适用于equals(Java.lang.Object) 方法,那么这两个对象的hashCode一定要相同;如果对象的equals方法被重写,那么对象的hashCode也尽量重写,并且产生hashCode使用的对象,一定要和equals方法中使用的一致,否则就会违反上面提到的第2点;两个对象的hashCode相同,并不一定表示两个对象就相同,也就是不一定适用于equals(java.lang.Object)方法,只能够说明这两个对象在散列存储结构中,如Hashtable,他们“存放在同一个篮子里”。


总结:再归纳一下就是hashCode是用于查找使用的,而equals是用于比较两个对象的是否相等的。


作者:求求了瘦10斤吧
链接:https://juejin.cn/post/7039596855012884510

收起阅读 »

如何优雅地在Vue页面中引入img图片

vue
我们在学习html的时候,图片标签<img>引入图片 <img src="../assets/images/avatar.png" width="100%"> 但是这样会有2个弊端:因为采用绝对路径引入,所以如果后面这张图片移动了目录,...
继续阅读 »

我们在学习html的时候,图片标签<img>引入图片


<img src="../assets/images/avatar.png" width="100%">

但是这样会有2个弊端:

  • 因为采用绝对路径引入,所以如果后面这张图片移动了目录,就需要修改代src里的路径
  • 如果这张图片在同一页面内有多个地方要使用到,就需要引入多次,而且图片移动了目录,这么多地方都要修改src路径

怎么办?使用动态路径import、require



首先讲讲这两个兄弟,在ES6之前,JS一直没有自己的模块语法,为了解决这种尴尬就有了require.js,在ES6发布之后JS又引入了import的概念

  • 使用import引入
  • import之后需要在data中注册一下,否则显示不了


    <script>
    import lf1 from '@/assets/images/lf1.png'
    import lf2 from '@/assets/images/lf2.png'
    import lf3 from '@/assets/images/lf3.png'
    import lf4 from '@/assets/images/lf4.png'
    import lf5 from '@/assets/images/lf5.png'
    import lf6 from '@/assets/images/lf6.png'
    import lf7 from '@/assets/images/lf7.png'
    import top1 from '@/assets/images/icon_top1.png'

    export default {
    name: 'Left',
    data () {
    return {
    lf1,
    lf2,
    lf3,
    lf4,
    lf5,
    lf6,
    lf7,
    top1
    }
    }
    }
    </script>
    • 使用require引入

    <script>
    import top1 from '@/assets/images/cityOfVitality/icon_top1.png'

    export default {
    name: 'Right',
    data () {
    return {
    rt1: require('@/assets/images/crt1.png'),
    rt2: require('@/assets/images/crt2.png'),
    rt3: require('@/assets/images/crt3.png'),
    rt4: require('@/assets/images/crt4.png'),
    rt5: require('@/assets/images/crt5.png'),
    rt6: require('@/assets/images/crt6.png'),
    top1
    }
    }
    }
    </script>

    作者:Jesse90s
    链接:https://juejin.cn/post/7019964864256802829

    收起阅读 »

    原来flex布局还能那么细?

    简介: flex布局(Flexible布局,弹性布局)是在小程序开发经常使用的布局方式 开启了flex布局的元素叫做flex container flex container里面的直接子元素叫做flex items(也就是开启了flex布局的盒子包裹的...
    继续阅读 »

    简介:



    • flex布局(Flexible布局,弹性布局)是在小程序开发经常使用的布局方式

    • 开启了flex布局的元素叫做flex container




    • flex container里面的直接子元素叫做flex items(也就是开启了flex布局的盒子包裹的第一层子元素)

    • 设置display的属性为flex或者inline-flex可以开启flex布局即成为flex container




    属性值设置为flex和inline-flex的区别:



    1. 如果display对应的值是flex的话,那么flex container是以block-level的形式存在的,相当于是一个块级元素

    2. 如果display的值设置为inline-flex的话,那么flex container是以inline-level的形式存在的,相当于是一个行内块元素




    1. 这两个属性值差异的影响在设置了属性值的元素上面,它们在子元素上的效果都是一样的

    2. 如果一个元素的父元素开启了flex布局;那么其子元素的display属性对自身的影响将会失效,但是对其内容的影响依旧存在的;


    举个例子:父元素设置了display: flex,即使子元素设置了display:block或者display:inline的属性,子元素还是会表现的像个行内块元素一样,这就是父元素对其的影响使其display属性对自身的影响失效了;


    但是为什么我们说其对内容的影响还在呢?假如说父子元素都设置了display: flex,那么子元素自身依然是行块级元素,并不会因为其开启了flex布局就变为块级元素,但是该子元素的内容依然会受到它flex布局的影响,各种flex特有的属性就会生效;


    总结:我们如果想让设置flex布局的盒子变成块级元素的话,那就dispaly的属性值就设置为flex;如果想让盒子变为行内块元素的话,就设置为inline-flex;父元素开启了flex布局之后,子元素的display属性对元素本身的影响就会失效,但是依旧可以影响盒子内部的元素;


    应用在flex container上的CSS属性



    1. flex-flow



    • felx-flowflex-direction || flex-wrap的缩写,这个属性很灵活,你可以只写一个属性,也可以两个都写,甚至交换前后顺序都是可以的

    • flex-flow:column wrap === flex-direction:column;flex-wrap:wrap




    • 如果只写了一个属性值的话,那么另一个属性就直接取默认值;flex-flow:row-reverse === flex-direction:row-reverse;flex-wrap:nowrap



    1. flex-direction


    flex items默认都是沿着main axis(主轴)从main start开始往main end方向排布的



    • flex-direction决定了主轴的方向,有四个取值

    • 分别为row(默认值)、row-reversecolumncolumn-reverse




    • 注意:flex-direction并不是直接改变flex items的排列顺序,他只是通过改变了主轴方向间接的改变了顺序


    1. flex-wrap


    flex-wrap能够决定flex items是在单行还是多行显示



    • nowrap(默认):单行


    本例中父盒子宽度为500px,子盒子为100px;当增加了多个子盒子并且给父盒子设置了flex-wrap:nowrap属性后,效果如下图所示:


    我们会惊奇的发现,父盒子的宽度没有变化,子盒子也确实没有换行,但是他们的宽度均缩小至能适应不换行的条件为止了,这也就是flex布局又称为弹性布局的原因


    所以,我们也可以得出一个结论:如果使用了flex布局的话,一个盒子的大小就算是将宽高写死了也是有可能发生改变的




    • wrap:多行


    换行后元素是往哪边排列跟交叉轴的方向有很大的关系,排列方向是顺着交叉轴的方向来的;


    用的还是刚刚的例子,只不过现在将属性flex-wrap的值设置为了wrap,效果如下图所示:


    子盒子的高度在能够正常换行的情况不会发生变化,但因为当前交叉轴的方向是从上往下的,那么要换行的元素就会排列在下方




    • wrap-reverse:多行(对比wrap,cross start与cross end相反),这个方法可以让交叉轴起点和终点相反,这样整体的布局就会翻转过来



    注意:这里就不是单纯的将要换行的元素向上排列,所有的元素都会受到影响,因为交叉轴的起始点和终止点已经反过来了



    1. justify-content


    Tip:下列图像灰色部分均无任何元素,其他颜色的区域为盒子内容区域


    justify-content决定了flex items在主轴上的对齐方式,总共有6个属性值:



    • flex-start(默认值):在主轴方向上与main start对齐




    • flex-end:在主轴方向上与main end对齐




    • center:在主轴方向上居中对齐




    • space-between


    特点:



    1. 与main start、main end两端对齐

    2. flex items之间的距离相等




    • space-evenly


    特点:



    1. flex items之间的距离相等

    2. flex items与main start、main end之间的距离等于flex items的距离




    • space-around


    特点:



    1. flex items之间的距离相等

    2. flex items与main start、main end之间的距离等于flex items的距离的一半




    1. align-items


    align-items决定了单行flex items在cross axis(交叉轴)上的对齐方式


    注意:主轴只要是横向的,无论flex-direction设置的是row还是row-reverse,其交叉轴都是从上指向下的;


    主轴只要是纵向的,无论flex-direction设置的是column还是column-reverse,其交叉轴都是从左指向右的;


    也就是说:主轴可能会有四种,但是交叉轴只有两种



    该属性具有如下几个属性值:



    • stretch(默认值):当flex items在交叉轴方向上的size(指width或者height,由交叉轴方向确定)为auto时,会自动拉伸至填充;但是如果flex items的size并不是auto,那么产生的效果就和设置为flex-start一样


    注意:触发条件为:父元素设置align-items的属性值为stretch,而子元素在交叉轴方向上的size设置为auto




    • flex-start:与cross start对齐




    • flex-end:与cross end对齐




    • center:居中对齐




    • baseline:与基准线对齐



    至于baseline这个属性值,平时用的并不是很多,基准线可以认为是盒子里面文字的底线,基准线对齐就是让每个盒子文字的底线对齐


    注意:align-items的默认值与justify-content的默认值不同,它并不是flex-start,而是stretch



    1. align-content



    • align-content决定了多行flex-items在主轴上的对齐方式,用法与justify-content类似,具有以下属性值

    • stretch(默认值)、flex-startflex-endcenterspace-bewteenspace-aroundspace-evenly




    • 大部分属性值看图应该就能明白,主要说一下stretch,当flex items在交叉轴方向上的size设置为auto之后,多行元素的高度之和会挤满父盒子,并且他们的高度是均分的,这和align-itemsstretch属性有点不一样,后者是每一个元素对应的size会填充父盒子,而前者则是均分



    应用在flex items上的CSS属性



    1. flex



    • flex是flex-grow flex-shrink?|| flex-basis的简写,说明flex属性值可以是一个、两个、或者是三个,剩下的为默认值

    • 默认值为flex: 0 1 auto(不放大但会缩小)

    • none: 0 0 auto(既不放大也不缩小)

    • auto:1 1 auto(放大且缩小)

    • 但是其简写方式是多种多样的,不过我们用到最多的还是flex:n;举个"栗子":如果flex是一个非负整数n,则该数字代表的是flex-grow的值,对应的flex-shrink默认为1,但是要格外注意:这里flex-basis的值并不是默认值auto,而是改成了0%;即flex:n === flex:n 1 0%;所以我们常用的flex:1 --> flex:1 1 0%;下图是flex简写的所有情况:




    1. flex-grow



    • flex-grow决定了flex-items如何扩展

    • 可以设置任何非负数字(正整数、正小数、0),默认值为0




    • 只有当flex container在主轴上有剩余的size时,该属性才会生效

    • 如果所有的flex itemsflex-grow属性值总和sum超过1,每个flex item扩展的size就为flex container剩余size * flex-grow / sum

    • 利用上一条计算公式,我们可以得出:当flex itemsflex-grow属性值总和sum不超过1时,扩展的总长度为剩余 size * sum,但是sum又小于1,所以最终flex items不可能完全填充felx container







    • 如果所有的flex itemsflex-grow属性值总和sum不超过1,每个flex item扩展的size就为flex container剩余size * flex-grow





    注意:不要认为flex item扩展的值都是按照flex-grow/sum的比例来进行分配,也并不是说看到flex-grow是小数,就认为其分配到的空间是剩余size*flex-grow,这些都是不准确的。当看到flex item使用了该属性时,首先判断的应该是sum是否大于1,再来判断通过哪种方法来计算比例



    • flex items扩展后的最终size不能超过max-width/max-height






    1. flex-basis



    • flex-basis用来设置flex items主轴方向上的base size,以后flew-growflex-shrink计算时所需要用的base size就是这个

    • auto(默认值)、content:取决于内容本身的size,这两个属性可以认为效果都是一样的,当然也可以设置具体的值和百分数(根据父盒子的比例计算)




    • 决定flex items最终base size因素的优先级为max-width/max-height/min-width/min-height > flex-basis > width/height > 内容本身的size

    • 可以理解为给flex items设置了flex-basis属性且属性值为具体的值或者百分数的话,主轴上对应的size(width/height)就不管用了



    1. flex-shrink



    • flex-shrink决定了flex items如何收缩

    • 可以设置任意非负数字(正小数、正整数、0),默认值是1




    • flex items在主轴方向上超过了flex container的size之后,flex-shrink属性才会生效

    • 注意:与flex-grow不同,计算每个flex item缩小的大小都是通过同一个公式来的,计算比例的方式也有所不同




    • 收缩比例 = flex-shrink * flex item的base size,base size就是flex item放入flex container之前的size

    • 每个flex item收缩的size为flex items超出flex container的size * 收缩比例 / 所有flex items 的收缩比例之和




    • flex items收缩后的最终size不能小于min-width/min-height

    • 总结:当flex items的flex-shrink属性值的总和小于1时,通过其计算收缩size的公式可知,其总共收缩的距离是超出的size * sum,由于sum是小于1的,那么无论如何子盒子都不会完全收缩至超过的距离,也就是说在不换行的情况下子元素一定会有超出





    不同的盒子缩小的值和其自身的flex-shrink属性有关,而且还与自己的原始宽度有关,这是跟flex-grow最大的区别




    1. order



    • order决定了flex items的排布顺序

    • 可以设置为任意整数(正整数、负整数、0),值越小就排在越前面




    • 默认值为0,当flex itemsorder一致时,则按照渲染的顺序排列






    1. align-self



    • flex items可以通过align-self覆盖flex container设置的align-items

    • 默认值为auto:默认遵从flex containeralign-items设置




    • stretchflex-startflex-endcenterbaseline,效果跟align-items一致,简单来说,就是align-items有什么属性,align-self就有哪些属性,当然auto除外


    .item:nth-child(2) {
    align-self: flex-start;
    background-color: #f8f;
    }


    疑难点解析:


    大家在看到flex-wrap那里换行的图片会不会有疑惑,为什么换行的元素不是紧挨着上一行的元素呢?而是有点像居中了的感觉



    想想多行元素在交叉轴上是上依靠哪一个属性进行排列的,当然是align-content了,那它的默认属性值是什么呢?--->stretch


    对,就是因为默认值是stretch,但是flex item又设置了高度,所以flex item不会被拉伸,但是它们会排列在要被拉伸的位置;我们可以测试一下,将flex-items交叉轴上的size设置为auto之后,stretch属性值才会表现的更加明显,平分flex-container在主轴上的高度,每个元素所在的位置就是上一张图所在的位置



    作者:Running53
    链接:https://juejin.cn/post/7033420158685151262

    收起阅读 »

    微信小程序iOS中JS的Date() 获取到的日期时间显示NaN的解决办法

    首先,js日期格式化函数(通过将日期转化为时间戳,再转化为指定格式):function formatDateTime(timeStamp) { var date = new Date(); date.setTime(timeStamp); var y = d...
    继续阅读 »

    首先,js日期格式化函数(通过将日期转化为时间戳,再转化为指定格式):

    function formatDateTime(timeStamp) { 
    var date = new Date();
    date.setTime(timeStamp);
    var y = date.getFullYear();
    var m = date.getMonth() + 1;
    var d = date.getDate();
    m = m < 10 ? ('0' + m) : m;
    d = d < 10 ? ('0' + d) : d;
    return y + '/' + m + '/' + d;
    };

    然后new Date('2018-08-12 23:00:00').getTime(); 安卓可以,苹果iOS却出现NanNan的问题

    这是因为iOS的日期格式是/不是-

    修改后:

    new Date('2018-08-12 23:00:00'.toString().replace(/\,/g, '/')

    OK。

    同理 new Date().getDay() 获取不到当前时间之前日期的星期几 也需要替换下


    原文链接:https://blog.csdn.net/gdali/article/details/88893549

    收起阅读 »

    字节跳动面试官:请你实现一个大文件上传和断点续传(下)

    接 字节跳动面试官:请你实现一个大文件上传和断点续传(上) 断点续传的原理在于前端/服务端需要记住已上传的切片,这样下次上传就可以跳过之前已上传的部分,有两种方案实现记忆的功能第一种是前端的解决方案,第二种是服务端,而前端方案有一个缺陷,如果换了个浏览器就失...
    继续阅读 »

    字节跳动面试官:请你实现一个大文件上传和断点续传(上)



    断点续传

    断点续传的原理在于前端/服务端需要记住已上传的切片,这样下次上传就可以跳过之前已上传的部分,有两种方案实现记忆的功能

    • 前端使用 localStorage 记录已上传的切片 hash

    • 服务端保存已上传的切片 hash,前端每次上传前向服务端获取已上传的切片

    第一种是前端的解决方案,第二种是服务端,而前端方案有一个缺陷,如果换了个浏览器就失去了记忆的效果,所以这里选取后者

    生成 hash

    无论是前端还是服务端,都必须要生成文件和切片的 hash,之前我们使用文件名 + 切片下标作为切片 hash,这样做文件名一旦修改就失去了效果,而事实上只要文件内容不变,hash 就不应该变化,所以正确的做法是根据文件内容生成 hash,所以我们修改一下 hash 的生成规则

    这里用到另一个库 spark-md5,它可以根据文件内容计算出文件的 hash 值,另外考虑到如果上传一个超大文件,读取文件内容计算 hash 是非常耗费时间的,并且会引起 UI 的阻塞,导致页面假死状态,所以我们使用 web-worker 在 worker 线程计算 hash,这样用户仍可以在主界面正常的交互

    由于实例化 web-worker 时,参数是一个 js 文件路径且不能跨域,所以我们单独创建一个 hash.js 文件放在 public 目录下,另外在 worker 中也是不允许访问 dom 的,但它提供了importScripts 函数用于导入外部脚本,通过它导入 spark-md5

    // /public/hash.js
    self.importScripts("/spark-md5.min.js"); // 导入脚本

    // 生成文件 hash
    self.onmessage = e => {
    const { fileChunkList } = e.data;
    const spark = new self.SparkMD5.ArrayBuffer();
    let percentage = 0;
    let count = 0;
    const loadNext = index => {
      const reader = new FileReader();
      reader.readAsArrayBuffer(fileChunkList[index].file);
      reader.onload = e => {
        count++;
        spark.append(e.target.result);
        if (count === fileChunkList.length) {
          self.postMessage({
            percentage: 100,
            hash: spark.end()
          });
          self.close();
        } else {
          percentage += 100 / fileChunkList.length;
          self.postMessage({
            percentage
          });
          // 递归计算下一个切片
          loadNext(count);
        }
      };
    };
    loadNext(0);
    };
    复制代码

    在 worker 线程中,接受文件切片 fileChunkList,利用 FileReader 读取每个切片的 ArrayBuffer 并不断传入 spark-md5 中,每计算完一个切片通过 postMessage 向主线程发送一个进度事件,全部完成后将最终的 hash 发送给主线程

    spark-md5 需要根据所有切片才能算出一个 hash 值,不能直接将整个文件放入计算,否则即使不同文件也会有相同的 hash,具体可以看官方文档

    spark-md5

    接着编写主线程与 worker 线程通讯的逻辑

    +      // 生成文件 hash(web-worker)
    +   calculateHash(fileChunkList) {
    +     return new Promise(resolve => {
    +       // 添加 worker 属性
    +       this.container.worker = new Worker("/hash.js");
    +       this.container.worker.postMessage({ fileChunkList });
    +       this.container.worker.onmessage = e => {
    +         const { percentage, hash } = e.data;
    +         this.hashPercentage = percentage;
    +         if (hash) {
    +           resolve(hash);
    +         }
    +       };
    +     });
      },
      async handleUpload() {
        if (!this.container.file) return;
        const fileChunkList = this.createFileChunk(this.container.file);
    +     this.container.hash = await this.calculateHash(fileChunkList);
        this.data = fileChunkList.map(({ file },index) => ({
    +       fileHash: this.container.hash,
          chunk: file,
          hash: this.container.file.name + "-" + index, // 文件名 + 数组下标
          percentage:0
        }));
        await this.uploadChunks();
      }  
    复制代码

    主线程使用 postMessage 给 worker 线程传入所有切片 fileChunkList,并监听 worker 线程发出的 postMessage 事件拿到文件 hash

    加上显示计算 hash 的进度条,看起来像这样

    img

    至此前端需要将之前用文件名作为 hash 的地方改写为 workder 返回的这个 hash

    img

    服务端则使用 hash 作为切片文件夹名,hash + 下标作为切片名,hash + 扩展名作为文件名,没有新增的逻辑

    img

    img

    文件秒传

    在实现断点续传前先简单介绍一下文件秒传

    所谓的文件秒传,即在服务端已经存在了上传的资源,所以当用户再次上传时会直接提示上传成功

    文件秒传需要依赖上一步生成的 hash,即在上传前,先计算出文件 hash,并把 hash 发送给服务端进行验证,由于 hash 的唯一性,所以一旦服务端能找到 hash 相同的文件,则直接返回上传成功的信息即可

    +    async verifyUpload(filename, fileHash) {
    +       const { data } = await this.request({
    +         url: "http://localhost:3000/verify",
    +         headers: {
    +           "content-type": "application/json"
    +         },
    +         data: JSON.stringify({
    +           filename,
    +           fileHash
    +         })
    +       });
    +       return JSON.parse(data);
    +     },
      async handleUpload() {
        if (!this.container.file) return;
        const fileChunkList = this.createFileChunk(this.container.file);
        this.container.hash = await this.calculateHash(fileChunkList);
    +     const { shouldUpload } = await this.verifyUpload(
    +       this.container.file.name,
    +       this.container.hash
    +     );
    +     if (!shouldUpload) {
    +       this.$message.success("秒传:上传成功");
    +       return;
    +   }
        this.data = fileChunkList.map(({ file }, index) => ({
          fileHash: this.container.hash,
          index,
          hash: this.container.hash + "-" + index,
          chunk: file,
          percentage: 0
        }));
        await this.uploadChunks();
      }  
    复制代码

    秒传其实就是给用户看的障眼法,实质上根本没有上传

    image-20200109143511277

    :)

    服务端的逻辑非常简单,新增一个验证接口,验证文件是否存在即可

    + const extractExt = filename =>
    + filename.slice(filename.lastIndexOf("."), filename.length); // 提取后缀名
    const UPLOAD_DIR = path.resolve(__dirname, "..", "target"); // 大文件存储目录

    const resolvePost = req =>
    new Promise(resolve => {
      let chunk = "";
      req.on("data", data => {
        chunk += data;
      });
      req.on("end", () => {
        resolve(JSON.parse(chunk));
      });
    });

    server.on("request", async (req, res) => {
    if (req.url === "/verify") {
    +   const data = await resolvePost(req);
    +   const { fileHash, filename } = data;
    +   const ext = extractExt(filename);
    +   const filePath = path.resolve(UPLOAD_DIR, `${fileHash}${ext}`);
    +   if (fse.existsSync(filePath)) {
    +     res.end(
    +       JSON.stringify({
    +         shouldUpload: false
    +       })
    +     );
    +   } else {
    +     res.end(
    +       JSON.stringify({
    +         shouldUpload: true
    +       })
    +     );
    +   }
    }
    });
    server.listen(3000, () => console.log("正在监听 3000 端口"));
    复制代码

    暂停上传

    讲完了生成 hash 和文件秒传,回到断点续传

    断点续传顾名思义即断点 + 续传,所以我们第一步先实现“断点”,也就是暂停上传

    原理是使用 XMLHttpRequest 的 abort 方法,可以取消一个 xhr 请求的发送,为此我们需要将上传每个切片的 xhr 对象保存起来,我们再改造一下 request 方法

       request({
        url,
        method = "post",
        data,
        headers = {},
        onProgress = e => e,
    +     requestList
      }) {
        return new Promise(resolve => {
          const xhr = new XMLHttpRequest();
          xhr.upload.onprogress = onProgress;
          xhr.open(method, url);
          Object.keys(headers).forEach(key =>
            xhr.setRequestHeader(key, headers[key])
          );
          xhr.send(data);
          xhr.onload = e => {
    +         // 将请求成功的 xhr 从列表中删除
    +         if (requestList) {
    +           const xhrIndex = requestList.findIndex(item => item === xhr);
    +           requestList.splice(xhrIndex, 1);
    +         }
            resolve({
              data: e.target.response
            });
          };
    +       // 暴露当前 xhr 给外部
    +       requestList?.push(xhr);
        });
      },
    复制代码

    这样在上传切片时传入 requestList 数组作为参数,request 方法就会将所有的 xhr 保存在数组中了

    img

    每当一个切片上传成功时,将对应的 xhr 从 requestList 中删除,所以 requestList 中只保存正在上传切片的 xhr

    之后新建一个暂停按钮,当点击按钮时,调用保存在 requestList 中 xhr 的 abort 方法,即取消并清空所有正在上传的切片

     handlePause() {
      this.requestList.forEach(xhr => xhr?.abort());
      this.requestList = [];
    }
    复制代码

    image-20200109143737924

    点击暂停按钮可以看到 xhr 都被取消了

    img

    恢复上传

    之前在介绍断点续传的时提到使用第二种服务端存储的方式实现续传

    由于当文件切片上传后,服务端会建立一个文件夹存储所有上传的切片,所以每次前端上传前可以调用一个接口,服务端将已上传的切片的切片名返回,前端再跳过这些已经上传切片,这样就实现了“续传”的效果

    而这个接口可以和之前秒传的验证接口合并,前端每次上传前发送一个验证的请求,返回两种结果

    • 服务端已存在该文件,不需要再次上传

    • 服务端不存在该文件或者已上传部分文件切片,通知前端进行上传,并把已上传的文件切片返回给前端

    所以我们改造一下之前文件秒传的服务端验证接口

    const extractExt = filename =>
    filename.slice(filename.lastIndexOf("."), filename.length); // 提取后缀名
    const UPLOAD_DIR = path.resolve(__dirname, "..", "target"); // 大文件存储目录

    const resolvePost = req =>
    new Promise(resolve => {
      let chunk = "";
      req.on("data", data => {
        chunk += data;
      });
      req.on("end", () => {
        resolve(JSON.parse(chunk));
      });
    });
     
    + // 返回已经上传切片名列表
    + const createUploadedList = async fileHash =>
    +   fse.existsSync(path.resolve(UPLOAD_DIR, fileHash))
    +   ? await fse.readdir(path.resolve(UPLOAD_DIR, fileHash))
    +   : [];

    server.on("request", async (req, res) => {
    if (req.url === "/verify") {
      const data = await resolvePost(req);
      const { fileHash, filename } = data;
      const ext = extractExt(filename);
      const filePath = path.resolve(UPLOAD_DIR, `${fileHash}${ext}`);
      if (fse.existsSync(filePath)) {
        res.end(
          JSON.stringify({
            shouldUpload: false
          })
        );
      } else {
        res.end(
          JSON.stringify({
            shouldUpload: true
    +         uploadedList: await createUploadedList(fileHash)
          })
        );
      }
    }
    });
    server.listen(3000, () => console.log("正在监听 3000 端口"));
    复制代码

    接着回到前端,前端有两个地方需要调用验证的接口

    • 点击上传时,检查是否需要上传和已上传的切片

    • 点击暂停后的恢复上传,返回已上传的切片

    新增恢复按钮并改造原来上传切片的逻辑



    +   async handleResume() {
    +     const { uploadedList } = await this.verifyUpload(
    +       this.container.file.name,
    +       this.container.hash
    +     );
    +     await this.uploadChunks(uploadedList);
      },
      async handleUpload() {
        if (!this.container.file) return;
        const fileChunkList = this.createFileChunk(this.container.file);
        this.container.hash = await this.calculateHash(fileChunkList);

    +     const { shouldUpload, uploadedList } = await this.verifyUpload(
          this.container.file.name,
          this.container.hash
        );
        if (!shouldUpload) {
          this.$message.success("秒传:上传成功");
          return;
        }

        this.data = fileChunkList.map(({ file }, index) => ({
          fileHash: this.container.hash,
          index,
          hash: this.container.hash + "-" + index,
          chunk: file,
          percentage: 0
        }));

    +     await this.uploadChunks(uploadedList);
      },
      // 上传切片,同时过滤已上传的切片
    +   async uploadChunks(uploadedList = []) {
        const requestList = this.data
    +       .filter(({ hash }) => !uploadedList.includes(hash))
          .map(({ chunk, hash, index }) => {
            const formData = new FormData();
            formData.append("chunk", chunk);
            formData.append("hash", hash);
            formData.append("filename", this.container.file.name);
            formData.append("fileHash", this.container.hash);
            return { formData, index };
          })
          .map(async ({ formData, index }) =>
            this.request({
              url: "http://localhost:3000",
              data: formData,
              onProgress: this.createProgressHandler(this.data[index]),
              requestList: this.requestList
            })
          );
        await Promise.all(requestList);
        // 之前上传的切片数量 + 本次上传的切片数量 = 所有切片数量时
        // 合并切片
    +     if (uploadedList.length + requestList.length === this.data.length) {
            await this.mergeRequest();
    +     }
      }
    复制代码

    image-20200109144331326

    这里给原来上传切片的函数新增 uploadedList 参数,即上图中服务端返回的切片名列表,通过 filter 过滤掉已上传的切片,并且由于新增了已上传的部分,所以之前合并接口的触发条件做了一些改动

    到这里断点续传的功能基本完成了

    进度条改进

    虽然实现了断点续传,但还需要修改一下进度条的显示规则,否则在暂停上传/接收到已上传切片时的进度条会出现偏差

    切片进度条

    由于在点击上传/恢复上传时,会调用验证接口返回已上传的切片,所以需要将已上传切片的进度变成 100%

       async handleUpload() {
        if (!this.container.file) return;
        const fileChunkList = this.createFileChunk(this.container.file);
        this.container.hash = await this.calculateHash(fileChunkList);
        const { shouldUpload, uploadedList } = await this.verifyUpload(
          this.container.file.name,
          this.container.hash
        );
        if (!shouldUpload) {
          this.$message.success("秒传:上传成功");
          return;
        }
        this.data = fileChunkList.map(({ file }, index) => ({
          fileHash: this.container.hash,
          index,
          hash: this.container.hash + "-" + index,
          chunk: file,
    +       percentage: uploadedList.includes(index) ? 100 : 0
        }));
        await this.uploadChunks(uploadedList);
      },
    复制代码

    uploadedList 会返回已上传的切片,在遍历所有切片时判断当前切片是否在已上传列表里即可

    文件进度条

    之前说到文件进度条是一个计算属性,根据所有切片的上传进度计算而来,这就遇到了一个问题

    img

    点击暂停会取消并清空切片的 xhr 请求,此时如果已经上传了一部分,就会发现文件进度条有倒退的现象

    img

    当点击恢复时,由于重新创建了 xhr 导致切片进度清零,所以总进度条就会倒退

    解决方案是创建一个“假”的进度条,这个假进度条基于文件进度条,但只会停止和增加,然后给用户展示这个假的进度条

    这里我们使用 Vue 的监听属性

      data: () => ({
    +   fakeUploadPercentage: 0
    }),
    computed: {
      uploadPercentage() {
        if (!this.container.file || !this.data.length) return 0;
        const loaded = this.data
          .map(item => item.size * item.percentage)
          .reduce((acc, cur) => acc + cur);
        return parseInt((loaded / this.container.file.size).toFixed(2));
      }
    },  
    watch: {
    +   uploadPercentage(now) {
    +     if (now > this.fakeUploadPercentage) {
    +       this.fakeUploadPercentage = now;
    +     }
      }
    },
    复制代码

    当 uploadPercentage 即真的文件进度条增加时,fakeUploadPercentage 也增加,一旦文件进度条后退,假的进度条只需停止即可

    至此一个大文件上传 + 断点续传的解决方案就完成了

    总结

    大文件上传

    • 前端上传大文件时使用 Blob.prototype.slice 将文件切片,并发上传多个切片,最后发送一个合并的请求通知服务端合并切片

    • 服务端接收切片并存储,收到合并请求后使用流将切片合并到最终文件

    • 原生 XMLHttpRequest 的 upload.onprogress 对切片上传进度的监听

    • 使用 Vue 计算属性根据每个切片的进度算出整个文件的上传进度

    断点续传

    • 使用 spark-md5 根据文件内容算出文件 hash

    • 通过 hash 可以判断服务端是否已经上传该文件,从而直接提示用户上传成功(秒传)

    • 通过 XMLHttpRequest 的 abort 方法暂停切片的上传

    • 上传前服务端返回已经上传的切片名,前端跳过这些切片的上传

    反馈的问题

    部分功能由于不方便测试,这里列出评论区收集到的一些问题,有兴趣的朋友可以提出你的想法/写个 demo 进一步交流

    • 没有做切片上传失败的处理

    • 使用 web socket 由服务端发送进度信息

    • 打开页面没有自动获取上传切片,而需要主动再次上传一次后才显示

    源代码

    源代码增加了一些按钮的状态,交互更加友好,文章表达比较晦涩的地方可以跳转到源代码查看

    file-upload

    谢谢观看 :)

    作者:yeyan1996
    来源:https://juejin.cn/post/6844904046436843527

    收起阅读 »

    字节跳动面试官:请你实现一个大文件上传和断点续传(上)

    前言事实上我在面试的时候确实被问到了这个问题,而且是一道在线 coding 的编程题,当时虽然思路正确,可惜最终也并不算完全答对本文将从零搭建前端和服务端,实现一个大文件上传和断点续传的 demo服务端:nodejs文章有误解的地方,欢迎指出,将在第一时间改正...
    继续阅读 »



    前言

    这段时间面试官都挺忙的,频频出现在博客文章标题,虽然我不是特别想蹭热度,但是实在想不到好的标题了-。-,蹭蹭就蹭蹭 :)

    事实上我在面试的时候确实被问到了这个问题,而且是一道在线 coding 的编程题,当时虽然思路正确,可惜最终也并不算完全答对

    结束后花了一段时间整理了下思路,那么究竟该如何实现一个大文件上传,以及在上传中如何实现断点续传的功能呢?

    本文将从零搭建前端和服务端,实现一个大文件上传和断点续传的 demo

    前端:vue element-ui

    服务端:nodejs

    文章有误解的地方,欢迎指出,将在第一时间改正,有更好的实现方式希望留下你的评论

    大文件上传

    整体思路

    前端

    前端大文件上传网上的大部分文章已经给出了解决方案,核心是利用 Blob.prototype.slice 方法,和数组的 slice 方法相似,调用的 slice 方法可以返回原文件的某个切片

    这样我们就可以根据预先设置好的切片最大数量将文件切分为一个个切片,然后借助 http 的可并发性,同时上传多个切片,这样从原本传一个大文件,变成了同时传多个小的文件切片,可以大大减少上传时间

    另外由于是并发,传输到服务端的顺序可能会发生变化,所以我们还需要给每个切片记录顺序

    服务端

    服务端需要负责接受这些切片,并在接收到所有切片后合并切片

    这里又引伸出两个问题

    1. 何时合并切片,即切片什么时候传输完成

    2. 如何合并切片

    第一个问题需要前端进行配合,前端在每个切片中都携带切片最大数量的信息,当服务端接受到这个数量的切片时自动合并,也可以额外发一个请求主动通知服务端进行切片的合并

    第二个问题,具体如何合并切片呢?这里可以使用 nodejs 的 读写流(readStream/writeStream),将所有切片的流传输到最终文件的流里

    talk is cheap,show me the code,接着我们用代码实现上面的思路

    前端部分

    前端使用 Vue 作为开发框架,对界面没有太大要求,原生也可以,考虑到美观使用 element-ui 作为 UI 框架

    上传控件

    首先创建选择文件的控件,监听 change 事件以及上传按钮




    复制代码

    请求逻辑

    考虑到通用性,这里没有用第三方的请求库,而是用原生 XMLHttpRequest 做一层简单的封装来发请求

    request({
        url,
        method = "post",
        data,
        headers = {},
        requestList
      }) {
        return new Promise(resolve => {
          const xhr = new XMLHttpRequest();
          xhr.open(method, url);
          Object.keys(headers).forEach(key =>
            xhr.setRequestHeader(key, headers[key])
          );
          xhr.send(data);
          xhr.onload = e => {
            resolve({
              data: e.target.response
            });
          };
        });
      }
    复制代码

    上传切片

    接着实现比较重要的上传功能,上传需要做两件事

    • 对文件进行切片

    • 将切片传输给服务端




    复制代码

    当点击上传按钮时,调用 createFileChunk 将文件切片,切片数量通过文件大小控制,这里设置 10MB,也就是说 100 MB 的文件会被分成 10 个切片

    createFileChunk 内使用 while 循环和 slice 方法将切片放入 fileChunkList 数组中返回

    在生成文件切片时,需要给每个切片一个标识作为 hash,这里暂时使用文件名 + 下标,这样后端可以知道当前切片是第几个切片,用于之后的合并切片

    随后调用 uploadChunks 上传所有的文件切片,将文件切片,切片 hash,以及文件名放入 FormData 中,再调用上一步的 request 函数返回一个 proimise,最后调用 Promise.all 并发上传所有的切片

    发送合并请求

    这里使用整体思路中提到的第二种合并切片的方式,即前端主动通知服务端进行合并,所以前端还需要额外发请求,服务端接受到这个请求时主动合并切片




    复制代码

    服务端部分

    简单使用 http 模块搭建服务端

    const http = require("http");
    const server = http.createServer();

    server.on("request", async (req, res) => {
    res.setHeader("Access-Control-Allow-Origin", "*");
    res.setHeader("Access-Control-Allow-Headers", "*");
    if (req.method === "OPTIONS") {
      res.status = 200;
      res.end();
      return;
    }
    });

    server.listen(3000, () => console.log("正在监听 3000 端口"));
    复制代码

    接受切片

    使用 multiparty 包处理前端传来的 FormData

    在 multiparty.parse 的回调中,files 参数保存了 FormData 中文件,fields 参数保存了 FormData 中非文件的字段

    const http = require("http");
    const path = require("path");
    const fse = require("fs-extra");
    const multiparty = require("multiparty");

    const server = http.createServer();
    + const UPLOAD_DIR = path.resolve(__dirname, "..", "target"); // 大文件存储目录

    server.on("request", async (req, res) => {
    res.setHeader("Access-Control-Allow-Origin", "*");
    res.setHeader("Access-Control-Allow-Headers", "*");
    if (req.method === "OPTIONS") {
      res.status = 200;
      res.end();
      return;
    }

    + const multipart = new multiparty.Form();

    + multipart.parse(req, async (err, fields, files) => {
    +   if (err) {
    +     return;
    +   }
    +   const [chunk] = files.chunk;
    +   const [hash] = fields.hash;
    +   const [filename] = fields.filename;
    +   const chunkDir = path.resolve(UPLOAD_DIR, filename);

    +   // 切片目录不存在,创建切片目录
    +   if (!fse.existsSync(chunkDir)) {
    +     await fse.mkdirs(chunkDir);
    +   }

    +     // fs-extra 专用方法,类似 fs.rename 并且跨平台
    +     // fs-extra 的 rename 方法 windows 平台会有权限问题
    +     // https://github.com/meteor/meteor/issues/7852#issuecomment-255767835
    +     await fse.move(chunk.path, `${chunkDir}/${hash}`);
    +   res.end("received file chunk");
    + });
    });

    server.listen(3000, () => console.log("正在监听 3000 端口"));
    复制代码

    image-20200110215559194

    查看 multiparty 处理后的 chunk 对象,path 是存储临时文件的路径,size 是临时文件大小,在 multiparty 文档中提到可以使用 fs.rename(由于我用的是 fs-extra,它的 rename 方法 windows 平台权限问题,所以换成了 fse.move) 移动临时文件,即移动文件切片

    在接受文件切片时,需要先创建存储切片的文件夹,由于前端在发送每个切片时额外携带了唯一值 hash,所以以 hash 作为文件名,将切片从临时路径移动切片文件夹中,最后的结果如下

    img

    合并切片

    在接收到前端发送的合并请求后,服务端将文件夹下的所有切片进行合并

    const http = require("http");
    const path = require("path");
    const fse = require("fs-extra");

    const server = http.createServer();
    const UPLOAD_DIR = path.resolve(__dirname, "..", "target"); // 大文件存储目录

    + const resolvePost = req =>
    +   new Promise(resolve => {
    +     let chunk = "";
    +     req.on("data", data => {
    +       chunk += data;
    +     });
    +     req.on("end", () => {
    +       resolve(JSON.parse(chunk));
    +     });
    +   });

    + const pipeStream = (path, writeStream) =>
    + new Promise(resolve => {
    +   const readStream = fse.createReadStream(path);
    +   readStream.on("end", () => {
    +     fse.unlinkSync(path);
    +     resolve();
    +   });
    +   readStream.pipe(writeStream);
    + });

    // 合并切片
    + const mergeFileChunk = async (filePath, filename, size) => {
    + const chunkDir = path.resolve(UPLOAD_DIR, filename);
    + const chunkPaths = await fse.readdir(chunkDir);
    + // 根据切片下标进行排序
    + // 否则直接读取目录的获得的顺序可能会错乱
    + chunkPaths.sort((a, b) => a.split("-")[1] - b.split("-")[1]);
    + await Promise.all(
    +   chunkPaths.map((chunkPath, index) =>
    +     pipeStream(
    +       path.resolve(chunkDir, chunkPath),
    +       // 指定位置创建可写流
    +       fse.createWriteStream(filePath, {
    +         start: index * size,
    +         end: (index + 1) * size
    +       })
    +     )
    +   )
    + );
    + fse.rmdirSync(chunkDir); // 合并后删除保存切片的目录
    +};

    server.on("request", async (req, res) => {
    res.setHeader("Access-Control-Allow-Origin", "*");
    res.setHeader("Access-Control-Allow-Headers", "*");
    if (req.method === "OPTIONS") {
      res.status = 200;
      res.end();
      return;
    }

    +   if (req.url === "/merge") {
    +     const data = await resolvePost(req);
    +     const { filename,size } = data;
    +     const filePath = path.resolve(UPLOAD_DIR, `${filename}`);
    +     await mergeFileChunk(filePath, filename);
    +     res.end(
    +       JSON.stringify({
    +         code: 0,
    +         message: "file merged success"
    +       })
    +     );
    +   }

    });

    server.listen(3000, () => console.log("正在监听 3000 端口"));
    复制代码

    由于前端在发送合并请求时会携带文件名,服务端根据文件名可以找到上一步创建的切片文件夹

    接着使用 fs.createWriteStream 创建一个可写流,可写流文件名就是切片文件夹名 + 后缀名组合而成

    随后遍历整个切片文件夹,将切片通过 fs.createReadStream 创建可读流,传输合并到目标文件中

    值得注意的是每次可读流都会传输到可写流的指定位置,这是通过 createWriteStream 的第二个参数 start/end 控制的,目的是能够并发合并多个可读流到可写流中,这样即使流的顺序不同也能传输到正确的位置,所以这里还需要让前端在请求的时候多提供一个 size 参数

       async mergeRequest() {
        await this.request({
          url: "http://localhost:3000/merge",
          headers: {
            "content-type": "application/json"
          },
          data: JSON.stringify({
    +         size: SIZE,
            filename: this.container.file.name
          })
        });
      },
    复制代码

    img

    其实也可以等上一个切片合并完后再合并下个切片,这样就不需要指定位置,但传输速度会降低,所以使用了并发合并的手段,接着只要保证每次合并完成后删除这个切片,等所有切片都合并完毕后最后删除切片文件夹即可

    img

    至此一个简单的大文件上传就完成了,接下来我们再此基础上扩展一些额外的功能

    显示上传进度条

    上传进度分两种,一个是每个切片的上传进度,另一个是整个文件的上传进度,而整个文件的上传进度是基于每个切片上传进度计算而来,所以我们先实现切片的上传进度

    切片进度条

    XMLHttpRequest 原生支持上传进度的监听,只需要监听 upload.onprogress 即可,我们在原来的 request 基础上传入 onProgress 参数,给 XMLHttpRequest 注册监听事件

     // xhr
      request({
        url,
        method = "post",
        data,
        headers = {},
    +     onProgress = e => e,
        requestList
      }) {
        return new Promise(resolve => {
          const xhr = new XMLHttpRequest();
    +       xhr.upload.onprogress = onProgress;
          xhr.open(method, url);
          Object.keys(headers).forEach(key =>
            xhr.setRequestHeader(key, headers[key])
          );
          xhr.send(data);
          xhr.onload = e => {
            resolve({
              data: e.target.response
            });
          };
        });
      }
    复制代码

    由于每个切片都需要触发独立的监听事件,所以还需要一个工厂函数,根据传入的切片返回不同的监听函数

    在原先的前端上传逻辑中新增监听函数部分

        // 上传切片,同时过滤已上传的切片
      async uploadChunks(uploadedList = []) {
        const requestList = this.data
    +       .map(({ chunk,hash,index }) => {
            const formData = new FormData();
            formData.append("chunk", chunk);
            formData.append("hash", hash);
            formData.append("filename", this.container.file.name);
    +         return { formData,index };
          })
    +       .map(async ({ formData,index }) =>
            this.request({
              url: "http://localhost:3000",
              data: formData,
    +           onProgress: this.createProgressHandler(this.data[index]),
            })
          );
        await Promise.all(requestList);
          // 合并切片
        await this.mergeRequest();
      },
      async handleUpload() {
        if (!this.container.file) return;
        const fileChunkList = this.createFileChunk(this.container.file);
        this.data = fileChunkList.map(({ file },index) => ({
          chunk: file,
    +       index,
          hash: this.container.file.name + "-" + index
    +       percentage:0
        }));
        await this.uploadChunks();
      }    
    +   createProgressHandler(item) {
    +     return e => {
    +       item.percentage = parseInt(String((e.loaded / e.total) * 100));
    +     };
    +   }
    复制代码

    每个切片在上传时都会通过监听函数更新 data 数组对应元素的 percentage 属性,之后把将 data 数组放到视图中展示即可

    文件进度条

    将每个切片已上传的部分累加,除以整个文件的大小,就能得出当前文件的上传进度,所以这里使用 Vue 计算属性

      computed: {
          uploadPercentage() {
            if (!this.container.file || !this.data.length) return 0;
            const loaded = this.data
              .map(item => item.size * item.percentage)
              .reduce((acc, cur) => acc + cur);
            return parseInt((loaded / this.container.file.size).toFixed(2));
          }
    }
    复制代码

    最终视图如下

    img

    字节跳动面试官:请你实现一个大文件上传和断点续传(下)

    作者:yeyan1996
    来源:https://juejin.cn/post/6844904046436843527

    收起阅读 »

    看完这篇文章保你面试稳操胜券——React篇

    ✨欢迎各位小伙伴:\textcolor{blue}{欢迎各位小伙伴:}欢迎各位小伙伴: ✨ 进大厂收藏这一系列就够了,全方位搜集总结,为大家归纳出这篇面试宝典,面试途中祝你一臂之力!,共分为四个系列 ✨包含Vue40道经典面试题\textcolor{g...
    继续阅读 »



    ✨欢迎各位小伙伴:\textcolor{blue}{欢迎各位小伙伴:}欢迎各位小伙伴:
    ✨ 进大厂收藏这一系列就够了,全方位搜集总结,为大家归纳出这篇面试宝典,面试途中祝你一臂之力!,共分为四个系列
    ✨包含Vue40道经典面试题\textcolor{green}{包含Vue40道经典面试题}包含Vue40道经典面试题
    ✨包含react12道高并发面试题\textcolor{green}{包含react12道高并发面试题}包含react12道高并发面试题
    ✨包含微信小程序34道必问面试题\textcolor{green}{包含微信小程序34道必问面试题}包含微信小程序34道必问面试题
    ✨包含javaScript80道扩展面试题\textcolor{green}{包含javaScript80道扩展面试题}包含javaScript80道扩展面试题
    ✨包含APP10道装逼面试题\textcolor{green}{包含APP10道装逼面试题}包含APP10道装逼面试题
    ✨包含HTML/CSS30道基础面试题\textcolor{green}{包含HTML/CSS30道基础面试题}包含HTML/CSS30道基础面试题
    ✨还包含Git、前端优化、ES6、Axios面试题\textcolor{green}{还包含Git、前端优化、ES6、Axios面试题}还包含Git、前端优化、ES6、Axios面试题
    ✨接下来让我们饱享这顿美味吧。一起来学习吧!!!\textcolor{pink}{接下来让我们饱享这顿美味吧。一起来学习吧!!!}接下来让我们饱享这顿美味吧。一起来学习吧!!!
    ✨本篇为《看完这篇文章保你面试稳操胜券》第五篇(react、app、git)\textcolor{pink}{本篇为《看完这篇文章保你面试稳操胜券》第五篇(react、app、git)}本篇为《看完这篇文章保你面试稳操胜券》第五篇(react、app、git)

    react

    React 中 keys 的作用是什么?

    Keys是React用于追踪哪些列表中元素被修改、被添加或者被移除的辅助标识 在开发过程中,我们需要保证某个元素的 key 在其同级元素中具有唯一性。 在 React Diff 算法中React 会借助元素的 Key 值来判断该元素是新近创建的还是被移动而来的元素, 从而减少不必要的元素重渲染。此外,React 还需要借助 Key 值来判断元素与本地状态的关联关系, 因此我们绝不可忽视转换函数中 Key 的重要性

    传入 setState 函数的第二个参数的作用是什么?

    该函数会在 setState 函数调用完成并且组件开始重渲染的时候被调用,我们可以用该函数来监听渲染是否完成

    React 中 refs 的作用是什么

    Refs 是 React 提供给我们的安全访问 DOM元素或者某个组件实例的句柄 可以为元素添加ref属性然后在回调函数中接受该元素在 DOM 树中的句柄,该值会作为回调函数的第一个参数返回

    在生命周期中的哪一步你应该发起 AJAX 请求

    我们应当将AJAX 请求放到 componentDidMount 函数中执行,主要原因有下

    React 下一代调和算法 Fiber 会通过开始或停止渲染的方式优化应用性能,其会影响到 componentWillMount 的触发次数。对于 componentWillMount 这个生命周期函数的调用次数会变得不确定,React 可能会多次频繁调用 componentWillMount。如果我们将 AJAX 请求放到 componentWillMount 函数中,那么显而易见其会被触发多次,自然也就不是好的选择。 如果我们将AJAX 请求放置在生命周期的其他函数中,我们并不能保证请求仅在组件挂载完毕后才会要求响应。如果我们的数据请求在组件挂载之前就完成,并且调用了setState函数将数据添加到组件状态中,对于未挂载的组件则会报错。而在 componentDidMount 函数中进行 AJAX 请求则能有效避免这个问题

    shouldComponentUpdate 的作用

    shouldComponentUpdate 允许我们手动地判断是否要进行组件更新,根据组件的应用场景设置函数的合理返回值能够帮我们避免不必要的更新

    如何告诉 React 它应该编译生产环境版

    通常情况下我们会使用 Webpack 的 DefinePlugin 方法来将 NODE_ENV 变量值设置为 production。 编译版本中 React会忽略 propType 验证以及其他的告警信息,同时还会降低代码库的大小, React 使用了 Uglify 插件来移除生产环境下不必要的注释等信息

    概述下 React 中的事件处理逻辑

    为了解决跨浏览器兼容性问题,React 会将浏览器原生事件(Browser Native Event)封装为合成事件(SyntheticEvent)传入设置的事件处理器中。 这里的合成事件提供了与原生事件相同的接口,不过它们屏蔽了底层浏览器的细节差异,保证了行为的一致性。 另外有意思的是,React 并没有直接将事件附着到子元素上,而是以单一事件监听器的方式将所有的事件发送到顶层进行处理。 这样 React 在更新 DOM 的时候就不需要考虑如何去处理附着在 DOM 上的事件监听器,最终达到优化性能的目的

    createElement 与 cloneElement 的区别是什么

    createElement 函数是 JSX 编译之后使用的创建 React Element 的函数,而 cloneElement 则是用于复制某个元素并传入新的 Props

    redux中间件

    中间件提供第三方插件的模式,自定义拦截 action -> reducer 的过程。变为 action -> middlewares -> reducer。 这种机制可以让我们改变数据流,实现如异步action ,action 过滤,日志输出,异常报告等功能 redux-logger:提供日志输出 redux-thunk:处理异步操作 redux-promise:处理异步操作,actionCreator的返回值是promise

    react组件的划分业务组件技术组件?

    根据组件的职责通常把组件分为UI组件和容器组件。 UI 组件负责 UI 的呈现,容器组件负责管理数据和逻辑。 两者通过React-Redux 提供connect方法联系起来

    react旧版生命周期函数

    初始化阶段

    getDefaultProps:获取实例的默认属性 getInitialState:获取每个实例的初始化状态 componentWillMount:组件即将被装载、渲染到页面上 render:组件在这里生成虚拟的DOM节点 componentDidMount:组件真正在被装载之后 运行中状态

    componentWillReceiveProps:组件将要接收到属性的时候调用 shouldComponentUpdate:组件接受到新属性或者新状态的时候(可以返回false,接收数据后不更新,阻止render调用,后面的函数不会被继续执行了) componentWillUpdate:组件即将更新不能修改属性和状态 render:组件重新描绘 componentDidUpdate:组件已经更新 销毁阶段

    componentWillUnmount:组件即将销毁

    新版生命周期

    在新版本中,React 官方对生命周期有了新的 变动建议:

    使用getDerivedStateFromProps替换componentWillMount; 使用getSnapshotBeforeUpdate替换componentWillUpdate; 避免使用componentWillReceiveProps; 其实该变动的原因,正是由于上述提到的 Fiber。首先,从上面我们知道 React 可以分成 reconciliation 与 commit两个阶段,对应的生命周期如下:

    reconciliation

    componentWillMount componentWillReceiveProps shouldComponentUpdate componentWillUpdate commit

    componentDidMount componentDidUpdate componentWillUnmount 在 Fiber 中,reconciliation 阶段进行了任务分割,涉及到 暂停 和 重启,因此可能会导致 reconciliation 中的生命周期函数在一次更新渲染循环中被 多次调用 的情况,产生一些意外错误

    Git相关面试题

    git代码冲突处理

    先将本地修改存储起来 git stash 暂存了本地修改之后,就可以pull了。 git pull 还原暂存的内容 git stash pop stash@{0}

    避免重复的合并冲突

    正如每个开发人员都知道的那样,修复合并冲突相当繁琐,但重复解决完全相同的冲突(例如,在长时间运行的功能分支中)更让人心烦。解决方案是:

    git config --global rerere.enabled true 或者你可以通过手动创建目录在每个项目的基础上启用.git/rr-cache。

    使用其他设备从GitHub中导出远程分支项目,无法成功。

    其原因在于本地中根本没有其分支。解决命令如下: git fetch -- 获取所有分支的更新 git branch -a -- 查看本地和远程分支列表,remotes开头的均为远程分支 -- 导出其远程分支,并通过-b设定本地分支跟踪远程分支 git checkout remotes/branch_name -b branch_name

    APP相关面试题

    你平常会看日志吗, 一般会出现哪些异常(Exception)?

    这个主要是面试官考察你会不会看日志,是不是看得懂java里面抛出的异常,Exception

    一般面试中java Exception(runtimeException )是必会被问到的问题 app崩溃的常见原因应该也是这些了。常见的异常列出四五种,是基本要求。

    常见的几种如下:

    NullPointerException - 空指针引用异常 ClassCastException - 类型强制转换异常。 IllegalArgumentException - 传递非法参数异常。 ArithmeticException - 算术运算异常 ArrayStoreException - 向数组中存放与声明类型不兼容对象异常 IndexOutOfBoundsException - 下标越界异常 NegativeArraySizeException - 创建一个大小为负数的数组错误异常 NumberFormatException - 数字格式异常 SecurityException - 安全异常 UnsupportedOperationException - 不支持的操作异常

    app的日志如何抓取?

    app本身的日志,可以用logcat抓取,参考这篇:http://www.cnblogs.com/yoyoketang/…

    adb logcat | find “com.sankuai.meituan” >d:\hello.txt

    也可以用ddms抓取,手机连上电脑,打开ddms工具,或者在Android Studio开发工具中,打开DDMS

    app对于不稳定偶然出现anr和crash时候你是怎么处理的?

    app偶然出现anr和crash是比较头疼的问题,由于偶然出现无法复现步骤,这也是一个测试人员必备的技能,需要抓日志。查看日志主要有3个方法:

    方法一:app开发保存错误日志到本地 一般app开发在debug版本,出现anr和crash的时候会自动把日志保存到本地实际的sd卡上,去对应的app目录取出来就可以了

    方法二:实时抓取 当出现偶然的crash时候,这时候可以把手机拉到你们app开发那,手机连上他的开发代码的环境,有ddms会抓日志,这时候出现crash就会记录下来日志。 尽量重复操作让bug复现就可以了

    也可以自己开着logcat,保存日志到电脑本地,参考这篇:http://www.cnblogs.com/yoyoketang/…

    adb logcat | find “com.sankuai.meituan” >d:\hello.txt

    方法三:第三方sdk统计工具

    一般接入了第三方统计sdk,比如友盟统计,在友盟的后台会抓到报错的日志

    App出现crash原因有哪些?

    为什么App会出现崩溃呢?百度了一下,查到和App崩溃相关的几个因素:内存管理错误,程序逻辑错误,设备兼容,网络因素等,如下: 1.内存管理错误:可能是可用内存过低,app所需的内存超过设备的限制,app跑不起来导致App crash。 或是内存泄露,程序运行的时间越长,所占用的内存越大,最终用尽全部内存,导致整个系统崩溃。 亦或非授权的内存位置的使用也可能会导致App crash。 2.程序逻辑错误:数组越界、堆栈溢出、并发操作、逻辑错误。 e.g. app新添加一个未经测试的新功能,调用了一个已释放的指针,运行的时候就会crash。 3.设备兼容:由于设备多样性,app在不同的设备上可能会有不同的表现。 4.网络因素:可能是网速欠佳,无法达到app所需的快速响应时间,导致app crash。或者是不同网络的切换也可能会影响app的稳定性。

    app出现ANR,是什么原因导致的?

    那么导致ANR的根本原因是什么呢?简单的总结有以下两点:

    1.主线程执行了耗时操作,比如数据库操作或网络编程 2.其他进程(就是其他程序)占用CPU导致本进程得不到CPU时间片,比如其他进程的频繁读写操作可能会导致这个问题。

    细分的话,导致ANR的原因有如下几点: 1.耗时的网络访问 2.大量的数据读写 3.数据库操作 4.硬件操作(比如camera) 5.调用thread的join()方法、sleep()方法、wait()方法或者等待线程锁的时候 6.service binder的数量达到上限 7.system server中发生WatchDog ANR 8.service忙导致超时无响应 9.其他线程持有锁,导致主线程等待超时 10.其它线程终止或崩溃导致主线程一直等待。

    android和ios测试区别?

    App测试中ios和Android有哪些区别呢? 1.Android长按home键呼出应用列表和切换应用,然后右滑则终止应用; 2.多分辨率测试,Android端20多种,ios较少; 3.手机操作系统,Android较多,ios较少且不能降级,只能单向升级;新的ios系统中的资源库不能完全兼容低版本中的ios系统中的应用,低版本ios系统中的应用调用了新的资源库,会直接导致闪退(Crash); 4.操作习惯:Android,Back键是否被重写,测试点击Back键后的反馈是否正确;应用数据从内存移动到SD卡后能否正常运行等; 5.push测试:Android:点击home键,程序后台运行时,此时接收到push,点击后唤醒应用,此时是否可以正确跳转;ios,点击home键关闭程序和屏幕锁屏的情况(红点的显示); 6.安装卸载测试:Android的下载和安装的平台和工具和渠道比较多,ios主要有app store,iTunes和testflight下载; 7.升级测试:可以被升级的必要条件:新旧版本具有相同的签名;新旧版本具有相同的包名;有一个标示符区分新旧版本(如版本号), 对于Android若有内置的应用需检查升级之后内置文件是否匹配(如内置的输入法)

    另外:对于测试还需要注意一下几点: 1.并发(中断)测试:闹铃弹出框提示,另一个应用的启动、视频音频的播放,来电、用户正在输入等,语音、录音等的播放时强制其他正在播放的要暂停; 2.数据来源的测试:输入,选择、复制、语音输入,安装不同输入法输入等; 3.push(推送)测试:在开关机、待机状态下执行推送,消息先死及其推送跳转的正确性; 应用在开发、未打开状态、应用启动且在后台运行的情况下是push显示和跳转否正确; 推送消息阅读前后数字的变化是否正确; 多条推送的合集的显示和跳转是否正确;

    4.分享跳转:分享后的文案是否正确;分享后跳转是否正确,显示的消息来源是否正确;

    5.触屏测试:同时触摸不同的位置或者同时进行不同操作,查看客户端的处理情况,是否会crash等

    app测试和web测试有什么区别?

    WEB测试和App测试从流程上来说,没有区别。 都需要经历测试计划方案,用例设计,测试执行,缺陷管理,测试报告等相关活动。 从技术上来说,WEB测试和APP测试其测试类型也基本相似,都需要进行功能测试、性能测试、安全性测试、GUI测试等测试类型。

    他们的主要区别在于具体测试的细节和方法有区别,比如:性能测试,在WEB测试只需要测试响应时间这个要素,在App测试中还需要考虑流量测试和耗电量测试。

    兼容性测试:在WEB端是兼容浏览器,在App端兼容的是手机设备。而且相对应的兼容性测试工具也不相同,WEB因为是测试兼容浏览器,所以需要使用不同的浏览器进行兼容性测试(常见的是兼容IE6,IE8,chrome,firefox)如果是手机端,那么就需要兼容不同品牌,不同分辨率,不同android版本甚至不同操作系统的兼容。(常见的兼容方式是兼容市场占用率前N位的手机即可),有时候也可以使用到兼容性测试工具,但WEB兼容性工具多用IETester等工具,而App兼容性测试会使用Testin这样的商业工具也可以做测试。

    安装测试:WEB测试基本上没有客户端层面的安装测试,但是App测试是存在客户端层面的安装测试,那么就具备相关的测试点。

    还有,App测试基于手机设备,还有一些手机设备的专项测试。如交叉事件测试,操作类型测试,网络测试(弱网测试,网络切换)

    交叉事件测试:就是在操作某个软件的时候,来电话、来短信,电量不足提示等外部事件。

    操作类型测试:如横屏测试,手势测试

    网络测试:包含弱网和网络切换测试。需要测试弱网所造成的用户体验,重点要考虑回退和刷新是否会造成二次提交。弱网络的模拟,据说可以用360wifi实现设置。

    从系统架构的层面,WEB测试只要更新了服务器端,客户端就会同步会更新。而且客户端是可以保证每一个用户的客户端完全一致的。但是APP端是不能够保证完全一致的,除非用户更新客户端。如果是APP下修改了服务器端,意味着客户端用户所使用的核心版本都需要进行回归测试一遍。

    还有升级测试:升级测试的提醒机制,升级取消是否会影响原有功能的使用,升级后用户数据是否被清除了。

    Android四大组件

    Android四大基本组件:Activity、BroadcastReceiver广播接收器、ContentProvider内容提供者、Service服务。

    Activity:

    应用程序中,一个Activity就相当于手机屏幕,它是一种可以包含用户界面的组件,主要用于和用户进行交互。一个应用程序可以包含许多活动,比如事件的点击,一般都会触发一个新的Activity。

    BroadcastReceiver广播接收器:

    应用可以使用它对外部事件进行过滤只对感兴趣的外部事件(如当电话呼入时,或者数据网络可用时)进行接收并做出响应。广播接收器没有用户界面。然而,它们可以启动一个activity或serice 来响应它们收到的信息,或者用NotificationManager来通知用户。通知可以用很多种方式来吸引用户的注意力──闪动背灯、震动、播放声音等。一般来说是在状态栏上放一个持久的图标,用户可以打开它并获取消息。

    ContentProvider内容提供者:

    内容提供者主要用于在不同应用程序之间实现数据共享的功能,它提供了一套完整的机制,允许一个程序访问另一个程序中的数据,同时还能保证被访问数据的安全性。只有需要在多个应用程序间共享数据时才需要内容提供者。例如:通讯录数据被多个应用程序使用,且必须存储在一个内容提供者中。它的好处:统一数据访问方式。

    Service服务:

    是Android中实现程序后台运行的解决方案,它非常适合去执行那些不需要和用户交互而且还要长期运行的任务(一边打电话,后台挂着QQ)。服务的运行不依赖于任何用户界面,即使程序被切换到后台,或者用户打开了另一个应用程序,服务扔然能够保持正常运行,不过服务并不是运行在一个独立的进程当中,而是依赖于创建服务时所在的应用程序进程。当某个应用程序进程被杀掉后,所有依赖于该进程的服务也会停止运行(正在听音乐,然后把音乐程序退出)。

    Activity生命周期?

    周期即活动从开始到结束所经历的各种状态。生命周期即活动从开始到结束所经历的各个状态。从一个状态到另一个状态的转变,从无到有再到无,这样一个过程中所经历的状态就叫做生命周期。

    Activity本质上有四种状态:

    1.运行(Active/Running):Activity处于活动状态,此时Activity处于栈顶,是可见状态,可以与用户进行交互

    2.暂停(Paused):当Activity失去焦点时,或被一个新的非全面屏的Activity,或被一个透明的Activity放置在栈顶时,Activity就转化为Paused状态。此刻并不会被销毁,只是失去了与用户交互的能力,其所有的状态信息及其成员变量都还在,只有在系统内存紧张的情况下,才有可能被系统回收掉

    3.停止(Stopped):当Activity被系统完全覆盖时,被覆盖的Activity就会进入Stopped状态,此时已不在可见,但是资源还是没有被收回

    4.系统回收(Killed):当Activity被系统回收掉,Activity就处于Killed状态

    如果一个活动在处于停止或者暂停的状态下,系统内存缺乏时会将其结束(finish)或者杀死(kill)。这种非正常情况下,系统在杀死或者结束之前会调用onSaveInstance()方法来保存信息,同时,当Activity被移动到前台时,重新启动该Activity并调用onRestoreInstance()方法加载保留的信息,以保持原有的状态。

    在上面的四中常有的状态之间,还有着其他的生命周期来作为不同状态之间的过度,用于在不同的状态之间进行转换,生命周期的具体说明见下。

    什么是activity

    什么是activity,这个前两年出去面试APP测试岗位,估计问的最多了,特别是一些大厂,先问你是不是做过APP测试,那好,你说说什么是activity? 如果没看过android的开发原理,估计这个很难回答,要是第一个问题就被难住了,面试的信心也会失去一半了,士气大减。

    Activity是Android的四大组件之一,也是平时我们用到最多的一个组件,可以用来显示View。 官方的说法是Activity一个应用程序的组件,它提供一个屏幕来与用户交互,以便做一些诸如打电话、发邮件和看地图之类的事情,原话如下: An Activity is an application component that provides a screen with which users can interact in order to do something, such as dial the phone, take a photo, send an email, or view a map.

    Activity是一个Android的应用组件,它提供屏幕进行交互。每个Activity都会获得一个用于绘制其用户界面的窗口,窗口可以充满哦屏幕也可以小于屏幕并浮动在其他窗口之上。 一个应用通常是由多个彼此松散联系的Activity组成,一般会指定应用中的某个Activity为主活动,也就是说首次启动应用时给用户呈现的Activity。将Activity设为主活动的方法 当然Activity之间可以进行互相跳转,以便执行不同的操作。每当新Activity启动时,旧的Activity便会停止,但是系统会在堆栈也就是返回栈中保留该Activity。 当新Activity启动时,系统也会将其推送到返回栈上,并取得用在这里插入图片描述 户的操作焦点。当用户完成当前Activity并按返回按钮是,系统就会从堆栈将其弹出销毁,然后回复前一Activity 当一个Activity因某个新Activity启动而停止时,系统会通过该Activity的生命周期回调方法通知其这一状态的变化。 Activity因状态变化每个变化可能有若干种,每一种回调都会提供执行与该状态相应的特定操作的机会

    语音通话功能

    WebRTC实时通讯的核心 WebRTC 建立连接步骤 1.为连接的两端创建一个 RTCPeerConnection 对象,并且给 RTCPeerConnection 对象添加本地流。

    2.获取本地媒体描述信息(SDP),并与对端进行交换。

    3.获取网络信息(Candidate,IP 地址和端口),并与远端进行交换。

    装逼神器

    一般通过面试的短短一个小时时间,面试官需要对你的技术底子进行磨盘,如果你看完下面这些材料,相信你一定能够让他心里直呼牛逼(下面所有链接文章均是小编自己总结的)

    关于scoped样式穿透问题

    blog.csdn.net/JHXL_/artic…

    Vue2和Vue3的区别

    blog.csdn.net/JHXL_/artic…

    项目中的登录流程

    blog.csdn.net/JHXL_/artic…

    构造函数、原型、继承

    blog.csdn.net/JHXL_/artic…

    项目中遇到的难点

    写在最后

    ✨原创不易,还希望各位大佬支持一下\textcolor{blue}{原创不易,还希望各位大佬支持一下}原创不易,还希望各位大佬支持一下
    👍 点赞,你的认可是我创作的动力!\textcolor{green}{点赞,你的认可是我创作的动力!}点赞,你的认可是我创作的动力!
    ⭐️ 收藏,你的青睐是我努力的方向!\textcolor{green}{收藏,你的青睐是我努力的方向!}收藏,你的青睐是我努力的方向!
    ✏️ 评论,你的意见是我进步的财富!\textcolor{green}{评论,你的意见是我进步的财富!}评论,你的意见是我进步的财富!

    作者:几何心凉
    来源:https://juejin.cn/post/7039640038509903909

    收起阅读 »

    撸一个 webpack 插件,希望对大家有所帮助

    最近,陆陆续续搞 了一个 UniUsingComponentsWebpackPlugin 插件(下面介绍),这是自己第三个开源项目,希望大家一起来维护,一起 star 呀,其它两个:vue-okr-tree基于 Vue 2的组织架构树组件地址:github....
    继续阅读 »

    最近,陆陆续续搞 了一个 UniUsingComponentsWebpackPlugin 插件(下面介绍),这是自己第三个开源项目,希望大家一起来维护,一起 star 呀,其它两个:

    • vue-okr-tree

      基于 Vue 2的组织架构树组件

      地址:github.com/qq449245884…

    • ztjy-cli

      团队的一个简易模板初始化脚手架

      地址:github.com/qq449245884…

    • UniUsingComponentsWebpackPlugin

      地址:github.com/qq449245884…

      配合UniApp,用于集成小程序原生组件

      • 配置第三方库后可以自动引入其下的原生组件,而无需手动配置

      • 生产构建时可以自动剔除没有使用到的原生组件

    背景

    第一个痛点

    用 uniapp开发小程序的小伙伴应该知道,我们在 uniapp 中要使用第三方 UI 库(vant-weappiView-weapp)的时候 ,想要在全局中使用,需要在 src/pages.json 中的 usingComponents 添加对应的组件声明,如:

    // src/pages.json
    "usingComponents": {
       "van-button": "/wxcomponents/@vant/weapp/button/index",
    }

    但在开发过程中,我们不太清楚需要哪些组件,所以我们可能会全部声明一遍(PS:这在做公共库的时候更常见),所以我们得一个个的写,做为程序员,我们绝不允许使用这种笨方法。这是第一个痛点

    第二个痛点

    使用第三方组件,除了在 src/pages.json 还需要在对应的生产目录下建立 wxcomponents,并将第三方的库拷贝至该文件下,这个是 uniapp 自定义的,详细就见:uniapp.dcloud.io/frame?id=%e…

    这是第二个痛点

    第三个痛点

    第二痛点,我们将整个UI库拷贝至 wxcomponents,但最终发布的时候,我们不太可能全都用到了里面的全局组件,所以就将不必要的组件也发布上去,增加代码的体积。

    有的小伙伴就会想到,那你将第三方的库拷贝至 wxcomponents时候,可以只拷使用到的就行啦。是这理没错,但组件里面可能还会使用到其它组件,我们还得一个个去看,然后一个个引入,这又回到了第一个痛点了

    有了这三个痛点,必须得有个插件来做这些傻事,处理这三个痛点。于是就有 UniUsingComponentsWebpackPlugin 插件,这个webpack 插件主要解决下面几个问题:

    • 配置第三方库后可以自动引入其下的原生组件,而无需手动配置

    • 生产构建时可以自动剔除没有使用到的原生组件

    webpack 插件

    webpack 的插件体系是一种基于 Tapable 实现的强耦合架构,它在特定时机触发钩子时会附带上足够的上下文信息,插件定义的钩子回调中,能也只能与这些上下文背后的数据结构、接口交互产生 side effect,进而影响到编译状态和后续流程。

    从形态上看,插件通常是一个带有 apply函数的类:

    class SomePlugin {
       apply(compiler) {
      }
    }

    Webpack 会在启动后按照注册的顺序逐次调用插件对象的 apply 函数,同时传入编译器对象 compiler ,插件开发者可以以此为起点触达到 webpack 内部定义的任意钩子,例如:

    class SomePlugin {
       apply(compiler) {
           compiler.hooks.thisCompilation.tap('SomePlugin', (compilation) => {
          })
      }
    }

    注意观察核心语句 compiler.hooks.thisCompilation.tap,其中 thisCompilation 为 tapable 仓库提供的钩子对象;tap 为订阅函数,用于注册回调。

    Webpack 的插件体系基于tapable 提供的各类钩子展开,所以有必要先熟悉一下 tapable 提供的钩子类型及各自的特点。

    到这里,就不做继续介绍了,关于插件的更多 详情可以去官网了解。

    这里推荐 Tecvan 大佬写的 《Webpack 插件架构深度讲解》mp.weixin.qq.com/s/tXkGx6Ckt…

    实现思路

    UniUsingComponentsWebpackPlugin 插件主要用到了三个 compiler 钩子。

    第一个钩子是 environment:

    compiler.hooks.environment.tap(
        'UniUsingComponentsWebpackPlugin',
        async () => {
          // todo someing
        }
      );

    这个钩子主要用来自动引入其下的原生组件,这样就无需手动配置。解决第一个痛点

    第二个钩子 thisCompilation,这个钩子可以获得 compilation,能对最终打包的产物进行操作:

    compiler.hooks.thisCompilation.tap(
        'UniUsingComponentsWebpackPlugin',
        (compilation) => {
          // 添加资源 hooks
          compilation.hooks.additionalAssets.tapAsync(
            'UniUsingComponentsWebpackPlugin',
            async (cb) => {
              await this.copyUsingComponents(compiler, compilation);
              cb();
            }
          );
        }
      );

    所以这个勾子用来将 node_modules 下的第三库拷贝到我们生产 dist 目录里面的 wxcomponents解决第二个痛点

    ps:这里也可直接用现有的 copy-webpack-plugin 插件来实现。

    第三个钩子 done,表示 compilation 执行完成:

        if (process.env.NODE_ENV === 'production') {
        compiler.hooks.done.tapAsync(
          'UniUsingComponentsWebpackPlugin',
          (stats, callback) => {
            this.deleteNoUseComponents();
            callback();
          }
        );
      }

    执行完成后,表示我们已经生成 dist 目录了,可以读取文件内容,分析,获取哪些组件被使用了,然后删除没有使用到组件对应的文件。这样就可以解决我们第三个痛点了

    PS:这里我判断只有在生产环境下才会 剔除,开发环境没有,也没太必要。

    使用

    安装

    npm install uni-using-components-webpack-plugin --save-dev

    然后将插件添加到 WebPack Config 中。例如:

    const UniUsingComponentsWebpackPlugin = require("uni-using-components-webpack-plugin");

    module.exports = {
     plugins: [
    new UniUsingComponentsWebpackPlugin({
      patterns: [
      {
      prefix: 'van',
      module: '@vant/weapp',
      },
      {
      prefix: 'i',
      module: 'iview-weapp',
      },
      ],
      })
    ],
    };

    注意:uni-using-components-webpack-plugin 只适用在 UniApp 开发的小程序。

    参数

    NameTypeDescription
    patterns{Array}为插件指定相关

    Patterns

    moduleprefix
    模块名组件前缀

    module 是指 package.json 里面的 name,如使用是 Vant 对应的 module@vant/weapp,如果使用是 iview,刚对应的 moduleiview-weapp,具体可看它们各自的 package.json

    prefix 是指组件的前缀,如 Vant 使用是 van 开头的前缀,iview 使用是 i 开头的前缀,具体可看它们各自的官方文档。

    PS: 这里得吐曹一下 vant,叫别人使用 van 的前缀,然后自己组件里面声明子组件时,却没有使用 van 前缀,如 picker 组件,它里面的 JSON 文件是这么写的:

    {
    "component": true,
    "usingComponents": {
    "picker-column": "../picker-column/index",
    "loading": "../loading/index"
    }
    }

    picker-columnloading 都没有带 van 前缀,因为这个问题,在做 自动剔除 功能中,我是根据 前缀来判断使用哪些组件的,由于这里的 loadingpicker-column 没有加前缀,所以就被会删除,导致最终的 picker 用不了。为了解决这个问题,增加了不少工作量。

    希望 Vant 官方后面的版本能优化一下。

    总结

    本文通用自定义 Webpack 插件来实现日常一些技术优化需求。主要为大家介绍了 Webpack 插件的基本组成和简单架构,通过三个痛点,引出了 uni-using-components-webpack-plugin 插件,并介绍了使用方式,实现思路。

    最后,关于 Webpack 插件开发,还有更多知识可以学习,建议多看看官方文档《Writing a Plugin》进行学习。

    代码部署后可能存在的BUG没法实时知道,事后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具 Fundebug

    作者:前端小智
    来源:https://juejin.cn/post/7039855875967696904

    收起阅读 »

    膜拜!用最少的代码却实现了最牛逼的滚动动画!

    今天老鱼带领大家学习如何使用最少的代码创建令人叹为观止的滚动动画~ 在聊ScrollTrigger插件之前我们先简单了解下GSAP。 GreenSock 动画平台 (GSAP) 可为 JavaScript 可以操作的任何内容(CSS 属性、SVG、Reac...
    继续阅读 »

    今天老鱼带领大家学习如何使用最少的代码创建令人叹为观止的滚动动画~



    在聊ScrollTrigger插件之前我们先简单了解下GSAP



    GreenSock 动画平台 (GSAP) 可为 JavaScript 可以操作的任何内容(CSS 属性、SVG、React、画布、通用对象等)动画化,并解决不同浏览器上存在的兼容问题,而且比 jQuery快 20 倍。大约1000万个网站和许多主要品牌都在使用GSAP。



    接下来老鱼带领大家一起学习ScrollTrigger插件的使用。


    插件简介


    ScrollTrigger是基于GSAP实现的一款高性能页面滚动触发HTML元素动画的插件。


    通过ScrollTrigger使用最少的代码创建令人叹为观止的滚动动画。我们需要知道ScrollTrigger是基于GSAP实现的插件,ScrollTrigger是处理滚动事件的,而真正处理动画是GSAP,二者组合使用才能实现滚动动画~


    插件特点



    • 将任何动画链接到特定元素,以便它仅在视图中显示该元素时才执行该动画。

    • 可以在进入/离开定义的区域或将其直接链接到滚动栏时在动画上执行操作(播放、暂停、恢复、重新启动、反转、完成、重置)。

    • 延迟动画和滚动条之间的同步。

    • 根据速度捕捉动画中的进度值。

    • 嵌入滚动直接触发到任何 GSAP 动画(包括时间线)或创建独立实例,并利用丰富的回调系统做任何您想做的事。

    • 高级固定功能可以在某些滚动位置之间锁定一个元素。

    • 灵活定义滚动位置。

    • 支持垂直或水平滚动。

    • 丰富的回调系统。

    • 当窗口调整大小时,自动重新计算位置。

    • 在开发过程中启用视觉标记,以准确查看开始/结束/触发点的位置。

    • 在滚动记录器处于活动状态时,如将active类添加到触发元素中:toggleClass: "active"

    • 使用 matchMedia() 标准媒体查询为各种屏幕尺寸创建不同的设置。

    • 自定义滚动触发器容器,可以定义一个 div 而不一定是浏览器视口。

    • 高度优化以实现最大性能。

    • 插件大约只有6.5kb大小。


    安装/引用


    CDN


    <script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.8.0/gsap.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.8.0/ScrollTrigger.min.js"></script>

    ES Modules


    import { gsap } from "gsap";
    import { ScrollTrigger } from "gsap/ScrollTrigger";

    gsap.registerPlugin(ScrollTrigger);

    UMD/CommonJS


    import { gsap } from "gsap/dist/gsap";
    import { ScrollTrigger } from "gsap/dist/ScrollTrigger";

    gsap.registerPlugin(ScrollTrigger);


    简单示例


    gsap.to(".box", {
    scrollTrigger: ".box", // start the animation when ".box" enters the viewport (once)
    x: 500
    });

    高级示例


    let tl = gsap.timeline({
      // 添加到整个时间线
      scrollTrigger: {
        trigger: ".container",
        pin: true,   // 在执行时固定触发器元素
        start: "top top", // 当触发器的顶部碰到视口的顶部时
        end: "+=500", // 在滚动 500 px后结束
        scrub: 1, // 触发器1秒后跟上滚动条
        snap: {
          snapTo: "labels", // 捕捉时间线中最近的标签
          duration: {min: 0.2, max: 3}, // 捕捉动画应至少为 0.2 秒,但不超过 3 秒(由速度决定)
          delay: 0.2, // 从上次滚动事件开始等待 0.2 秒,然后再进行捕捉
          ease: "power1.inOut" // 捕捉动画的过度时间(默认为“power3”)
        }
      }
    });

    // 向时间线添加动画和标签
    tl.addLabel("start")
    .from(".box p", {scale: 0.3, rotation:45, autoAlpha: 0})
    .addLabel("color")
    .from(".box", {backgroundColor: "#28a92b"})
    .addLabel("spin")
    .to(".box", {rotation: 360})
    .addLabel("end");

    自定义示例


    ScrollTrigger.create({
    trigger: "#id",
    start: "top top",
    endTrigger: "#otherID",
    end: "bottom 50%+=100px",
    onToggle: self => console.log("toggled, isActive:", self.isActive),
    onUpdate: self => {
      console.log("progress:", self.progress.toFixed(3), "direction:", self.direction, "velocity", self.getVelocity());
    }
    });

    接下来,我们一起来看使用ScrollTrigger可以实现怎样的效果吧。


    利用ScrollTrigger可以实现很多炫酷的效果,还有更多示例及源代码,快去公众号后台回复aaa滚动获取学习吧!也欢迎同学们和老鱼讨论哦~


    作者:大前端实验室
    链接:https://juejin.cn/post/7038378577028448293

    收起阅读 »

    领导:小伙子,咱们这个页面出来太慢了!赶紧给我优化一下。

    性能优化 这样一个词应该已经是老生常谈了,不仅在面试中面试官会以此和你掰头,而且在工作中领导也会因为网页加载速度慢来敲打你学(打)习(工),那么前端性能优化,如果判断到底需不需要做,如果需要做又怎么去做或者说怎么去找到优化的切入点? 接下来让我们一起来探索前端...
    继续阅读 »

    性能优化


    这样一个词应该已经是老生常谈了,不仅在面试中面试官会以此和你掰头,而且在工作中领导也会因为网页加载速度慢来敲打你学(打)习(工),那么前端性能优化,如果判断到底需不需要做,如果需要做又怎么去做或者说怎么去找到优化的切入点?


    接下来让我们一起来探索前端性能优化(emo~


    如何量化网站是否需要做性能优化?


    首先现在工具碎片化的时代,各种工具满天飞,如何找到一个方便又能直击痛点的工具,是重中之重的首要任务。



    下面使用的就是Chrome自带的插件工具进行分析



    可以使用chrome自带的lightHouse工具进行分析。得出的分数会列举出三个档次。然后再根据提出不同建议进行优化。


    例如:打开掘金的页面,然后点开开发者工具中的Lighthouse插件


    1.png


    我们可以看到几项指标:



    • First Contentful Paint 首屏加载时间(FCP)

    • Time to interactive 可互动的时间(TTI) 衡量一个页面多长时间才能完全交互

    • Speed Index 内容明显填充的速度(SI) 分数越低越好

    • Total Blocking Time 总阻塞时间(TBT) 主线程运行超过50ms的任务叫做Long Task,Total Blocking Time (TBT) 是 Long Tasks(所有超过 50ms 的任务)阻塞主线程并影响页面可用性的时间量,比如异步任务过长就会导致阻塞主线程渲染,这时就需要处理这部分任务

    • Largest Contentful Paint 最大视觉元素加载的时间(LCP) 对于SEO来说最重要的指标,用户如果打开页面很久都不能看清楚完整页面,那么SEO就会很低。(对于Google来说)

    • Cumulative Layout Shift 累计布局偏移(CLS) 衡量页面点击某些内容位置发生偏移后对页面对影响 eg:当图片宽高不确定时会时该指标更高,还比如异步或者dom动态加载到现有内容上的情况也会造成CLS升高


    以上的6个指标就能很好的量化我们网页的性能。得出类似以下结论,并采取措施。



    下面的图片是分析自己的项目得出的图表



    2.png


    3.png



    • 比如打包体积 (webpack优化,tree-sharking和按需加载插件,以及css合并)

    • 图片加载大小优化(使用可压缩图片,搭配上懒加载和预加载)

    • http1.0替换为http2.0后可使用二进制标头和多路复用。(某些图片使用cdn请求时使用了http1.0)

    • 图片没有加上width和heigth(或者说没有父容器限制),当页面重绘重排时容易造成页面排版混乱的情况

    • 避免巨大的网络负载,比如图片的同时请求和减少同时请求的数量

    • 静态资源缓存

    • 减少未使用的 JavaScript 并推迟加载脚本(defer和async)



    千遍万遍,不如自己行动一遍。dev your project!然后再对比服用,效果更好哦!



    如何做性能优化


    Vue-cli已经做了的优化:



    • 使用cache-loader默认为Vue/Babel/TypeScript编译开启,文件会缓存在node_modules/.cache里

    • 图片小于4k的会转为base64储存在js文件中

    • 生产环境会将css提取成单独的文件

    • 提取公共代码

    • 代码压缩

    • 给所有的js文件和css文件加上preload


    我们需要做的优化:(下面做出的优化都是根据分析工具得出后,对应自己的项目进行细化而来)

    1. 首先代码层面:

      1. 多图片的页面需要做图片懒加载+预加载+cdn请求以及压缩。后期会推出一篇关于图片优化的文章...
      2. 组件按需加载
      3. 对于迫不得已的dom操作,尽量一次性操作。避免多次操作dom造成页面重绘重排
      4. 公共组件的提取
      5. ajax的请求尽量能够减少多个,如果ajax请求比较慢,但是又必须得请求。那么可以考虑使用 Web Worker
    2. 打包项目。

      1. 使用webpack插件 例如 tree-sharking进行剔除无关的依赖加载。使用terser进行代码压缩,给执行时间长的loader加 cache-loader,可以使得下次打包就会使用 node_modules/.cache 里的
      2. 静态资源使用缓存或者cdn加载,部分动态文件设置缓存过期时间

    作者:Tzyito
    链接:https://juejin.cn/post/7008422231403397134

    收起阅读 »

    知道这个,再也不用写一堆el-table-column了

    前言 最近在写一个练手项目,接触到了一个问题,就是el-table中的项太多了,我写了一堆el-table-column,导致代码太长了,看起来特别费劲,后来发现了一个让人眼前一亮的方法,瞬间挽救了我的眼睛。 下面就来分享一下! 进入正题 上面就是table...
    继续阅读 »

    前言


    最近在写一个练手项目,接触到了一个问题,就是el-table中的项太多了,我写了一堆el-table-column,导致代码太长了,看起来特别费劲,后来发现了一个让人眼前一亮的方法,瞬间挽救了我的眼睛。


    下面就来分享一下!


    进入正题


    image.png
    上面就是table中的全部项,去除第一个复选框,最后一个操作的插槽,一共七项,也就是说el-table-column一共要写9对。这简直不能忍!


    image.png



    这个图只作举一个例子用,跟上面不产生对应关系。



    其中就有5个el-form-item,就这么一大堆。


    所以,我当时就想,可不可以用v-for去渲染el-table-column这个标签呢?保留复选框和最后的操作插槽,我们只需要渲染中间的那几项就行。


    经过我的实验,确实是可以实现的。



    这么写之后就开始质疑之前的我为什么没有这个想法? 要不就能少写一堆💩啦



    实现代码如下(标签部分):


    
                v-for="item in columns"
    :key="item.prop"
    :prop="item.prop"
    :label="item.label"
    :formatter="item.formatter"
    :width="item.width">



    思路是这样,把标签需要显示的定义在一个数组中,遍历数组来达到我们想要的效果,formatter是我们完成提交的数据和页面显示数据的一个转换所用到的。具体写法在下面js部分有写。


    定义数组的写法是vue3 composition api的写法,这个思路的话,用Vue2的写法也能实现的,重要的毕竟是思想(啊,我之前还是想不到这种思路)。



    再吐槽一下下,这种写法每写一个函数或者变量就要return回去,也挺麻烦的感觉,hhhhh



    实现代码如下(JS部分):


    const columns = reactive([
    {
    label:'用户ID',
    prop:'userId'
    },
    {
    label:'用户名',
    prop:'userName'
    },
    {
    label:'用户邮箱',
    prop:'userEmail'
    },
    {
    label:'用户角色',
    prop:'role',
    formatter(row,column,value){
    return {
    0:"管理员",
    1:"普通用户"
    }[value]
    }
    },
    {
    label:'用户状态',
    prop:'state',
    formatter(row,column,value){
    return {
    1:"在职",
    2:"离职",
    3:"试用期"
    }[value]
    }
    },
    {
    label:'注册时间',
    prop:'createTime'
    },
    {
    label:'最后登陆时间',
    prop:'lastLoginTime'
    }
    ])

    作者:Ned
    链接:https://juejin.cn/post/7025921628684943396

    收起阅读 »

    浏览器为什么能唤起App的页面

    疑问的开端 大家有没有想过一个问题:在浏览器里打开某个网页,网页上有一个按钮点击可以唤起App。 这样的效果是怎么实现的呢?浏览器是一个app;为什么一个app可以调起其他app的页面? 说到跨app的页面调用,大家是不是能够想到一个机制:Activity的...
    继续阅读 »

    疑问的开端


    大家有没有想过一个问题:在浏览器里打开某个网页,网页上有一个按钮点击可以唤起App。


    image.png


    这样的效果是怎么实现的呢?浏览器是一个app;为什么一个app可以调起其他app的页面?


    说到跨app的页面调用,大家是不是能够想到一个机制:Activity的隐式调用?


    一、隐式启动原理


    当我们有需要调起其他app的页面时,使用的API就是隐式调用。


    比如我们有一个app声明了这样的Activity:


    <activity android:name=".OtherActivity"
    android:screenOrientation="portrait">
    <intent-filter>
    <action android:name="mdove"/>
    <category android:name="android.intent.category.DEFAULT"/>
    </intent-filter>
    </activity>

    其他App想启动上边这个Activity如下的调用就好:


    val intent = Intent()
    intent.action = "mdove"
    startActivity(intent)

    我们没有主动声明Activity的class,那么系统是怎么为我们找到对应的Activity的呢?其实这里和正常的Activity启动流程是一样的,无非是if / else的实现不同而已。


    接下来咱们就回顾一下Activity的启动流程,为了避免陷入细节,这里只展开和大家相对“耳熟能详”的类和调用栈,以串流程为主。


    1.1、跨进程


    首先我们必须明确一点:无论是隐式启动还是显示启动;无论是启动App内Activity还是启动App外的Activity都是跨进程的。比如我们上述的例子,一个App想要启动另一个App的页面。



    注意没有root的手机,是看不到系统孵化出来的进程的。也就是我们常见的为什么有些代码打不上断点。



    image.png


    追过startActivity()的同学,应该很熟悉下边这个调用流程,跟进几个方法之后就发现进到了一个叫做ActivityTread的类里边。



    ActivityTread这个类有什么特点?有main函数,就是我们的主线程。



    很快我们能看到一个比较常见类的调用:Instrumentation


    // Activity.java
    public void startActivityForResult(@RequiresPermission Intent intent, int requestCode, @Nullable Bundle options) {
    mInstrumentation.execStartActivity(this, mMainThread.getApplicationThread(), mToken, this, intent, requestCode, options);
    // 省略
    }

    注意mInstrumentation#execStartActivity()有一个标黄的入参,它是ActivityThread中的内部类ApplicationThread



    ApplicationThread这个类有什么特点,它实现了IApplicationThread.Stub,也就是aidl的“跨进程调用的客户端回调”。



    此外mInstrumentation#execStartActivity()中又会看到一个大名鼎鼎的调用:


    public ActivityResult execStartActivity(Context who, IBinder contextThread, IBinder token, Activity target, Intent intent, int requestCode, Bundle options) {
    // 省略...
    ActivityManager.getService()
    .startActivity(whoThread, who.getBasePackageName(), intent,
    intent.resolveTypeIfNeeded(who.getContentResolver()),
    token, target != null ? target.mEmbeddedID : null,
    requestCode, 0, null, options);
    return null;
    }

    我们点击去getService()会看到一个标红的IActivityManager的类。



    它并不是一个.java文件,而是aidl文件。



    所以ActivityManager.``getService``()本质返回的是“进程的服务端”接口实例,也就是:


    1.2、ActivityManagerService



    public class ActivityManagerService extends IActivityManager.Stub



    所以执行到这就转到了系统进程(system_process进程)。省略一下代码细节,看一下调用栈:


    image.png


    从过上述debug截图,看一看到此时已经拿到了我们的目标Activitiy的相关信息。


    这里简化一些获取目标类的源码,直接引入结论:


    1.3、PackageManagerService


    这里类相当于解析手机内的所有apk,将其信息构造到内存之中,比如下图这样:



    image.png



    小tips:手机目录中/data/system/packages.xml,可以看到所有apk的path、进程名、权限等信息。



    1.4、启动新进程


    打开目标Activity的前提是:目标Activity的进程启动了。所以第一次想要打开目标Activity,就意味着要启动进程。


    启动进程的代码就在启动Activity的方法中:


    resumeTopActivityInnerLocked->startProcessLocked


    image.png


    这里便引入了另一个另一个大名鼎鼎的类:ZygoteInit。这里简单来说会通过ZygoteInit来进行App进程启动的。


    1.5、ApplicationThread


    进程启动后,继续回到目标Activity的启动流程。这里依旧是一系列的system_process进行的转来转去,然后IApplicationThread进入目标进程。



    注意看,在这里再次通过IApplicationThread回调到ActivityThread


    class H extends Handler {
    // 省略
    public void handleMessage(Message msg) {
    switch (msg.what) {
    case EXECUTE_TRANSACTION:
    final ClientTransaction transaction = (ClientTransaction) msg.obj;
    mTransactionExecutor.execute(transaction);
    // 省略
    break;
    case RELAUNCH_ACTIVITY:
    handleRelaunchActivityLocally((IBinder) msg.obj);
    break;
    }
    // 省略...
    }
    }

    // 执行Callback
    public void execute(ClientTransaction transaction) {
    final IBinder token = transaction.getActivityToken();
    executeCallbacks(transaction);
    }

    这里所谓的CallBack的实现是LaunchActivityItem#execute(),对应的实现:


    public void execute(ClientTransactionHandler client, IBinder token,
    PendingTransactionActions pendingActions) {
    ActivityClientRecord r = new ActivityClientRecord(token, mIntent, mIdent, mInfo,
    mOverrideConfig, mCompatInfo, mReferrer, mVoiceInteractor, mState, mPersistentState,
    mPendingResults, mPendingNewIntents, mIsForward,
    mProfilerInfo, client);
    client.handleLaunchActivity(r, pendingActions, null);
    }

    此时就转到了ActivityThread#handleLaunchActivity(),也就转到了咱们日常的生命周期里边,调用栈如下:



    上述截图的调用链中暗含了Activity实例化的过程(反射):


    public @NonNull Activity instantiateActivity(@NonNull ClassLoader cl, @NonNull String className, @Nullable Intent intent) throws InstantiationException, IllegalAccessException, ClassNotFoundException {

    return (Activity) cl.loadClass(className).newInstance();

    }
    复制代码

    二、浏览器启动原理


    Helo站内的回流页就是一个标准的,浏览器唤起另一个App的实例。


    2.1、交互流程


    html标签有一个属性href,比如:<a href="...">


    我们常见的一种用法:<a href="``https://www.baidu.com``">。也就是点击之后跳转到百度。


    因为这个是前端的标签,依托于浏览器及其内核的实现,跳转到一个网页似乎很“顺其自然”(不然叫什么浏览器)。


    当然这里和android交互的流程基本一致:用隐式调用的方式,声明需要启动的Activity;然后<a href="">传入对应的协议(scheme)即可。比如:


    前端页面:


    <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    </head>
    <body>
    <a href="mdove1://haha"> 启动OtherActivity </a>
    </body>

    android声明:


    <activity
    android:name=".OtherActivity"
    android:screenOrientation="portrait">
    <intent-filter>
    <data
    android:host="haha"
    android:scheme="mdove1" />
    <action android:name="android.intent.action.VIEW" />
    <category android:name="android.intent.category.BROWSABLE" />
    <category android:name="android.intent.category.DEFAULT" />
    </intent-filter>
    </activity>

    2.2、推理实现


    浏览器能够加载scheme,可以理解为是浏览器内核做了封装。那么想要让android也能支持对scheme的解析,难道是由浏览器内核做处理吗?


    很明显不可能,做了一套移动端的操作系统,然后让浏览器过来实现,是不是有点杀人诛心。


    所以大概率能猜测出来,应该是手机中的浏览器app做的处理。我们就基于这个猜想去看一看浏览器.apk的实现。


    2.3、浏览器实现


    基于上边说的/data/system/packages.xml文件,我们可以pull出来浏览器的.apk。



    然后jadx反编译一下Browser.apk中WebView相关的源码:




    我们可以发现对href的处理来自于隐式跳转,所以一切就和上边的流程串了起来。


    作者:咸鱼正翻身
    链接:https://juejin.cn/post/7033751175551942692

    收起阅读 »

    浅探Google V8引擎

    探析它之前,我们先抛出以下几个疑问:为什么需要 V8 引擎呢?V8 引擎到底是个啥?它可以做些什么呢?了解它能有什么收获呢?接下来就针对以上几个问题进行详细描述。由来我们都知道,JS 是一种解释型语言,支持动态类型(明显不同于 Java 等这类静态语言就是在命...
    继续阅读 »

    探析它之前,我们先抛出以下几个疑问:

    • 为什么需要 V8 引擎呢?

    • V8 引擎到底是个啥?

    • 它可以做些什么呢?

    • 了解它能有什么收获呢?

    接下来就针对以上几个问题进行详细描述。

    由来

    我们都知道,JS 是一种解释型语言,支持动态类型(明显不同于 Java 等这类静态语言就是在命名一个变量时不需要声明变量的类型)、弱类型、基于原型的语言,内置支持类型。而一般 JS 都是在前端执行(直接影响界面),需要能够快速响应用户,那么就要求语言本身可以被快速地解析和执行,JS 引擎就为此而问世。

    这里提到了解释型语言和静态语言(编译型语言),先简单介绍一下二者:

    • 解释型语言(JS)

      • 每次运行时需要解释器按句依次解释源代码执行,将它们翻译成机器认识的机器代码再执行

    • 编译型语言(Java)

      • 运行时可经过编译器翻译成可执行文件,再由机器运行该可执行文件即可

    从上面的描述中可以看到 JS 运行时每次都要根据源文件进行解释然后执行,而编译型的只需要编译一次,下次可直接运行其可执行文件,但是这样就会导致跨平台的兼容性很差,因此各有优劣。

    而众多 JS 引擎(V8、JavaScriptCore、SpiderMonkey、Chakra等)中 V8 是最为出色的,加上它也是应用于当前最流行的谷歌浏览器,所以我们非常有必要去认识和了解一下,这样对于开发者也就更清楚 JS 在浏览器中到底是如何运行的了。

    认识

    定义

    • 使用 C++ 开发

    • 谷歌开源

    • 编译成原生机器码(支持IA-32, x86-64, ARM, or MIPS CPUs)

    • 使用了如内联缓存(inline caching)等方法来提高性能

    • 运行速度快,可媲美二进制程序

    • 支持众多操作系统,如 windows、linux、android 等

    • 支持其他硬件架构,如 IA32,X64,ARM 等

    • 具有很好的可移植和跨平台特性

    运行

    先来一张官方流程图:

    img

    准备

    JS 文件加载(不归 V8 管):可能来自于网络请求、本地的cache或者是也可以是来自service worker,这是 V8 运行的前提(有源文件才有要解释执行的)。 3种加载方式 & V8的优化

    • Cold load: 首次加载脚本文件时,没有任何数据缓存

    • Warm load:V8 分析到如果使用了相同的脚本文件,会将编译后的代码与脚本文件一起缓存到磁盘缓存中

    • Hot load: 当第三次加载相同的脚本文件时,V8 可以从磁盘缓存中载入脚本,并且还能拿到上次加载时编译后的代码,这样可以避免完全从头开始解析和编译脚本

    而在 V8 6.6 版本的时候进一步改进代码缓存策略,简单讲就是从缓存代码依赖编译过程的模式,改变成两个过程解耦,并增加了可缓存的代码量,从而提升了解析和编译的时间,大大提升了性能,具体细节见V8 6.6 进一步改进缓存性能

    分析

    此过程是将上面环节得到的 JS 代码转换为 AST(抽象语法树)。

    词法分析

    从左往右逐个字符地扫描源代码,通过分析,产生一个不同的标记,这里的标记称为 token,代表着源代码的最小单位,通俗讲就是将一段代码拆分成最小的不可再拆分的单元,这个过程称为词法标记,该过程的产物供下面的语法分析环节使用。

    这里罗列一下词法分析器常用的 token 标记种类:

    • 常数(整数、小数、字符、字符串等)

    • 操作符(算术操作符、比较操作符、逻辑操作符)

    • 分隔符(逗号、分号、括号等)

    • 保留字

    • 标识符(变量名、函数名、类名等)

    TOKEN-TYPE TOKEN-VALUE\
    -----------------------------------------------\
    T_IF                 if\
    T_WHILE              while\
    T_ASSIGN             =\
    T_GREATTHAN          >\
    T_GREATEQUAL         >=\
    T_IDENTIFIER name    / numTickets / ...\
    T_INTEGERCONSTANT    100 / 1 / 12 / ....\
    T_STRINGCONSTANT     "This is a string" / "hello" / ...

    上面提到会逐个从左至右扫描代码然后分析,那么很明显就会想到两种方案,扫描完再分析(非流式处理)和边扫描边分析(流式处理),简单画一下他们的时序图就能发现流式处理效率要高得多,同时分析完也会释放分析过程中占用的内存,也能大大提高内存使用效率,可见该优化的细节处理。

    语法分析

    语法分析是指根据某种给定的形式文法对由单词序列构成的输入文本(例如上个阶段的词法分析产物-tokens stream),进行分析并确定其语法结构的过程,最后产出其 AST(抽象语法树)。

    V8 会将语法分析的过程分为两个阶段来执行:

    • Pre-parser

      • 跳过还未使用的代码

      • 不会生成对应的 AST,会产生不带有变量的引用和声明的 scopes 信息

      • 解析速度会是 Full-parser 的 2 倍

      • 根据 JS 的语法规则仅抛出一些特定的错误信息

    • Full-parser

      • 解析那些使用的代码

      • 生成对应的 AST

      • 产生具体的 scopes 信息,带有变量引用和声明等信息

      • 抛出所有的 JS 语法错误

    为什么要做两次解析?

    如果仅有一次,那只能是 Full-parser,但这样的话,大量未使用的代码会消耗非常多的解析时间,结合实例来看下:通过 Coverage 录制的方式可以分析页面哪些代码没有用到,如下图可以看到最高有 75% 的没有被执行。

    img

    但是预解析并不是万能的,得失是并存的,很明显的一个场景:该文件中的代码全都执行了,那其实就是没必要的,当然这种情况其实还是占比远不如上面的例子,所以这里其实也是一种权衡,需要照顾大多数来达到综合性能的提升。

    下面给出一个示例:

    function add(x, y) {
       if (typeof x === "number") {
           return x + y;
      } else {
           return x + 'tadm';
      }
    }

    复制上面的代码到 web1web2 可以很直观的看到他们的 tokens 和 AST 结构(也可自行写一些代码体验)。

    img

    • tokens

    [
      {
           "type": "Keyword",
           "value": "function"
      },
      {
           "type": "Identifier",
           "value": "add"
      },
      {
           "type": "Punctuator",
           "value": "("
      },
      {
           "type": "Identifier",
           "value": "x"
      },
      {
           "type": "Punctuator",
           "value": ","
      },
      {
           "type": "Identifier",
           "value": "y"
      },
      {
           "type": "Punctuator",
           "value": ")"
      },
      {
           "type": "Punctuator",
           "value": "{"
      },
      {
           "type": "Keyword",
           "value": "if"
      },
      {
           "type": "Punctuator",
           "value": "("
      },
      {
           "type": "Keyword",
           "value": "typeof"
      },
      {
           "type": "Identifier",
           "value": "x"
      },
      {
           "type": "Punctuator",
           "value": "==="
      },
      {
           "type": "String",
           "value": "\"number\""
      },
      {
           "type": "Punctuator",
           "value": ")"
      },
      {
           "type": "Punctuator",
           "value": "{"
      },
      {
           "type": "Keyword",
           "value": "return"
      },
      {
           "type": "Identifier",
           "value": "x"
      },
      {
           "type": "Punctuator",
           "value": "+"
      },
      {
           "type": "Identifier",
           "value": "y"
      },
      {
           "type": "Punctuator",
           "value": ";"
      },
      {
           "type": "Punctuator",
           "value": "}"
      },
      {
           "type": "Keyword",
           "value": "else"
      },
      {
           "type": "Punctuator",
           "value": "{"
      },
      {
           "type": "Keyword",
           "value": "return"
      },
      {
           "type": "Identifier",
           "value": "x"
      },
      {
           "type": "Punctuator",
           "value": "+"
      },
      {
           "type": "String",
           "value": "'tadm'"
      },
      {
           "type": "Punctuator",
           "value": ";"
      },
      {
           "type": "Punctuator",
           "value": "}"
      },
      {
           "type": "Punctuator",
           "value": "}"
      }
    ]
    • AST

    {
     "type": "Program",
     "body": [
      {
         "type": "FunctionDeclaration",
         "id": {
           "type": "Identifier",
           "name": "add"
        },
         "params": [
          {
             "type": "Identifier",
             "name": "x"
          },
          {
             "type": "Identifier",
             "name": "y"
          }
        ],
         "body": {
           "type": "BlockStatement",
           "body": [
            {
               "type": "IfStatement",
               "test": {
                 "type": "BinaryExpression",
                 "operator": "===",
                 "left": {
                   "type": "UnaryExpression",
                   "operator": "typeof",
                   "argument": {
                     "type": "Identifier",
                     "name": "x"
                  },
                   "prefix": true
                },
                 "right": {
                   "type": "Literal",
                   "value": "number",
                   "raw": "\"number\""
                }
              },
               "consequent": {
                 "type": "BlockStatement",
                 "body": [
                  {
                     "type": "ReturnStatement",
                     "argument": {
                       "type": "BinaryExpression",
                       "operator": "+",
                       "left": {
                         "type": "Identifier",
                         "name": "x"
                      },
                       "right": {
                         "type": "Identifier",
                         "name": "y"
                      }
                    }
                  }
                ]
              },
               "alternate": {
                 "type": "BlockStatement",
                 "body": [
                  {
                     "type": "ReturnStatement",
                     "argument": {
                       "type": "BinaryExpression",
                       "operator": "+",
                       "left": {
                         "type": "Identifier",
                         "name": "x"
                      },
                       "right": {
                         "type": "Literal",
                         "value": "tadm",
                         "raw": "'tadm'"
                      }
                    }
                  }
                ]
              }
            }
          ]
        },
         "generator": false,
         "expression": false,
         "async": false
      }
    ],
     "sourceType": "script"
    }

    解释

    该阶段就是将上面产生的 AST 转换成字节码。

    这里增加字节码(中间产物)的好处是,并不是将 AST 直接翻译成机器码,因为对应的 cpu 系统会不一致,翻译成机器码时要结合每种 cpu 底层的指令集,这样实现起来代码复杂度会非常高;还有个就是内存占用的问题,因为机器码会存储在内存中,而退出进程后又会存储在磁盘上,加上转换后的机器码多出来很多信息,会比源文件大很多,导致了严重的内存占用问题。

    V8 在执行字节码的过程中,使用到了通用寄存器累加寄存器,函数参数和局部变量保存在通用寄存器里面,累加器中保存中间计算结果,在执行指令的过程中,如果直接由 cpu 从内存中读取数据的话,比较影响程序执行的性能,使用寄存器存储中间数据的设计,可以大大提升 cpu 执行的速度。

    编译

    这个过程主要是 V8 的 TurboFan编译器 将字节码翻译成机器码的过程。

    字节码配合解释器和编译器这一技术设计,可以称为JIT(即时编译技术),Java 虚拟机也是类似的技术,解释器在解释执行字节码时,会收集代码信息,标记一些热点代码(就是一段代码被重复执行多次),TurboFan 会将热点代码直接编译成机器码,缓存起来,下次调用直接运行对应的二进制的机器码,加快执行速度。

    在 TurboFan 将字节码编译成机器码的过程中,还进行了简化处理:常量合并、强制折减、代数重新组合。

    比如:3 + 4 --> 7,x + 1 + 2 --> x + 3 ......

    执行

    到这里我们就开始执行上一阶段产出的机器码。

    而在 JS 的执行过程中,经常遇到的就是对象属性的访问。作为一种动态的语言,一个简单的属性访问可能包含着复杂的语义,比如Object.xxx的形式,可能是属性的直接访问,也可能去调用的对象的Getter方法,还有可能是要通过原型链往上层对象中查找。这种不确定性而且动态判断的情况,会浪费很多查找时间,所以 V8 会把第一次分析的结果放在缓存中,当再次访问相同的属性时,会优先从缓存中去取,调用 GetProperty(Object, "xxx", feedback_cache) 的方法获取缓存,如果有缓存结果,就会跳过查找过程,又大大提升了运行性能。

    除了上面针对读取对象属性的结果缓存的优化,V8 还引入了 Object Shapes(隐藏类)的概念,这里面会记录一些对象的基本信息(比如对象拥有的所有属性、每个属性对于这个对象的偏移量等),这样我们去访问属性时就可以直接通过属性名和偏移量直接定位到他的内存地址,读取即可,大大提升访问效率。

    既然 V8 提出了隐藏类(两个形状相同的对象会去复用同一个隐藏类,何为形状相同的对象?两个对象满足有相同个数的相同属性名称和相同的属性顺序),那么我们开发者也可以很好的去利用它:

    • 尽量创建形状相同的对象

    • 创建完对象后尽量不要再去操作属性,即不增加或者删除属性,也就不会破环对象的形状

    完成

    到此 V8 已经完成了一份 JS 代码的读取、分析、解释、编译、执行。

    总结

    以上就是从 JS 代码下载到最终在 V8 引擎执行的过程分析,可以发现 V8 其实有很多实现的技术点,有着很巧妙的设计思想,比如流式处理、缓存中间产物、垃圾回收等,这里面又会涉及到很多细节,很值得继续深入研究。

    作者:Tadm
    来源:https://juejin.cn/post/7032278688192430117

    收起阅读 »

    手写清除console的loader

    前言删除console方式介绍通过编辑器查找所有console,或者eslint编译时的报错提示定位语句,然后清除,就是有点费手,不够优雅 因此下面需要介绍几种优雅的清除方式该插件可用于压缩我们的js代码,同时可以通过配置去掉console语句,安装后配置在...
    继续阅读 »




    前言

    作为一个前端,对于console.log的调试可谓是相当熟悉,话不多说就是好用!帮助我们解决了很多bug^_^
    但是!有因必有果(虽然不知道为什么说这句但是很顺口),如果把console发到生产环境也是很头疼的,尤其是如果打印的信息很私密的话,可能要凉凉TT

    删除console方式介绍

    对于在生产环境必须要清除的console语句,如果手动一个个删除,听上去就很辛苦,因此这篇文章本着看到了就要学,学到了就要用的精神我打算介绍一下手写loader的方式清除代码中的console语句,在此之前也介绍一下其他可以清除console语句的方式吧哈哈

    1. 方式一:暴力清除

    通过编辑器查找所有console,或者eslint编译时的报错提示定位语句,然后清除,就是有点费手,不够优雅
    因此下面需要介绍几种优雅的清除方式

    2. 方式二 :uglifyjs-webpack-plugin

    该插件可用于压缩我们的js代码,同时可以通过配置去掉console语句,安装后配置在webpack的optimization下,即可使用,需要注意的是:此配置只在production环境下生效

    安装
    npm i uglifyjs-webpack-plugin

    其中drop_console和pure_funcs的区别是:

    • drop_console的配置值为boolean,也就是说如果为true,那么代码中所有带console前缀的调试方式都会被清除,包括console.log,console.warn等

    • pure_funcs的配置值是一个数组,也就是可以配置清除那些带console前缀的语句,截图中配的是['console.log'],因此生产环境上只会清除console.log,如果代码中包含其他带console的前缀,如console.warn则保留

    但是需要注意的是,该方法只对ES5语法有效,如果你的代码中涉及ES6就会报错

    3. 方式三:terser-webpack-plugin

    webpack v5 开箱即带有最新版本的 terser-webpack-plugin。如果你使用的是 webpack v5 或更高版本,同时希望自定义配置,那么仍需要安装 terser-webpack-plugin。如果使用 webpack v4,则必须安装 terser-webpack-plugin v4 的版本。

    安装
    npm i terser-webpack-plugin@4

    terser-webpack-plugin对于清楚console的配置可谓是跟uglifyjs-webpack-plugin一点没差,但是他们最大的差别就是TerserWebpackPlugin支持ES6的语法

    4. 方式四:手写loader删除console

    终于进入了主题了,朋友们

    1. 什么是loader

    众所周知,webpack只能理解js,json等文件,那么除了js,json之外的文件就需要通过loader去顺利加载,因此loader在其中担任的就是翻译工作。loader可以看作一个node模块,实际上就是一个函数,但他不能是一个箭头函数,因为它需要继承webpack的this,可以在loader中使用webpack的方法。

    • 单一原则,一个loader只做一件事

    • 调用方式,loader是从右向左调用,遵循链式调用

    • 统一原则,输入输出都是字符串或者二进制数据

    根据第三点,下面的代码就会报错,因为输出的是数字而不是字符串或二进制数据

    module.exports = function(source) {
      return 111
    }

    1. 新建清除console语句的loader

    首先新建一个dropConsole.js文件

    // source:表示当前要处理的内容
    const reg = /(console.log\()(.*)(\))/g;
    module.exports = function(source) {
      // 通过正则表达式将当前处理内容中的console替换为空字符串
      source = source.replace(reg, "")
      // 再把处理好的内容return出去,坚守输入输出都是字符串的原则,并可达到链式调用的目的供下一个loader处理
      return source
    }
    1. 在webpack的配置文件中引入

    module: {
      rules:[
          {
              test: /\.js/,
              use: [
                  {
                  loader: path.resolve(__dirname, "./dropConsole.js"),
                  options: {
                    name: "前端"
                  }
                  }
              ]
          },
        {
      ]
    }

    在webpack的配置中,loader的导入需要绝对路径,否则导入失效,如果想要像第三方loader一样引入,就需要配置resolveLoader 中的modules属性,告诉webpack,当node_modules中找不到时,去别的目录下找

    module: {
      rules:[
          {
              test: /\.js/,
              use: [
                  {
                  loader: 'dropConsole',
                  options: {
                    name: "前端"
                  }
                  }
              ]
          },
        {
      ]
    }
    resolveLoader:{
      modules:["./node_modules","./build"] //此时我的loader写在build目录下
    },

    正常运行后,调试台将不会打印console信息

    1. 最后介绍几种在loader中常用的webpack api

    • this.query:返回webpack的参数即options的对象

    • this.callback:同步模式,可以把自定义处理好的数据传递给webpack

    const reg = /(console.log\()(.*)(\))/g;
    module.exports = function(source) {
      source = source.replace(reg, "");
      this.callback(null,source);
      // return的作用是让webpack知道loader返回的结果应该在this.callback当中,而不是return中
      return    
    }
    • this.async():异步模式,可以大致的认为是this.callback的异步版本,因为最终返回的也是this.callback

    const  path = require('path')
    const util = require('util')
    const babel = require('@babel/core')


    const transform = util.promisify(babel.transform)

    module.exports = function(source,map,meta) {
    var callback = this.async();

    transform(source).then(({code,map})=> {
        callback(null, code,map)
    }).catch(err=> {
        callback(err)
    })
    };

    最后的最后,webpack博大精深,值得我们好好学习,深入研究!

    作者:我也想一夜暴富
    来源:https://juejin.cn/post/7038413043084034062

    收起阅读 »

    给团队做个分享,用30张图带你快速了解TypeScript

    正文30张脑图常见的基本类型我们知道TS是JS的超集,那我们先从几种JS中常见的数据类型说起,当然这些类型在TS中都有相应的,如下:特殊类型除了一些在JS中常见的类型,也还有一些TS所特有的类型类型断言和类型守卫如何在运行时需要保证和检测来自其他地方的数据也符...
    继续阅读 »

    正文

    30张脑图

    常见的基本类型

    我们知道TSJS的超集,那我们先从几种JS中常见的数据类型说起,当然这些类型在TS中都有相应的,如下:

    1常见的基本类型.png

    特殊类型

    除了一些在JS中常见的类型,也还有一些TS所特有的类型

    2特殊类型.png

    类型断言和类型守卫

    如何在运行时需要保证和检测来自其他地方的数据也符合我们的要求,这就需要用到断言,而断言需要类型守卫

    3类型断言.png

    接口

    接口本身只是一种规范,里头定义了一些必须有的属性或者方法,接口可以用于规范functionclass或者constructor,只是规则有点区别

    4TS中的接口.png

    类和修饰符

    JS一样,类class出现的目的,其实就是把一些相关的东西放在一起,方便管理

    TS主要也是通过class关键字来定义一个类,并且它还提供了3个修饰符

    5类和修饰符.png

    类的继承和抽象类

    TS中的继承ES6中的类的继承极其相识,子类可以通过extends关键字继承一个类

    但是它还有抽象类的概念,而且抽象类作为基类,不能new

    6.0类的继承和抽象类.png

    泛型

    将泛型理解为宽泛的类型,它通常用于类和函数

    但不管是用于类还是用于函数,核心思想都是:把类型当一种特殊的参数传入进去

    7泛型.png

    类型推断

    TS中是有类型推论的,即在有些没有明确指出类型的地方,类型推论会帮助提供类型

    8类型推断.png

    函数类型

    为了让我们更容易使用,TS为函数添加了类型等

    9函数.png

    数字枚举和字符串枚举

    枚举的好处是,我们可以定义一些带名字的常量,而且可以清晰地表达意图或创建一组有区别的用例

    TS支持数字的和基于字符串的枚举

    10枚举.png

    类型兼容性

    TS里的类型兼容性是基于结构子类型的 11类型兼容性.png

    联合类型和交叉类型

    补充两个TS的类型:联合类型和交叉类型

    12联合类型和交叉类型.png

    for..of和for..in

    TS也支持for..offor..in,但你知道他们两个主要的区别吗

    13forin和forof.png

    模块

    TS的模块化沿用了JS模块的概念,模块是在自身的作用域中执行,在一个模块里的变量,函数,类等等在模块外部是不可见的,除非你明确地使用export形式之一导出它们

    14模块.png

    命名空间的使用

    使用命名空间的方式,其实非常简单,格式如下: namespace X {}

    15命名空间的使用.png

    解决单个命名空间过大的问题

    16解决单个命名空间过大的问题.png

    简化命名空间

    要简化命名空间,核心就是给常用的对象起一个短的名字

    TS中使用import为指定的符号创建一个别名,格式大概是:import q = x.y.z

    17简化命名空间.png

    规避2个TS中命名空间和模块的陷阱

    18陷阱.png

    模块解析流程

    模块解析是指编译器在查找导入模块内容时所遵循的流程

    流程大致如下:

    image.png

    相对和非相对模块导入

    相对和非相对模块导入主要有以下两点不同

    image.png

    Classic模块解析策略

    TS的模块解析策略,其中的一种就叫Classic

    21Classic模块解析策略.png

    Node.js模块解析过程

    为什么要说Node.js模块解析过程,其实是为了讲TS的另一种模块解析策略做铺垫---Node模块解析策略。

    因为Node模块解析策略就是一种试图在运行时模仿Node.js模块解析的策略

    22Node.js的模块解析过程.png

    Node模块解析策略

    Node模块解析策略模仿Node.js运行时的解析策略来在编译阶段定位模块定义文件的模块解析的策略,但是跟Node.js会有点区别

    23Node模块解析策略.png

    声明合并之接口合并

    声明合并指的就是编译器会针对同名的声明合并为一个声明

    声明合并包括接口合并,接口的合并需要区分接口里面的成员有函数成员和非函数成员,两者有差异

    24接口合并.png

    合并命名空间

    命名空间的合并需要分两种情况:一是同名的命名空间之间的合并,二是命名空间和其他类型的合并

    25合并命名空间.png

    JSX模式

    TS具有三种JSX模式:preservereactreact-native

    26JSX.png

    三斜线指令

    三斜线指令其实上面有讲过,像/// <reference>

    它的格式就是三条斜线后面跟一个标签

    27三斜线指令.png


    作者:LBJ
    链接:https://juejin.cn/post/7036266588227502093

    收起阅读 »

    js实现放大镜

    借助宽高等比例放大的两张图片,结合js中鼠标偏移量、元素偏移量、元素自身宽高等属性完成;左侧遮罩移动Xpx,右侧大图移动X*倍数px;其余部分就是用小学数学算一下就OK了。JS // 获取小图和遮罩、大图、大盒子    var small ...
    继续阅读 »



    先看效果图

    实现原理

    借助宽高等比例放大的两张图片,结合js中鼠标偏移量、元素偏移量、元素自身宽高等属性完成;左侧遮罩移动Xpx,右侧大图移动X*倍数px;其余部分就是用小学数学算一下就OK了。

    HTML和CSS

     <div class="wrap">
       
       <div id="small">
         <img src="img/1.jpg" alt="" >
         <div id="mark">div>
       div>
       
       <div id="big">
         <img src="img/2.jpg" alt="" id="bigimg">
       div>
     div>
    * {
        margin: 0;
        padding: 0;
      }
      .wrap {
        width: 1500px;
        margin: 100px auto;
      }

      #small {
        width: 432px;
        height: 768px;
        float: left;
        position: relative;
      }

      #big {
        /* background-color: seagreen; */
        width: 768px;
        height: 768px;
        float: left;
        /* 超出取景框的部分隐藏 */
        overflow: hidden;
        margin-left: 20px;
        position: relative;
        display: none;
      }

      #bigimg {
        /* width: 864px; */
        position: absolute;
        left: 0;
        top: 0;
      }

      #mark {
        width: 220px;
        height: 220px;
        background-color: #fff;
        opacity: .5;
        position: absolute;
        left: 0;
        top: 0;
        /* 鼠标箭头样式 */
        cursor: move;
        display: none;
      }

    JS

     // 获取小图和遮罩、大图、大盒子
       var small = document.getElementById("small")
       var mark = document.getElementById("mark")
       var big = document.getElementById("big")
       var bigimg = document.getElementById("bigimg")
       // 在小图区域内获取鼠标移动事件;遮罩跟随鼠标移动
       small.onmousemove = function (e) {
         // 得到遮罩相对于小图的偏移量(鼠标所在坐标-小图相对于body的偏移-遮罩本身宽度或高度的一半)
         var s_left = e.pageX - mark.offsetWidth / 2 - small.offsetLeft
         var s_top = e.pageY - mark.offsetHeight / 2 - small.offsetTop
         // 遮罩仅可以在小图内移动,所以需要计算遮罩偏移量的临界值(相对于小图的值)
         var max_left = small.offsetWidth - mark.offsetWidth;
         var max_top = small.offsetHeight - mark.offsetHeight;
         // 遮罩移动右侧大图也跟随移动(遮罩每移动1px,图片需要向相反对的方向移动n倍的距离)
         var n = big.offsetWidth / mark.offsetWidth
         // 遮罩跟随鼠标移动前判断:遮罩相对于小图的偏移量不能超出范围,超出范围要重新赋值(临界值在上边已经计算完成:max_left和max_top)
         // 判断水平边界
         if (s_left < 0) {
           s_left = 0
        } else if (s_left > max_left) {
           s_left = max_left
        }
         //判断垂直边界
         if (s_top < 0) {
           s_top = 0
        } else if (s_top > max_top) {
           s_top = max_top
        }
         // 给遮罩left和top赋值(动态的?因为e.pageX和e.pageY为变化的量),动起来!
         mark.style.left = s_left + "px";
         mark.style.top = s_top + "px";
         // 计算大图移动的距离
         var levelx = -n * s_left;
         var verticaly = -n * s_top;
         // 让图片动起来
         bigimg.style.left = levelx + "px";
         bigimg.style.top = verticaly + "px";
      }
       // 鼠标移入小图内才会显示遮罩和跟随移动样式,移出小图后消失
       small.onmouseenter = function () {
         mark.style.display = "block"
         big.style.display= "block"
      }
       small.onmouseleave = function () {
         mark.style.display = "none"
         big.style.display= "none"
      }

    总结

    • 鼠标焦点一旦动起来,它的偏移量就是动态的;父元素和子元素加上定位后,通过动态改变某个元素的lefttop值来实现“动”的效果。

    • 大图/小图=放大镜(遮罩)/取景框

    • 两张图片一定要等比例缩放

    作者:Onion韩
    来源:https://juejin.cn/post/7030963292818374670

    收起阅读 »

    从谷歌一行代码学到的姿势

    网上很流行的一行代码,据说是谷歌工程师写的,它的作用是给页面所有元素增加一个随机颜色的外边框。[].forEach.call($$("*"),function(a){a.style.outline="1px solid #"+(~~(Math.random()...
    继续阅读 »

    网上很流行的一行代码,据说是谷歌工程师写的,它的作用是给页面所有元素增加一个随机颜色的外边框

    [].forEach.call($$("*"),function(a){a.style.outline="1px solid #"+(~~(Math.random()*(1<<24))).toString(16)})

    运行效果如下图:

    这个代码虽然只有一行,但是包含的知识点不少,网上有很多解析。我也说下自己的理解,然后最后推荐在实务中使用TreeWalker对象进行遍历。

    我的理解其中主要包含如下4个知识点:

    1. [].forEach.call
    2. $$("*")
    3. a.style.outline
    4. (~~(Math.random()*(1<<24))).toString(16)

    1 [].forEach.call

    1.1 [].forEach

    forEach是数组遍历的一个方法,接收一个函数参数用来处理每一个遍历的元素,常规的使用姿势是:

    let arr = [3, 5, 8];
    arr.forEach((item) => {
    console.log(item);
    })
    // 控制台输出:
    // 3
    // 5
    // 8

    那么下面的写法:

    [].forEach

    只是为了得到 forEach 这个方法,这个方法是定义都在Array.prototype上的方法,[] 表示空数组,可以访问到数组原型对象上的方法。

    得到 forEach 这个方法后,就可以通过 call 发起调用。

    1.2 call

    call函数用来调用一个函数,和普通调用不同,call调用可以修改函数内this指向。

    常规调用函数的姿势:

    let object1 = {
    id: 1,
    printId() {
    console.log(this.id)
    }
    }
    object1.printId();
    // 控制台输出:
    // 1

    因为是正常调用,方法内的this指向object1对象,所以上例输出1。

    使用call调用printId方法,并传入另外一个对象object2:

    let object2 = {
    id: 2
    }
    object1.printId.call(object2);
    // 控制台输出:
    // 2

    这里使用call调用object1.printId函数,传入了object2对象,那么printId函数内的this就是指向object2这个对象,所以结果输出2。

    1.3 综合分析

    综合来看:

    [].forEach.call( $$("*"), function(a){} )

    这行代码的意思就是遍历如下对象:

    $$("*") 

    然后用如下方法处理每个元素:

    function(a){}

    其中,a就是遍历的的每一个元素。

    那么

    $$("*") 

    指什么呢?我们接着往后看。

    2 $$("*")

    这个写法用来获取页面所有元素,相当于

    document.querySelectorAll('*')

    只是

    $$("*") 

    只能在浏览器开发控制台内使用,这个是浏览器开发控制台提供出来的预定义API,至于为什么,大家可以参考底部的参考文章。

    3 a.style.outline

    设置元素边框,估计很多人都知道,但是设置外边框就比较少人了解了,外边框的效果和边框类似,唯一不同的点是外边框盒子模型的算式,仅仅做装饰使用。

    <style type="text/css">
    #swiper {
    width: 100px;
    height: 100px;
    outline: 10px solid;
    }
    style>

    <div id="swiper">div>

    运行效果:

    div元素实际的宽高还是100 * 100,如果把outline改成border,那么div元素的实际宽高就是120 * 120,因为要加上border的宽度。

    外边框设置的最大作用就是:

    可以设置元素边框效果,但是不影响页面布局。

    4 (~~(Math.random()*(1<<24))).toString(16)

    这个代码从结果是得到一个16进制的颜色值,但是为什么能得到呢?

    16进制的颜色值:81f262

    4.1 Math.random()

    这个容易理解,就是随机 [0, 1) 的小数。

    4.2 1<<24

    这个表示1左移24位,二进制表示如下所示:

    1 0000 0000 0000 0000 0000 0000  

    十进制就是表示:

    2^24

    那么

    Math.random() * (1<<24)

    就会得到如下范围的一个随机浮点数:

    [0, 2^24) 

    4.3 两次按位取反

    因为Math.random()得到是一个小数,所以两次按位取反就是为了过滤掉小数部分,最后得到整数。

    所以

    (~~(Math.random()*(1<<24)))

    就会得到如下范围的一个随机整数:

    [0, 2^24) 

    4.4 转成字符串toString(16)

    最后就是把上面得到的数字转成16进制,我们知道toString()是用来把相关的对象转成字符串的,它可以接收一个进制参数,转成不同的进制,默认是转成10进制。

    对象.toString(2); // 转成2进制
    对象.toString(8); // 转成8进制
    对象.toString(10); // 转成10进制
    对象.toString(16); // 转成16进制

    上面的得到的随机整数用二进制表示就是:

    0000 0000 0000 0000 0000 0000  

    1111 1111 1111 1111 1111 1111

    那么2进制转成16进制,是不是就是每4位转一个?

    最终是不是就得到一个6个长度的16进制数了?

    这个字符串加上#是不是就是16进制的颜色值了?

    形如:

    #ac83ce
    #b74384
    等等...

    实务应用

    虽然上面的代码简短,并且知识含量也很高,但是在实务中如果要遍历元素,我并不建议使用这样的方式。

    主要原因是两个:

    1. $$("*") 只在开发控制台可以用,正常项目代码中不能用。
    2. 选中所有元素再遍历,性能低。

    如果实务中要遍历元素,建议是用 TreeWalker。querySelectorAll是一次性获取所有元素然后遍历,TreeWalker是迭代器的方式,性能上 TreeWalker 更优,另外 TreeWalker 还支持各种过滤。

    参考如下示例:

    // 实例化 TreeWalker 对象
    let walker = document.createTreeWalker(
    document.documentElement,
    NodeFilter.SHOW_ELEMENT
    );
    // 遍历
    let node = walker.nextNode();
    while (node !== null) {
    node.style.outline = "1px solid #" + (~~(Math.random() * (1 << 24))).toString(16);
    node = walker.nextNode();
    }

    虽然代码更多,当时性能更好,并且支持各种过滤等,功能也更加强大。

    如果大家有学到新姿势,麻烦帮忙点个赞,谢谢。欢迎大家留言讨论。

    参考资料

    JavaScript中的$$(*)代表什么和$选择器的由来:ourjs.com/detail/54ab…

    querySelectorAll vs NodeIterator vs TreeWalker:stackoverflow.com/questions/6…

    作者:晴空闲云
    来源:https://juejin.cn/post/7034777643014684703

    收起阅读 »

    现在实现倒计时都这么卷了吗?

    但是在校准时间的过程中,为了快速追赶落后的时间,时间跳动太快了,导致体验不太好,体感上感觉这时间不准呀,因此我再在那基础上再优化了一版 为求实现一版超准确!超平稳!性能极好!体验极佳的倒计时 旧版的功能实现代码 const totalDuration = 10...
    继续阅读 »

    但是在校准时间的过程中,为了快速追赶落后的时间,时间跳动太快了,导致体验不太好,体感上感觉这时间不准呀,因此我再在那基础上再优化了一版


    为求实现一版超准确!超平稳!性能极好!体验极佳的倒计时


    旧版的功能实现代码


    const totalDuration = 10 * 1000;
    let requestRef = null;
    let startTime;
    let prevEndTime;
    let prevTime;
    let currentCount = totalDuration;
    let endTime;
    let timeDifferance = 0; // 每1s倒计时偏差值,单位ms
    let interval = 1000;
    let nextTime = interval;

    setInterval(() => {
    let n = 0;
    while (n++ < 1000000000);
    }, 0);

    const animate = (timestamp) => {
    if (prevTime !== undefined) {
    const deltaTime = timestamp - prevTime;
    if (deltaTime >= nextTime) {
    prevTime = timestamp;
    prevEndTime = endTime;
    endTime = new Date().getTime();
    currentCount = currentCount - 1000;
    console.log("currentCount: ", currentCount / 1000);
    timeDifferance = endTime - startTime - (totalDuration - currentCount);
    console.log(timeDifferance);
    nextTime = interval - timeDifferance;
    // 慢太多了,就立刻执行下一个循环
    if (nextTime < 0) {
    nextTime = 0;
    }
    console.log(`执行下一次渲染的时间是:${nextTime}ms`);
    if (currentCount <= 0) {
    currentCount = 0;
    cancelAnimationFrame(requestRef);
    console.log(`累计偏差值: ${endTime - startTime - totalDuration}ms`);
    return;
    }
    }
    } else {
    startTime = new Date().getTime();
    prevTime = timestamp;
    endTime = new Date().getTime();
    }
    requestRef = requestAnimationFrame(animate);
    };

    requestRef = requestAnimationFrame(animate);


    然后有个细小的问题在于这段代码


    // 慢太多了,就立刻执行下一个循环
    if (nextTime < 0) {
    nextTime = 0;
    }

    问题在于,假如遇到线程阻塞的情况,出现了倒计时落后情况严重,假设3s,我这里设置下一个循环是0s,然后现在倒计时当前15s,就会看到快速倒计时到12s,产品同学说你这倒计时还怎么加速了呀


    这倒计时加速像极了职业生涯结束在加速倒计时一样,瑟瑟发抖的我立刻赶紧修复一下


    其实很简单,就是把这个临近值0设置接近每次循环的时间数即可,那么其实是看不出来每次是有在稍微加速/减速的,这里每次循环的时间数是1s,那么我们可以将上面这段代码修改下,把以前立刻就追赶描述的操作,放缓一下追赶的脚步,以此优化用户体验


    例如以前追赶2s3s~4s内立刻追赶上,那么波动是很明显的,但是如果把2s的落后秒数,平躺到接下来要倒计时的1min里,每次大概追赶30ms,那是看不出来滴


    // 慢到一定临界点,比正常循环的时间数稍微慢点,再执行下一个循环
    if (nextTime < 900) {
    nextTime = 900;
    }

    这里我设置落后太多时,每秒追赶100ms,假如落后2s20s后就能追赶回来啦,而且看不出明显波动,时间又是被校验准确的,得到了产品同学的好评!


    虽然修改很小,但是也是反复思考得到的~如果对时间要求比较严格,而且倒计时时间范围比较小,来不及把差距平摊到这么大的时间段,可建议让后端同学定时推送最新的倒计时给前端来校验时间准确性,这就万无一失啦


    结语


    以上是我使用requestAnimationFrame实现倒计时功能反复雕琢的心得,希望能对大家有帮助~如果能获得一个小小的赞作为鼓励会十分感激!!


    作者:一只凤梨
    链接:https://juejin.cn/post/7026735190634414087

    收起阅读 »

    中高级前端不一定了解的setTimeout | 网易实践小总结

    setTimeout的创建和执行 我们知道setTimeout是一个延时器,它会在规定的时间后延迟执行回调函数,这篇文章就来说说setTimeout它是怎么执行的。 首先我们知道消息队列是用来存储宏任务的,并且主线程会按照顺序取出队列里的任务依次执行,所以为了...
    继续阅读 »

    setTimeout的创建和执行


    我们知道setTimeout是一个延时器,它会在规定的时间后延迟执行回调函数,这篇文章就来说说setTimeout它是怎么执行的。


    首先我们知道消息队列是用来存储宏任务的,并且主线程会按照顺序取出队列里的任务依次执行,所以为了保证setTimeout能够在规定的时间内执行,setTimeout创建的任务不会被添加到消息队列里,与此同时,浏览器还维护了另外一个队列叫做延迟消息队列,该队列就是用来存放延迟任务,setTimeout创建的任务会被存放于此,同时它会被记住创建时间,延迟执行时间。


    然后我们看下具体例子:


    setTimeout(function showName() { console.log('showName') }, 1000)
    setTimeout(function showName() { console.log('showName1') }, 1000)
    console.log('martincai')

    以上例子执行是这样:



    • 1.从消息队列中取出宏任务进行执行(首次任务直接执行)

    • 2.执行setTimeout,此时会创建一个延迟任务,延迟任务的回调函数是showName,发起时间是当前的时间,延迟时间是第二个参数1000ms,然后该延迟任务会被推入到延迟任务队列

    • 3.执行console.log('martincai')代码

    • 4.从延迟队列里去筛选所有已过期任务(当前时间 >= 发起时间 + 延迟时间),然后依次执行


    所以我们可以看到showName和showName1同时执行,原因是两个延迟任务都已经过期


    循环源码:


    void MainTherad(){
    for(;;){
    // 执行消息队列中的任务
    Task task = task_queue.takeTask();
    ProcessTask(task);

    // 执行延迟队列中的任务
    ProcessDelayTask()

    if(!keep_running) // 如果设置了退出标志,那么直接退出线程循环
    break;
    }
    }

    删除延迟任务


    clearTimeout是windows下提供的原生方法,用于删除特定的setTimeout的延迟任务,我们在定义setTimeout的时候返回值就是当前任务的唯一id值,那么clearTimeout就会拿着id在延迟消息队列里查找对应的任务,将其踢出队列即可


    setTimeout的几个注意点:



    1. setTimeout会受到消息队列里的宏任务的执行时间影响,上面我们可以看到延迟消息队列的任务会在消息队列的弹出的当前任务执行完之后再执行,所以当前任务的执行时间会阻碍到setTimeout的延迟任务的执行时间


      function showName() {
    setTimeout(function show() {
    console.log('show')
    }, 0)
    for (let i = 0; i <= 5000; i++) {}
    }
    showName()

    这里去执行一遍可以发现setTimeout并不是在0ms左右执行,中间会有明显的延迟,因为setTimeout在执行的时候首先会将任务放入到延迟消息队列里,等到showName执行完之后,才会去延迟队列里去查找已过期的任务,这里setTimeout任务会被showName耽误



    1. setTimeout嵌套下会有4ms的延迟


    Chrome会把嵌套5层以上的setTimeout后当作阻塞方法,在第6次调用setTimeout的时候会自动将延时器更改为至少4ms的延迟时间



    1. 未激活的页面的setTimeout更改为至少1000ms


    当前tab页面不在active状态的时候,setTimeout的延迟至少会被更改1000ms,这样做是为了减少性能消耗和电量消耗



    1. 延迟时间有最大值


    目前Chrome、Firefox等主流浏览器都是用32bit去存储延时时间,所以最大值是2的31次方 - 1


      setTimeout(() => {
    console.log(1)
    }, 2 ** 31)

    以上代码会立即执行


    作者:我在曾经眺望彼岸
    链接:https://juejin.cn/post/7032091028609990692

    收起阅读 »

    你可以永远相信debugger,但是不能永远相信console.log

    总结放前面:console.log在打印引用数据类型的时候表现和我们的预期不相符合是因为console.log打印的是引用数据类型的一个快照,因为浏览器或者我们异步代码的原因在快照之后修改了对应的内存空间的值,所以等我们展开打印浏览器通过指针重新访问内存空间的...
    继续阅读 »

    总结放前面:

    console.log在打印引用数据类型的时候表现和我们的预期不相符合是因为console.log打印的是引用数据类型的一个快照,因为浏览器或者我们异步代码的原因在快照之后修改了对应的内存空间的值,所以等我们展开打印浏览器通过指针重新访问内存空间的时候会获得最新的值导致展开和不展开的表现不一致。

    不知道各位大佬有没有遇到过这样的情况,我在代码里面console.log()了一个数组,然后打开浏览器控制台,看着是空的就像这样[],结果我点展开它里面又有值了,但是在代码打印的位置使用length或者获取数组里面的值都是不行的,🤯 就像下面这样:

    let arr = [];
    const setFun = () => {
       return new Promise((reslove) => {
           let arr1 = [1, 2, 3];
           setTimeout(() => {
               reslove(arr1);
          }, 2000)
      });
    }
    const getFun = async () => {
       let result = await setFun();
       result.forEach((item) => {
           arr.push(item);
      })
    }
    getFun();
    console.log(arr);


    或者说我在某处代码console.log()一个对象,明明控制台打印对象的某一个key是1,但是我展开这个对象里面的key居然是2,我在代码里面获取的也是2,就像下面这样:
    不知道各位大佬遇到这样的情况是怎么个想法,反正我第一次遇到的时候我还以为是我的谷歌浏览器出问题了,擦💦我甚至都想卸载重装一波。后来动了动🧠,觉得可能是代码执行顺序的原因,所以我就在代码里面打了断点看了一下,在执行console.log()的时候arr的确是一个空的对象,对arr数组的操作是在console.log()执行之后才进行的。

    所以说这到底是为什么呐?
    其实这个还是和js的引用数据类型还有console.log()的设计有关系。我们都知道引用数据类型大体上可以说是由两部分组成:指针和内容,指针保存的内容就是一个内存地址的指向,指针一般都是基本数据类型保存在栈内存,内容就包含着这个引用数据类型的实际值一般保存在堆内存。😍 而console.log呐打印的时候只是打印了这个引用数据类型的一个快照,快照中的指针和内容都是照相的时候的内容,在console.log()之后,修改了这个引用数据类型,或者说在这之前修改的操作在一个异步的内容里面,当我们去看打印的时候,这个引用数据类型的内容可能就被修改了,但是因为快照的原因我们看到的还是以前的值。
    然后当我们展开的时候,浏览器会利用指针去内存重新读取内容,因为快找的指针是没有发生变化的,所以就看到了改变之后内存,这就是为什么我们展开和不展开看到的结果是不一样的原因了。当然造成这样的原因不一定都是因为我们代码在异步里面操作这个引用数据类型。
    还有就是浏览器在进行I/O的时候异步会提升性能,所有这就是为什么有时候我们写的同步代码依然会出现不一致的情况,就像我第二个图一样。
    下面就验证一下我上面的想法,当我把上面的代码修改一下,直接替换:

    let arr = [];
    const setFun = () => {
       return new Promise((reslove) => {
           let arr1 = [1, 2, 3];
           setTimeout(() => {
               reslove(arr1);
          }, 2000)
      });
    }
    const getFun = async () => {
       let result = await setFun();
       arr = result; // 修改部分
    }
    getFun();
    console.log(arr);

    那么我们看到的结果就和上面不一样了,这个展开的表现是和不展开是一样的。
    相信各位大佬也知道是啥原因了,因为这次直接替换,修改的是指针的指向并没有修改之前引用数据饿类型的内存空间,所以当我们展开的时候快照中指针保存的地址还是空的,这样我们看到的和看之前的想法就对应上了。
    注:该问题只存在于打印引用数据类型,基本数据类型不会出现。

    作者:江湖不渡i
    来源:https://juejin.cn/post/7032504319584780325

    收起阅读 »

    12 个救命的 CSS 技巧

    ✨12 个救命的 CSS 技巧✨ 1. 使用 Shape-outside 在浮动图像周围弯曲文本它是一个允许设置形状的 CSS 属性。它还有助于定义文本流动的区域。css代码:.any-shape {  width: 300px...
    继续阅读 »



    ✨12 个救命的 CSS 技巧✨

    1. 使用 Shape-outside 在浮动图像周围弯曲文本

    它是一个允许设置形状的 CSS 属性。它还有助于定义文本流动的区域。css代码:

    .any-shape {
     width: 300px;
     float: left;
     shape-outside: circle(50%);
    }

    2. 魔法组合

    这个小组合实际上可以防止你在 HTML 中遇到的大多数布局错误的问题。我们确实不希望水平滑块或绝对定位的项目做他们想做的事情,也不希望到处都是随机的边距和填充。所以这是你们的魔法组合。

    * {
    padding: 0;
    margin: 0;
    max-width: 100%;
    overflow-x: hidden;
    position: relative;
    display: block;
    }

    有时“display:block”没有用,但在大多数情况下,你会将 <a><span> 视为与其他块一样的块。所以,在大多数情况下,它实际上会帮助你!

    3. 拆分 HTML 和 CSS

    这更像是一种“工作流程”类型的技巧。我建议你在开发时创建不同的 CSS 文件,最后才合并它们。例如,一个用于桌面,一个用于移动等。最后,你必须合并它们,因为这将有助于最大限度地减少您网站的 HTTP 请求数量。

    同样的原则也适用于 HTML。如果你不是在 Gatsby 等 SPA 环境中进行开发,那么 PHP 可用于包含 HTML 代码片段。例如,你希望在单独的文件中保留一个“/modules”文件夹,该文件夹将包含导航栏、页脚等。因此,如果需要进行任何更改,你不必在每个页面上都对其进行编辑。模块化越多,结果就越好。

    4. ::首字母

    它将样式应用于块级元素的第一个字母。因此,我们可以从印刷或纸质杂志中引入我们熟悉的效果。如果没有这个伪元素,我们将不得不创建许多跨度来实现这种效果。例如:

    这是如何做到的?代码如下:

    p.intro:first-letter {
     font-size: 100px;
     display: block;
     float: left;
     line-height: .5;
     margin: 15px 15px 10px 0 ;
    }

    5. 四大核心属性

    CSS 动画提供了一种相对简单的方法来在大量属性之间平滑过渡。良好的动画界面依赖于流畅流畅的体验。为了在我们的动画时间线中保持良好的性能,我们必须将我们的动画属性限制为以下四个核心:

    • 缩放 - transform:scale(2)

    • 旋转 - transform:rotate(180deg)

    • 位置 – transform:translateX(50rem)

    • 不透明度 - opacity: 0.5

    边框半径、高度/宽度或边距等动画属性会影响浏览器布局方法,而背景、颜色或框阴影的动画会影响浏览器绘制方法。所有这些都会大大降低您的 FPS (FramesPerSecond)。您可以使用这些属性来产生一些有趣的效果,但应谨慎使用它们以保持良好的性能。

    6. 使用变量保持一致

    保持一致性的一个好方法是使用 CSS 变量或预处理器变量来预定义动画时间。

    :root{ timing-base: 1000;}

    在不定义单元的情况下设置基线动画或过渡持续时间为我们提供了在 calc() 函数中调用此持续时间的灵活性。此持续时间可能与我们的基本 CSS 变量不同,但它始终是对该数字的简单修改,并将始终保持一致的体验。

    7. 圆锥梯度

    有没有想过是否可以只使用 CSS 创建饼图?好消息是,您实际上可以!这可以使用 conic-gradient 函数来完成。此函数创建一个由渐变组成的图像,其中设置的颜色过渡围绕中心点旋转。您可以使用以下代码行执行此操作:

    .piechart {
     background: conic-gradient(rgb(255, 132, 45) 0% 25%, rgb(166, 195, 209) 25% 56%, #ffb50d  56% 100%);
     border-radius: 50%;
     width: 300px;
     height: 300px;
    }

    8. 更改文本选择颜色

    要更改文本选择颜色,我们使用 ::selection。它是一个伪元素,在浏览器级别覆盖以使用您选择的颜色替换文本突出显示颜色。使用光标选择内容后即可看到效果。

    ::selection {
        background-color: #f3b70f;
    }

    9. 悬停效果

    悬停效果通常用于按钮、文本链接、站点的块部分、图标等。如果您想在有人将鼠标悬停在其上时更改颜色,只需使用相同的 CSS,但要添加 :hover到它并更改样式。这是您的方法;

    .m h2{ 
       font-size:36px;
       color:#000;
       font-weight:800;
    }
    .m h2:hover{
       color:#f00;
    }

    当有人将鼠标悬停在 h2 标签上时,这会将您的 h2 标签的颜色从黑色更改为红色。它非常有用,因为如果您不想更改它,则不必再次声明字体大小或粗细。它只会更改您指定的任何属性。

    10.投影

    添加此属性可为透明图像带来更好的阴影效果。您可以使用给定的代码行执行此操作。

    .img-wrapper img{
             width: 100% ;
             height: 100% ;
             object-fit: cover ;
             filter: drop-shadow(30px 10px 4px #757575);
    }

    11. 使用放置项居中 Div

    居中 div 元素是我们必须执行的最可怕的任务之一。但不要害怕我的朋友,你可以用几行 CSS 将任何 div 居中。只是不要忘记设置display:grid; 对于父元素,然后使用如下所示的 place-items 属性。

    main{
    width: 100% ;
    height: 80vh ;
    display: grid ;
    place-items: center center;
    }

    12. 使用 Flexbox 居中 Div

    我们已经使用地点项目将项目居中。但是现在我们解决了一个经典问题,使用 flexbox 将 div 居中。为此,让我们看一下下面的示例:

    <div>
    <div></div>
    </div>
    .center {
    display: flex;
    align-items: center;
    justify-content: center;
    }

    .center div {
    width: 100px;
    height: 100px;
    border-radius: 50%;
    background: #b8b7cd;
    }

    首先,我们需要确保父容器持有圆,即 flex-container。在它里面,我们有一个简单的 div 来制作我们的圆圈。我们需要使用以下与 flexbox 相关的重要属性:

    • display: flex; 这确保父容器具有 flexbox 布局。

    • align-items: center; 这可确保 flex 子项与横轴的中心对齐。

    • justify-content: center; 这确保 flex 子项与主轴的中心对齐。

    之后,我们就有了常用的圆形 CSS 代码。现在这个圆是垂直和水平居中的,试试吧!

    作者:海拥
    来源:https://juejin.cn/post/7024372412632268813

    收起阅读 »

    vscode调试入门——不要只会console了!什么是launch.json?

    前言 记得我还是一个小菜鸡的时候,就有人问过我,都用什么调试,我红着脸说到,我只会用console调试。羞愧的我想再继续掌握一下vscode调试的方法,可惜当时没有找到很好的教程,加上相关基础较差,只能是一知半解。如今进化为大菜鸡的我,总结一下基础的vscod...
    继续阅读 »

    前言


    记得我还是一个小菜鸡的时候,就有人问过我,都用什么调试,我红着脸说到,我只会用console调试。羞愧的我想再继续掌握一下vscode调试的方法,可惜当时没有找到很好的教程,加上相关基础较差,只能是一知半解。如今进化为大菜鸡的我,总结一下基础的vscode调试


    基本调试


    可以说vscode对js代码的调试非常友好了,它内置了node的debugger插件,如果是要用vscdoe调试python,c++等还是要后续安装插件的


    基本的调试方法很简单,写一段简单的代码


    图片.png


    然后在调试项里找到这个小三角箭头


    图片.png


    然后就可以进入node的调试界面


    图片.png


    怎么样!是不是很简单就达到了我想要的效果,比单纯一个个console出来要更好


    深入一下


    上边的方法虽然很简单,但是只适用比较简单的情况,对于大多数调试场景,去创建launch配置文件是更好的,因为它允许配置和保存调试设置细节


    launch.json


    当你刚开始创建还没有launch.json的时候 vscoed会自动帮你自动检测你的debug环境,开始debug
    但如果失败了,会让你进行选择


    图片.png


    然后他会在你当前工作区下,给你创建个.vscode文件夹,里面有我们要的launch.json文件。简单说我们可以通过这个文件可配置的debug


    假如你的launch.json文件是这样的(搞懂意思就好)



    {
    "version": "0.2.0",
    "configurations": [
    {
    "type": "node",
    "request": "launch",
    "name": "Launch Program",
    "skipFiles": ["<node_internals>/**"],

    }
    ]
    }

    其中


    type属性是指你这次debug的类型 我这里介绍常用的两类 node和chrome 下边都会说到


    request 指的是请求配置类型,氛围launch和attach


    name 指的就是你这一条调试配置的name,会出现在start绿箭头,选择具体方式的时候


    这几个是必用的


    有更多的会在之后涉及,了解更多可以看这里的文档


    attach还是launch


    这是两个核心的debugging模式,用不同方式去处理工作流


    简单来说 launch会在 你调试的工具 也就是我们用的vscode 启动另外的应用,这很适合你习惯于用浏览器的方法


    attach 意为附加 会在你的开发者工具上附加调试程序


    chrome debugger


    除了用上述的type为node的调试方法,我们也可以用调用的chrome的工作台去调试


    这里要安装一个插件 debugger for chrome 我在之前的文章当你买了新的mac 曾经提到过


    当安装好之后


    你就可以在我们的launch.json里添加配置啦!


    图片.png


    假如我们添加一个这样的配置


    {
    "type": "chrome",
    "request": "launch",
    "name": "Launch Chrome",
    "url": "http://localhost:8080",
    "file": "${workspaceFolder}/index.html"
    },

    file 值得就是打开的文件 workspaceFolder是我们当前的工作区


    假如我们的index.html是这样的并打上断点


    图片.png


    可以进入我们的chrome调试页面


    图片.png


    总结


    基本的调试入门方法就是这样啦,其实还有更深层的内容,我会继续学习,完善这篇文章


    作者:douxpang
    链接:https://juejin.cn/post/6956832271236071431

    收起阅读 »

    前端架构师的 git 功力,你有几成火候?

    Git
    分支管理策略 git 分支强大的同时也非常灵活,如果没有一个好的分支管理策略,团队人员随意合并推送,就会造成分支混乱,各种覆盖,冲突,丢失等问题。 目前最流行的分支管理策略,也称工作流(Workflow),主要包含三种: Git Flow GitHub Fl...
    继续阅读 »

    分支管理策略


    git 分支强大的同时也非常灵活,如果没有一个好的分支管理策略,团队人员随意合并推送,就会造成分支混乱,各种覆盖,冲突,丢失等问题。


    目前最流行的分支管理策略,也称工作流(Workflow),主要包含三种:



    • Git Flow

    • GitHub Flow

    • GitLab Flow


    我司前端团队结合实际情况,制定出自己的一套分支管理策略。


    我们将分支分为 4 个大类:



    • dev-*

    • develop

    • staging

    • release


    dev-* 是一组开发分支的统称,包括个人分支,模块分支,修复分支等,团队开发人员在这组分支上进行开发。


    开发前,先通过 merge 合并 develop 分支的最新代码;开发完成后,必须通过 cherry-pick 合并回 develop 分支。


    develop 是一个单独分支,对应开发环境,保留最新的完整的开发代码。它只接受 cherry-pick 的合并,不允许使用 merge。


    staging 分支对应测试环境。当 develop 分支有更新并且准备发布测试时,staging 要通过 rebase 合并 develop 分支,然后将最新代码发布到测试服务器,供测试人员测试。


    测试发现问题后,再走 dev-* -> develop -> staging 的流程,直到测试通过。


    release 则表示生产环境。release 分支的最新提交永远与线上生产环境代码保持同步,也就是说,release 分支是随时可发布的。


    当 staging 测试通过后,release 分支通过 rebase 合并 staging 分支,然后将最新代码发布到生产服务器。


    总结下合并规则:



    • develop -> (merge) -> dev-*

    • dev-* -> (cherry-pick) -> develop

    • develop -> (rebase) -> staging

    • staging -> (rebase) -> release


    为什么合并到 develop 必须用 cherry-pick?


    使用 merge 合并,如果有冲突,会产生分叉;dev-* 分支多而杂,直接 merge 到 develop 会产生错综复杂的分叉,难以理清提交进度。


    而 cherry-pick 只将需要的 commit 合并到 develop 分支上,且不会产生分叉,使 git 提交图谱(git graph)永远保持一条直线。


    再有,模块开发分支完成后,需要将多个 commit 合为一个 commit,再合并到 develop 分支,避免了多余的 commit,这也是不用 merge 的原因之一。


    为什么合并到 staging/release 必须用 rebase?


    rebase 译为变基,合并同样不会产生分叉。当 develop 更新了许多功能,要合并到 staging 测试,不可能用 cherry-pick 一个一个把 commit 合并过去。因此要通过 rebase 一次性合并过去,并且保证了 staging 与 develop 完全同步。


    release 也一样,测试通过后,用 rebase 一次性将 staging 合并过去,同样保证了 staging 与 release 完全同步。


    commit 规范与提交验证


    commit 规范是指 git commit 时填写的描述信息,要符合统一规范。


    试想,如果团队成员的 commit 是随意填写的,在协作开发和 review 代码时,其他人根本不知道这个 commit 是完成了什么功能,或是修复了什么 Bug,很难把控进度。


    为了直观的看出 commit 的更新内容,开发者社区诞生了一种规范,将 commit 按照功能划分,加一些固定前缀,比如 fix:feat:,用来标记这个 commit 主要做了什么事情。


    目前主流的前缀包括以下部分:



    • build:表示构建,发布版本可用这个

    • ci:更新 CI/CD 等自动化配置

    • chore:杂项,其他更改

    • docs:更新文档

    • feat:常用,表示新增功能

    • fix:常用:表示修复 bug

    • perf:性能优化

    • refactor:重构

    • revert:代码回滚

    • style:样式更改

    • test:单元测试更改


    这些前缀每次提交都要写,刚开始很多人还是记不住的。这里推荐一个非常好用的工具,可以自动生成前缀。地址在这里


    首先全局安装:


    npm install -g commitizen cz-conventional-changelog

    创建 ~/.czrc 文件,写入如下内容:


    { "path": "cz-conventional-changelog" }

    现在可以用 git cz 命令来代替 git commit 命令,效果如下:


    WX20210922.png


    然后上下箭选择前缀,根据提示即可方便的创建符合规范的提交。


    有了规范之后,光靠人的自觉遵守是不行的,还要在流程上对提交信息进行校验。


    这个时候,我们要用到一个新东西 —— git hook,也就是 git 钩子。


    git hook 的作用是在 git 动作发生前后触发自定义脚本。这些动作包括提交,合并,推送等,我们可以利用这些钩子在 git 流程的各个环节实现自己的业务逻辑。


    git hook 分为客户端 hook 和服务端 hook。


    客户端 hook 主要有四个:



    • pre-commit:提交信息前运行,可检查暂存区的代码

    • prepare-commit-msg:不常用

    • commit-msg:非常重要,检查提交信息就用这个钩子

    • post-commit:提交完成后运行


    服务端 hook 包括:



    • pre-receive:非常重要,推送前的各种检查都在这

    • post-receive:不常用

    • update:不常用


    大多数团队是在客户端做校验,所以我们用 commit-msg 钩子在客户端对 commit 信息做校验。


    幸运的是,不需要我们手动去写校验逻辑,社区有成熟的方案:husky + commitlint


    husky 是创建 git 客户端钩子的神器,commitlint 是校验 commit 信息是否符合上述规范。两者配合,可以阻止创建不符合 commit 规范的提交,从源头保证提交的规范。


    husky + commitlint 的具体使用方法请看这里


    误操作的撤回方案


    开发中频繁使用 git 拉取推送代码,难免会有误操作。这个时候不要慌,git 支持绝大多数场景的撤回方案,我们来总结一下。


    撤回主要是两个命令:resetrevert


    git reset


    reset 命令的原理是根据 commitId 来恢复版本。因为每次提交都会生成一个 commitId,所以说 reset 可以帮你恢复到历史的任何一个版本。



    这里的版本和提交是一个意思,一个 commitId 就是一个版本



    reset 命令格式如下:


    $ git reset [option] [commitId]

    比如,要撤回到某一次提交,命令是这样:


    $ git reset --hard cc7b5be

    上面的命令,commitId 是如何获取的?很简单,用 git log 命令查看提交记录,可以看到 commitId 值,这个值很长,我们取前 7 位即可。


    这里的 option 用的是 --hard,其实共有 3 个值,具体含义如下:



    • --hard:撤销 commit,撤销 add,删除工作区改动代码

    • --mixed:默认参数。撤销 commit,撤销 add,还原工作区改动代码

    • --soft:撤销 commit,不撤销 add,还原工作区改动代码


    这里要格外注意 --hard,使用这个参数恢复会删除工作区代码。也就是说,如果你的项目中有未提交的代码,使用该参数会直接删除掉,不可恢复,慎重啊!


    除了使用 commitId 恢复,git reset 还提供了恢复到上一次提交的快捷方式:


    $ git reset --soft HEAD^

    HEAD^ 表示上一个提交,可多次使用。


    其实平日开发中最多的误操作是这样:刚刚提交完,突然发现了问题,比如提交信息没写好,或者代码更改有遗漏,这时需要撤回到上次提交,修改代码,然后重新提交。


    这个流程大致是这样的:


    # 1. 回退到上次提交
    $ git reset HEAD^
    # 2. 修改代码...
    ...
    # 3. 加入暂存
    $ git add .
    # 4. 重新提交
    $ git commit -m 'fix: ***'

    针对这个流程,git 还提供了一个更便捷的方法:


    $ git commit --amend

    这个命令会直接修改当前的提交信息。如果代码有更改,先执行 git add,然后再执行这个命令,比上述的流程更快捷更方便。


    reset 还有一个非常重要的特性,就是真正的后退一个版本


    什么意思呢?比如说当前提交,你已经推送到了远程仓库;现在你用 reset 撤回了一次提交,此时本地 git 仓库要落后于远程仓库一个版本。此时你再 push,远程仓库会拒绝,要求你先 pull。


    如果你需要远程仓库也后退版本,就需要 -f 参数,强制推送,这时本地代码会覆盖远程代码。


    注意,-f 参数非常危险!如果你对 git 原理和命令行不是非常熟悉,切记不要用这个参数。


    那撤回上一个版本的代码,怎么同步到远程更安全呢?


    方案就是下面要说的第二个命令:git revert


    git revert


    revert 与 reset 的作用一样,都是恢复版本,但是它们两的实现方式不同。


    简单来说,reset 直接恢复到上一个提交,工作区代码自然也是上一个提交的代码;而 revert 是新增一个提交,但是这个提交是使用上一个提交的代码。


    因此,它们两恢复后的代码是一致的,区别是一个新增提交(revert),一个回退提交(reset)。


    正因为 revert 永远是在新增提交,因此本地仓库版本永远不可能落后于远程仓库,可以直接推送到远程仓库,故而解决了 reset 后推送需要加 -f 参数的问题,提高了安全性。


    说完了原理,我们再看一下使用方法:


    $ git revert -n [commitId]

    掌握了原理使用就很简单,只要一个 commitId 就可以了。


    Tag 与生产环境


    git 支持对于历史的某个提交,打一个 tag 标签,常用于标识重要的版本更新。


    目前普遍的做法是,用 tag 来表示生产环境的版本。当最新的提交通过测试,准备发布之时,我们就可以创建一个 tag,表示要发布的生产环境版本。


    比如我要发一个 v1.2.4 的版本:


    $ git tag -a v1.2.4 -m "my version 1.2.4"

    然后可以查看:


    $ git show v1.2.4

    > tag v1.2.4
    Tagger: ruims <2218466341@qq.com>
    Date: Sun Sep 26 10:24:30 2021 +0800

    my version 1.2.4

    最后用 git push 将 tag 推到远程:


    $ git push origin v1.2.4

    这里注意:tag 和在哪个分支创建是没有关系的,tag 只是提交的别名。因此 commit 的能力 tag 均可使用,比如上面说的 git resetgit revert 命令。


    当生产环境出问题,需要版本回退时,可以这样:


    $ git revert [pre-tag]
    # 若上一个版本是 v1.2.3,则:
    $ git revert v1.2.3

    在频繁更新,commit 数量庞大的仓库里,用 tag 标识版本显然更清爽,可读性更佳。


    再换一个角度思考 tag 的用处。


    上面分支管理策略的部分说过,release 分支与生产环境代码同步。在 CI/CD(下面会讲到)持续部署的流程中,我们是监听 release 分支的推送然后触发自动构建。


    那是不是也可以监听 tag 推送再触发自动构建,这样版本更新的直观性是不是更好?


    诸多用处,还待大家思考。


    永久杜绝 443 Timeout


    我们团队内部的代码仓库是 GitHub,众所周知的原因,GitHub 拉取和推送的速度非常慢,甚至直接报错:443 Timeout。


    我们开始的方案是,全员开启 VPN。虽然大多时候速度不错,但是确实有偶尔的一个小时,甚至一天,代码死活推不上去,严重影响开发进度。


    后来突然想到,速度慢超时是因为被墙,比如 GitHub 首页打不开。再究其根源,被墙的是访问网站时的 http 或 https 协议,那么其他协议是不是就不会有墙的情况?


    想到就做。我们发现 GitHub 除了默认的 https 协议,还支持 ssh 协议。于是准备尝试一下使用 ssh 协议克隆代码。


    用 ssh 协议比较麻烦的一点,是要配置免密登录,否则每次 pull/push 时都要输入账号密码。


    GitHub 配置 SSH 的官方文档在这里


    英文吃力的同学,可以看这里


    总之,生成公钥后,打开 GitHub 首页,点 Account -> Settings -> SSH and GPG keys -> Add SSH key,然后将公钥粘贴进去即可。


    现在,我们用 ssh 协议克隆代码,例子如下:


    $ git clone git@github.com:[organi-name]/[project-name]

    发现瞬间克隆下来了!再测几次 pull/push,速度飞起!


    不管你用哪个代码管理平台,如果遇到 443 Timeout 问题,请试试 ssh 协议!


    hook 实现部署?


    利用 git hook 实现部署,应该是 hook 的高级应用了。


    现在有很多工具,比如 GitHub,GitLab,都提供了持续集成功能,也就是监听某一分支推送,然后触发自动构建,并自动部署。


    其实,不管这些工具有多少花样,核心的功能(监听和构建)还是由 git 提供。只不过在核心功能上做了与自家平台更好的融合。


    我们今天就抛开这些工具,追本溯源,使用纯 git 实现一个 react 项目的自动部署。掌握了这套核心逻辑,其他任何平台的持续部署也就没那么神秘了。


    由于这一部分内容较多,所以单独拆出去一篇文章,地址如下:


    纯 Git 实现前端 CI/CD


    终极应用: CI/CD


    上面的一些地方也提到了持续集成,持续部署这些字眼,现在,千呼万唤始出来,主角正式登场了!


    可以这么说,上面写到的所有规范规则,都是为了更好的设计和实现这个主角 ——— CI/CD。


    首先了解一下,什么是 CI/CD ?


    核心概念,CI(Continuous Integration)译为持续集成,CD 包括两部分,持续交付(Continuous Delivery)和持续部署(Continuous Deployment)


    从全局看,CI/CD 是一种通过自动化流程来频繁向客户交付应用的方法。这个流程贯穿了应用的集成,测试,交付和部署的整个生命周期,统称为 “CI/CD 管道”。


    虽然都是像流水线一样自动化的管道,但是 CI 和 CD 各有分工。


    持续集成是频繁地将代码集成到主干分支。当新代码提交,会自动执行构建、测试,测试通过则自动合并到主干分支,实现了产品快速迭代的同时保持高质量。


    持续交付是频繁地将软件的新版本,交付给质量团队或者用户,以供评审。评审通过则可以发布生产环境。持续交付要求代码(某个分支的最新提交)是随时可发布的状态。


    持续部署是代码通过评审后,自动部署到生产环境。持续部署要求代码(某个分支的最新提交)是随时可部署的。


    持续部署与持续交付的唯一区别,就是部署到生产环境这一步,是否是自动化


    部署自动化,看似是小小的一步,但是在实践过程中你会发现,这反而是 CI/CD 流水线中最难落实的一环。


    为什么?首先,从持续集成到持续交付,这些个环节都是由开发团队实施的。我们通过团队内部协作,产出了新版本的待发布的应用。


    然而将应用部署到服务器,这是运维团队的工作。我们要实现部署,就要与运维团队沟通,然而开发同学不了解服务器,运维同学不了解代码,沟通起来困难重重。


    再有,运维是手动部署,我们要实现自动部署,就要有服务器权限,与服务器交互。这也是个大问题,因为运维团队一定会顾虑安全问题,因而推动起来节节受阻。


    目前社区成熟的 CI/CD 方案有很多,比如老牌的 jenkins,react 使用的 circleci,还有我认为最好用的GitHub Action等,我们可以将这些方案接入到自己的系统当中。


    这篇文章篇幅已经很长了,就到这里结束吧。接下来我会基于 GitHub Action 单独出一篇详细的 react 前端项目 CI/CD 实践,记得关注我的专栏哦。



    作者:杨成功
    链接:https://juejin.cn/post/7024043015794589727

    收起阅读 »

    先睹为快即将到来的HTML6

    HTML,超文本标记语言,是一种用于创建网页的标准标记语言。自从引入 HTML 以来,它就一直用于构建互联网。与 JavaScript 和 CSS 一起,HTML 构成前端开发的三剑客。 尽管许多新技术使网站创建过程变得更简单、更高效,但 HTML 始终是核心...
    继续阅读 »

    HTML,超文本标记语言,是一种用于创建网页的标准标记语言。自从引入 HTML 以来,它就一直用于构建互联网。与 JavaScript 和 CSS 一起,HTML 构成前端开发的三剑客。


    尽管许多新技术使网站创建过程变得更简单、更高效,但 HTML 始终是核心。随着 HTML5 的普及,在 2014 年,这种标记语言发生了很多变化,变得更加友好,浏览器对新标准的支持热度也越来越高。而HTML并不止于此,还在不断发生变化,并且可能会获得一些特性来证明对 HTML6 的命名更改是合理的。


    支持原生模式


    该元素<dialog> 将随 HTML6 一起提供。它被认为等同于用 JavaScript 开发的模态,并且已经标准化,但只有少数浏览器完全支持。但这种现象会改变,很快它将在所有浏览器中得到支持。


    这个元素在其默认格式下,只会将光标显示在它所在的位置上,但可以使用 JavaScript 打开模式。


    <dialog>
    <form method="dialog">
    <input type="submit" value="确定" />
    <input type="submit" value="取消" />
    </form>
    </dialog>

    在默认形式下,该元素创建一个灰色背景,其下方是非交互式内容。


    可以在 <dialog> 其中的表单上使用一种方法,该方法将发送值并将其传递回自身 <dialog>


    总的来说,这个标签在用户交互和改进的界面中变得有益。


    可以通过更改 <dialog> 标签的 open 属性以控制打开和关闭。


    <dialog open>
    <p>组件内容</p>
    </dialog>

    没有 JavaScript 的单页应用程序


    FutureClaw 杂志主编 Bobby Mozumder 建议:



    将锚元素链接到 JSON/XML、API 端点,让浏览器在内部将数据加载到新的数据结构中,然后浏览器将 DOM 元素替换为根据需要加载的任何数据。初始数据(以及标准错误响应)可以放在标题装置中,如果需要,可以稍后替换。



    据他介绍,这是单页应用程序网页设计模式,可以提高响应速度和加载时间,因为不需要加载 JavaScript。


    自由调整图像大小


    HTML6 爱好者相信即将到来的更新将允许浏览器调整图像大小以获得更好的观看体验。


    每个浏览器都难以呈现相对于设备和屏幕尺寸的最佳图像尺寸,不幸的是,srce 标签 img 在处理这个问题时不是很有效。


    这个问题可以通过一个新标签 <srcset> 来解决,它使浏览器在多个图像之间进行选择的工作变得更加容易。


    专用库


    将可用库引入 HTML6 绝对是提高开发效率的重要一步。


    微格式


    很多时候,需要在互联网上定义一般信息,而这些一般信息可以是任何公开的信息,例如电话号码、姓名、地址等。微格式是能够定义一般数据的标准。微格式可以增强设计者的能力,并可以减少搜索引擎推断公共信息所需的努力。


    自定义菜单


    尽管标签<ul><ol>非常有用,但在某些情况下仍有一些不足之处。可以处理交互元素的标签将是一个不错的选择。


    这就是创建标签 <menu> 的驱动力,它可以处理按钮驱动的列表元素。


    <menu type="toolbar">
    <li><button>个人信息</button></li>
    <li><button>系统设置</button></li>
    <li><button>账号注销</button></li>
    </menu>

    因此 <menu>,除了能够像普通列表一样运行之外,还可以增强 HTML 列表的功能。


    增强身份验证


    虽然HTML5在安全性方面还不错,浏览器和网络技术也提供了合理的保护。毫无疑问,在身份验证和安全领域还有很多事情可以做。如密钥可以异地存储;这将防止不受欢迎的人访问并支持身份验证。使用嵌入式密钥而不是 cookie,使数字签名更好等。


    集成摄像头


    HTML6 允许以更好的方式使用设备上的相机和媒体。将能够控制相机、它的效果、模式、全景图像、HDR 和其他属性。


    总结


    没有什么是完美的,HTML 也不是完美的,所以 HTML 规范可以做很多事情来使它更好。应该对一些有用的规范进行标准化,以增强 HTML 的能力。小的变化已经开始推出。如增强蓝牙支持、p2p 文件传输、恶意软件保护、云存储集成,下一个 HTML 版本可以考虑一下。


    作者:天行无忌
    链接:https://juejin.cn/post/7032874253573685261

    收起阅读 »

    先睹为快即将到来的HTML6

    HTML,超文本标记语言,是一种用于创建网页的标准标记语言。自从引入 HTML 以来,它就一直用于构建互联网。与 JavaScript 和 CSS 一起,HTML 构成前端开发的三剑客。 尽管许多新技术使网站创建过程变得更简单、更高效,但 HTML 始终是核...
    继续阅读 »
    HTML,超文本标记语言,是一种用于创建网页的标准标记语言。自从引入 HTML 以来,它就一直用于构建互联网。与 JavaScript 和 CSS 一起,HTML 构成前端开发的三剑客。

    尽管许多新技术使网站创建过程变得更简单、更高效,但 HTML 始终是核心。随着 HTML5 的普及,在 2014 年,这种标记语言发生了很多变化,变得更加友好,浏览器对新标准的支持热度也越来越高。而HTML并不止于此,还在不断发生变化,并且可能会获得一些特性来证明对 HTML6 的命名更改是合理的。

    支持原生模式

    该元素<dialog> 将随 HTML6 一起提供。它被认为等同于用 JavaScript 开发的模态,并且已经标准化,但只有少数浏览器完全支持。但这种现象会改变,很快它将在所有浏览器中得到支持。

    这个元素在其默认格式下,只会将光标显示在它所在的位置上,但可以使用 JavaScript 打开模式。

    <dialog>
    <form method="dialog">
      <input type="submit" value="确定" />
      <input type="submit" value="取消" />
    </form>
    </dialog>

    在默认形式下,该元素创建一个灰色背景,其下方是非交互式内容。

    可以在 <dialog> 其中的表单上使用一种方法,该方法将发送值并将其传递回自身 <dialog>

    总的来说,这个标签在用户交互和改进的界面中变得有益。

    可以通过更改 <dialog> 标签的 open 属性以控制打开和关闭。

    <dialog open>
    <p>组件内容</p>
    </dialog>

    没有 JavaScript 的单页应用程序

    FutureClaw 杂志主编 Bobby Mozumder 建议:

    将锚元素链接到 JSON/XML、API 端点,让浏览器在内部将数据加载到新的数据结构中,然后浏览器将 DOM 元素替换为根据需要加载的任何数据。初始数据(以及标准错误响应)可以放在标题装置中,如果需要,可以稍后替换。

    据他介绍,这是单页应用程序网页设计模式,可以提高响应速度和加载时间,因为不需要加载 JavaScript。

    自由调整图像大小

    HTML6 爱好者相信即将到来的更新将允许浏览器调整图像大小以获得更好的观看体验。

    每个浏览器都难以呈现相对于设备和屏幕尺寸的最佳图像尺寸,不幸的是,srce 标签 img 在处理这个问题时不是很有效。

    这个问题可以通过一个新标签 <srcset> 来解决,它使浏览器在多个图像之间进行选择的工作变得更加容易。

    专用库

    将可用库引入 HTML6 绝对是提高开发效率的重要一步。

    微格式

    很多时候,需要在互联网上定义一般信息,而这些一般信息可以是任何公开的信息,例如电话号码、姓名、地址等。微格式是能够定义一般数据的标准。微格式可以增强设计者的能力,并可以减少搜索引擎推断公共信息所需的努力。

    自定义菜单

    尽管标签<ul><ol>非常有用,但在某些情况下仍有一些不足之处。可以处理交互元素的标签将是一个不错的选择。

    这就是创建标签 <menu> 的驱动力,它可以处理按钮驱动的列表元素。

    <menu type="toolbar">
    <li><button>个人信息</button></li>
    <li><button>系统设置</button></li>
    <li><button>账号注销</button></li>
    </menu>

    因此 <menu>,除了能够像普通列表一样运行之外,还可以增强 HTML 列表的功能。

    增强身份验证

    虽然HTML5在安全性方面还不错,浏览器和网络技术也提供了合理的保护。毫无疑问,在身份验证和安全领域还有很多事情可以做。如密钥可以异地存储;这将防止不受欢迎的人访问并支持身份验证。使用嵌入式密钥而不是 cookie,使数字签名更好等。

    集成摄像头

    HTML6 允许以更好的方式使用设备上的相机和媒体。将能够控制相机、它的效果、模式、全景图像、HDR 和其他属性。

    总结

    没有什么是完美的,HTML 也不是完美的,所以 HTML 规范可以做很多事情来使它更好。应该对一些有用的规范进行标准化,以增强 HTML 的能力。小的变化已经开始推出。如增强蓝牙支持、p2p 文件传输、恶意软件保护、云存储集成,下一个 HTML 版本可以考虑一下。

    作者:天行无忌
    来源:https://juejin.cn/post/7032874253573685261

    收起阅读 »

    是时候封装一个DOM库了

    增首先,如果用原始的DOM API,我们想要创建一个div,div里面含有一个文本'hi',需要分为两步而这里,只需要一步就能完成dom.create('hi') 它可以直接创建多标签的嵌套,如create('你好') 为什么能这样写? 因为我们用inn...
    继续阅读 »

    由于原始的DOM提供的API过长,不方便记忆,
    于是我采用对象风格的形式封装了一个DOM库->源代码链接
    这里对新封装的API进行总结
    同样,用增删改查进行划分,我们先提供一个全局的window.dom对象

    创建节点

    create (string){
           const container = document.createElement("template")//template可以容纳任意元素
           container.innerHTML = string.trim();//去除字符串两边空格
           return container.content.firstChild;//用template,里面的元素必须这样获取  
    }

    首先,如果用原始的DOM API,我们想要创建一个div,div里面含有一个文本'hi',需要分为两步

    1. document.createElement('div')

    2. div.innerText = 'hi'

    而这里,只需要一步就能完成dom.create('

    hi
    ')
    它可以直接创建多标签的嵌套,如create('
    你好
    ')

    为什么能这样写?
    因为我们用innerHTML直接把字符串写进了HTML里,字符串直接变成了HTML里面的内容
    为什么使用template?
    因为template可以容纳任意元素,如果使用div,div不能直接容纳标签,但template就可以

    新增哥哥

    before(node,node2){
       node.parentNode.insertBefore(node2,node);
    }

    这个比较简单,找到爸爸节点,然后使用```JavaScript,新增一个node2即可

    新增弟弟

    after(node,node2){
       node.parentNode.insertBefore(node2,node.nextSibling); //把node2插到node下一个节点的前面,即使node的下一个节点为空,也能插入  
    }

    由于原始的DOM只有insertBefore,并没有insertAfter,所以要实现这个功能我们需要一个曲线救国的方法:
    node.nextSibling 表示node节点的下一个节点,
    而想在node的后面插入一个节点,就等于说在node的下一个节点前插入一个新节点node2即可
    如上代码就是实现了这个操作,而即使node的下一个节点为空,也能成功插入

    新增儿子

     append(parent,node){
           parent.appendChild(node)
    }

    找到爸爸节点,用appendChild即可

    新增爸爸

     wrap(node,parent){
           dom.before(node,parent)
           dom.append(parent,node)
    }

    思路如图:

    image.png

    分为两步走:

    1. 先把新增的爸爸节点,放到老节点的前面

    2. 再把老节点放入新增的爸爸节点的里面

    这样就可以使新的爸爸节点包裹住老节点

    使用示例:

    const newDiv = dom.create('
    '
    )

    dom.wrap(test, newDiv)

    删节点

    remove(node){
          node.parentNode.removeChild(node)
          return node
    }

    找到爸爸节点,removeChild即可

    删除所有子节点

    empty(node){
       const array = []
       let x = node.firstChild
       while (x) {
           array.push(dom.remove(node.firstChild))
           x = node.firstChild//x指向下一个节点
      }
       return array
    }

    其实一开始的思路,是用for循环

    for(let i = 0;i<childNodes.length;i++){
       dom.remove(childNodes[i])
    }

    但这样的思路有一个问题:childNodes.length是会随着删除而变化的
    所以我们需要改变思路,用while循环:

    1. 先找到该节点的第一个儿子赋值为x

    2. 当x是存在的,我们就把它移除,并放入数组里面(用于获取删除的节点的引用)

    3. 再把x赋值给它的下一个节点(当第一个儿子被删除后,下一个儿子就变成了第一个儿子)

    4. 反复操作,直到所有子节点被删完

    读写属性

     attr(node,name,value){
           if(arguments.length === 3){
               node.setAttribute(name,value)
          }else if(arguments.length === 2){
               return node.getAttribute(name)
          }
    }

    这里运用重载,实现两种不同的功能:

    1. 当输入的参数是3个时,就写属性

    2. 当如数的参数是2个时,读属性

    使用示例:

    //写:
    //给
    test
    添加属性

    dom.attr(test,'title','Hi,I am Wang')
    //添加之后:
    test

    //读:
    const title = dom.attr(test,'title')
    console.log(`title:${title}`)
    //打印出:title:Hi,Hi,I am Wang

    读写文本内容

    text(node,string){
       if(arguments.length === 2){
           if('innerText' in node){
               node.innerText = string
          }else{
               node.textContent = string
          }
      }else if(arguments.length === 1){
           if('innerText' in node){
               return node.innerText
          }else{
               return node.textContent
          }
      }
    }

    为什么这里需要适配,innerText与textContent?
    因为虽然现在绝大多数浏览器都支持两种,但还是有非常旧的IE只支持innerText,所以这里是为了适配所有浏览器

    同时与读写属性思路相同:

    1. 当输入的参数是2个时,就在节点里写文本

    2. 当输入的参数是1个时,就读文本内容

    读写HTML的内容

    html(node,string){
       if(arguments.length === 2){
           node.innerHTML = string
      }else if(arguments.length === 1){
               return node.innerHTML
      }
    }

    同样,2参数写内容,1参数读内容

    修改Style

    style(node,name,value){
           if(arguments.length === 3){
               //dom.style(div,'color','red')
               node.style[name] = value
          }else if(arguments.length === 2){
               if(typeof name === 'string'){
                //dom.style(div,'color')
               return node.style[name]    
              }else if(name instanceof Object){
                   //dom.style(div,{color:'red'})
                   const Object = name
                   for(let key in Object){
                       //key:border/color
                       //node.style.border = ...
                       //node.style.color = ...
                       node.style[key] = Object[key]
                  }
              }
          }
    }

    思路:

    1. 首先判断输入的参数,如果为3个如:dom.style(div,'color','red')

    2. 就更改它的style

    3. 如果输入参数为2个时,先判断输入name的值的类型

    4. 如果是字符串,如dom.style(div,'color'),就返回style的属性

    5. 如果是对象,如dom.style(div,{border:'1px solid red',color:'blue'}),就更改它的style

    增删查class

    class:{
       add(node,className){
           node.classList.add(className)    
      },
       remove(node,className){
           node.classList.remove(className)
      },
       has(node,className){
           return node.classList.contains(className)
      }
    }

    注:查找一个元素的classList里是否有某一个class, 用的是contains

    添加事件监听

    on(node,eventName,fn){
          node.addEventListener(eventName,fn)
    }

    使用示例:

    const fn = ()=>{
       console.log('点击了')
    }
    dom.on(test,'click',fn)

    这样当点击id为test的div时,就会打印出'点击了'

    删除事件监听

    off(node,eventName,fn){
       node.removeEventListener(eventName,fn)
    }

    获取单个或多个标签

    find(selector,scope){
       return (scope || document).querySelectorAll(selector)
    }

    可以在指定区域或者全局的document里找

    使用示例:
    在document中查询:

    const testDiv = dom.find('#test')[0]
    console.log(testDiv)

    在指定范围内查询:

     <div>
           <div id="test"><span>test1span>
           <p class="red">段落标签p>
           div>
           <div id="test2">
               <p class="red">段落标签p>
           div>
    div>

    我只想找test2里面的red,应该怎么做

    const test2 = dom.find('#test2')[0]
    console.log(dom.find('.red',test2)[0])

    注意:末尾的[0]别忘记写

    获取父元素

    parent(node){
       return node.parentNode
    }

    获取子元素

    children(node){
       return node.children
    }

    获取兄弟姐妹元素

    siblings(node){
       return Array.from(node.parentNode.children).filter(n=>n!==node) //伪数组变数组再过滤本身
    }

    找到爸爸节点,然后过滤掉自己本身

    获取弟弟

    next(node){
       let x = node.nextSibling
       while(x && x.nodeType === 3){
           x = x.nextSibling
      }
       return x
    }

    为什么这里需要while(x && x.nodeType === 3)
    因为我们不想获取文本节点(空格回车等)
    所以当读到文本节点时,自动再去读取下一个节点,直到读到的内容不是文本节点为止

    获取哥哥

    previous(node){
           let x = node.previousSibling
           while(x && x.nodeType === 3){
               x = x.previousSibling
          }
           return x
    }

    与上面思路相同

    遍历所有节点

    each(nodeList,fn){
           for(let i=0;i<nodeList.length;i++){
               fn.call(null,nodeList[i])
          }
    }

    注:null用于填充this的位置
    使用示例:
    利用fn可以更改所有节点的style

    const t = dom.find('#travel')[0]
    dom.each(dom.children(t),(n)=>dom.style(n,'color','red'))

    遍历每个节点,把每个节点的style都更改

    用于获取排行老几

    index(node){
       const list = dom.children(node.parentNode)
       let i;
       for(i=0;i<list.length;i++){
           if(list[i]===node){
                break
          }
      }
       return i
    }

    思路:

    1. 获取爸爸节点的所有儿子

    2. 设置一个变量i

    3. 如果i等于想要查询的node

    4. 退出循环,返回i值

    作者:PrayWang
    来源:https://juejin.cn/post/7038171258617331719

    收起阅读 »

    这一次,彻底搞懂 async...await

    执行 async 函数,返回的都是 Promise 对象Promise.then() 对应 awaitPromise.catch() 对应 try...catch先看下面两个函数:async function test1() {  return 1;...
    继续阅读 »

    先上结论:

    • 执行 async 函数,返回的都是 Promise 对象

    • Promise.then() 对应 await

    • Promise.catch() 对应 try...catch

    执行 async 函数,返回的都是 Promise 对象

    先看下面两个函数:

    async function test1() {
     return 1;
    }
    async function test2() {
     return Promise.resolve(2);
    }
    const res1 = test1();
    const res2 = test2();
    console.log('res1', res1);
    console.log('res1', res2);

    test1 和 test2 两个函数前面都加了 async,说明这两个都是异步函数,并且如果一个函数前加了 async,那么这个函数的返回值就是一个 Promise(不论这个函数返回的是什么,都会被 JS 引擎包装成 Promise 对象)。

    输出结果如下图:

    Promise.then() 对应 await

    1.直接在一个 await 后面加 promise 对象

    看下面的代码:

    async function test3() {
     const p3 = Promise.resolve(3);
     p3.then(data => {
       console.log('data', data);
    });

     const data = await p3;
     console.log('data', data);
    }
    test3();

    输出结果如下图:

    可以看到输出是相同的,这就说明了 Promise 的 then() 方法对应 await。

    2.直接在一个 await 后面加一个基本数据类型的值

    看下面的例子:

    async function test4() {
     const data4 = await 4; // await Promise.resolve(4)
     console.log('data4', data4);
    }
    test4();

    输出结果如下图:

    可以看到输出的是 4,上面的 await 4 就相当于 await Promise.resolve(4),又因为 await 相当于 then(),所以输出的就是 4。

    3.直接在一个 await 后面加一个异步函数

    看下面的例子:

    async function test1() {
     return 1;
    }
    async function test5() {
     const data5 = await test1();
     console.log('data5', data5);
    }
    test5();

    输出结果如下图:

    可以看到输出的是 1,首先 test5() 执行,然后执行 test1(),test1 返回数字 1,相当于返回 Promise.resolve(1),await 又相当于 then(),所以输出 1。

    :::tip 提示 开发中最常用的就是第三种,await 后面跟一个异步函数,所以一定要掌握! :::

    Promise.catch() 对应 try...catch

    看下面的例子:

    async function test6() {
     const p6 = Promise.reject(6);
     const data6 = await p6;
     console.log('data6', data6);
    }
    test6();

    输出结果如下图:

    可以看到没有捕获到错误,那应该怎么做呢?没错,可以使用 try...catch。 看下面的例子:

    async function test6() {
     const p6 = Promise.reject(6);
     try {
       const data6 = await p6;
       console.log('data6', data6);
    } catch (e) {
       console.log('e', e); // 顺利捕获错误
    }
    }
    test6();

    输出结果如下图:

    可以看到已经成功捕获到错误了!

    作者:ShiYan_Chen
    来源:https://juejin.cn/post/7038152028664627230

    收起阅读 »

    会话过期后token刷新,重新请求接口(订阅发布模式)

    需求响应拦截拦截到302后,我们进入到刷新token逻辑我们后台的数据格式是根据statusCode来判断过期(你们可以根据自己的实际情况判断),接着进入refrshToken方法~看到这,有的小伙伴就有点奇怪retryOldRequest这个又是什么?没错,...
    继续阅读 »

    前言


    ❝ 最近,我们老大让小白搞一下登录模块,登录模块说简单也简单,复杂也复杂。本章主要讲一下,会话过期后,token 刷新的一系列的事。 ❞

    需求

    在一个页面内,当请求失败并且返回 302 后,判断是接口过期还是登录过期,如果是接口过期,则去请求新的token,然后拿新的token去再次发起请求.

    思路


    • 当初,想了一个黑科技(为了偷懒),就是拿到新的token后,直接强制刷新页面,这样一个页面内的接口就自动刷新啦~(方便是方便,用户体验却不好)

    • 目前,想到了重新请求接口时,可以配合订阅发布模式来提高用户体验

    响应拦截

    首先我们发起一个请求 axios({url:'/test',data:xxx}).then(res=>{})

    拦截到302后,我们进入到刷新token逻辑

    响应拦截代码

    axios.interceptors.response.use(
       function (response) {
           if (response.status == 200) {
               return response;
          }
      },
      (err) => {
           //刷新token
           let res = err.response || {};
           if (res.data.meta?.statusCode == 302) {
               return refeshToken(res);
          } else {  
               return err;
          }
      }
    );

    我们后台的数据格式是根据statusCode来判断过期(你们可以根据自己的实际情况判断),接着进入refrshToken方法~

    刷新token方法

    //避免其他接口同时请求(只请求一次token接口)
    let isRefreshToken = false;
    const refeshToken = (response) => {
      if (!isRefreshToken) {
               isRefreshToken = true;
               axios({
                   //获取新token接口
                   url: `/api/refreshToken`,
              })
                  .then((res) => {
                       const { data = '', meta = {} } = res.data;
                       if (meta.statusCode === 200) {
                           isRefreshToken = false;
                           //发布 消息
                           retryOldRequest.trigger(data);
                      } else {
                           history.push('/user/login');
                      }
                  })
                  .catch((err) => {
                       history.push('/user/login');
                  });
          }
           //收集订阅者 并把成功后的数据返回原接口
           return retryOldRequest.listen(response);
    };

    看到这,有的小伙伴就有点奇怪retryOldRequest这个又是什么?没错,这就是我们男二 订阅发布模式队列。

    订阅发布模式


    大家如果还不了解订阅发布模式,可以点击看一下,里面有大神写的通俗易懂的例子(觉得学到的话,可以顺便帮点赞哦~)。

    把失败的接口当订阅者,成功拿到新的token后再发布(重新请求接口)。

    以下便是订阅发布模式代码

    const retryOldRequest = {
       //维护失败请求的response
       requestQuery: [],

       //添加订阅者
       listen(response) {
           return new Promise((resolve) => {
               this.requestQuery.push((newToken) => {
                   let config = response.config || {};
                   //Authorization是传给后台的身份令牌
                   config.headers['Authorization'] = newToken;
                   resolve(axios(config));
              });
          });
      },

       //发布消息
       trigger(newToken) {
           this.requestQuery.forEach((fn) => {
               fn(newToken);
          });
           this.requestQuery = [];
      },
    };

    大家可以先不用关注订阅者的逻辑,只需要知道订阅者是每次请求失败后的接口(reponse)就好了。

    每次进入refeshToken方法,我们失败的接口都会触发retryOldRequest.listen去订阅,而我们的requestQuery则是保存这些订阅者的队列。

    注意:我们订阅者队列requestQuery是保存待发布的方法。而在成功获取新token后,retryOldRequest.trigger就会去发布这些消息(新token)给订阅者(触发订阅队列的方法)。

    而订阅者(response)里面有config配置,我们拿到新的token后(发布后),修改config里面的请求头Autorzation.而借助Promise我们可以更好的拿到新token请求回来的接口数据,一旦请求到数据,我们可以原封不动的返回给原来的接口/test了(因为我们在响应拦截那里返回的是refreshToken,而refreshToken又返回的是订阅者retryOldRequest.listen返回的数据,而Listiner又返回Promise的数据,Promise又在成功请求后resolve出去)。

    看到这,小伙伴们是不是觉得有点绕了~

    而在真实开发中,我们的逻辑还含有登录过期(与请求过期区分开来)。我们是根据当前时间 - 过去时间 < expiresTime(epiresTime:登录后返回的有效时间)来判断是请求过期还是登录过期的。 以下是完整逻辑
    以下是完整代码

    const retryOldRequest = {
       //维护失败请求的response
       requestQuery: [],

       //添加订阅者
       listen(response) {
           return new Promise((resolve) => {
               this.requestQuery.push((newToken) => {
                   let config = response.config || {};
                   config.headers['Authorization'] = newToken;
                   resolve(axios(config));
              });
          });
      },

       //发布消息
       trigger(newToken) {
           this.requestQuery.forEach((fn) => {
               fn(newToken);
          });
           this.requestQuery = [];
      },
    };
    /**
    * sessionExpiredTips
    * 会话过期:
    * 刷新token失败,得重新登录
    * 用户未授权,页面跳转到登录页面
    * 接口过期 => 刷新token
    * 登录过期 => 重新登录
    * expiresTime => 在本业务中返回18000ms == 5h
    * ****/

    //避免其他接口同时请求
    let isRefreshToken = false;
    let timer = null;
    const refeshToken = (response) => {
       //登录后拿到的有效期
       let userExpir = localStorage.getItem('expiresTime');
       //当前时间
       let nowTime = Math.floor(new Date().getTime() / 1000);
       //最后请求的时间
       let lastResTime = localStorage.getItem('lastResponseTime') || nowTime;
       let token = localStorage.getItem('token');

       if (token && nowTime - lastResTime < userExpir) {
           if (!isRefreshToken) {
               isRefreshToken = true;
               axios({
                   url: `/api/refreshToken`,
              })
                  .then((res) => {
                       const { data = '', meta = {} } = res.data;
                       isRefreshToken = false;
                       if (meta.statusCode === 200) {
                           localStorage.getItem('token', data);
                           localStorage.getItem('lastResponseTime', Math.floor(new Date().getTime() / 1000)
                          );
                           //发布 消息
                           retryOldRequest.trigger(data);
                      } else {
                          //去登录
                      }
                  })
                  .catch((err) => {
                       isRefreshToken = false;
                      //去登录
                  });
          }
           //收集订阅者 并把成功后的数据返回原接口
           return retryOldRequest.listen(response);
      } else {
           //节流:避免重复运行
          //去登录
      }
    };

    // http response 响应拦截
    axios.interceptors.response.use(
       function (response) {
           if (response.status == 200) {
               //记录最后操作时间
              localStorage.getItem('lastResponseTime', Math.floor(new Date().getTime() / 1000));
               return response;
          }
      },
      (err) => {
           let res = err.response || {};
           if (res.data.meta?.statusCode == 302) {
               return refeshToken(res);
          } else {
               //302 报的错误;
               return err;
          }
      }
    );

    以上便是我们这边的业务,如果写的不好请大佬多担待~~

    如果有好方案的小伙伴也可以在评论区内互相讨论~


    作者:用户3797421129853
    来源:https://juejin.cn/post/7037787299202990093

    收起阅读 »

    通过 Performance 证明,网页的渲染是一个宏任务

    别着急反驳,后面我会给出证据。调试是通过工具获取运行过程中的某一时刻或某一段时间的各方面的数据,帮助开发者理清逻辑、分析性能、排查问题等。 JS 的各种运行环境都会提供调试器,除此以外我们也会自己做一些埋点上报来做调试和统计。但是性能分析的调试工具却不能这样做...
    继续阅读 »

    网页的渲染是一个宏任务。 这是我下的一个结论。

    别着急反驳,后面我会给出证据。

    我们先来聊下什么是调试:

    调试是通过工具获取运行过程中的某一时刻或某一段时间的各方面的数据,帮助开发者理清逻辑、分析性能、排查问题等。 JS 的各种运行环境都会提供调试器,除此以外我们也会自己做一些埋点上报来做调试和统计。

    我们最常用的调试工具是 JS Debugger,它支持断点,可以在某处断住,查看当前上下文的变量、调用栈等,这对于理清逻辑很有帮助。

    但是性能分析的调试工具却不能这样做,不能用断住的方式实时查看,因为会影响数据的真实性。所以这类工具都是通过录制一段时间的数据,然后作事后的统计和分析的方式,常用的是 Chrome Devtools 里的 Performance 工具。(甚至为了避免浏览器插件的影响,还要用无痕模式来运行网页)点击录制按钮 record 开始录制(如果想录制从页面加载开始的数据,就点击 reload 按钮),Performance 会记录下录制时间内各方面的数据。

    有哪些数据呢?

    网页的运行是有多个线程的,主线程负责通过 Event Loop 的方式来不断的执行 JS 和渲染,也有一些别的线程,比如合成渲染图层的线程,Web Worker 的线程等,渲染的每一帧会绘制到界面上。

    网页是这样运行的,那记录的自然也都是这些数据:Performance 会记录网页的每个线程的数据,其中最重要的是主线程,也就是图中的 Main,这部分记录着 Event Loop 的执行过程,记录着 JS 执行的调用栈和页面渲染的流程。看到图中标出的一个个小灰块了么,那就是一个个 Task,也就是宏任务。Event Loop 就是循环执行宏任务。每个 Task 都有自己的调用栈,可以看到函数的执行路径,耗时等信息。图中宽度代表了耗时,可以直观的通过块的宽窄来分析性能。

    执行完宏任务会执行所有的微任务,在图中也可以清晰的看到:点击每一个块可以看到代码的位置,可以定位到对应代码,这样就可以分析出哪块代码性能不好。这些是 Main 线程的执行逻辑,也就是通过 Event Loop 来不断执行 JS 和渲染。

    当然,还有其他线程,比如光栅化线程,也就是负责把渲染出的图层合并成一帧的线程:总之,就像 Debugger 面前,JS 的执行过程没有秘密一样,在 Performance 面前,网页各线程的执行过程也没有秘密。

    说了这么多,就是为了讲清楚调试工具和 Performance 都是干啥的,它记录了哪些信息。

    我们想知道渲染是不是一个宏任务,自然可以通过 Performance 来轻易的分析出来。

    我们继续看 Main 线程的 Event Loop 执行过程:你会看到一个个很小的灰块,也就是一个个 Task,每隔一段时间都会执行,点击它,就会看到其实他做的就是渲染,包括计算布局,更新渲染树,合并图层、渲染等。

    这说明了什么,不就说明了渲染是一个宏任务么。

    所以,我们得到了结论:渲染是一个宏任务,通过 Event Loop 来做一帧帧的渲染。

    通过 Performance 调试工具,我们可以看到 Main 线程 Event Loop 的细节,看到 JS 执行和渲染的详细过程。

    有时你可能会看到有的 Task 部分被标红了,还警告说这是 Long Task。因为渲染和 JS 执行都是在同一个 Event Loop 内做的,那如果有执行时间过长的 Task,自然会导致渲染被延后,也就是掉帧,用户感受到的就是页面的卡顿。

    避免 Long Task,这是网页性能优化的一个重点。这也是为什么 React 使用了 Fiber 架构的可打断的组件树渲染,替代掉了之前的递归渲染整个组件树的方式,就是为了不产生 Long Task。

    总结

    本文目的为了证明渲染是不是一个宏任务,但其实更重要的是想讲清楚调试工具的意义。

    调试工具可以分析程序运行过程中某一刻或某一段时间的各方面的数据,有两种方式:一种是 Debugger 那种断点的方式,可以看到上下文变量的值、调用栈,可以帮助理清逻辑、定位问题。而性能分析工具则是另一种方式,通过录制一段时间内的各种数据,做事后的分析和统计,这样能保证数据的真实性。

    网页的性能分析工具 Performance 可以记录网页执行过程中的各个线程的执行情况,主要是主线程的 Event Loop 的执行过程,包括 JS 执行、渲染等。

    通过 Performance,我们可以轻易的得出“渲染是一个宏任务”的结论。

    就像在 Debugger 面前,JS 执行过程没有秘密一样。在 Performance 面前,网页的执行过程也同样没有秘密。

    作者:zxg_神说要有光
    来源:https://juejin.cn/post/7037839989018722340

    收起阅读 »

    技术选型,Vue和React的对比

    1. MVVM和MVCVue是MVVM,React是MVC。MVVM(Model-View-ViewModel)是在MVC(Model View Controller)的基础上,VM抽离Controller中展示的业务逻辑,而不是替代Controller,其它...
    继续阅读 »

    1. MVVM和MVC

    Vue是MVVM,React是MVC。

    MVVM(Model-View-ViewModel)是在MVC(Model View Controller)的基础上,VM抽离Controller中展示的业务逻辑,而不是替代Controller,其它视图操作业务等还是应该放在Controller中实现。

    也就是说MVVM实现的是业务逻辑组件的重用,使开发更高效,结构更清晰,增加代码的复用性。

    可以理解为MVVM是MVC的升级版。

    虽然React不算一个完整的MVC框架,可以认为是MVC中的V(View),但是Vue的MVVM还是更面向未来一些。

    2. 数据绑定

    vue是双向绑定,react是单向绑定。

    单向绑定的优点是相应的可以带来单向数据流,这样做的好处是所有状态变化都可以被记录、跟踪,状态变化通过手动调用通知,源头易追溯,没有“暗箱操作”。同时组件数据只有唯一的入口和出口,使得程序更直观更容易理解,有利于项目的可维护性。

    但是Vue虽然是双向绑定,但是也是单向数据流,它的双向绑定只是一个语法糖,想看正经的双向绑定可以去看下Dva。

    单向绑定的缺点则是代码量会相应的上升,数据的流转过程变长,从而出现很多类似的重复代码。同时由于对应用状态独立管理的严格要求(单一的全局store),在处理局部状态较多的场景时(如用户输入交互较多的“富表单型”应用),会显得冗余。

    双向绑定可以在表单交互较多的场景下,会简化大量业务无关的代码。

    我认为Vue的设计方案好一些,全局性数据流使用单向,局部性数据流使用双向。

    3. 数据更新

    3.1 React 更新流程

    React 推崇 Immutable(不可变),通过重新render去发现和更新自身。

    3.2 Vue 更新流程

    Vue通过收集数据依赖去发现更新。

    Vue很吸引人的就是它的响应式更新,Vue首次渲染触发data的getter,从而触发依赖收集,为对应的数据创建watcher,当数据发生更改的时候,setter被触发,然后通知各个watcher在下个tick的时候更新数据。

    所以说,如果data中某些数据没有在模板template 中使用的话,更新这些数据的时候,是不会触发更新的。这样的设计非常好,没有在模版上用到的变量,当它的值发生变化时,不更新视图,相当于内置了React的shouldComponentUpdate。

    3.3 更新比较

    • 获取数据更新的手段和更新的粒度不一样

    Vue通过依赖收集,当数据更新时 ,Vue明确知道是哪些数据更新了,每个组件都有自己的渲渲染watcher,掌管当前组件的视图更新,所以可以精确地更新对应的组件,所以更新的粒度是组件级别的。

    React会递归地把所有的子组件重新render一下,不管是不是更新的数据,此时,都是新的。然后通过 diff 算法 来决定更新哪部分的视图。所以,React 的更新粒度是一个整体。

    • 对更新数据是否需要渲染页面的处理不一样

    • 只有依赖收集的数据发生更新,Vue 才会去重新渲染页面

    • 只要数据有更新(setState,useState 等手段触发更新),都会去重新渲染页面 (可以使用shouldComponentUpdate/ PureComponent 改善)

    Vue的文档里有一描述说,Vue是细粒度数据响应机制,所以说数据更新这一块,我认为Vue的设计方案好一些。

    4. 性能对比

    借用尤大大的一段话:

    模板在性能这块吊打 tsx,在 IDE 支持抹平了的前提下用 tsx 本质上是在为了开发者的偏好牺牲用户体验的性能(性能没遇到瓶颈就无所谓) 这边自己不维护框架的人吐槽吐槽我也能理解,毕竟作为使用者只需要考虑自己爽不爽。作为维护者,Vue 的已有的用户习惯、生态和历史包袱摆在那里,能激进的程度是有限的,Vue 3 的大部分设计都是戴着镣铐跳舞,需要做很多折衷。如果真要激进还不如开个新项目,或者没人用的玩票项目,想怎么设计都可以。 组件泛型的问题也有不少人提出了,这个目前确实不行,但不表示以后不会有。 最后实话实说,所有前端里面像这个问题下面的类型体操运动员们毕竟是少数,绝大部分有 intellisense + 类型校验就满足需求了。真的对类型特别特别较真的用 React 也没什么不好,无非就是性能差点。

    为什么模板性能吊打TSX?

    tsx和vue template其实都是一样的模版语言,tsx最终也会被编译成createElement,模板被编译成render函数,所以本质上两者都有compile-time和runtime,但tsx的特殊性在于它本身是在ts语义下的,过于灵活导致优化无从下手。但是vue的模板得益于自身本来就是DSL,有自己的文法和语义,所以vue在模板的compile-time做了巨多的优化,比如提升不变的vnode,以及blocktree配合patchflag靶向更新,这些优化在最终的runtime上会把性能拉开不少。

    DSL: 一种为特定领域设计的,具有受限表达性编程语言。

    所以说Vue的性能是优于React的。

    5. React Hooks和Vue Hooks

    其实 React Hook 的限制非常多,比如官方文档中就专门有一个章节介绍它的限制:

    1. 不要在循环,条件或嵌套函数中调用 Hook

    2. 确保总是在你的 React 函数的最顶层调用他们。

    3. 遵守这条规则,你就能确保 Hook 在每一次渲染中都按照同样的顺序被调用。这让 React 能够在多次的 useState 和 useEffect 调用之间保持 hook 状态的正确。

    而 Vue 带来的不同在于:

    1. 与 React Hooks 相同级别的逻辑组合功能,但有一些重要的区别。 与 React Hook 不同,setup函数仅被调用一次,这在性能上比较占优。

    2. 对调用顺序没什么要求,每次渲染中不会反复调用 Hook 函数,产生的的 GC 压力较小。

    3. 不必考虑几乎总是需要 useCallback 的问题,以防止传递函数prop给子组件的引用变化,导致无必要的重新渲染。

    4. React Hook 有臭名昭著的闭包陷阱问题,如果用户忘记传递正确的依赖项数组,useEffect 和 useMemo 可能会捕获过时的变量,这不受此问题的影响。 Vue 的自动依赖关系跟踪确保观察者和计算值始终正确无误。

    5. 不得不提一句,React Hook 里的「依赖」是需要你去手动声明的,而且官方提供了一个 eslint 插件,这个插件虽然大部分时候挺有用的,但是有时候也特别烦人,需要你手动加一行丑陋的注释去关闭它。

    我们认可 React Hooks 的创造力,这也是 Vue-Composition-Api 的主要灵感来源。上面提到的问题确实存在于 React Hook 的设计中,我们注意到 Vue 的响应式模型恰好完美的解决了这些问题。

    --- 来自ssh

    Vue的组合式API刚出来的时候确实一看好像React Hooks,我也对它的.value进行了吐槽,

    但是总体来说还是更偏向于Vue Hooks。

    6. 写法

    React的思路是all in js,通过js来生成html,所以设计了jsx,还有通过js来操作css,社区的styled-component、jss等,所以说React的写法感觉相对自由一些,逻辑正确老子想怎么写怎么写,对于我来说,我确实更偏向于React的写法。

    Vue则是把html,css,js组合到一起,就像 Web 开发多年的传统开发方式一样, vue-loader会解析文件,提取每个语言块用各自的处理方式,vue有单文件组件(SFC),可以把html、css、js写到一个文件中,html提供了模板引擎来处理。Vue感觉是给你搭了一个框架,告诉你什么地方该写什么东西,你只要按照他的要求向里面填内容就可以了,没有React那么自由,但是上手难度简单了许多。而且因为SFC,一个组件的代码会看起来很长,维护起来很头痛。

    7. 理念及设计

    Vue 和 React 的核心差异,以及核心差异对后续设计产生的“不可逆”影响。

    Vue 和 React 在 API 设计风格和哲学理念(甚至作者个人魅力)上的不同。

    Vue 和 React 在工程化预编译构建阶段,AOT 和 JIT 优化的本质差异和设计。

    这个层次的比较确实对我难度确实大,我也懒得去copy,下面是Lucas大佬的分析,可以去看一下,时空隧道

    作者:黑色的枫
    来源:https://juejin.cn/post/7037365650251055134

    收起阅读 »

    微前端-从了解到动手搭建

    前言微前端是 2016 年thoughtWorks提出的概念,它将微服务的理念应用于浏览器端,即将前端应用由单体应用转变成多个小型前端应用聚合的应用。各个小型前端应用可以独立运行、独立开发、独立部署。与微服务出现的原因相似,随着前端业务越来越复杂,前端的代码和...
    继续阅读 »

    前言

    微前端是 2016 年thoughtWorks提出的概念,它将微服务的理念应用于浏览器端,即将前端应用由单体应用转变成多个小型前端应用聚合的应用。各个小型前端应用可以独立运行、独立开发、独立部署

    为什么出现?

    与微服务出现的原因相似,随着前端业务越来越复杂,前端的代码和业务逻辑也愈发难以维护,尤其对于中后台系统,很容易出现巨石应用,微前端由此应运而生,其根本目的就是解决巨石应用的项目复杂,系统庞大,开发人员众多,难以维护的问题。

    微前端 vs 巨石应用


    微前端巨石应用
    可维护性拆分为框架应用、微应用、微模块后,每个业务页面都对应一个单独的仓库,应用风险性降低。所有页面都在一个仓库,经常会出现动一处则动全身,随着系统增大维护成本会逐渐升高。
    开发效率结合发布、回滚、团队协作三个方面来看,单个仓库只关心一个业务页面,可以更方便快速迭代。团队多人协作时,发布排队;回滚有可能会把其他人发布的代码同时回滚掉;多分支开发时发布前沟通增加成本。
    代码复用所有页面都分开维护,使用公用代码成本较大,不过共用代码抽离为npm包使用可以减小成本。一个仓库中很容易抽离公用的部分,但是要注意动一处就会动全身的结果。

    架构方案

    基座模式是当前比较常见的微前端架构设计。

    首先以容器应用作为整个项目的主应用,负责子应用的注册,聚合,提供子运行环境、管理生命周期等。子应用就是各个独立部署、独立开发的单元。

    应用注册表拥有每个应用及对应的入口。在前端领域里,入口的直接表现形式可以是路由,又或者对应的应用映射。

    目前可以实现微前端架构的方案有如下:

    HTTP后端路由转发(nginx)

    • ✅ 简单高效快速,同时不需要前端做额外的工作。

    • ❌ 体验并不好,相当于mpa页面,路由到每个应用需要重新刷新

    iframe

    • ✅ 前端最简单的应用方式,直接嵌入,门槛最低,改动最小

    • ❌ iframe都会遇到的一些典型问题:UI 不同步,DOM 结构不共享(比如iframe中的弹框),跨域通信等

    各个业务独立打到npm包中

    • ✅ 门槛低,易上手

    • ❌ 模块修改后需要重新部署发布,太麻烦。

    组合式应用路由分发(基座模式)

    • ✅ 纯前端改造,体验良好,各个业务相互独立

    • ❌ 需要设计和开发,有一定成本,同时需要兼顾子页面和基座的变量污染,样式互相影响等问题

    web component

    • ✅ 是一项标准,目前它包含三项主要技术,它们可以一起使用来创建封装功能的定制元素,可以在你喜欢的任何地方重用,不必担心代码冲突。应该是微前端的最终态

    • ❌ 比较新,兼容性较差

    微前端页面形态

    微前端基座框架需要解决的问题

    路由分发

    作为微前端的基座应用,是整个应用的入口,负责承载当前子应用的展示和对其他路由子应用的转发,对于当前子应用的展示,一般是由以下几步构成:

    1. 远程拉取子应用内容

    2. 将子应用的 js 和 css 抽离,采用eval来运行 js,并将 css 和 html 内容append到基座应用中留给子应用的展示区域

    3. 当子应用切换走时,同步卸载这些内容

    对于路由分发而言,以采用react-router开发的基座SPA应用来举例,主要是下面这个流程:

    1. 当浏览器的路径变化后,react-router会监听hashchange或者popstate事件,从而获取到路由切换的时机。

    2. 最先接收到这个变化的是基座的router,通过查询注册信息可以获取到转发到那个子应用,经过一些逻辑处理后,采用修改hash方法或者pushState方法来路由信息推送给子应用的路由,子应用可以是手动监听hashchange或者popstate事件接收,或者采用react-router接管路由,后面的逻辑就由子应用自己控制。

    应用隔离

    应用隔离问题主要分为主应用和子应用,子应用和子应用之间的JavaScript执行环境隔离,CSS样式隔离,

    CSS

    • 当主应用和子应用同屏渲染时,就可能会有一些样式会相互污染,如果要彻底隔离CSS污染,可以采用CSS Module 或者命名空间的方式,给每个子应用模块以特定前缀,即可保证不会互相干扰,可以采用webpack的postcss插件,在打包时添加特定的前缀。

    • 而对于子应用与子应用之间的CSS隔离就非常简单,在每次应用加载时,将该应用所有的link和style 内容进行标记。在应用卸载后,同步卸载页面上对应的link和style即可。

    JavaScript隔离

    • 每当子应用的JavaScript被加载并运行时,它的核心实际上是对全局对象Window的修改以及一些全局事件的改变,例如jQuery这个js运行后,会在Window上挂载一个window.$对象,对于其他库React,Vue也不例外。为此,需要在加载和卸载每个子应用的同时,尽可能消除这种冲突和影响,最普遍的做法是采用沙箱机制(SandBox)。

    • 沙箱机制的核心是让局部的JavaScript运行时,对外部对象的访问和修改处在可控的范围内,即无论内部怎么运行,都不会影响外部的对象,需要结合 with 关键字和window.Proxy对象来实现浏览器端的沙箱。

    消息通信

    应用间通信有很多种方式,当然,要让多个分离的子应用之间要做到通信,本质上仍离不开中间媒介或者说全局对象。所以对于消息订阅(pub/sub)模式的通信机制是非常适用的,在基座应用中会定义事件中心Event,每个子应用分别来注册事件,当被触发事件时再有事件中心统一分发,这就构成了基本的通信机制。

    当然,如果基座和子应用采用的是React或者是Vue,是可以结合Redux和Vuex来一起使用,实现应用之间的通信。

    搭一个看看?

    qiankun 是一个基于 single-spa 的微前端实现库,旨在帮助大家能更简单、无痛的构建一个生产可用微前端架构系统。也是支付宝内部广泛使用的微前端框架。

    那么我们就使用 qiankun 从头搭一个demo出来体验一下

    基座

    • 基座我们使用react,自行使用 create-react-app 创建一个react项目即可。

    • npm install qiankun -s

    • 在基座中需要调用 registerMicroApps 注册子应用,然后调用start启动

    因此在 index.js 中插入如下代码

    import { registerMicroApps, start } from 'qiankun';

    registerMicroApps([
    {
       name: 'vueApp',
       entry: '//localhost:8080',
       container: '#container',
       activeRule: '/app-vue',
    },
    ]);

    // 启动 qiankun
    start();
    • 修改App.js

      • 加入一些 antd 元素,让demo像样一些

      • 同时,由于qiankun根据路由来加载不同微应用,我们也安装 react-router-dom

      • npm install react-router-dom

      • 安装完之后修改 App.js 如下:

    import { useState } from 'react';
    import { Layout, Menu } from 'antd';
    import { PieChartOutlined } from '@ant-design/icons';
    import { Link } from 'react-router-dom'
    import './App.css';

    const { Header, Content, Footer, Sider } = Layout;

    const App = () => {
     const [collapsed, setCollapsed] = useState(false);
     
     const onCollapse = collapsed => {
       setCollapsed(collapsed);
    };

     return (
       <Layout style={{ minHeight: '100vh' }}>
         <Sider collapsible collapsed={collapsed} onCollapse={onCollapse}>
           <div className="logo" />
           <Menu theme="dark" defaultSelectedKeys={['1']} mode="inline">
             <Menu.Item key="1" icon={<PieChartOutlined />}>
               <Link to="/app-vue">Vue应用</Link>
             </Menu.Item>
           </Menu>
         </Sider>
         <Layout className="site-layout">
           <Header className="site-layout-background" style={{ padding: 0 }} />
           <Content style={{ margin: '16px' }}>
             <div id="container" className="site-layout-background" style={{ minHeight: 360 }}></div>
           </Content>
           <Footer style={{ textAlign: 'center' }}>This Project ©2021 Created by DiDi</Footer>
         </Layout>
       </Layout>
    );
    }

    export default App;
    • 记得修改 index.js,把 App 组件用 react-router-dom 的 BrowserRouter 包一层,让 BrowserRouter 作为顶层组件才可以跳转

    • 至此,基座搭好了

    子页面

    尝试使用vue作为子页面,来体现微前端的技术隔离性。

    • 使用vue-cli创建vue2.x项目

    • 修改main.js如下:

    import Vue from "vue/dist/vue.js";
    import App from "./App.vue";
    import router from "./router";

    Vue.config.productionTip = false;

    // window.__POWERED_BY_QIANKUN__ 为true 说明在 qiankun 架构中
    // 修改webpack的publicPath,将子应用资源加载的公共基础路径设为 qiankun 包装后的路径
    // 这个 __INJECTED_PUBLIC_PATH_BY_QIANKUN__ 的实际地址是子应用的服务器地址,子应用的应用资源都在他本身的实际服务器上
    if (window.__POWERED_BY_QIANKUN__) {
     // eslint-disable-next-line no-undef
     __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
    }

    let instance = null;
    function render(props = {}) {
     const { container } = props;
     instance = new Vue({
       router,
       render: (h) => h(App),
    }).$mount(container ? container.querySelector("#app") : "#app");
    }

    // 独立运行时 直接渲染
    if (!window.__POWERED_BY_QIANKUN__) {
     render();
    }

    // 应用需要导出 bootstrap、mount、unmount 三个生命周期钩子,以供主应用在适当的时机调用。
    export async function bootstrap() {
     console.log("[vue] vue app bootstraped");
    }

    export async function mount(props) {
     console.log("[vue] props from main framework", props);
     render(props);
    }

    export async function unmount() {
     instance.$destroy();
     instance.$el.innerHTML = "";
     instance = null;
    }
    • router.js配置如下:

    import Vue from "vue/dist/vue.js";
    import VueRouter from "vue-router";

    Vue.use(VueRouter);

    const routes = [
    {
       path: "/test",
       name: "Test",
       component: () => import("./components/Test.vue"),
    },
    {
       path: "/hello",
       name: "Hello",
       component: () => import("./components/Hello.vue"),
    },
    ];

    const router = new VueRouter({
     base: window.__POWERED_BY_QIANKUN__ ? "/app-vue/" : "/",
     mode: "history",
     routes,
    });

    export default router;
    • 根目录下新建vue.config.js 用来配置webpack,内容如下:

    const { name } = require("./package");
    module.exports = {
     devServer: {
       // 跨域
       headers: {
         "Access-Control-Allow-Origin": "*",
      },
    },
     configureWebpack: {
       output: {
         library: `${name}-[name]`,
         // 把微应用打包成 umd 库格式
         libraryTarget: "umd",
         jsonpFunction: `webpackJsonp_${name}`,
      },
    },
    };

    启动

    基座和子应用分别启动,可以看到,子应用已经加载到了主应用中:


    作者:visa
    来源:https://juejin.cn/post/7037386845751083021

    收起阅读 »

    实现一个逐步递增的数字动画

    背景 可视化大屏项目使用最多的组件就是数字组件,展示数据的一个变化,为了提高视觉效果,需要给数字增加一个滚动效果,实现一个数字到另一个数字逐步递增的滚动动画。 先上一个思维导图: 一、实现类似滚轮的效果,容器固定,数字向上滚动 先列举所有的可能的值形成一个...
    继续阅读 »

    背景


    可视化大屏项目使用最多的组件就是数字组件,展示数据的一个变化,为了提高视觉效果,需要给数字增加一个滚动效果,实现一个数字到另一个数字逐步递增的滚动动画。


    先上一个思维导图:


    思维导图.png


    一、实现类似滚轮的效果,容器固定,数字向上滚动


    demo1.gif


    先列举所有的可能的值形成一个纵向的列表,然后固定一个容器,匀速更改数字的偏移值。


    下面来介绍一下这种方案的实现,元素值从0到9一共十个值,每个数字占纵向列表的10%,所以纵向偏移值依次为为0% —> -90%


    实现:


    <ul>
    <li>
    <span>0123456789</span>
    </li>
    </ul>

    ul{
    margin-top: 200px;
    }
    ul li{
    margin:0 auto;
    width: 20px;
    height: 30px;
    text-align: center;
    border:2px solid rgba(221,221,221,1);
    border-radius:4px;
    }
    ul li span{
    position: absolute;
    color: #fff;
    top: 30%;
    left: 50%;
    transform: translate(-50%,0);
    transition: transform 500ms ease-in-out;
    writing-mode: vertical-rl;
    text-orientation: upright;
    letter-spacing: 17px;
    }

    let spanDom = document.querySelector('span')
    let start = 0
    setInterval(() =>{
    start++
    if(start>9){
    start = 0
    }
    spanDom.style.transform = `translate(-50%,-${start*10}%)`
    }, 1000)

    上述代码存在一个问题,当我们从9到0的时候,容器偏移从-90%直接到了0%。 但是由于设定了固定的过渡动画时间,就会出现一个向反方向滚动的情况,为了解决这个问题,可以参考无缝滚动的思路




    • 在9后面复制一份0,




    • 当纵向列表滚动到9的时候,继续滚动到复制的0




    • 滚动到复制的0的时候,把列表的偏移位置改为0,并且控制动画时间为0




    <ul>
    <li>
    <span>01234567890</span>
    </li>
    </ul>

    let spanDom = document.querySelector('span')
    let start = 0
    var timer = setInterval(fn, 1000);
    function fn() {
    start++
    clearInterval(timer)
    timer = setInterval(fn,start >10 ? 0 : 1000);
    if(start>10){
    spanDom.style.transition = `none`
    start = 0
    }else{
    spanDom.style.transition = `transform 500ms ease-in-out`
    }
    spanDom.style.transform = `translate(-50%,-${start/11*100}%)`
    }

    demo2.gif


    利用两个元素实现滚动


    仔细看动图的效果,事实上在在视口只有两个元素,一个值之前的值,一个为当前的值,滚动偏移值只需设置translateY(-100%)


    具体思路:




    • 声明两个变量,分别存放之前的值prev,以及变化后的值cur;声明一个变量play作为这两个值的滚动动画的开关




    • 使用useEffect监听监听传入的值:如果是有效的数字,那么把没有变化前的值赋值给prev,把当前传入的值赋值给cur,并且设置palytrue开启滚动动画




    下面是调整后的代码结构:


     <div className={styles.slider}>
    {[prev, cur].map((item, index) => (
    <span key={index} className={`${styles['slider-text']} ${playing && styles['slider-ani']} ${(prev === 0 && cur === 0 && index ===0) && styles['slider-hide']}`}>
    {item}
    </span>
    ))}
    </div>

    const { value} = props
    const [prev, setPrev] = useState(0)
    const [cur, setCur] = useState(0)
    const [playing, setPlaying] = useState(false)

    const play = (pre, current) => {
    setPrev(pre)
    setCur(current)
    setPlaying(false)
    setTimeout(() => {
    setPlaying(true)
    }, 20)
    }

    useEffect(() => {
    if (!Number.isNaN(value)) {
    play(cur, value)
    } else {
    setPrev(value)
    setCur(value)
    }
    }, [value])

    .slider {
    display: flex;
    flex-direction: column;
    height: 36px;
    margin-top: 24%;
    overflow: hidden;
    text-align: left;
    }

    .slider-text {
    display: block;
    height: 100%;
    transform: translateY(0%);
    }

    .slider-ani {
    transform: translateY(-100%);
    transition: transform 1s ease;
    }
    .slider-hide {
    opacity: 0;
    }


    实现多个滚轮的向上滚动的数字组件


    组件.gif


    利用H5的requestAnimationFrame()API实现数字逐步递增的动画效果


    实现一个数字的逐渐递增的滚动动画,并且要在指定时间内完成。要看到流畅的动画效果,就需要在更新元素状态时以一定的频率进行,JS动画都是通过在很短的时间内不停的渲染/绘制元素做到的,所以计时器一直都是Javascript动画的核心技术,关键就是刷新的间隔时间,刷新时间需要尽量短,这样动画效果才能显得更加流畅,不卡顿;同时刷新间隔又不能太短,需要确保浏览器有能力渲染动画


    大多数电脑显示器的刷新频率是 60Hz,即每秒重绘 60次。因此平滑动画的最佳循环间隔是通常是 1000ms/60,约等于16.6ms


    计时器对比




    • 与 setTimeout 和 setInterval 不同,requestAnimationFrame 不需要程序员自己设置时间间隔。setTimeout 和 setInterval 的问题是精确度低。它们的内在运行机制决定了时间间隔参数实际上只是指定了把动画代码添加到浏览器 UI 线程队列中以等待执行的时间。如果队列前面已经加入了其他任务,那动画代码就要等前面的任务完成后再执行。




    • requestAnimationFrame 采用系统时间间隔,它会要求浏览器根据自己的频率进行一次重绘,保持最佳绘制效率,不会因为间隔时间过短,造成过度绘制,增加开销;也不会因为间隔时间太长,使用动画卡顿不流畅,让各种网页动画效果能够有一个统一的刷新机制,从而节省系统资源,提高系统性能,改善视觉效果。




    • requestAnimationFrame 会把每一帧中的所有 DOM 操作集中起来,在一次重绘或回流中就完成,并且重绘或回流的时间间隔紧紧跟随浏览器的刷新频率。




    • requestAnimationFrame 对于隐藏或不可见元素,将不会进行重绘或回流,就意味着使用更少的 CPU、GPU 和内存使用量。




    • requestAnimationFrame 是由浏览器专门为动画提供的API,在运行时浏览器会自动优化方法的调用,并且如果页面不是激活状态下的话,动画会自动暂停,有效节省了CPU开销。




    requestAnimationFrame实现滚动动画思路



    • 动画开始,记录开始动画的时间 startTimeRef.current


    const startTimeRef = useRef(Date.now());
    const [t, setT] = useState(Date.now());


    • 之后每一帧动画,记录从开始动画经过了多长时间,计算出当前帧的所到达的数字应该是多少,即currentValue


    useEffect(() => {
    const rafFunc = () => {
    const now = Date.now();
    const t = now - startTimeRef.current;
    if (t >= period) {
    setT(period);
    } else {
    setT(t);
    requestAnimationFrame(rafFunc);
    }
    };
    let raf;
    if (autoScroll) {
    raf = requestAnimationFrame(rafFunc);
    startTimeRef.current = Date.now();
    } else {
    raf && cancelAnimationFrame(raf);
    }
    return () => raf && cancelAnimationFrame(raf);
    }, [period, autoScroll]);

    const currentValue = useMemo(() => ((to - from) / period) * t + from, [t, period, from, to]);


    • 针对当前每个数字位上的数字进行比较,如果有变化,进行偏移量的变化,偏移量体现在当前数字位上的数字与下一位数字之间的差值,这个变化每一帧都串起来形成了滚动动画


    成果展示


    成果.gif


    成果2.gif


    作者:我就是胖虎
    链接:https://juejin.cn/post/7025913017627836452

    收起阅读 »

    前端金额格式化处理

    前端项目中,金额格式化展示是很常见的需求,在此整理了一些通用的处理方式,如 toLocaleString();正则匹配;slice()循环截取等等;也解决了小数点精度问题 以此为例:12341234.246 => ¥ 12,341,234.25 方式一...
    继续阅读 »

    前端项目中,金额格式化展示是很常见的需求,在此整理了一些通用的处理方式,如 toLocaleString();正则匹配;slice()循环截取等等;也解决了小数点精度问题



    以此为例:12341234.246 => ¥ 12,341,234.25


    方式一:采用浏览器自带的Number.prototype.toLocaleString()处理整数部分,小数部分直接用Number.prototype.toFixed()四舍五入处理


    // v1.0
    const formatMoney = (money, symbol = "", decimals = 2) => {
    if (!(money && money > 0)) {
    return 0.0;
    }

    let arr = money.toFixed(decimals).toString().split(".");
    let first = parseInt(arr[0]).toLocaleString();
    let result = [first, arr[1]].join(".");
    return `${symbol} ${money.toFixed(decimals)}`;
    };

    formatMoney(12341234.246); // 12,341,234.25
    formatMoney(12341234.246, "¥", 1); // ¥ 12,341,234.2

    2021.11.9 更改记录 我之前写复杂了,经过评论区[黄景圣]的指点,优化如下:


    // v2.0 简化函数
    const formatMoney = (money, symbol = "", decimals = 2) =>
    `${symbol} ${parseFloat(money.toFixed(decimals)).toLocaleString()}`;

    formatMoney(12341234.246, "¥", 2) // ¥ 12,341,234.25

    // 或者只用toLocaleString()处理
    const format = (money, decimals = 2) =>
    money.toLocaleString("zh", {
    style: "currency",
    currency: "CNY",
    maximumFractionDigits: decimals,
    useGrouping: true, // false-没有千位分隔符;true-有千位分隔符
    });
    format(12341234.246); // ¥12,341,234.25

    2021.11.10 更改记录 经过评论区[你摸摸我这料子]的提示,解决了 toFixed() 精度失效的问题,具体可查看前端小数展示精度处理


    // 测试数据如下:
    formatMoney(12.035); // 12.04 正常四舍五入
    formatMoney(12.045); // 12.04 异常,应该为12.05,没有四舍五入

    // v3.0 解决toFixed()问题
    const formatToFixed = (money, decimals = 2) => {
    return (
    Math.round((parseFloat(money) + Number.EPSILON) * Math.pow(10, decimals)) /
    Math.pow(10, decimals)
    ).toFixed(decimals);
    };
    const formatMoney = (money, symbol = "", decimals = 2) =>
    `${symbol}${parseFloat(formatToFixed(money, decimals)).toLocaleString()}`;

    formatMoney(12341234.035, '¥'); // ¥12,341,234.04
    formatMoney(12341234.045, '¥'); // ¥12,341,234.05

    2021.11.17 更改记录 通过评论区[Ryan_zhang]的提醒,解决了保留四位小数显示的问题


    // v4.0 只更改了formatMoney函数,其他的不变
    const formatMoney = (money, symbol = "", decimals = 2) =>
    `${symbol}${parseFloat(formatToFixed(money, decimals)).toLocaleString(
    "zh",
    {
    maximumFractionDigits: decimals,
    useGrouping: true,
    }
    )}`;
    formatMoney(12341234.12335, "¥", 4); // ¥12,341,234.1234
    formatMoney(12341234.12345, "¥", 4); // ¥12,341,234.1235

    方式二:使用正则表达式处理整数部分,小数部分同上所示。有个《JS 正则迷你书》介绍正则表达式挺好的,在 2.4.2 章就讲了“数字的千位分隔符表示法”,介绍的很详细,推荐看看。



    • \b:单词边界,具体就是 \w 与 \W 之间的位置,也包括 \w 与 ^ 之间的位置,和 \w 与 $ 之间的位置

    • \B :\b 的反面的意思,非单词边界

    • (?=p):其中 p 是一个子模式,即 p 前面的位置,或者说,该位置后面的字符要匹配 p


    /**
    * @params {Number} money 金额
    * @params {Number} decimals 保留小数点后位数
    * @params {String} symbol 前置符号
    */
    const formatMoney = (money, symbol = "", decimals = 2) => {
    let result = money
    .toFixed(decimals)
    .replace(/\B(?=(\d{3})+\b)/g, ",")
    .replace(/^/, `${symbol}`);
    return result;
    };

    formatMoney(12341234.246, "$", 2); // $12,341,234.25

    // v2.0 解决toFixed()问题
    const formatMoneyNew = (money, symbol = "", decimals = 2) =>
    formatToFixed(money, decimals)
    .replace(/\B(?=(\d{3})+\b)/g, ",")
    .replace(/^/, `${symbol}`);

    formatMoneyNew(12341234.035, "¥", 2); // ¥12,341,234.04
    formatMoneyNew(12341234.045, "¥", 2); // ¥12,341,234.05

    方式三:循环字符串,通过 slice 截取实现



    • substring(start, end):包含 start,不包含 end

    • substr(start, length):包含 start,长度为 length

    • slice(start, end):可操作数组和字符串;包含 start,不包含 end

    • splice(start, length, items):只能针对数组;增删改都可以


    const formatMoney = (money, symbol = "", decimals = 2) => {
    // 改造前
    // let arr = money.toFixed(decimals).toString().split(".");
    // 改造后
    let arr = formatToFixed(money, decimals).toString().split(".");
    let num = arr[0];
    let first = "";
    su;
    while (num.length > 3) {
    first = "," + num.slice(-3) + first;
    num = num.slice(0, num.length - 3);
    }
    if (num) {
    first = num + first;
    }
    return `${symbol} ${[first, arr[1]].join(".")}`;
    };

    formatMoney(12341234.246, "$", 2); // $ 12,341,234.25
    formatMoney(12341234.035, "¥", 2); // ¥ 12,341,234.04
    formatMoney(12341234.045, "¥", 2); // ¥ 12,341,234.05

    2021.11.24 更改记录 通过评论区[SpriteBoy]和[maxxx]的提醒,采用Intl内置的NumberFormat试试


    方式四:Intl.NumberFormat,用法和toLocaleString()挺相似的


    const formatMoney = (money, decimals = 2) => {
    return new Intl.NumberFormat("zh-CN", {
    style: "currency", // 货币形式
    currency: "CNY", // "CNY"是人民币
    currencyDisplay: "symbol", // 默认“symbol”,中文中代表“¥”符号
    // useGrouping: true, // 是否使用分组分隔符,如千位分隔符或千/万/亿分隔符,默认为true
    // minimumIntegerDigits: 1, // 使用的整数数字的最小数目.可能的值是从1到21,默认值是1
    // minimumFractionDigits: 2, // 使用的小数位数的最小数目.可能的值是从 0 到 20
    maximumFractionDigits: decimals, // 使用的小数位数的最大数目。可能的值是从 0 到 20
    }).format(money);
    };

    console.log(formatMoney(12341234.2, 2)); // ¥12,341,234.20
    console.log(formatMoney(12341234.246, 1)); // ¥12,341,234.2
    console.log(formatMoney(12341234.035, 2)); // ¥12,341,234.04
    console.log(formatMoney(12341234.045, 2)); // ¥12,341,234.05
    console.log(formatMoney(12341234.12335, 4)); // ¥12,341,234.1234
    console.log(formatMoney(12341234.12345, 4)); // ¥12,341,234.1235

    作者:时光足迹
    链接:https://juejin.cn/post/7028086399601475591

    收起阅读 »

    清空数组的几个方式

    1. 前言 前两天在工作当中遇到一个问题,在vue3中使用reactive生成的响应式数组如何清空,当然我一般清空都是这么写: let array = [1,2,3]; array = []; 不过这么用在reactive代理的方式中还是有点问题,比如...
    继续阅读 »

    1. 前言


    前两天在工作当中遇到一个问题,在vue3中使用reactive生成的响应式数组如何清空,当然我一般清空都是这么写:


      let array = [1,2,3];
    array = [];

    不过这么用在reactive代理的方式中还是有点问题,比如这样:


        let array = reactive([1,2,3]);
    watch(()=>[...array],()=>{
    console.log(array);
    },)
    array = reactive([]);

    很显然,因为丢失了对原来响应式对象的引用,这样就直接失去了监听


    2. 清空数据的几种方式


    当然,作为一名十年代码经验常年摸鱼的我,立马就给出了几个解决方案。


    2.1 使用ref()


    使用ref,这是最简便的方法:


        const array = ref([1,2,3]);

    watch(array,()=>{
    console.log(array.value);
    },)

    array.value = [];

    image.png


    2.2 使用slice


    slice顾名思义,就是对数组进行切片,然后返回一个新数组,感觉和go语言的切片有点类似。当然用过react的小伙伴应该经常用slice,清空一个数组只需要这样写:


        const array = ref([1,2,3]);

    watch(array,()=>{
    console.log(array.value);
    },)

    array.value = array.value.slice(0,0);

    image.png
    不过需要注意要使用ref


    2.3 length赋值为0


    个人比较喜欢这种,直接将length赋值为0


        const array = ref([1,2,3]);

    watch(array,()=>{
    console.log(array.value);
    },{
    deep:true
    })

    array.value.length = 0;

    而且,这种只会触发一次,但是需要注意watch要开启deep:


    image.png


    不过,这种方式,使用reactive会更加方便,也不用开启deep:


        const array = reactive([1,2,3]);

    watch(()=>[...array],()=>{
    console.log(array);
    })

    array.length = 0;

    image.png


    2.4 使用splice


    副作用函数splice也是一种方案,这种情况同时也可以使用reactive:


        const array = reactive([1,2,3]);

    watch(()=>[...array],()=>{
    console.log(array);
    },)

    array.splice(0,array.length)

    不过要注意,watch会触发多次:


    1636352459(1).jpg


    当然也可以使用ref,但是注意这种情况下,需要开启deep:


        const array = ref([1,2,3]);

    watch(array,()=>{
    console.log(array.value);
    },{
    deep:true
    })

    array.value.splice(0,array.value.length)

    image.png


    但是可以看到ref也和reactive一样,会触发多次。


    3. 总结


    以上是我个人工作中的对于清空数组的总结,但是可以看到splice还是有点特殊的,会触发多次,不过为什么会产生这种差异还有待研究。


    v2-db16a663d4445bb2044d2635ab81f2a2_720w.jpg


    作者:RadiumAg
    链接:https://juejin.cn/post/7028086044285206564

    收起阅读 »

    手写一个 ts-node 来深入理解它的原理

    当我们用 Typesript 来写 Node.js 的代码,写完代码之后要用 tsc 作编译,之后再用 Node.js 来跑,这样比较麻烦,所以我们会用 ts-node 来直接跑 ts 代码,省去了编译阶段。 有没有觉得很神奇,ts-node 怎么做到的直接跑...
    继续阅读 »

    当我们用 Typesript 来写 Node.js 的代码,写完代码之后要用 tsc 作编译,之后再用 Node.js 来跑,这样比较麻烦,所以我们会用 ts-node 来直接跑 ts 代码,省去了编译阶段。


    有没有觉得很神奇,ts-node 怎么做到的直接跑 ts 代码的?


    其实原理并不难,今天我们来实现一个 ts-node 吧。


    相关基础

    实现 ts-node 需要 3 方面的基础知识:



    • require hook
    • repl 模块、vm 模块
    • ts compiler api

    我们先学下这些基础


    require hook

    Node.js 当 require 一个 js 模块的时候,内部会分别调用 Module.load、 Module._extensions[‘.js’],Module._compile 这三个方法,然后才是执行。


    img

    同理,ts 模块、json 模块等也是一样的流程,那么我们只需要修改 Module._extensions[扩展名] 的方法,就能达到 hook 的目的:


    require.extensions['.ts'] = function(module, filename) {
    // 修改代码
    module._compile(修改后的代码, filename);
    }

    比如上面我们注册了 ts 的处理函数,这样当处理 ts 模块时就会调用这个方法,所以我们在这里面做编译就可以了,这就是 ts-node 能够直接执行 ts 的原理。


    repl 模块

    Node.js 提供了 repl 模块可以创建 Read、Evaluate、Print、Loop 的命令行交互环境,就是那种一问一答的方式。ts-node 也支持 repl 的模式,可以直接写 ts 代码然后执行,原理就是基于 repl 模块做的扩展。


    repl 的 api 是这样的: 通过 start 方法来创建一个 repl 的交互,可以指定提示符 prompt,可以自己实现 eval 的处理逻辑:


    const repl = require('repl');

    const r = repl.start({
    prompt: '- . - > ',
    eval: myEval
    });

    function myEval(cmd, context, filename, callback) {
    // 对输入的命令做处理
    callback(null, 处理后的内容);
    }

    repl 的执行时有一个上下文的,在这里就是 r.context,我们在这个上下文里执行代码要使用 vm 模块:


    const vm = require('vm');

    const res = vm.runInContext(要执行的代码, r.context);

    这两个模块结合,就可以实现一问一答的命令行交互,而且 ts 的编译也可以放在 eval 的时候做,这样就实现了直接执行 ts 代码。


    ts compiler api

    ts 的编译我们主要是使用 tsc 的命令行工具,但其实它同样也提供了编译的 api,叫做 ts compiler api。我们做工具的时候就需要直接调用 compiler api 来做编译。


    转换 ts 代码为 js 代码的 api 是这个:


    const { outputText } = ts.transpileModule(ts代码, {
    compilerOptions: {
    strict: false,
    sourceMap: false,
    // 其他编译选项
    }
    });

    当然,ts 也提供了类型检查的 api,因为参数比较多,我们后面一篇文章再做展开,这里只了解 transpileModule 的 api 就够了。


    了解了 require hook、repl 和 vm、ts compiler api 这三方面的知识之后,ts-node 的实现原理就呼之欲出了,接下来我们就来实现一下。


    实现 ts-node

    直接执行的模式

    我们可以使用 ts-node + 某个 ts 文件,来直接执行这个 ts 文件,它的原理就是修改了 require hook,也就是 Module._extensions['.ts'] 来实现的。


    在 require hook 里面做 ts 的编译,然后后面直接执行编译后的 js,这样就能达到直接执行 ts 文件的效果。


    所以我们重写 Module._extensions['.ts'] 方法,在里面读取文件内容,然后调用 ts.transpileModule 来把 ts 转成 js,之后调用 Module._compile 来处理编译后的 js。


    这样,我们就可以直接执行 ts 模块了,具体的模块路径是通过命令行参数执行的,可以用 process.argv 来取。


    const path = require('path');
    const ts = require('typescript');
    const fs = require('fs');

    const filePath = process.argv[2];

    require.extensions['.ts'] = function(module, filename) {
    const fileFullPath = path.resolve(__dirname, filename);
    const content = fs.readFileSync(fileFullPath, 'utf-8');

    const { outputText } = ts.transpileModule(content, {
    compilerOptions: require('./tsconfig.json')
    });

    module._compile(outputText, filename);
    }

    require(filePath);

    我们准备一个这样的 ts 文件 test.ts:


    const a = 1;
    const b = 2;

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

    console.log(add(a, b));

    然后用这个工具 hook.js 来跑:


    img

    可以看到,成功的执行了 ts,这就是 ts-node 的原理。


    当然,细节的逻辑还有很多,但是最主要的原理就是 require hook + ts compiler api。


    repl 模式

    ts-node 支持启动一个 repl 的环境,交互式的输入 ts 代码然后执行,它的原理就是基于 Node.js 提供的 repl 模块做的扩展,在自定义的 eval 函数里面做了 ts 的编译,然后使用 vm.runInContext 的 api 在 repl 的上下文中执行 js 代码。


    我们也启动一个 repl 的环境,设置提示符和自定义的 eval 实现。


    const repl = require('repl');

    const r = repl.start({
    prompt: '- . - > ',
    eval: myEval
    });

    function myEval(cmd, context, filename, callback) {

    }

    eval 的实现就是编译 ts 代码为 js,然后用 vm.runInContext 来执行编译后的 js 代码,执行的 context 指定为 repl 的 context:


    function myEval(cmd, context, filename, callback) {
    const { outputText } = ts.transpileModule(cmd, {
    compilerOptions: {
    strict: false,
    sourceMap: false
    }
    });
    const res = vm.runInContext(outputText, r.context);
    callback(null, res);
    }

    同时,我们还可以对 repl 的 context 做一些扩展,比如注入一个 who 的环境变量:


    Object.defineProperty(r.context, 'who', {
    configurable: false,
    enumerable: true,
    value: '神说要有光'
    });

    我们来测试下效果:


    img

    可以看到,执行后启动了一个 repl 环境,提示符修改成了 -.- >,可以直接执行 ts 代码,还可以访问全局变量 who。


    这就是 ts-node 的 repl 模式的大概原理: repl + vm + ts compiler api。


    全部代码如下:


    const repl = require('repl');
    const ts = require('typescript');
    const vm = require('vm');

    const r = repl.start({
    prompt: '- . - > ',
    eval: myEval
    });

    Object.defineProperty(r.context, 'who', {
    configurable: false,
    enumerable: true,
    value: '神说要有光'
    });

    function myEval(cmd, context, filename, callback) {
    const { outputText } = ts.transpileModule(cmd, {
    compilerOptions: {
    strict: false,
    sourceMap: false
    }
    });
    const res = vm.runInContext(outputText, r.context);
    callback(null, res);
    }

    总结

    ts-node 可以直接执行 ts 代码,不需要手动编译,为了深入理解它,我们我们实现了一个简易 ts-node,支持了直接执行和 repl 模式。


    直接执行的原理是通过 require hook,也就是 Module._extensions[ext] 里通过 ts compiler api 对代码做转换,之后再执行,这样的效果就是可以直接执行 ts 代码。


    repl 的原理是基于 Node.js 的 repl 模块做的扩展,可以定制提示符、上下文、eval 逻辑等,我们在 eval 里用 ts compiler api 做了编译,然后通过 vm.runInContext 在 repl 的 context 中执行编译后的 js。这样的效果就是可以在 repl 里直接执行 ts 代码。


    当然,完整的 ts-node 还有很多细节,但是大概的原理我们已经懂了,而且还学到了 require hook、repl 和 vm 模块、 ts compiler api 等知识。


    题外话

    其实 ts-node 的原理是应一个同学的要求写的,大家有想读的 nodejs 工具的源码也可以告诉我呀(可以加我微信),无偿提供源码带读 + 简易实现的服务,不过会做一些筛选。


    img
    作者:zxg_神说要有光
    来源:https://juejin.cn/post/7036688014206042143

    收起阅读 »

    为什么我不用 Typescript

    前言 我算是久仰 Typescript 的大名了,因而之前就想学习,但是一直没有抽出时间来看看它。直到最近有一天我在知乎上被邀请回答了 一个问题 —— 一个我以为的中学生问怎么样提升他的开源仓库。我点进去,先是被惊艳到了;然后发现,他用的是 Typescrip...
    继续阅读 »

    前言


    我算是久仰 Typescript 的大名了,因而之前就想学习,但是一直没有抽出时间来看看它。直到最近有一天我在知乎上被邀请回答了 一个问题 —— 一个我以为的中学生问怎么样提升他的开源仓库。我点进去,先是被惊艳到了;然后发现,他用的是 Typescript。我顿时感觉我似乎落后了,于是鼓起劲,开始学起了 Typescript。


    但是我学了下,再用了下,发现它没有像被吹的那么神。虽说是 Javascript 的超集,也的确有些地方挺好的,但是还是不足够改变我使用 Javascript 编程。就像虽然有 Deno,但是我还是用 Node.js 一样。


    所以,我就写这篇文章,说下我个人感觉 Typescript 的缺点、为何它的优点无法打动我用它替代 Javascript,以及跟推荐我使用 Typescript 的大家讲一下我不用 Typescript 的逻辑。


    各位想骂我心里骂骂就好了,我今天过个生日也不容易。


    缺陷


    1. 语法丑陋,代码臃肿


    我写两段相同的代码,大家感受下:


    // js
    const multiply = (i, j) => i * j

    // ts
    function multiply(i: number, j: number) {
    return i + j
    }

    你看这类型注释,把好好的一段代码弄得这么乱……反正我看这样的 Typescript,花的反应时间一定比看上面的 Javascript 代码长。——不过也有可能是我比较熟悉 Javascript 吧。


    复杂一点的东西也是一个道理(Apollo GraphQL 的代码):


    // js
    import React from 'react';
    import ApolloClient from 'apollo-client';

    let apolloContext;

    export function getApolloContext() {
    if (!apolloContext) {
    apolloContext = React.createContext({});
    }
    return apolloContext;
    }

    export function resetApolloContext() {
    apolloContext = React.createContext({});
    }

    铁定要比这个好得多:


    // ts
    import React from 'react';
    import ApolloClient from 'apollo-client';

    export interface ApolloContextValue {
    client?: ApolloClient<object>;
    renderPromises?: Record<any, any>;
    }

    let apolloContext: React.Context<ApolloContextValue>;

    export function getApolloContext() {
    if (!apolloContext) {
    apolloContext = React.createContext<ApolloContextValue>({});
    }
    return apolloContext;
    }

    export function resetApolloContext() {
    apolloContext = React.createContext<ApolloContextValue>({});
    }

    甚至有人提了个 issue 就是抱怨 Type 让它变得难用。


    这么看,实在是为了这个假静态类型语言牺牲太多了,毕竟代码可读性还是很重要的。——之所以说它是假静态语言,是因为在真正的静态类型语言中,如 C 和 C++,不同的变量类型在内存中的存储方式不同,而在 Typescript 中不是这样。


    比如,缺了这个可读性,debug 会变得更难。你是不是没有注意到我上面 multiply 的 Typescript 代码其实有 bug——应该是 * 而不是 +


    2. 麻烦


    浏览器不能直接执行 Typescript,所以 Typescript 必须要被编译成 Javascript 才能执行,要花一段时间;项目越大,花的时间越长,所以 Deno 才要停用它。并且,使用 Typescript 要安装新的依赖,虽然的确不麻烦,但是不用 Typescript,就不用再多装一个依赖了是不是。


    其实还有一点,但是放不上台面来讲,因为这是我自己的问题。


    我一直不大喜欢给添花样的东西,比如 pugtypescriptdeno 等;虽然 scss 啥的我觉得还是不错的——没有它我就写不出 @knowscount/vue-lib 这个仓库。


    3. 文件体积会变大


    随随便便就能猜到,我写那么多额外的类型注释、代码变得那么臃肿肯定会让 Typescript 文件比用 Javascript 编写的文件更大。作为一个用 “tab 会让文件体积更小” 作为论据的 tab 党,我当然讨厌 Typescript 啦哈哈哈哈。


    我理解在编译过后都是一样的,但是反正……我还是不爽。而且正是由于 TypeScript 会被编译到JavaScript 中,所以才会出现无论你的类型设计得多么仔细,还是有可能让不同的值类型潜入 JavaScript 变量中的问题。这是不可避免的,因为 JavaScript 仍然是没有类型的。


    4. 报错使我老花


    单纯吐槽一句,为什么它的报错那么丑,我就拼错了一个单词他给我又臭又长报一大段,还没有颜色。。


    为何无法打动我


    在讲为什么 Typescript 的优点无法打动我之前,我先来讲一讲 Typescript 有哪些优点吧:



    1. 大厂的产品

    2. 大厂在用

    3. 可以用未来的特性

    4. 降低出 bug 的可能性

    5. 面对对象编程(OOP)


    对于它是微软的产品,我不能多说啥,毕竟我用 Visual Studio Code 用得很香;但是大厂在用这个论点,就不一样了。


    有个逻辑谬误叫做「reductio ad absurdum」,也就是「归谬法」。什么意思呢:



    大厂用 Typescript,所以我要用 Typescript。

    大厂几百万改个 logo,我就借几百万改个 logo,因为大厂是大厂,肯定做得对。



    这就很荒谬。


    的确,大公司会采用 Typescript,必定有他的道理。但是,同样的论证思路也可以用于 FlowAngularVueEmberjQueryBootstrap 等等等等,几乎所有流行的库都是如此,那么它们一定都适合你吗?


    关于它可以让你提前接触到未来的特性……大哥,babel 不香吗?


    最后就是 OOP 以及降低出 bug 的可能性(Typesafe)。OOP 是 Typescript 的核心部分,而现在 OOP 已经不吃香了……例如 Ilya Suzdalnitski 就说过它是「万亿美元的灾难」。


    v2-9cc25cb80ddc2df7ab06f0184e433790_1440w.jpg


    至于为什么这么说,无非就两点——面向对象代码难以重构,也难以进行单元测试。重构时的抓狂不提,单元测试的重要性,大家都清楚吧。


    而在 Javascript 这种非 OOP 语言里头,函数可以独立于对象存在。不用为了包含这些函数而去发明一些奇怪的概念真是一种解脱。


    总之,TypeScript 的所谓优点(更好的错误处理、类型推理)都不是最佳方案。你还是得写测试,还是得好好命名你的函数和变量。个人觉得单单像 Typescript 一样添加一个接口或类型不能解决任何这些问题。


    正好一千五百字。


    作者:TurpinHero
    链接:https://juejin.cn/post/6961012856573657095

    收起阅读 »

    我是如何把vue项目启动时间从70s优化到7秒的

    可怕的启动时间 公司的产品是一个比较大的后台管理系统,而且使用的是webpack3的vue模板项目,单次项目启动时间达到了70s左右 启动个项目都够吃一碗豆腐脑了,可是没有豆腐脑怎么办,那就优化启动时间吧! 考虑到升级webpack版本的风险还是比较大的,出...
    继续阅读 »

    可怕的启动时间


    公司的产品是一个比较大的后台管理系统,而且使用的是webpack3的vue模板项目,单次项目启动时间达到了70s左右


    image.png


    启动个项目都够吃一碗豆腐脑了,可是没有豆腐脑怎么办,那就优化启动时间吧!


    考虑到升级webpack版本的风险还是比较大的,出了一点问题都得找我,想想还是先别冒险,稳妥为主,所以我选择了通过插件来优化构建时间。


    通过查阅资料,提升webpack的构建时间有以下几个方向:



    • 多进程处理文件,同一时间处理多个文件

    • 预编译资源模块,比如把长时间不变的库提取出来做预编译,构建的时候直接取编译结果就好

    • 缓存,未修改的模块直接拿到处理结果,不必编译

    • 减少构建搜索和处理的文件数量


    针对以上几种优化方向,给出以下几种优化方案。


    多进程构建


    happypack


    happypack 的作用就是将文件解析任务分解成多个子进程并发执行。


    子进程处理完任务后再将结果发送给主进程。所以可以大大提升 Webpack 的项目构件速度。


    查看happypack的github,发现作者已经不再维护该插件,并且作者推荐使用webpack官方的多进程插件thread-loader,所以我放弃了happypacy,选择了thread-loader。


    thread-loader


    thread-loader是官方维护的多进程loader,功能类似于happypack,也是通过开启子任务来并行解析文件,从而提高构建速度。


    把这个loader放在其他loader前面。不过该loader是有限制的。示例:



    • loader无法发出文件。

    • loader不能使用自定义加载器API。

    • loader无法访问网页包选项。


    每个worker都是一个单独的node.js进程,其开销约为600毫秒。还有进程间通信的开销。在小型项目中使用thread-loader可能并不能优化项目的构建速度,反而会拖慢构建速度,所以使用该loader时需要明确项目构建构成中真正耗时的过程。


    我的项目中我主要是用该loader用来解析vue和js文件,作用于vue-loaderbabel-loader,如下代码:


    const threadLoader = {
    loader: 'thread-loader',
    options: {
    workers: require('os').cpus().length - 1,
    }
    }

    module.exports = {
    module:{
    rules: [
    {
    test: /\.vue$/,
    use: [
    threadLoader, // vue-loader前使用该loader
    {
    loader: 'vue-loader',
    options: vueLoaderConfig
    }
    ],
    },
    {
    test: /\.js$/,
    use: [
    threadLoader, // babel-loader前使用该loader
    {
    loader: 'babel-loader',
    options: {
    cacheDirectory: true
    }
    }
    ]
    }
    ]
    }
    }


    配置了thread-loader后,重新构建试试,如下图所示,大概缩短了10秒的构建时间,还不错。


    image.png


    利用缓存提升二次构建的速度


    虽然使用了多进程构建项目使构建时间缩短了10秒,但是一分钟的构建时间依然让人无法接受,这种挤牙膏似的优化方式多少让人有点不爽,有没有比较爽的方法来进一步缩短构建时间呢?


    答案是有的,使用缓存。


    缓存,不难理解就是第一次构建的时候将构建的结果缓存起来,当第二构建时,查看对应缓存是否修改,如果没有修改,直接使用缓存,由此,我们可以想象,当项目的变化不大时,大部分缓存都是可复用的,拿构建的速度岂不是会有质的飞跃。


    cache-loader


    说到缓存,当然百度一查,最先出现的就是cache-loader,github搜索下官方文档,得到如下结果:


    该loader会缓存其他loader的处理结果,把该loader放到其他loader的前面,同时该loader保存和读取缓存文件也会有开销,所以建议在开销较大的loader前使用该loader。


    文档很简单,考虑到项目中的vue-loaderbabel-loadercss-loader会有比较大的开销,所以为这些loader加上缓存,那么接下来就把cache-loader加到项目中吧:


    const cacheLoader = {
    loader: 'cache-loader'
    }

    const threadLoader = {
    loader: 'thread-loader',
    options: {
    workers: require('os').cpus().length - 1,
    }
    }

    module.exports = {
    module:{
    rules: [
    {
    test: /\.vue$/,
    use: [
    cacheLoader,
    threadLoader, // vue-loader前使用该loader
    {
    loader: 'vue-loader',
    options: vueLoaderConfig
    }
    ],
    },
    {
    test: /\.js$/,
    use: [
    cacheLoader,
    threadLoader, // babel-loader前使用该loader
    {
    loader: 'babel-loader',
    options: {
    cacheDirectory: true
    }
    }
    ]
    }
    ]
    }
    }


    util.js文件中,该文件主要是生成css相关的webpack配置,找到generateLoaders函数,修改如下:


      const cacheLoader = {
    loader: 'cache-loader'
    }

    function generateLoaders(loader, loaderOptions) {
    // 在css-loader前增加cache-loader
    const loaders = options.usePostCSS ? [cacheLoader, cssLoader, postcssLoader] : [cacheLoader, cssLoader]

    if (loader) {
    loaders.push({
    loader: loader + '-loader',
    options: Object.assign({}, loaderOptions, {
    sourceMap: options.sourceMap
    })
    })
    }

    // Extract CSS when that option is specified
    // (which is the case during production build)
    if (options.extract) {
    return ExtractTextPlugin.extract({
    use: loaders,
    fallback: 'vue-style-loader',
    // 添加这句配置解决element-ui的图标路径问题
    publicPath: '../../'
    })
    } else {
    return ['vue-style-loader'].concat(loaders)
    }
    }

    如上配置完成后,再次启动项目,可以发现,现在的启动时间没什么变化,然后我们二次启动项目,可以发现现在的启动时间来到了30s左右,前面我们已经说过了,cache-loader缓存只有在二次启动的时候才会生效。


    image.png


    虽然项目启动时间优化了一半还多,但是我们的欲望是无限大的,30秒的时间离我们的预期还是有点差距的,继续优化!


    hard-source-webpack-plugin


    HardSourceWebpackPlugin是一个webpack插件,为模块提供中间缓存步骤。为了查看结果,您需要使用此插件运行webpack两次:第一次构建将花费正常的时间。第二次建设将大大加快。


    话不多说,直接配置到项目中:


    const HardSourceWebpackPlugin = require('hard-source-webpack-plugin');
    module.exports = {
    //...
    plugins: [
    new HardSourceWebpackPlugin()
    ]
    }

    image.png


    二次构建时,我们会发现构建时间来到了个位数,只有短短的7秒钟。


    在二次构建中,我发现了一个现象,构建的进度会从10% 一下跳到 80%,甚至是一瞬间就完成了中间构建过程。这正验证了该插件为模块提供中间缓存的说法。


    为模块提供中间缓存,我的理解是cache-loader缓存的是对应loader的处理结果 ,而这个插件甚至可以缓存整个项目全部的处理结果,直接引用最终输出的缓存文件,从而大大提高构建速度。


    其他优化方法


    babel-loader开启缓存


    babel-loader自带缓存功能,开启cacheDirectory配置项即可,官网的说法是,开启缓存会提高大约两倍的转换时间。


    module.exports = {
    module: {
    rules: [
    {
    test: /\.js$/,
    use: [
    ...
    {
    loader: 'babel-loader',
    options: {
    cacheDirectory: true // 开启缓存
    }
    }
    ]
    }
    ]
    }
    }

    uglifyjs-webpack-plugin开启多进程压缩


    uglifyjs-webpack-plugin或是其他的代码压缩工具都提供了多进程压缩代码的功能,开启可加速代码压缩。


    动态polyfill


    建议查看该篇文章


    一文搞清楚前端 polyfill


    总结


    至此,我们完成了项目构建时间从70s到7s的优化过程,文中主要使用:





























    一步步的将项目优化到几乎立马启动,哎,看来这下摸鱼的时间又少了,加油干吧,打工人!


    作者:进击的小超人
    链接:https://juejin.cn/post/6979879230297341989

    收起阅读 »

    从零到一编写 IOC 容器

    前言本文的编写主要是最近在使用 midway 编写后端应用,midway 的 IOC 控制反转能力跟我们平时常写的前端应用,例如 react、vue 这些单应用还是有蛮大区别的,所以促使我想一探究竟,这种类 Spring IOC 容器是如何用 JavaScri...
    继续阅读 »




    前言

    本文的编写主要是最近在使用 midway 编写后端应用,midway 的 IOC 控制反转能力跟我们平时常写的前端应用,例如 react、vue 这些单应用还是有蛮大区别的,所以促使我想一探究竟,这种类 Spring IOC 容器是如何用 JavaScript 来实现的。为方便读者阅读,本文的组织结构依次为 TS 装饰器、Reflect Metadata、IOC 容器源码简单解读、以及自定义实现 IOC 容器。阅读完本文后,我希望你能有这样的感悟:元数据(metadata)和 装饰器(Decorator) 本是 ES 中两个独立的部分,但是结合它们, 竟然能实现 控制反转 这样的能力。本文的所有演示实例都已经上传到 github 仓库 ioc-container ,读者可以克隆下来进行调试运行。

    辛苦整理良久,还望手动点赞鼓励~ 博客 github地址为:github.com/fengshi123/… ,汇总了作者的所有博客,欢迎关注及 star ~

    一、TS 装饰器

    1、类装饰器

    (1)类型声明

    type ClassDecorator = <TFunction extends Function>
    (target: TFunction) => TFunction | void;
    • 参数:

      target: 类的构造器。

    • 返回:

    如果类装饰器返回了一个值,它将会被用来代替原有的类构造器的声明。因此,类装饰器适合用于继承一个现有类并添加一些属性和方法。例如我们可以添加一个 toString 方法给所有的类来覆盖它原有的 toString 方法,以及增加一个新的属性 school,如下所示

    type Consturctor = { new (...args: any[]): any };

    function School<T extends Consturctor>(BaseClass: T) {
     // 新构造器继承原有的构造器,并且返回
     return class extends BaseClass {  
       // 新增属性 school
       public school = 'qinghua'
       // 重写方法 toString
       toString() {
         return JSON.stringify(this);
      }
    };
    }

    @School
    class Student {
     public name = 'tom';
     public age = 14;
    }

    console.log(new Student().toString())
    // {"name":"tom","age":14,"school":"qinghua"}

    但是存在一个问题:装饰器并没有类型保护,这意味着在类装饰器的构造函数中新增的属性,通过原有的类实例将报无法找到的错误,如下所示

    type Consturctor = { new (...args: any[]): any };

    function School<T extends Consturctor>(BaseClass: T){
     return class extends BaseClass {
       // 新增属性 school
       public school = 'qinghua'
    };
    }


    @School
    class Student{
     getSchool() {
       return this.school; // Property 'school' does not exist on type 'Student'
    }
    }

    new Student().school  // Property 'school' does not exist on type 'Student'

    这是 一个TypeScript的已知的缺陷。 目前我们能做的可以额外提供一个类用于提供类型信息,如下所示

    type Consturctor = { new (...args: any[]): any };

    function School<T extends Consturctor>(BaseClass: T){
     return class extends BaseClass {
       // 新增属性 school
       public school = 'qinghua'
    };
    }

    // 新增一个类用于提供类型信息
    class Base {
     school: string;
    }

    @School
    class Student extends Base{
     getSchool() {
       return this.school;
    }
    }

    new Student().school)

    2、属性装饰器

    (1)类型声明

    type PropertyDecorator = (
    target: Object,
     propertyKey: string | symbol
    ) => void;
    复制代码
    • 参数:

      1. target: 对于静态成员来说是类的构造器,对于实例成员来说是类的原型链。

      2. propertyKey: 属性的名称。

    • 返回:

    返回的结果将被忽略。

    我们可以通过属性装饰器给属性添加对应的验证判断,如下所示

    function NameObserve(target: Object, property: string): void {
     console.log('target:', target)
     console.log('property:', property)
     let _property = Symbol(property)
     Object.defineProperty(target, property, {
       set(val){
         if(val.length > 4){
           throw new Error('名称不能超过4位!')
        }
         this[_property] = val;
      },
       get: function() {
         return this[_property];
    }
    })
    }

    class Student {
     @NameObserve
     public name: string;  // target: Student {}   key: 'name'
    }

    const stu = new Student();
    stu.name = 'jack'
    console.log(stu.name); // jack
    // stu.name = 'jack1'; // Error: 名称不能超过4位!

    export default Student;

    3、方法装饰器

    (1)类型声明:

    type MethodDecorator = <T>(
     target: Object,
     propertyKey: string | symbol,
     descriptor: TypedPropertyDescriptor<T>
    ) => TypedPropertyDescriptor<T> | void;
    • 参数:

      1. target: 对于静态成员来说是类的构造器,对于实例成员来说是类的原型链;

      2. propertyKey: 属性的名称;

      3. descriptor: 属性的描述器;

    • 返回: 如果返回了值,它会被用于替代属性的描述器。

    方法装饰器不同于属性装饰器的地方在于 descriptor 参数。 通过这个参数我们可以修改方法原本的实现,添加一些共用逻辑。 例如我们可以给一些方法添加打印输入与输出的能力

    function logger(target: Object, property: string, 
       descriptor: PropertyDescriptor): PropertyDescriptor | void {
     const origin = descriptor.value;
     console.log(descriptor)
     descriptor.value = function(...args: number[]){
       console.log('params:', ...args)
       const result = origin.call(this, ...args);
       console.log('result:', result);
       return result;
    }
    }

    class Person {
     @logger
     add(x: number, y: number){
       return x + y;
    }
    }

    const person = new Person();
    const result = person.add(1, 2);
    console.log('查看 result:', result) // 3

    4、访问器装饰器

    访问器装饰器总体上讲和方法装饰器很接近,唯一的区别在于描述器中有的 key 不同: 方法装饰器的描述器的 key 为:

    • value

    • writable

    • enumerable

    • configurable

    访问器装饰器的描述器的key为:

    • get

    • set

    • enumerable

    • configurable

    例如,我们可以对访问器进行统一更改:

    function descDecorator(target: Object, property: string, 
       descriptor: PropertyDescriptor): PropertyDescriptor | void {
     const originalSet = descriptor.set;
     const originalGet = descriptor.get;
     descriptor.set = function(value: any){
       return originalSet.call(this, value)
    }
     descriptor.get = function(): string{
       return 'name:' + originalGet.call(this)
    }
    }

    class Person {
     private _name = 'tom';

     @descDecorator
     set name(value: string){
       this._name = value;
    }

     get name(){
       return this._name;
    }
    }

    const person = new Person();
    person.name = ('tom');
    console.log('查看:', person.name) // name:'tom'

    5、参数装饰器

    类型声明:

    type ParameterDecorator = (
     target: Object,
     propertyKey: string | symbol,
     parameterIndex: number
    ) => void;
    • 参数:

      1. target: 对于静态成员来说是类的构造器,对于实例成员来说是类的原型链。

      2. propertyKey: 属性的名称(注意是方法的名称,而不是参数的名称)。

      3. parameterIndex: 参数在方法中所处的位置的下标。

    • 返回:

    返回的值将会被忽略。

    单独的参数装饰器能做的事情很有限,它一般都被用于记录可被其它装饰器使用的信息。

    function ParamDecorator(target: Object, property: string, 
       paramIndex: number): void {
     console.log(property);
     console.log(paramIndex);
    }

    class Person {
     private name: string;

     public setNmae(@ParamDecorator school: string, name: string){  // setNmae 0
       this.name = school + '_' + name
    }
    }

    6、执行时机

    装饰器只在解释执行时应用一次,如下所示,这里的代码会在终端中打印 apply decorator,即便我们其实并没有使用类 A。

    function f(C) {
     console.log('apply decorator')
     return C
    }

    @f
    class A {}

    // output: apply decorator

    7、执行顺序

    不同类型的装饰器的执行顺序是明确定义的:

    • 实例成员:参数装饰器 -> 方法/访问器/属性 装饰器

    • 静态成员:参数装饰器 -> 方法/访问器/属性 装饰器

    • 构造器:参数装饰器

    • 类装饰器

    示例如下所示

    function f(key: string): any {
     console.log("evaluate: ", key);
     return function () {
       console.log("call: ", key);
    };
    }

    @f("Class Decorator")
    class C {
     @f("Static Property")
     static prop?: number;

     @f("Static Method")
     static method(@f("Static Method Parameter") foo:any) {}

     constructor(@f("Constructor Parameter") foo:any) {}

     @f("Instance Method")
     method(@f("Instance Method Parameter") foo:any) {}

     @f("Instance Property")
     prop?: number;
    }

    /* 输出顺序如下
    evaluate: Instance Method
    evaluate: Instance Method Parameter
    call: Instance Method Parameter
    call: Instance Method
    evaluate: Instance Property
    call: Instance Property
    evaluate: Static Property
    call: Static Property
    evaluate: Static Method
    evaluate: Static Method Parameter
    call: Static Method Parameter
    call: Static Method
    evaluate: Class Decorator
    evaluate: Constructor Parameter
    call: Constructor Parameter
    call: Class Decorator
    */

    我们从上注意到执行实例属性 prop 晚于实例方法 method 然而执行静态属性 static prop 早于静态方法static method。 这是因为对于属性/方法/访问器装饰器而言,执行顺序取决于声明它们的顺序。 然而,同一方法中不同参数的装饰器的执行顺序是相反的, 最后一个参数的装饰器会最先被执行。

    function f(key: string): any {
     console.log("evaluate: ", key);
     return function () {
       console.log("call: ", key);
    };
    }

    class C {
     method(
       @f("Parameter Foo") foo,
       @f("Parameter Bar") bar
    ) {}
    }

    /* 输出顺序如下
    evaluate: Parameter Foo
    evaluate: Parameter Bar
    call: Parameter Bar
    call: Parameter Foo
    */

    8、多个装饰器组合

    我们可以对同一目标应用多个装饰器。它们的组合顺序为:

    • 求值外层装饰器

    • 求值内层装饰器

    • 调用内层装饰器

    • 调用外层装饰器

    如下示例所示

    function f(key: string) {
     console.log("evaluate: ", key);
     return function () {
       console.log("call: ", key);
    };
    }

    class C {
     @f("Outer Method")
     @f("Inner Method")
     method() {}
    }

    /* 输出顺序如下
    evaluate: Outer Method
    evaluate: Inner Method
    call: Inner Method
    call: Outer Method
    */

    二、Reflect Metadata

    1、背景

    在 ES6 的规范当中,ES6 支持元编程,核心是因为提供了对 Proxy 和 Reflect 对象的支持。简单来说这个 API 的作用就是可以实现对变量操作的函数化,也就是反射。然而 ES6 的 Reflect 规范里面还缺失一个规范,那就是 Reflect Metadata。这会造成什么样的情境呢? 由于 JS/TS 现有的 装饰器更多的是存在于对函数或者属性进行一些操作,比如修改他们的值,代理变量,自动绑定 this 等等功能。但是却无法实现通过反射来获取究竟有哪些装饰器添加到这个类/方法上... 这就限制了 JS 中元编程的能力。 此时 Relfect Metadata 就派上用场了,可以通过装饰器来给类添加一些自定义的信息。然后通过反射将这些信息提取出来(当然你也可以通过反射来添加这些信息)。 综合一下, JS 中对 Reflect Metadata 的诉求,简单概括就是:

    • 其他 C#、Java、Pythone 语言已经有的高级功能,我 JS 也应该要有(诸如C# 和 Java 之类的语言支持将元数据添加到类型的属性或注释,以及用于读取元数据的反射API,而目前 JS 缺少这种能力)

    • 许多用例(组合/依赖注入,运行时类型断言,反射/镜像,测试)都希望能够以一致的方式向类中添加其他元数据。

    • 为了使各种工具和库能够推理出元数据,需要一种标准一致的方法;

    • 元数据不仅可以用在对象上,也可以通过相关捕获器用在 Proxy 上;

    • 对开发人员来说,定义新的元数据生成装饰器应该简洁易用;

    2、使用

    TypeScript 在 1.5+ 的版本已经支持 reflect-metadata,但是我们在使用的时候还需要额外进行安装,如下所示

    • npm i reflect-metadata --save

    • 在 tsconfig.json 里配置选项 emitDecoratorMetadata: true

    关于 reflect-metadata 的基本使用 api 可以阅读 reflect-metadata 文档,其包含常见的增删改查基本功能,我们来看下其基本的使用示例,其中 Reflect.metadata 当作 Decorator 使用,当修饰类时,在类上添加元数据,当修饰类属性时,在类原型的属性上添加元数据,如:

    import "reflect-metadata";

    @Reflect.metadata('classMetaData', 'A')
    class SomeClass {
     @Reflect.metadata('methodMetaData', 'B')
     public someMethod(): string {
       return 'hello someMethod';
    }
    }

    console.log(Reflect.getMetadata('classMetaData', SomeClass)); // 'A'
    console.log(Reflect.getMetadata('methodMetaData', new SomeClass(), 'someMethod')); // 'B

    当然跟我们平时看到的 IOC 不同,我们进一步结合装饰器,如下所示,与前面的功能是一样的

    import "reflect-metadata";

    function classDecorator(): ClassDecorator {
     return target => {
       // 在类上定义元数据,key 为 `classMetaData`,value 为 `a`
       Reflect.defineMetadata('classMetaData', 'A', target);
    };
    }

    function methodDecorator(): MethodDecorator {
     return (target, key, descriptor) => {
       // 在类的原型属性 'someMethod' 上定义元数据,key 为 `methodMetaData`,value 为 `b`
       Reflect.defineMetadata('methodMetaData', 'B', target, key);
    };
    }

    @classDecorator()
    class SomeClass {
     @methodDecorator()
     someMethod() {}
    }

    console.log(Reflect.getMetadata('classMetaData', SomeClass)); // 'A'
    console.log(Reflect.getMetadata('methodMetaData', new SomeClass(), 'someMethod')); // 'B'

    3、design:类型元数据

    在 TS 中的 reflect-metadata 的功能是经过增强的,其添加 "design:type"、"design:paramtypes" 和 "design:returntype" 这 3 个类型相关的元数据

    • design:type 表示被装饰的对象是什么类型, 比如是字符串、数字、还是函数等;

    • design:paramtypes 表示被装饰对象的参数类型, 是一个表示类型的数组, 如果不是函数, 则没有该 key;

    • design:returntype 表示被装饰对象的返回值属性, 比如字符串、数字或函数等;

    示例如下所示

    import "reflect-metadata";

    @Reflect.metadata('type', 'class')
    class A {  
     constructor(
       public name: string,
       public age: number
    ) { }  

     @Reflect.metadata(undefined, undefined)  
     method(name: string, age: number):boolean {    
       return true  
    }
    }

     const t1 = Reflect.getMetadata('design:type', A.prototype, 'method')
     const t2 = Reflect.getMetadata('design:paramtypes', A.prototype, 'method')
     const t3 = Reflect.getMetadata('design:returntype', A.prototype, 'method')
     
     console.log(t1)  // [Function: Function]
     console.log(...t2) // [Function: String] [Function: Number]
     console.log(t3) // [Function: Boolean]

    三、IOC 容器实现

    1、源码解读

    我们可以从 github 上克隆 midway 仓库的代码到本地,然后进行代码阅读以及 debug 调试。本篇博文我们主要想探究下 midway 的依赖注入合控制反转是如何实现的,其主要源码存在于两个目录:packages/core 和 packages/decorator,其中 packages/core 包含依赖注入的核心实现,加载对象的class,同步、异步创建对象实例化,对象的属性绑定等。 IOC 容器就像是一个对象池,管理着每个对象实例的信息(Class Definition),所以用户无需关心什么时候创建,当用户希望拿到对象的实例 (Object Instance) 时,可以直接拿到依赖对象的实例,容器会 自动将所有依赖的对象都自动实例化。packages/core 中主要有以下几种,分别处理不同的逻辑:

    • AppliationContext 基础容器,提供了基础的增加定义和根据定义获取对象实例的能力;

    • MidwayContainer 用的最多的容器,做了上层封装,通过 bind 函数能够方便的生成类定义,midway 从此类开始扩展;

    • RequestContainer 用于请求链路上的容器,会自动销毁对象并依赖另一个容器创建实例;

    packages/decorator 包含装饰器 provide.ts、inject.ts 的实现,在midwayjs中是有一个装饰器管理类DecoratorManager, 用来管理 midwayjs 的所有装饰器:

    • @provide() 的作用是简化绑定,能被 IOC 容器自动扫描,并绑定定义到容器上,对应的逻辑是绑定对象定义;

    • @inject() 的作用是将容器中的定义实例化成一个对象,并且绑定到属性中,这样,在调用的时候就可以访问到该属性。

    2、简单实现

    2.1、装饰器 Provider

    实现装饰器 Provider 类,作用为将对应类注册到 IOC 容器中。

    import 'reflect-metadata'
    import * as camelcase from 'camelcase'
    import { class_key } from './constant'

    // Provider 装饰的类,表示要注册到 IOC 容器中
    export function Provider (identifier?: string, args?: Array<any>) {
     return function (target: any) {
       // 类注册的唯一标识符
       identifier = identifier ?? camelcase(target.name)

       Reflect.defineMetadata(class_key, {
         id: identifier,  // 唯一标识符
         args: args || [] // 实例化所需参数
      }, target)
       return target
    }
    }

    2.2、装饰器 Inject

    实现装饰器 Inject 类,作用为将对应的类注入到对应的地方。

    import 'reflect-metadata'
    import { props_key } from './constant'

    export function Inject () {
     return function (target: any, targetKey: string) {
       // 注入对象
       const annotationTarget = target.constructor
       let props = {}
       // 同一个类,多个属性注入类
       if (Reflect.hasOwnMetadata(props_key, annotationTarget)) {
         props = Reflect.getMetadata(props_key, annotationTarget)
      }

       //@ts-ignore
       props[targetKey] = {
         value: targetKey
      }

       Reflect.defineMetadata(props_key, props, annotationTarget)
    }
    }

    2.3、管理容器 Container

    管理容器 Container 的实现,用于绑定实例信息并且在对应的地方获取它们。

    import 'reflect-metadata'
    import { props_key } from './constant'

    export class Container {
     bindMap = new Map()

     // 绑定类信息
     bind(identifier: string, registerClass: any, constructorArgs: any[]) {
       this.bindMap.set(identifier, {registerClass, constructorArgs})
    }

     // 获取实例,将实例绑定到需要注入的对象上
     get<T>(identifier: string): T {
       const target = this.bindMap.get(identifier)
       if (target) {
         const { registerClass, constructorArgs } = target
         // 等价于 const instance = new registerClass([...constructorArgs])
         const instance = Reflect.construct(registerClass, constructorArgs)

         const props = Reflect.getMetadata(props_key, registerClass)
         for (let prop in props) {
           const identifier = props[prop].value
           // 递归进行实例化获取 injected object
           instance[prop] = this.get(identifier)
        }
         return instance
      }
    }
    }

    2.4、加载类文件 load

    启动时扫描所有文件,获取文件导出的所有类,然后根据元数据进行绑定。

    import * as fs from 'fs'
    import { resolve } from 'path'
    import { class_key } from './constant'

    // 启动时扫描所有文件,获取定义的类,根据元数据进行绑定
    export function load(container: any, path: string) {
     const list = fs.readdirSync(path)
     for (const file of list) {
       if (/\.ts$/.test(file)) {
         const exports = require(resolve(path, file))

         for (const m in exports) {
           const module = exports[m]
           if (typeof module === 'function') {
             const metadata = Reflect.getMetadata(class_key, module)
             // register
             if (metadata) {
               container.bind(metadata.id, module, metadata.args)
            }
          }
        }
      }
    }
    }

    2.5、示例类

    三个示例类如下所示

    // class A
    import { Provider } from "../provide";
    import { Inject } from "../inject";
    import B from './classB'
    import C from './classC'

    @Provider('a')
    export default class A {
     @Inject()
     private b: B

     @Inject()
     c: C

     print () {
       this.c.print()
    }
    }

    // class B
    import { Provider } from '../provide'

    @Provider('b', [10])
    export default class B {
     n: number
     constructor (n: number) {
       this.n = n
    }
    }

    // class C
    import { Provider } from '../provide'

    @Provider()
    export default class C {
     print () {
       console.log('hello')
    }
    }

    2.6、初始化

    我们能从以下示例结果中看到,我们已经实现了一个基本的 IOC 容器能力。

    import { Container } from './container'
    import { load } from './load'
    import { class_path } from './constant'

    const init =  function () {

     const container = new Container()
     // 通过加载,会先执行装饰器(设置元数据),
     // 再由 container 统一管理元数据中,供后续使用
     load(container, class_path)
     const a:any = container.get('a') // A { b: B { n: 10 }, c: C {} }
     console.log(a);
     a.c.print() // hello
    }

    init()

    总结

    本文的依次从 TS 装饰器、Reflect Metadata、IOC 容器源码简单解读、以及自定义实现 IOC 容器四个部分由零到一编写自定义 IOC 容器,希望对你有所启发。本文的所有演示实例都已经上传到 github 仓库 ioc-container ,读者可以克隆下来进行调试运行。

    作者:我是你的超级英雄
    来源:https://juejin.cn/post/7036895697865555982

    收起阅读 »

    手写迷你版Vue

    手写迷你版Vue参考代码:github.com/57code/vue-…Vue响应式设计思路Vue响应式主要包含:数据响应式监听数据变化,并在视图中更新Vue2使用Object.defineProperty实现数据劫持Vu3使用Proxy实现数据劫持模板引擎提...
    继续阅读 »




    手写迷你版Vue

    参考代码:github.com/57code/vue-…

    Vue响应式设计思路

    Vue响应式主要包含:

    • 数据响应式

    • 监听数据变化,并在视图中更新

    • Vue2使用Object.defineProperty实现数据劫持

    • Vu3使用Proxy实现数据劫持

    • 模板引擎

    • 提供描述视图的模板语法

    • 插值表达式{{}}

    • 指令 v-bind, v-on, v-model, v-for,v-if

    • 渲染

    • 将模板转换为html

    • 解析模板,生成vdom,把vdom渲染为普通dom

    数据响应式原理

    image.png

    数据变化时能自动更新视图,就是数据响应式 Vue2使用Object.defineProperty实现数据变化的检测

    原理解析

    • new Vue()⾸先执⾏初始化,对data执⾏响应化处理,这个过程发⽣在Observer

    • 同时对模板执⾏编译,找到其中动态绑定的数据,从data中获取并初始化视图,这个过程发⽣在

    Compile

    • 同时定义⼀个更新函数和Watcher实例,将来对应数据变化时,Watcher会调⽤更新函数

    • 由于data的某个key在⼀个视图中可能出现多次,所以每个key都需要⼀个管家Dep来管理多个

    Watcher

    • 将来data中数据⼀旦发⽣变化,会⾸先找到对应的Dep,通知所有Watcher执⾏更新函数

    image.png

    一些关键类说明

    CVue:自定义Vue类 Observer:执⾏数据响应化(分辨数据是对象还是数组) Compile:编译模板,初始化视图,收集依赖(更新函数、 watcher创建) Watcher:执⾏更新函数(更新dom) Dep:管理多个Watcher实例,批量更新

    涉及关键方法说明

    observe: 遍历vm.data的所有属性,对其所有属性做响应式,会做简易判断,创建Observer实例进行真正响应式处理

    html页面

    <!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>cvue</title>
    <script src="./cvue.js"></script>
    </head>
    <body>
    <div id="app">
      <p>{{ count }}</p>
    </div>

    <script>
      const app = new CVue({
        el: '#app',
        data: {
          count: 0
        }
      })
      setInterval(() => {
        app.count +=1
      }, 1000);
    </script>
    </body>
    </html>

    CVue

    • 创建基本CVue构造函数:

    • 执⾏初始化,对data执⾏响应化处理

    // 自定义Vue类
    class CVue {
    constructor(options) {
      this.$options = options
      this.$data = options.data

      // 响应化处理
      observe(this.$data)
    }
    }

    // 数据响应式, 修改对象的getter,setter
    function defineReactive(obj, key, val) {
    // 递归处理,处理val是嵌套对象情况
    observe(val)
    Object.defineProperty(obj, key, {
      get() {
        return val
      },
      set(newVal) {
        if(val !== newVal) {
          console.log(`set ${key}:${newVal}, old is ${val}`)

          val = newVal
          // 继续进行响应式处理,处理newVal是对象情况
          observe(val)
        }
      }
    })
    }

    // 遍历obj,对其所有属性做响应式
    function observe(obj) {
    // 只处理对象类型的
    if(typeof obj !== 'object' || obj == null) {
      return
    }
    // 实例化Observe实例
    new Observe(obj)
    }

    // 根据传入value的类型做相应的响应式处理
    class Observe {
    constructor(obj) {
      if(Array.isArray(obj)) {
        // TODO
      } else {
        // 对象
        this.walk(obj)
      }
    }
    walk(obj) {
      // 遍历obj所有属性,调用defineReactive进行响应化
      Object.keys(obj).forEach(key => defineReactive(obj, key, obj[key]))
    }
    }

    为vm.$data做代理

    方便实例上设置和获取数据

    例如

    原本应该是

    vm.$data.count
    vm.$data.count = 233

    代理之后后,可以使用如下方式

    vm.count
    vm.count = 233

    给vm.$data做代理

    class CVue {
    constructor(options) {
      // 省略
      // 响应化处理
      observe(this.$data)

      // 代理data上属性到实例上
      proxy(this)
    }
    }

    // 把CVue实例上data对象的属性到代理到实例上
    function proxy(vm) {
    Object.keys(vm.$data).forEach(key => {
      Object.defineProperty(vm, key, {
        get() {
          // 实现 vm.count 取值
          return vm.$data[key]
        },
        set(newVal) {
          // 实现 vm.count = 123赋值
          vm.$data[key] = newVal
        }
      })
    })
    }

    编译

    image.png

    初始化视图

    根据节点类型进行编译
    class CVue {
    constructor(options) {
      // 省略。。
      // 2 代理data上属性到实例上
      proxy(this)

      // 3 编译
      new Compile(this, this.$options.el)
    }
    }

    // 编译模板中vue语法,初始化视图,更新视图
    class Compile {
    constructor(vm, el) {
      this.$vm = vm
      this.$el = document.querySelector(el)

      if(this.$el) {
        this.complie(this.$el)
      }
    }
    // 编译
    complie(el) {
      // 取出所有子节点
      const childNodes = el.childNodes
      // 遍历节点,进行初始化视图
      Array.from(childNodes).forEach(node => {
        if(this.isElement(node)) {
          // TODO
          console.log(`编译元素 ${node.nodeName}`)
        } else if(this.isInterpolation(node)) {
          console.log(`编译插值文本 ${node.nodeName}`)
        }
        // 递归编译,处理嵌套情况
        if(node.childNodes) {
          this.complie(node)
        }
      })
    }
    // 是元素节点
    isElement(node) {
      return node.nodeType === 1
    }
    // 是插值表达式
    isInterpolation(node) {
      return node.nodeType === 3
        && /\{\{(.*)\}\}/.test(node.textContent)
    }
    }
    编译插值表达式
    // 编译模板中vue语法,初始化视图,更新视图
    class Compile {
    complie(el) {
      Array.from(childNodes).forEach(node => {
        if(this.isElement(node)) {
          console.log(`编译元素 ${node.nodeName}`)
        } else if(this.isInterpolation(node)) {
          // console.log(`编译插值文本 ${node.textContent}`)
          this.complieText(node)
        }
        // 省略
      })
    }
    // 是插值表达式
    isInterpolation(node) {
      return node.nodeType === 3
        && /\{\{(.*)\}\}/.test(node.textContent)
    }
    // 编译插值
    complieText(node) {
      // RegExp.$1是isInterpolation()中/\{\{(.*)\}\}/匹配出来的组内容
      // 相等于{{ count }}中的count
      const exp = String(RegExp.$1).trim()
      node.textContent = this.$vm[exp]
    }
    }
    编译元素节点和指令

    需要取出指令和指令绑定值 使用数据更新视图

    // 编译模板中vue语法,初始化视图,更新视图
    class Compile {
    complie(el) {
      Array.from(childNodes).forEach(node => {
        if(this.isElement(node)) {
          console.log(`编译元素 ${node.nodeName}`)
          this.complieElement(node)
        }
        // 省略
      })
    }
    // 是元素节点
    isElement(node) {
      return node.nodeType === 1
    }
    // 编译元素
    complieElement(node) {
      // 取出元素上属性
      const attrs = node.attributes
      Array.from(attrs).forEach(attr => {
        // c-text="count"中c-text是attr.name,count是attr.value
        const { name: attrName, value: exp } = attr
        if(this.isDirective(attrName)) {
          // 取出指令
          const dir = attrName.substring(2)
          this[dir] && this[dir](node, exp)
        }
      })
    }
    // 是指令
    isDirective(attrName) {
      return attrName.startsWith('')
    }
    // 处理c-text文本指令
    text(node, exp) {
      node.textContent = this.$vm[exp]
    }
    // 处理c-html指令
    html(node, exp) {
      node.innerHTML = this.$vm[exp]
    }
    }

    以上完成初次渲染,但是数据变化后,不会触发页面更新

    依赖收集

    视图中会⽤到data中某key,这称为依赖。 同⼀个key可能出现多次,每次出现都需要收集(⽤⼀个Watcher来维护维护他们的关系),此过程称为依赖收集。 多个Watcher需要⼀个Dep来管理,需要更新时由Dep统⼀通知。

    image.png

    • data中的key和dep是一对一关系

    • 视图中key出现和Watcher关系,key出现一次就对应一个Watcher

    • dep和Watcher是一对多关系

    实现思路

    • defineReactive中为每个key定义一个Dep实例

    • 编译阶段,初始化视图时读取key, 会创建Watcher实例

    • 由于读取过程中会触发key的getter方法,便可以把Watcher实例存储到key对应的Dep实例

    • 当key更新时,触发setter方法,取出对应的Dep实例Dep实例调用notiy方法通知所有Watcher更新

    定义Watcher类

    监听器,数据变化更新对应节点视图

    // 创建Watcher监听器,负责更新视图
    class Watcher {
    // vm vue实例,依赖key,updateFn更新函数(编译阶段传递进来)
    constructor(vm, key, updateFn) {
      this.$vm = vm
      this.$key = key
      this.$updateFn = updateFn
    }
    update() {
      // 调用更新函数,获取最新值传递进去
      this.$updateFn.call(this.$vm, this.$vm[this.$key])
    }
    }
    修改Compile类中的更新函数,创建Watcher实例
    class Complie {
    // 省略。。。
    // 编译插值
    complieText(node) {
      // RegExp.$1是isInterpolation()中/\{\{(.*)\}\}/匹配出来的组内容
      // 相等于{{ count }}中的count
      const exp = String(RegExp.$1).trim()
      // node.textContent = this.$vm[exp]
      this.update(node, exp, 'text')
    }
    // 处理c-text文本指令
    text(node, exp) {
      // node.textContent = this.$vm[exp]
      this.update(node, exp, 'text')
    }
    // 处理c-html指令
    html(node, exp) {
      // node.innerHTML = this.$vm[exp]
      this.update(node, exp, 'html')
    }
    // 更新函数
    update(node, exp, dir) {
      const fn = this[`${dir}Updater`]
      fn && fn(node, this.$vm[exp])

      // 创建监听器
      new Watcher(this.$vm, exp, function(newVal) {
        fn && fn(node, newVal)
      })
    }
    // 文本更新器
    textUpdater(node, value) {
      node.textContent = value
    }
    // html更新器
    htmlUpdater(node, value) {
      node.innerHTML = value
    }
    }
    定义Dep类
    • data的一个属性对应一个Dep实例

    • 管理多个Watcher实例,通知所有Watcher实例更新

    // 创建订阅器,每个Dep实例对应data中的一个属性
    class Dep {
    constructor() {
      this.deps = []
    }
    // 添加Watcher实例
    addDep(dep) {
      this.deps.push(dep)
    }
    notify() {
      // 通知所有Wather更新视图
      this.deps.forEach(dep => dep.update())
    }
    }
    创建Watcher时触发getter
    class Watcher {
    // vm vue实例,依赖key,updateFn更新函数(编译阶段传递进来)
    constructor(vm, key, updateFn) {
      // 省略
      // 把Wather实例临时挂载在Dep.target上
      Dep.target = this
      // 获取一次属性,触发getter, 从Dep.target上获取Wather实例存放到Dep实例中
      this.$vm[key]
      // 添加后,重置Dep.target
      Dep.target = null
    }
    }
    defineReactive中作依赖收集,创建Dep实例
    function defineReactive(obj, key, val) {
    // 递归处理,处理val是嵌套对象情况
    observe(val)

    const dep = new Dep()
    Object.defineProperty(obj, key, {
      get() {
        Dep.target && dep.addDep(Dep.target)
        return val
      },
      set(newVal) {
        if(val !== newVal) {
          val = newVal
          // 继续进行响应式处理,处理newVal是对象情况
          observe(val)
          // 更新视图
          dep.notify()
        }
      }
    })
    }

    监听事件指令@xxx

    • 在创建vue实例时,需要缓存methods到vue实例上

    • 编译阶段取出methods挂载到Compile实例上

    • 编译元素时

    • 识别出v-on指令时,进行事件的绑定

    • 识别出@属性时,进行事件绑定

    • 事件绑定:通过指令或者属性获取对应的函数,给元素新增事件监听,使用bind修改监听函数的this指向为组件实例

    // 自定义Vue类
    class CVue {
    constructor(options) {
      this.$methods = options.methods
    }
    }

    // 编译模板中vue语法,初始化视图,更新视图
    class Compile {
    constructor(vm, el) {
      this.$vm = vm
      this.$el = document.querySelector(el)
      this.$methods = vm.$methods
    }

    // 编译元素
    complieElement(node) {
      // 取出元素上属性
      const attrs = node.attributes
      Array.from(attrs).forEach(attr => {
        // c-text="count"中c-text是attr.name,count是attr.value
        const { name: attrName, value: exp } = attr
        if(this.isDirective(attrName)) {
          // 省略。。。
          if(this.isEventListener(attrName)) {
            // v-on:click, subStr(5)即可截取到click
            const eventType = attrName.substring(5)
            this.bindEvent(eventType, node, exp)
          }
        } else if(this.isEventListener(attrName)) {
          // @click, subStr(1)即可截取到click
          const eventType = attrName.substring(1)
          this.bindEvent(eventType, node, exp)
        }
      })
    }
    // 是事件监听
    isEventListener(attrName) {
      return attrName.startsWith('@') || attrName.startsWith('c-on')
    }
    // 绑定事件
    bindEvent(eventType, node, exp) {
      // 取出表达式对应函数
      const method = this.$methods[exp]
      // 增加监听并修改this指向当前组件实例
      node.addEventListener(eventType, method.bind(this.$vm))
    }
    }

    v-model双向绑定

    实现v-model绑定input元素时的双向绑定功能

    // 编译模板中vue语法,初始化视图,更新视图
    class Compile {
    // 省略...
    // 处理c-model指令
    model(node, exp) {
      // 渲染视图
      this.update(node, exp, 'model')
      // 监听input变化
      node.addEventListener('input', (e) => {
        const { value } = e.target
        // 更新数据,相当于this.username = 'mio'
        this.$vm[exp] = value
      })
    }
    // model更新器
    modelUpdater(node, value) {
      node.value = value
    }
    }

    数组响应式

    • 获取数组原型

    • 数组原型创建对象作为数组拦截器

    • 重写数组的7个方法

    // 数组响应式
    // 获取数组原型, 后面修改7个方法
    const originProto = Array.prototype
    // 创建对象做备份,修改响应式都是在备份的上进行,不影响原始数组方法
    const arrayProto = Object.create(originProto)
    // 拦截数组方法,在变更时发出通知
    ;['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'].forEach(method => {
    // 在备份的原型上做修改
    arrayProto[method] = function() {
      // 调用原始操作
      originProto[method].apply(this, arguments)
      // 发出变更通知
      console.log(`method:${method} value:${Array.from(arguments)}`)
    }
    })

    class Observe {
    constructor(obj) {
      if(Array.isArray(obj)) {
        // 修改数组原型为自定义的
        obj.__proto__ = arrayProto
        this.observeArray(obj)
      } else {
        // 对象
        this.walk(obj)
      }
    }
    observeArray(items) {
      // 如果数组内部元素时对象,继续做响应化处理
      items.forEach(item => observe(item))
    }
    }

    作者:LastStarDust
    来源:https://juejin.cn/post/7036291383153393701

    收起阅读 »

    LRU缓存-keep-alive实现原理

    相信大部分同学在日常需求开发中或多或少的会有需要一个组件状态被持久化、不被重新渲染的场景,熟悉 vue 的同学一定会想到 keep-alive 这个内置组件。 keep-alive 是 Vue.js 的一个 内置组件。它能够将不活动的组件实例保存在内存中,而不...
    继续阅读 »



    前言

    相信大部分同学在日常需求开发中或多或少的会有需要一个组件状态被持久化、不被重新渲染的场景,熟悉 vue 的同学一定会想到 keep-alive 这个内置组件。

    那么什么是 keep-alive 呢?

    keep-alive 是 Vue.js 的一个 内置组件。它能够将不活动的组件实例保存在内存中,而不是直接将其销毁,它是一个抽象组件,不会被渲染到真实 DOM 中,也不会出现在父组件链中。简单的说,keep-alive用于保存组件的渲染状态,避免组件反复创建和渲染,有效提升系统性能。 keep-alivemax 属性,用于限制可以缓存多少组件实例,一旦这个数字达到了上限,在新实例被创建之前,已缓存组件中最久没有被访问的实例会被销毁掉,而这里所运用到的缓存机制就是 LRU 算法

    LRU 缓存淘汰算法

    LRU( least recently used)根据数据的历史记录来淘汰数据,重点在于保护最近被访问/使用过的数据,淘汰现阶段最久未被访问的数据

    LRU的主体思想在于:如果数据最近被访问过,那么将来被访问的几率也更高

    fifo对比lru原理

    1. 新数据插入到链表尾部;

    2. 每当缓存命中(即缓存数据被访问),则将数据移到链表尾部

    3. 当链表满的时候,将链表头部的数据丢弃。

    实现LRU的数据结构

    经典的 LRU 一般都使用 hashMap + 双向链表。考虑可能需要频繁删除一个元素,并将这个元素的前一个节点指向下一个节点,所以使用双链接最合适。并且它是按照结点最近被使用的时间顺序来存储的。 如果一个结点被访问了, 我们有理由相信它在接下来的一段时间被访问的概率要大于其它结点。

    map.keys()

    不过既然已经在 js 里都已经使用 Map 了,何不直接取用现成的迭代器获取下一个结点的 key 值(keys().next( )

    // ./LRU.ts
    export class LRUCache {
    capacity: number; // 容量
    cache: Map; // 缓存
    constructor(capacity: number) {
      this.capacity = capacity;
      this.cache = new Map();
    }
    get(key: number): number {
      if (this.cache.has(key)) {
        let temp = this.cache.get(key) as number;
        //访问到的 key 若在缓存中,将其提前
        this.cache.delete(key);
        this.cache.set(key, temp);
        return temp;
      }
      return -1;
    }
    put(key: number, value: number): void {
      if (this.cache.has(key)) {
        this.cache.delete(key);
        //存在则删除,if 结束再提前
      } else if (this.cache.size >= this.capacity) {
        // 超过缓存长度,淘汰最近没使用的
        this.cache.delete(this.cache.keys().next().value);
        console.log(`refresh: key:${key} , value:${value}`)
      }
      this.cache.set(key, value);
    }
    toString(){
      console.log('capacity',this.capacity)
      console.table(this.cache)
    }
    }
    // ./index.ts
    import {LRUCache} from './lru'
    const list = new LRUCache(4)
    list.put(2,2)   // 2,剩余容量3
    list.put(3,3)   // 3,剩余容量2
    list.put(4,4)   // 4,剩余容量1
    list.put(5,5)   // 5,已满   从头至尾         2-3-4-5
    list.put(4,4)   // 入4,已存在 ——> 置队尾         2-3-5-4
    list.put(1,1)   // 入1,不存在 ——> 删除队首 插入1 3-5-4-1
    list.get(3)     // 获取3,刷新3——> 置队尾         5-4-1-3
    list.toString()
    // ./index.ts
    import {LRUCache} from './lru'
    const list = new LRUCache(4)

    list.put(2,2)   // 2,剩余容量3
    list.put(3,3)   // 3,剩余容量2
    list.put(4,4)   // 4,剩余容量1
    list.put(5,5)   // 5,已满   从头至尾 2-3-4-5
    list.put(4,4)   // 入4,已存在 ——> 置队尾 2-3-5-4
    list.put(1,1)   // 入1,不存在 ——> 删除队首 插入1 3-5-4-1
    list.get(3)     // 获取3,刷新3——> 置队尾 5-4-1-3
    list.toString()

    结果如下: lru打印结果.jpg

    vue 中 Keep-Alive

    原理

    1. 使用 LRU 缓存机制进行缓存,max 限制缓存表的最大容量

    2. 根据设定的 include/exclude(如果有)进行条件匹配,决定是否缓存。不匹配,直接返回组件实例

    3. 根据组件 ID 和 tag 生成缓存 Key ,并在缓存对象中查找是否已缓存过该组件实例。如果存在,直接取出缓存值并更新该 key 在 this.keys 中的位置(更新 key 的位置是实现 LRU 置换策略的关键)

    4. 获取节点名称,或者根据节点 cid 等信息拼出当前 组件名称

    5. 获取 keep-alive 包裹着的第一个子组件对象及其组件名

    源码分析

    初始化 keepAlive 组件
    const KeepAliveImpl: ComponentOptions = {
     name: `KeepAlive`,
     props: {
       include: [String, RegExp, Array],
       exclude: [String, RegExp, Array],
       max: [String, Number],
    },
     setup(props: KeepAliveProps, { slots }: SetupContext) {
       // 初始化数据
       const cache: Cache = new Map();
       const keys: Keys = new Set();
       let current: VNode | null = null;
       // 当 props 上的 include 或者 exclude 变化时移除缓存
       watch(
        () => [props.include, props.exclude],
        ([include, exclude]) => {
         include && pruneCache((name) => matches(include, name));
         exclude && pruneCache((name) => !matches(exclude, name));
        },
        { flush: "post", deep: true }
      );
       // 缓存组件的子树 subTree
       let pendingCacheKey: CacheKey | null = null;
       const cacheSubtree = () => {
         // fix #1621, the pendingCacheKey could be 0
         if (pendingCacheKey != null) {
           cache.set(pendingCacheKey, getInnerChild(instance.subTree));
        }
      };
       // KeepAlive 组件的设计,本质上就是空间换时间。
       // 在 KeepAlive 组件内部,
       // 当组件渲染挂载和更新前都会缓存组件的渲染子树 subTree
       onMounted(cacheSubtree);
       onUpdated(cacheSubtree);
       onBeforeUnmount(() => {
       // 卸载缓存表里的所有组件和其中的子树...
      }
       return ()=>{
         // 返回 keepAlive 实例
      }
    }
    }

    return ()=>{
     // 省略部分代码,以下是缓存逻辑
     pendingCacheKey = null
     const children = slots.default()
     let vnode = children[0]
     const comp = vnode.type as Component
     const name = getName(comp)
     const { include, exclude, max } = props
     // key 值是 KeepAlive 子节点创建时添加的,作为缓存节点的唯一标识
     const key = vnode.key == null ? comp : vnode.key
     // 通过 key 值获取缓存节点
     const cachedVNode = cache.get(key)
     if (cachedVNode) {
       // 缓存存在,则使用缓存装载数据
       vnode.el = cachedVNode.el
       vnode.component = cachedVNode.component
       if (vnode.transition) {
         // 递归更新子树上的 transition hooks
         setTransitionHooks(vnode, vnode.transition!)
      }
         // 阻止 vNode 节点作为新节点被挂载
         vnode.shapeFlag |= ShapeFlags.COMPONENT_KEPT_ALIVE
         // 刷新key的优先级
         keys.delete(key)
         keys.add(key)
    } else {
         keys.add(key)
         // 属性配置 max 值,删除最久不用的 key ,这很符合 LRU 的思想
         if (max && keys.size > parseInt(max as string, 10)) {
           pruneCacheEntry(keys.values().next().value)
        }
      }
       // 避免 vNode 被卸载
       vnode.shapeFlag |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
       current = vnode
       return vnode;
    }
    将组件移出缓存表
    // 遍历缓存表
    function pruneCache(filter?: (name: string) => boolean) {
     cache.forEach((vnode, key) => {
       const name = getComponentName(vnode.type as ConcreteComponent);
       if (name && (!filter || !filter(name))) {
         // !filter(name) 即 name 在 includes 或不在 excludes 中
         pruneCacheEntry(key);
      }
    });
    }
    // 依据 key 值从缓存表中移除对应组件
    function pruneCacheEntry(key: CacheKey) {
     const cached = cache.get(key) as VNode;
     if (!current || cached.type !== current.type) {
       /* 当前没有处在 activated 状态的组件
        * 或者当前处在 activated 组件不是要删除的 key 时
        * 卸载这个组件
      */
       unmount(cached); // unmount方法里同样包含了 resetShapeFlag
    } else if (current) {
       // 当前组件在未来应该不再被 keepAlive 缓存
       // 虽然仍在 keepAlive 的容量中但是需要刷新当前组件的优先级
       resetShapeFlag(current);
       // resetShapeFlag
    }
     cache.delete(key);
     keys.delete(key);
    }
    function resetShapeFlag(vnode: VNode) {
     let shapeFlag = vnode.shapeFlag; // shapeFlag 是 VNode 的标识
      // ... 清除组件的 shapeFlag
    }

    keep-alive案例

    本部分将使用 vue 3.x 的新特性来模拟 keep-alive 的具体应用场景

    在 index.vue 里我们引入了 CountUp 、timer 和 ColorRandom 三个带有状态的组件 在容量为 2 的 中包裹了一个动态组件

    // index.vue
    <script setup>
    import { ref } from "vue"
    import CountUp from '../components/CountUp.vue'
    import ColorRandom from '../components/ColorRandom.vue'
    import Timer from '../components/Timer.vue'
    const tabs = ref([    // 组件列表
    {
       title: "ColorPicker",
       comp: ColorRandom,
    },
    {
       title: "timer1",
       comp: Timer,
    },
    {
       title: "timer2",
       comp: Timer,
    },
    {
       title: "CountUp",
       comp: CountUp,
    },
    ])
    const currentTab = ref(tabs.value[0]) // tab 默认展示第一个组件
    const tabSwitch = (tab) => {
     currentTab.value = tab
    }
    script>
    <template>
     <div id="main-page">keep-alive demo belowdiv>
     <div class="tab-group">
       <button
       v-for="tab in tabs"
       :key="tab"
       :class="['tab-button', { active: currentTab === tab }]"
       @click="tabSwitch(tab)"
     >
       {{ tab.title }}
     button>
     div>
     <keep-alive max="2">
       
       <component
         v-if="currentTab"
         :is="currentTab.comp"
         :key="currentTab.title"
         :name="currentTab.title"
       />
     keep-alive>
    template>

    缓存状态

    缓存流程如下:

    缓存流程图

    可以看到被包裹在 keep-alive 的动态组件缓存了前一个组件的状态。

    通过观察 vue devtools 里节点的变化,可以看到此时 keepAlive 中包含了 ColorRandomTimer 两个组件,当前展示的组件会处在 activated 的状态,而其他被缓存的组件则处在 inactivated 的状态

    如果我们注释了两个 keep-alive 会发现不管怎么切换组件,都只会重新渲染,并不会保留前次的状态
    keepAlive-cache.gif

    移除组件

    移除流程如下:

    移除流程图

    为了验证组件是否在切换tab时能被成功卸载,在每个组件的 onUnmounted 中加上了 log

    onUnmounted(()=>{
     console.log(`${props.name} 组件被卸载`)
    })
    • 当缓存数据长度小于等于 max ,切换组件并不会卸载其他组件,就像上面在 vue devtools 里展示的一样,只会触发组件的 activateddeactivated 两个生命周期

    • 若此时缓存数据长度大于 max ,则会从缓存列表中删除优先级较低的,优先被淘汰的组件,对应的可以看到该组件 umounted 生命周期触发。

    性能优化

    使用 KeepAlive 后,被 KeepAlive 包裹的组件在经过第一次渲染后,的 vnode 以及 DOM 都会被缓存起来,然后再下一次再次渲染该组件的时候,直接从缓存中拿到对应的 vnode 和 DOM,然后渲染,并不需要再走一次组件初始化,render 和 patch 等一系列流程,减少了 script 的执行时间,性能更好。

    总结

    Vue 内部将 DOM 节点抽象成了一个个的 VNode 节点,keep-alive 组件的缓存也是基于 VNode 节点的而不是直接存储 DOM 结构。它将满足条件( include 与 exclude )的组件在 cache 对象中缓存起来,在需要重新渲染的时候再将 vnode 节点从 cache 对象中取出并渲染。

    具体缓存过程如下:

    1. 声明有序集合 keys 作为缓存容器,存入组件的唯一 key 值

    2. 在缓存容器 keys 中,越靠前的 key 值意味着被访问的越少也越优先被淘汰

    3. 渲染函数执行时,若命中缓存时,则从 keys 中删除当前命中的 key,并往 keys 末尾追加 key 值,刷新该 key 的优先级

    4. 未命中缓存时,则 keys 追加缓存数据 key 值,若此时缓存数据长度大于 max 最大值,则删除最旧的数据

    5. 当触发 beforeMount/update 生命周期,缓存当前 activated 组件的子树的数据


    参考

    作者:政采云前端团队
    来源:https://juejin.cn/post/7036483610920091656

    收起阅读 »

    统一路由,让小程序跳转更智能

    我们在小程序开发及运营过程中,不可避免的需要进行页面之间的跳转。如果使用小程序自带的路由功能来实现这个功能,是非常简单的,如: // 根据不同的场景选择 navigateTo、redirectTo、switchTab 等 wx.navigateTo({ u...
    继续阅读 »

    我们在小程序开发及运营过程中,不可避免的需要进行页面之间的跳转。如果使用小程序自带的路由功能来实现这个功能,是非常简单的,如:


    // 根据不同的场景选择 navigateTo、redirectTo、switchTab 等
    wx.navigateTo({
    url: "pages/somepage?id=1",
    success: function (res) {},
    });

    但这里面存在几个问题:



    • 需要代码里面写死或者运营人员维护小程序页面的长长的具体路径,这显然是很不友好的

    • 需要知道页面是否为 tabbar 页面(switchTab)

    • 如果某个页面在 tabbar 和非 tabbar 页面之间发生了变化,或路径因为重构、主包瘦身等各种原因发生变化,原来的代码就会报错导致无法运行

    • navigateBack 不支持传参


    为了解决以上问题,我们在项目中实现了一套基于命令别名(cmd)的统一路由跳转方式(以下称为统一路由),很好解决了遇到的实际问题,统一路由特点如下:



    • 页面别名声明使用注释方式,不侵入业务代码

    • 页面可以存在多个别名,方便新老版本页面的流量切换

    • 路由内自动判断是否 tabbar 页面,自行处理跳转及传参,业务代码无需关心

    • 支持纯 js api 的页面跳转及需要用户点击的任意类型跳转(如联系客服、打开小程序等等)

    • 对于页面栈中存在相同页面时,可以自动返回并根据参数是否相同决定是否需要刷新页面,可有效减少页面栈层级,规避小程序 10 层限制


    实现思路


    step1. 资源描述约定


    小程序内的跳转类操作存在以下几种



    1. js api 直接可以操作的内部页面间跳转(wx.navigateTo、wx.navigateBack、wx.redirectTo、wx.reLaunch、wx.switchTab)

    2. js api 直接可以操作的打开微信原生功能的跳转(扫码、拨打电话等)

    3. 需要借助点击操作的跳转(如打开小程序及客服等需要 open-type 配合的场景 )


    针对这三类操作,我们使用常见的 URL(统一资源定位系统)方式描述不同的待跳转资源



    1. 内部页面


    https://host?cmd=${pagename}&param1=a  // 打开普通页面并传参,标准的H5容器也算在普通页面内


    1. 微信原生 API


    https://host?cmd=nativeAPI&API=makePhoneCall&phoneNumber=123456  // 拨打电话
    https://host?cmd=nativeAPI&API=scanCode&callback=scanCallback // 扫码并执行回调


    1. 需要借助按钮 open-type 的微信原生能力


    https://host?cmd=nativeButtonAPI&openType=contact  // 在线客服


    1. 打开另一个小程序


    https://host?cmd=miniProgram&appId=wx637bb****&path=pages/order/index&version=trial&uid=${uid} 


    小程序跳转需要携带更多的参数,所以做了cmd的区分,这里实际会解析成 nativeButtonAPI 运行



    step2. 在页面内定义需要的数据


    在每个页面的顶部添加注释,注意 cmd 不能重复,支持多个 cmd。为了方便后续解析,我们的注释大体上遵循 JSDoc 注释规范


    // pages/detail/index.tsx

    /**
    * @cmd detail, newdetail
    * @description 详情
    * @param skuid {number} skuid
    */

    step3. 在编译阶段扫描并生成配置文件


    根据入口文件的页面定义,匹配出需要的注释部分,使用 doctrine 解析需要的数据,解析后的数据如下:


    // config/router.config.ts
    export default {
    index: {
    description: "首页", // 页面描述
    path: "/pages/index/index", // 真实路径
    isTabbar: true, // 是否tabbar页面
    ensureLogin: false, // 是否需要强制登录
    },
    detail: {
    description: "详情",
    path: "/pages/detail/index",
    isTabbar: false,
    ensureLogin: true,
    },
    };

    这里顺便可以使用 param 等生成详细的页面名称及入参文档,提供给其他研发或运营同学使用。


    step4. 资源描述解析为标准数据


    根据上面的资源描述约定及扫描得到的配置文件,我们可以将其转换为方便在小程序内解析的数据定义,基本格式如下


    {
    origin: 'https://host?cmd=detail&skuid=1', // 原始数据
    parsed: {
    type: 'PAGE', // 类型,PAGE,NATIVE_API,NATIVE_BUTTON_API,UNKNOW
    data: {
    path: 'pages/detail/index', // 实际的页面路径,如果type是PAGE则会解析出此字段
    action: undefined, // 动作,scanCode,makePhoneCall,openType,miniprogram ……。如果type是NATIVE_API,NATIVE_BUTTON_API,则会解析出此字段
    params: {
    skuid: '1' // 需要携带的参数
    }
    }
    }
    }

    step5. 根据标准数据执行对应逻辑


    由于我们的项目使用的是 Taro 框架,以下伪代码都是以 Taro 为例。


    // utils/router.ts

    // 用于解析原始链接为标准数据
    const parseURL = (origin) => {
    // balabala,一顿操作格式化成上文的数据
    const data = {
    ...
    };
    return data;
    };

    // 执行除 NATIVE_BUTTON_API 之外的跳转
    const routeURL = (origin) => {
    const parsedData = parseURL(origin)
    const {parsed: {type, data}} = parsedData

    switch(type){
    case 'PAGE':
    ...
    break;
    case 'NATIVE_API':
    ...
    break;
    case 'UNKNOW':
    ...
    break;
    }
    };

    export default {
    parseURL,
    routeURL,
    };

    对于需要点击的类型,我们需要借助 UI 组件实现


    // components/router.tsx

    import router from "/utils/router";
    import { Button } from "@tarojs/components";
    import Taro, { Component, eventCenter } from "@tarojs/taro";

    export default class Router extends Component {
    componentWillMount() {
    const { path } = this.props;
    const data = router.parseURL(path);
    const { parsed, origin } = data;
    const openType =
    (parsed &&
    parsed.data &&
    parsed.data.params &&
    parsed.data.params.openType) ||
    false;
    this.setState({
    parsed,
    openType,
    });
    }

    // 点击事件
    async handleClick(parsed, origin) {
    // 点击执行动作
    let {
    type,
    data: { action, params },
    } = parsed;
    if (!type) {
    return;
    }

    // 内部页面
    if (["PAGE", "CMD_UNKNOW"].includes(type)) {
    console.log(`CMD_NATIVE_PAGE 参数:`, origin, options);
    router.routeURL(origin);
    return;
    }

    // 拨打电话、扫码等原生API
    if (["NATIVE_API"].includes(type) && action) {
    if (action === "makePhoneCall") {
    let { phoneNumber = "" } = params;
    if (!phoneNumber || phoneNumber.replace(/\s/g, "") == "") {
    Taro.showToast({
    icon: "none",
    title: "未查询到号码,无法呼叫哦~",
    });
    return;
    }
    }

    let res = await Taro[action]({ ...params });

    // 扫码事件,需要在扫码完成后发送全局广播,业务内自行处理
    if (action === "scanCode" && params.callback) {
    let eventName = `${params.callback}_event`;
    eventCenter.trigger(eventName, res);
    }
    }

    // 打开小程序
    if (
    ["NATIVE_BUTTON_API"].includes(type) &&
    ["miniprogram"].includes(action)
    ) {
    await Taro.navigateToMiniProgram({
    ...params,
    });
    }
    }

    render() {
    const { parsed, openType, origin } = this.state;

    return (
    <Button
    onClick={this.handleClick.bind(this, parsed, origin)}
    hoverClass="none"
    openType={openType}
    >
    {this.props.children}
    </Button>
    );
    }
    }

    在具体业务中使用


    // pages/index/index.tsx
    import router from "/utils/router";
    import Router from "/components/router";

    // js方式直接跳转
    router.routeURL('https://host?cmd=detail&skuid=1')

    // UI组件方式
    ...
    render(){
    return <Router path='https://host?cmd=detail&skuid=1'></Router>
    }
    ...

    当然这里面可以附加你自己需要的功能,比如:增加跳转方式控制、数据处理、埋点、加锁防连续点击,相对来说并不复杂。甚至你还可以顺手实现一下上面提到的 navigateBack 传参。


    结语


    上文的思考及实现过程比较简单,纯属抛砖引玉,欢迎大家交流互动。


    作者:胖纳特
    链接:https://juejin.cn/post/6930899487250448398

    收起阅读 »

    如何美化checkbox

    前言 对于前端开发人员,checkbox应该是经常见到的东西。利用checkbox的checked属性,我们可以做出很多精彩的效果,之前还用checkbox来做动画暂停。前几天还看到外国大佬使用 checkbok做游戏:http://www.bryanbrau...
    继续阅读 »

    前言


    对于前端开发人员,checkbox应该是经常见到的东西。利用checkbox的checked属性,我们可以做出很多精彩的效果,之前还用checkbox来做动画暂停。前几天还看到外国大佬使用 checkbok做游戏:http://www.bryanbraun.com/2021/09/21/… ,真的是佩服的五体投地,不过对于我这种菜鸡选手,还是只能实现一些简单的东西。对于下面的这个switch按钮,大家应该非常熟悉了,同样的在这个效果上还衍生出了各种华丽花哨的效果,例如暗黑模式的切换。一生万,掌握了一,万!还不是手到擒来。


    image-20211128233027149


    推荐大家看看codepen上的这个仓库:文章封面的效果,也是从这里录制的!
    tql

    codepen.io/oliviale/pe…


    image-20211128235343983


    标签


    这里使用for将label和input捆绑


    <input type="checkbox" id="toggle" />
    <label for="toggle"></label>

    同时设置input不可见


    input {
    display: none;
    }

    美化label


    遇到checkbox的美化问题,基本上都是考虑用美化labl替代美化input。


    设置背景颜色,宽高,以及圆角


    .switch {
      display: inline-block;
      display:relative;
      width: 40px;
      height: 20px;
      background-color: rgba(0, 0, 0, 0.25);
      border-radius: 20px;
    }


    最终的效果如下:


    image-20211128233616100


    切换的圆


    在label上会有一个圆,一开始是在左边的,效果如下,其实这个只需要利用伪元素+positon定位,就可以实现了。


    image-20211128233732168


    这是postion:absolute,同时将位置定位在top1px,left1px。同时设置圆角。


          .switch:after {
          content: "";
          position: absolute;
          width: 18px;
          height: 18px;
          border-radius: 18px;
          background-color: white;
          top: 1px;
          left: 1px;
          transition: all 0.3s;
        }

    checked+小球右移动


    这里点击之后圆会跑到右边,这里有两种实现方案


    1.仍然通过定位


    当checkbox处于checked状态,会设置top,left,bottom,right。这里将top,left设置为auto是必须的,这种的好处就是,不需要考虑label的宽度。


      input[type="checkbox"]:checked + .switch:after {
          top: auto;
          left: auto;
          bottom: 1px ;
          right: 1px ;
        }

    当然知道label的宽度可以直接,设置top和left


    top: 1px;
    left: 21px;

    2.translateX


    *transform: translateX(20px)*

    美化切换后的label


    加上背景色


    input[type="checkbox"]:checked + .switch {
    background-color: #7983ff;
    }

    效果:


    switch


    后记


    看上去本文是一篇介绍一个checkbox美化的效果,其实是一篇告诉你如何美化checkbox的文章,最终的思想就是依赖for的捆绑效果,美化label来达到最终的效果。


    作者:半夏的故事
    链接:https://juejin.cn/post/7035650204829220877

    收起阅读 »

    Metaverse 已经到来:5 家公司正在构建我们的虚拟现实未来

    如果你相信 Facebook,未来就是一个虚拟现实的“元宇宙”。这家上个月更名为 Meta的科技巨头计划今年投资100 亿美元来开发支持增强现实和虚拟现实的产品——机械手、高科技 VR 眼镜和复杂的软件应用程序,仅举几例。分析师预计该公司至少要花费 500 亿...
    继续阅读 »

    如果你相信 Facebook,未来就是一个虚拟现实的“元宇宙”

    这家上个月更名为 Meta的科技巨头计划今年投资100 亿美元来开发支持增强现实虚拟现实的产品——机械手高科技 VR 眼镜和复杂的软件应用程序,仅举几例。分析师预计该公司至少要花费 500 亿美元来实现其对虚拟现实未来的承诺。

    但 Meta 远非唯一的玩家。事实上,六家其他公司已经在构建将成为下一代虚拟交互的硬件和软件——华尔街认为这是一个价值 1 万亿美元的市场。这些公司包括谷歌、微软、苹果、Valve 和其他开发工作和通信产品的公司。随着投资者涌入市场,规模较小的初创公司可能会加入他们的行列。

    “元宇宙是真实的,华尔街正在寻找赢家,”韦德布什分析师丹艾夫斯在一份报告中说。

    在 Facebook 试图在元领域打上烙印时,这些公司的产品将不得不与之抗衡。

    谷歌

    Google Cardboard 可能是历史上最成功的 VR 项目。2014 年,当时世界上最大的科技公司要求数百万人用一块硬纸板将智能手机绑在脸上。谷歌表示,它出货了“数千万”可折叠耳机,谷歌 Cardboard应用程序的下载量超过 1.6 亿次。这不是最高分辨率或高科技的体验,但该策略帮助向数百万学生有抱负的开发人员介绍了虚拟现实。

    它还帮助谷歌摆脱了之前的增强现实实验Glass今天,增强现实眼镜作为企业业务的工具进行销售,但当它推出时,谷歌的期望值很高。字面意思是:谷歌创始人谢尔盖·布林 (Sergey Brin) 从飞机上跳下时宣布了 1,500 美元的产品。

    玻璃本质上是智能手机的内脏,在非处方眼镜框架上安装了一个小型摄像头。该项目失败了,但在催生了无数模因之前就失败了。 

    微软

    微软于 2015 年发布Hololens混合现实眼镜。一年后,微软并没有用营销炒作充斥消费者市场,而是悄悄推出了 Hololens 作为工业制造工具,面向选定的企业集团。价值 3,000 美元的商业套件附带专业版 Windows,具有额外的安全功能和软件以帮助应用程序开发。第二次迭代于 2019 年首次亮相,价格稍贵,但拥有更好的相机和镜头卡口,可实现更精确的操作,并提供更广泛的软件功能,包括工业应用。 

    目前Hololens用户包括像肯沃斯,三得利和丰田,它使用耳机,以加快培养和汽车修理重量级人物,根据微软

    苹果

    如果你相信传言,苹果一直在释放的风口浪尖AR眼镜多年这家 iPhone 制造商于 2017 年在 iOS 11 中发布了ARKit,这是为 Apple 设备创建增强现实应用程序的开发者框架。 据科技网站The Information报道,Apple 在 2019 年举行了一次 1000 人的会议,讨论了 iPhone 上的 AR 和两个潜力未来的产品,N421 智能眼镜和 N301 VR 耳机分析师现在推测,苹果正准备在 2022 年及以后发布 AR 产品。   

    阀门

    Valve 的Index耳机可以说是市场上最强大的消费虚拟现实产品。高分辨率屏幕流畅,控制器在虚拟现实和游戏环境中提供无与伦比的控制。该索引还与 Value 的Steam视频游戏市场集成,这意味着该设备已经堆满了兼容的内容。

    它也很贵而且很笨拙。完整的 Index VR 套件的价格接近 1,000 美元,要正常运行,耳机需要多条电缆和传感器。Valve 继续创新和试验沉浸式虚拟现实耳机。分析师预计,这家总部位于贝尔维尤的游戏公司将很快发布一款独立耳机,与 Facebook 的Oculus Quest 2展开竞争

    魔法飞跃

    尽管虚拟现实的想法部分受到科幻小说的启发,但 Big Tech 对 AR 和 VR 未来的现代愿景直接受到 Magic Leap 的启发。该公司成立于 2010 年,2014 年从谷歌和芯片制造商高通等公司筹集了超过 5 亿美元 2015 年,该公司发布了一段令人惊叹的视频,旨在展示该产品的技术。但是怀疑论者质疑这项技术,最终的产品遭到了抨击

    最初的 Magic Leap 耳机是在设计和广告等创意协作行业销售的。

    Magic Leap于 2018 年推出了一款精致的 AR 设备,筹集了更多资金,并计划在 2022 年初发布Magic Leap 2。该公司还计划瞄准国防、医疗保健和工业制造。

    收起阅读 »

    跨域问题及常见解决方法

    1.出现跨域问题是因为浏览器的同源策列限制,下面是MDN文档对浏览器同源策略的描述,简单来说就是:同源策略会阻止一个域的javascript脚本和另外一个域的内容进行交互。所谓同源(即指在同一个域)就是两个页面具有相同的协议(protocol),主机(host...
    继续阅读 »



    1.出现跨域问题是因为浏览器的同源策列限制,下面是MDN文档对浏览器同源策略的描述,简单来说就是:

    同源策略会阻止一个域的javascript脚本和另外一个域的内容进行交互。所谓同源(即指在同一个域)就是两个页面具有相同的协议(protocol),主机(host)和端口号(port)

    浏览器的同源策略

    同源策略是一个重要的安全策略,它用于限制一个origin的文档或者它加载的脚本如何能与另一个源的资源进行交互。它能帮助阻隔恶意文档,减少可能被攻击的媒介。

    同源的定义

    如果两个 URL 的 protocol、port (en-US) (如果有指定的话)和 host 都相同的话,则这两个 URL 是同源。这个方案也被称为“协议/主机/端口元组”,或者直接是 “元组”。(“元组” 是指一组项目构成的整体,双重/三重/四重/五重/等的通用形式)。

    2.四种常见解决跨域的方法:

    一,CORS:

    跨域资源共享,它允许浏览器向非同源服务器,发出XMLHttpRequest请求。它对一般请求和非一般请求的处理方式不同: 1、一般跨域请求(对服务器没有要求):只需服务器端设置Access-Control-Allow-Origin 2、非一般跨域请求(比如要请求时要携带cookie):前后端都需要设置。

    一般跨域请求服务器设置代码: (1)Node.JS

    const http = require('http');
    const server = http.createServer();
    const qs = require('querystring');

    server.on('request', function(req, res) {
       var postData = '';
       // 数据块接收中
       req.addListener('data', function(chunk) {
           postData += chunk;
      });
       // 数据接收完毕
       req.addListener('end', function() {
           postData = qs.parse(postData);
           // 跨域后台设置
           res.writeHead(200, {
               'Access-Control-Allow-Credentials': 'true',     // 后端允许发送Cookie
               'Access-Control-Allow-Origin': 'http://www.example.com',    // 允许访问的域(协议+域名+端口)  
          });
           res.end(JSON.stringify(postData));
      });
    });
    server.listen('8080');
    console.log('running at port 8080...');

    复制代码

    (2)PHP

    <?php
    header("Access-Control-Allow-Origin:*");
    复制代码

    如果要发送Cookie,Access-Control-Allow-Origin就不能设为星号,必须指定明确的、与请求网页一致的域名。同时,Cookie依然遵循同源政策,只有用服务器域名设置的Cookie才会上传,其他域名的Cookie并不会上传,且(跨源)原网页代码中的document.cookie也无法读取服务器域名下的Cookie。

    前端请求携带cookie代码:

    (1)原生JavaScript

    const xhr = new XMLHttpRequest(); 
    // 前端设置是否带cookie
    xhr.withCredentials = true;
    };
    复制代码

    (2)axios

    axios.defaults.withCredentials = true
    复制代码

    二,JSONP

    JSONP 只支持get请求,不支持post请求。 核心思想:网页通过添加一个<scriot>标签,向服务器请求 JSON 数据,服务器收到请求后,将数据放在一个指定名字的回调函数的参数位置传回来。

    原生JavaScript代码:

    <script src="http://example.php?callback=getData"></script>
    // 向服务器发出请求,请求参数callback是下面定义的函数名字

    // 处理服务器返回回调函数的数据
    <script type="text/javascript">
       function getData(res)
      {
           console.log(res.data)
      }
    </script>
    复制代码

    三,设置document.domain

    因为浏览器是通过document.domain属性来检查两个页面是否同源,因此只要通过设置相同的document.domain,两个页面就可以共享Cookie(此方案仅限主域相同,子域不同的跨域应用场景。)

    // 两个页面都设置
    document.domain = 'test.com';
    复制代码

    四,跨文档通信 API:window.postMessage()

    调用postMessage方法实现父窗口向子窗口发消息(子窗口同样可以通过该方法发送消息给父窗口)

    var openWindow = window.open('http://test2.com', 'title');

    // 父窗口向子窗口发消息(第一个参数代表发送的内容,第二个参数代表接收消息窗口的url)
    openWindow.postMessage('Nice to meet you!', 'http://test2.com');
    //调用message事件,监听对方发送的消息

    // 监听 message 消息
    window.addEventListener('message', function (e) {
     console.log(e.source); // e.source 发送消息的窗口
     console.log(e.origin); // e.origin 消息发向的网址
     console.log(e.data);   // e.data   发送的消息
    },false);


    作者:玩具大兵
    来源:https://juejin.cn/post/7035562059152490526

    收起阅读 »

    TypeScript 原始类型、函数、接口、类、泛型 基础总结

    原始数据类型原始数据类型包括:BooleanStringNumberNullundefined类型声明是TS非常重要的一个特点,通过类型声明可以指定TS中变量、参数、形参的类型。Boolean 类型let boolean: boolean = truebool...
    继续阅读 »



    原始数据类型

    原始数据类型包括:

    • Boolean

    • String

    • Number

    • Null

    • undefined

    类型声明是TS非常重要的一个特点,通过类型声明可以指定TS中变量、参数、形参的类型。

    • Boolean 类型

      let boolean: boolean = true
      boolean = false
      boolean = null
      // bollean = 123 报错不可以将数字 123 赋值给 boolean类型的变量
    • Number 类型

      //ES6 Number 类型 新增支持2进制和8进制
      let num: number = 123
      num = 0b1111
    • String 类型

      let str1: string = 'hello TS'
      let sre2: string = `模板字符串也支持使用 ${str1}`]
    • Null 和 Undefined

      let n: null = null
      let u: undefined = undefined
      n = undefined
      u = null
      // undefined 和 null 是所有类型的子类型 所以可以赋值给number类型的变量
      let num: number = 123
      num = undefined
      num = null

    any 类型

    any 表示队变量没有任何显示,编译器失去了对 TS 的检测功能与 JS 无异(不建议使用)。

    let notSure: any = 4
    // any类型可以随意赋值
    notSure = `任意模板字符串`
    notSure = true
    notSure = null

    // 当 notSure 为any 类型时,在any类型上访问任何属性和调用方法都是允许的, 很有可能出现错误
    notSure.name // 现在调用name属性是允许的,但很明显我们定义的notSure没有name这个属性,下面的调用sayName方法也是如此
    notSure.sayName()

    array 类型

    // 数组类型,可以指定数组的类型和使用数组的方法和属性
    let arrOfNumbers: number[] = [1, 2, 3]
    console.log(arrOfNumbers.length);

    arrOfNumbers.push(4)

    tuple 元组类型

    // 元组类型  元组就是固定长度,类型的数组  
    // 类型和长度必须一致
    let u: [string, number] = ['12', 12]
    // let U: [string, number] = ['12', 12, true]   报错信息为:不能将类型“[string, number, boolean]”分配给类型“[string, number]”。源具有 3 个元素,但目标仅允许 2 个。

    // 也可以使用数组的方法,如下所示push一个值给元组u
    u.push(33)

    Interface 接口

    • 对对象的形状(shape)进行描述

    • Duck Typing(鸭子类型)

    interface Person {
       // readonly id 表示只读属性的id不可以修改
       readonly id: number;
       name: string;
       age: number;
       // weight? 表示可选属性,可以选用也可以不选用
       weight?: number
    }

    let host: Person = {
       id: 1,
       name: 'host',
       age: 20,
       weight: 70
    }

    //host.id = 2 报错 提示信息为:无法分配到 "id" ,因为它是只读属性

    function 函数类型

    // 方式一:函数声明的写法   z 为可选参数 ,
    function add1 (x: number, y: number, z?: number): number {
       if (typeof z === 'number') {
           return x + y + z
      } else {
           return x + y
      }
    }
    // 需要注意的是:可选参数必须置于所有必选参数之后,否则会报错

    add1(1, 2, 3)

    // 方式二:函数表达式
    const add2 = (x: number, y: number, z?: number): number => {
       if (typeof z === 'number') {
           return x + y + z
      } else {
           return x + y
      }
    }


    // 使用interface接口 描述函数类型
    interface ISum {
      (x: number, y: number, z?: number): number
    }
    let add3: ISum = add1

    值的注意的是:可选参数必须置于所有必选参数之后,否则会报错,如下图展示的错误案例所示:·

    image-20211120152558241

    类型推论

    当定义变量时没有指定类型,编译器会自动推论第一次赋的值为默认类型

    let s = 'str'
    // s = 12 本句将会报错,提示为:不能将类型“number”分配给类型“string”

    联合类型

    使用| 分隔可选类型

    let StringOrNumber: string | number
    StringOrNumber = 123
    StringOrNumber = '111'

    类型断言

    使用 as 关键字进行类型断言

    function getLength (rod: number | string): number {
       const str = rod as string
       //这里我们可以用 as 关键字,告诉typescript 编译器,你没法判断我的代码,但是我本人很清楚,这里我就把它看作是一个 string,你可以给他用 string 的方法。
       if (str.length) {
           return str.length
      } else {
           const num = rod as number
           return num.toString().length
      }
    }

    类型守卫

    // 4.类型守卫 type guard     typescript 在不同的条件分支里面,智能的缩小了范围
    function getLength2 (rod: number | string): number {
       if (typeof rod === 'string') {
           return rod.length
      } else {
           // else 里面的rod 会自动默认为number类型
           return rod.toString().length
           
      }
    }

    Class 类

    面向对象编程的三大特点:

    • 封装(Encapsulation):将对数据的操作细节隐藏起来,只暴露对外的接口。外界调用端不需要(也不可能)知道细节,就能通过对外提供的接口来访问该对象,

    • 继承(Inheritance):子类继承父类,子类除了拥有父类的所有特性外,还有一些更具体的特性。

    • 多态(Polymorphism):由继承而产生了相关的不同的类,对同一个方法可以有不同的响应。

    话不多少,看代码:

    class Animal {
       readonly name: string
       constructor(name: string) {
           this.name = name
           console.log(this.run())
      }
       // private run ():私有的   protected run () 受保护的
       run () {
           return `${this.name} is running`
      }

    }
    const animal = new Animal('elephant')
    // console.log(animal.name)
    animal.run() //elephant is running

    // 继承
    class Dog extends Animal {
       age: number
       constructor(name, age) {
           super(name)
           console.log(this.name)
           this.age = age
      }
       bark () {
           console.log(`这只在叫的狗狗叫${this.name},它今年${this.age}岁了`)
      }
    }
    const dog = new Dog('旺财', 5)
    dog.run() // 旺财 is running
    dog.bark() // 这只在叫的狗狗叫旺财,它今年5岁了


    // 多态
    class Cat extends Animal {
       static catAge = 2

       constructor(name) {
           super(name)
           console.log(this.name) // 布丁
      }
       run () {
           return 'Meow,' + super.run()
      }
    }
    const cat = new Cat('布丁')
    console.log(cat.run()) // Meow,布丁 is running
    console.log(Cat.catAge) // 2

    class中还提供了readonly关键字,readonly为只读属性,在调用的时候不能修改。如下所示:

    image-20211124155826105

    类成员修饰符

    • public修饰的属性或方法是公有的,可以在任何地方被访问到,默认所有的属性和方法都是public的。

    • private修饰的属性或方法是私有的,不能在声明它的类的外部访问。

      上述示例代码中,在父类Animalrun 方法身上加上private修饰符之后就会产生如下图的报错信息:

      image-20211124154129377

    • protected 修饰的属性或方法是受保护的,它和private类似,区别在于它在子类中也是可以访问的。

    上述示例代码中,在父类Animalrun 方法身上加上protected修饰符之后就会产生如下图的报错信息:

    image-20211124153823828

    接口和类

    类可以使用 implements来实现接口。

    // interface可以用来抽象验证类的方法和方法
    interface Person {
       Speak (trigger: boolean): void;
    }
    interface Teenagers {
       Young (sge: number): void
    }
    // 接口之间的继承
    interface PersonAndTeenagers extends Teenagers {
       Speak (trigger: boolean): void;
    }

    // implements 实现接口
    class Boy implements Person {
       Speak (mouth: boolean) { }
    }

    // class Girl implements Person, Teenagers 和 class Girl implements PersonAndTeenagers 作用相同
    class Girl implements PersonAndTeenagers {
       Speak (mouth: boolean) { }
       Young (sge: number) { }
    }

    enum枚举

    枚举成员会被赋值为从 0 开始递增的数字,同时也会对枚举值到枚举名进行反向映射:

    enum Color {
       red = 'red',
       blue = 'blue',
       yellow = 'yellow',
       green = 'green'
    }
    // 常量枚举
    const enum Color {
       red = 'red',
       blue = 'blue',
       yellow = 'yellow',
       green = 'green'
    }
    console.log(Color.red) // 0
    // 反向映射
    console.log(Color[0]) // red

    const value = 'red'
    if (value === Color.red) {
       console.log('Go Go Go ')
    • 常量枚举经过编译后形成的js文件如下:

    image-20211124182628644

    • 非常量枚举经过编译器编译之后的js文件如下:

    image-20211124182633413

    Generics 泛型

    泛型(Generics)是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性。

    • 约束泛型

    • 类与泛型

    • 接口与泛

    示例代码如下:

    function echo (arg) {
       return arg
    }

    let result1 = echo(123) // 参数传递123后result1 的类型为any

    // 泛型
    function echo2<T> (arg: T): T {
       return arg
    }
    let result2 = echo2(123)  // 加上泛型之后 参数传递 123后result2的类型为number

    function swap<T, U> (tuple: [T, U]): [U, T] {
       return [tuple[1], tuple[0]]
    }
    console.log(swap(['hero', 123]))//[ 123, 'hero' ]


    // 约束泛型
    interface IWithLength {
       length: number
    }

    function echoWithLength<T extends IWithLength> (arg: T): T {
       console.log(arg.length)
       return arg
    }

    const str = echoWithLength('123')
    const obj = echoWithLength({ length: 3, name: 'Tom' })
    const arr = echoWithLength([1, 2, 3, 4])


    // 类与泛型
    class Queue<T> {
       private data = []
       push (item: T) {
           return this.data.push(item)
      }
       pop (): T {
           return this.data.shift()
      }
    }
    const queue = new Queue<number>()

    queue.push(1)
    console.log(queue.pop().toFixed())// 1


    // 接口与泛型
    interface KeyPair<T, U> {
       key: T
       value: U
    }
    let kp1: KeyPair<string, number> = { key: 'str', value: 123 }
    let kp2: KeyPair<number, string> = { key: 123, value: 'str' }
    let arr2: Array<string> = ['1', '2'] // 使用 Array<string> 等价于 interface Array<T>

    类型别名 type-alias

    类型别名,就是给类型起一个别名,让它可以更方便的被重用。

    let sum: (x: number, y: string) => number
    const result1 = sum(1, '2')
    // 将(x: number, y: string) => number类型取一个别名 为 PlusType
    type PlusType = (x: number, y: string) => number
    let sum2: PlusType
    const result2 = sum2(2, '2')

    type StrOrNum = string | number
    let result3: StrOrNum = 123
    result3 = '123'

    字面量

    let Name: 'name' = 'name'
    // Name = '123' //报错信息:不能将类型“"123"”分配给类型“"name"”
    let age: 19 = 19

    type Directions = 'Up' | 'Down' | 'Left' | 'Right'
    let up: Directions = 'Up'

    交叉类型

    // 交叉类型  使用 ‘&’ 符号进行类型的扩展
    interface IName {
       name: string
    }

    type IPerson = IName & { age: number }
    let person: IPerson = { name: 'Tom', age: 19 }

    内置类型

    • 全局对象

    // global objects 全局对象
    const a: Array<string> = ['123', '456']
    const time = new Date()
    time.getTime()
    const reg = /abc/ // 此时reg为RegExp类型
    reg.test('abc')
    • build-in object 内置对象

      Math.pow(2, 2) //返回 2 的 2次幂。
      console.log(Math.pow(2, 2)) // 4
    • DOM and BOM

      // document 对象,返回的是一个 HTMLElement
      let body: HTMLElement = document.body
      // document 上面的query 方法,返回的是一个 nodeList 类型
      let allLis = document.querySelectorAll('li')

      //当然添加事件也是很重要的一部分,document 上面有 addEventListener 方法,注意这个回调函数,因为类型推断,这里面的 e 事件对象也自动获得了类型,这里是个 mouseEvent 类型,因为点击是一个鼠标事件,现在我们可以方便的使用 e 上面的方法和属性。
      document.addEventListener('click', (e) => {
         e.preventDefault()
      })
    • utility types实用类型

      interface IPerson2 {
         name: string
         age: number
      }
      let viking: IPerson2 = { name: 'viking', age: 20 }
      // partial,它可以把传入的类型都变成可选
      type IPartial = Partial<IPerson>
      let viking2: IPartial = {} // Partial 将IPerson 中的类型变成了可选类型 所以 viking2 可以等于一个空对象

      // Omit,它返回的类型可以忽略传入类型的某个属性
      type IOmit = Omit<IPerson2, 'name'> // 忽略name属性
      let viking3: IOmit = { age: 20 }

    如果加上name属性将会报错:不能将类型“{ age: number; name: string; }”分配给类型“IOmit”。对象文字可以只指定已知属性,并且“name”不在类型“IOmit”中。


    作者:不一213
    来源:https://juejin.cn/post/7035563509882552334

    收起阅读 »

    神奇的交叉观察器 - IntersectionObserver

    1. 背景网页开发时,不管是在移动端,还是PC端,都有个很重要的概念,叫做动态懒加载,适用于一些图片资源(或者数据)特别多的场景中,这个时候,我们常常需要了解某个元素是否进入了“视口”(viewport),即用户能不能看到它。 传统的实现方法是,监听到scro...
    继续阅读 »

    1. 背景

    网页开发时,不管是在移动端,还是PC端,都有个很重要的概念,叫做动态懒加载,适用于一些图片资源(或者数据)特别多的场景中,这个时候,我们常常需要了解某个元素是否进入了“视口”(viewport),即用户能不能看到它。


    传统的实现方法是,监听到scroll事件或者使用setInterval来判断,调用目标元素的getBoundingClientRect()方法,得到它对应于视口左上角的坐标,再判断是否在视口之内。这种方法的缺点是,由于scroll事件触发频率高,计算量很大,如果不做防抖节流的话,很容易造成性能问题,而setInterval由于其有间歇期,也会出现体验问题。


    所以在几年前,Chrome率先提供了一个新的API,就是IntersectionObserver,它可以用来自动监听元素是否进入了设备的可视区域之内,而不需要频繁的计算来做这个判断。由于可见(visible)的本质是,目标元素与视口产生一个交叉区,所以这个 API 叫做”交叉观察器”。


    2. 兼容性

    由于这个api问世已经很多年了,所以对浏览器的支持性还是不错的,完全可以上生产环境,点击这里可以看看当前浏览器对于IntersectionObserver的支持性:


    111111.png

    3. 用法

    API的调用非常简单:


    const io = new IntersectionObserver(callback, option);

    上面代码中,IntersectionObserver 是浏览器原生提供的构造函数,接受两个参数:



    • callback:可见性发现变化时的回调函数
    • option:配置对象(可选)。

    构造函数的返回值是一个观察器实例。实例一共有4个方法:



    • observe:开始监听特定元素
    • unobserve:停止监听特定元素
    • disconnect:关闭监听工作
    • takeRecords:返回所有观察目标的对象数组

    3.1 observe

    该方法需要接收一个target参数,值是Element类型,用来指定被监听的目标元素


    // 获取元素
    const target = document.getElementById("dom");

    // 开始观察
    io.observe(target);

    3.2 unobserve

    该方法需要接收一个target参数,值是Element类型,用来指定停止监听的目标元素


    // 获取元素
    const target = document.getElementById("dom");

    // 停止观察
    io.unobserve(target);

    3.3 disconnect

    该方法不需要接收参数,用来关闭观察器


    // 关闭观察器
    io.disconnect();

    3.4 takeRecords

    该方法不需要接收参数,返回所有被观察的对象,返回值是一个数组


    // 获取被观察元素
    const observerList = io.takeRecords();

    注意:

    observe方法的参数是一个 DOM 节点,如果需要观察多个节点,就要多次调用这个方法:


    // 开始观察多个元素
    io.observe(domA);
    io.observe(domB);
    io.observe(domC);

    4. callback 参数

    目标元素的可见性变化时,就会调用观察器的回调函数callback


    callback一般会触发两次。一次是目标元素刚刚进入视口,另一次是完全离开视口。


    const io = new IntersectionObserver((changes, observer) => {
    console.log(changes);
    console.log(observer);
    });

    上面代码中,callback函数的参数接收两个参数changesobserver



    • changes:这是一个数组,每个成员都是一个被观察对象。举例来说,如果同时有两个被观察的对象的可见性发生变化,那么changes数组里面就会打印出两个元素,如果只观察一个元素,我们打印changes[0]就能获取到被观察对象
    • observer: 这是一个对象,返回我们在实例中传入的第二个参数option(如果没传,则返回默认值)

    5. IntersectionObserverEntry 对象

    上面提到的changes数组中的每一项都是一个IntersectionObserverEntry 对象(下文简称io对象),对象提供目标元素的信息,一共有八个属性,我们打印这个对象:


    // 创建实例
    const io = new IntersectionObserver(changes => {
    changes.forEach(change => {
    console.log(change);
    });
    });

    // 获取元素
    const target = document.getElementById("dom");

    // 开始监听
    io.observe(target);

    运行上面代码,并且改变dom的可见性,这时控制台可以看到一个对象:


    555.png

    每个属性的含义如下:



    • boundingClientRect:目标元素的矩形区域的信息
    • intersectionRatio:目标元素的可见比例,即intersectionRectboundingClientRect的比例,完全可见时为1,完全不可见时小于等于0
    • intersectionRect:目标元素与视口(或根元素)的交叉区域的信息
    • isIntersecting: 布尔值,目标元素与交集观察者的根节点是否相交
    • isVisible: 布尔值,目标元素是否可见(该属性还在试验阶段,不建议在生产环境中使用)
    • rootBounds:根元素的矩形区域的信息,getBoundingClientRect()方法的返回值,如果没有根元素(即直接相对于视口滚动),则返回null
    • target:被观察的目标元素,是一个 DOM 节点对象
    • time:可见性发生变化的时间,是一个高精度时间戳,单位为毫秒

    6. 应用


    1. 预加载(滚动加载,翻页加载,无限加载)
    2. 懒加载(后加载、惰性加载)
    3. 其它

    7. 注意点

    IntersectionObserver API 是异步的,不随着目标元素的滚动同步触发。


    规格写明,IntersectionObserver的实现,应该采用requestIdleCallback(),即只有线程空闲下来,才会执行观察器。这意味着,这个观察器的优先级非常低,只在其他任务执行完,浏览器有了空闲才会执行。


    8. 参考链接


    作者:三年没洗澡
    来源:https://juejin.cn/post/7035490578015977480

    收起阅读 »

    js打包时间缩短90%,bundleless生产环境实践总结

    最近尝试将bundleless的构建结果直接用到了线上生产环境,因为bundleless只会编译代码,不会打包,因此构建速度极快,同比bundle模式时间缩短了90%以上。得益于大部分浏览器都已经支持了http2和浏览器的es module,对于我们没有强兼容...
    继续阅读 »




    最近尝试将bundleless的构建结果直接用到了线上生产环境,因为bundleless只会编译代码,不会打包,因此构建速度极快,同比bundle模式时间缩短了90%以上。得益于大部分浏览器都已经支持了http2和浏览器的es module,对于我们没有强兼容场景的中后台系统,将bundleless构建结果应用于线上是有可能的。本文主要介绍一下,本人在使用bundleless构建工具实践中遇到的问题。

    • 起源

    • 结合snowpack实践

    • snowpack的Streaming Imports

    • 性能比较

    • 总结

    • 附录snowpack和vite的对比


    本文原文来自我的博客: github.com/fortheallli…

    一、起源

    1.1 从http2谈起

    以前因为http1.x不支持多路服用, HTTP 1.x 中,如果想并发多个请求,必须使用多个 TCP 链接,且浏览器为了控制资源,还会对单个域名有 6-8个的TCP链接请求限制.因此我们需要做的就是将同域的一些静态资源比如js等,做一个资源合并,将多次请求不同的js文件,合并成单次请求一个合并后的大js文件。这就是webpack等bundle工具的由来。

    而从http2开始,实现了TCP链接的多路复用,因此同域名下不再有请求并发数的限制,我们可以同时请求同域名的多个资源,这个并发数可以很大,比如并发10,50,100个请求同时去请求同一个服务下的多个资源。

    因为http2实现了多路复用,因此一定程度上,将多个静态文件打包到一起,从而减少请求次数,就不是必须的

    主流浏览器对http2的支持情况如下:

    Lark20210825-203949

    除了IE以外,大部分浏览器对http2的支持性都很好,因为我的项目不需要兼容IE,同时也不需要兼容低版本浏览器,不需要考虑不支持http2的场景。(这也是我们能将不bundle的代码用于线上生产环境的前提之一)

    1.2 浏览器esm

    对于es modules,我们并不陌生,什么是es modules也不是本文的重点,一些流行的打包构建工具比如babel、webpack等早就支持es modules。

    我们来看一个最简单的es modules的写法:

    //main.js
    import a from 'a.js'
    console.log(a)

    //a.js
    export let  a = 1

    上述的es modules就是我们经常在项目中使用的es modules,这种es modules,在支持es6的浏览器中是可以直接使用的。

    我们来举一个例子,直接在浏览器中使用es modules

    <html  lang="en">
       <body>
           <div id="container">my name is {name}</div>
           <script type="module">
              import Vue from 'https://cdn.jsdelivr.net/npm/vue@2.6.12/dist/vue.esm.browser.js'
              new Vue({
                el: '#container',
                data:{
                   name: 'Bob'
                }
              })
           </script>
       </body>
    </html>

    上述的代码中我们直接可以运行,我们根据script的type="module"可以判断浏览器支不支持es modules,如果不支持,该script里面的内容就不会运行。

    首先我们来看主流浏览器对于ES modules的支持情况:

    Lark20201119-151747

    从上图可以看出来,主流的Edge, Chrome, Safari, and Firefox (+60)等浏览器都已经开始支持es modules。

    同样的因为我们的中后台项目不需要强兼容,因此不需要兼容不支持esm的浏览器(这也是我们能将不bundle的代码用于线上生产环境的前提之二)。

    1.3 小结

    浏览器对于http2和esm的支持,使得我们可以减少模块的合并,以及减少对于js模块化的处理。

    • 如果浏览器支持http2,那么一定程度上,我们不需要合并静态资源

    • 如果浏览器支持esm,那么我们就不需要通过构建工具去维护复杂的模块依赖和加载关系。

    这两点正是webpack等打包工具在bundle的时候所做的事情。浏览器对于http2和esm的支持使得我们减少bundle代码的场景。

    二、结合snowpack实践

    我们比较了snowpack和vite,最后选择采用了snowpack(选型的原因以及snowpack和vite的对比看最后附录),本章节讲讲如何结合snowpack构建工具,构建出不打包形式的线上代码。

    2.1 snowpack的基础用法

    我们的中后台项目是react和typescript编写的,我们可以直接使用snowpack相应的模版:

    npx create-snowpack-app myproject --template @snowpack/app-template-react-typescript

    snowpack构建工具内置了tsc,可以处理tsx等后缀的文件。上述就完成了项目初始化。

    2.2 前端路由处理

    前端路由我们直接使用react-router或者vue-router等,需要注意的时,如果是在开发环境,那么必须要指定在snowpack.config.mjs配置文件,在刷新时让匹配到前端路由:

    snowpack.config.mjs
    ...
     routes: [{ match: 'routes', src: '.*', dest: '/index.html' }],
    ...

    类似的配置跟webpack devserver等一样,使其在后端路由404的时候,获取前端静态文件,从而执行前端路由匹配。

    2.3 css、jpg等模块的处理

    在snowpack中同样也自带了对css和image等文件的处理。

    • css

    以sass为例,

    snowpack.config.mjs

    plugins: [
        '@snowpack/plugin-sass',
        {
          /* see options below */
        },
      ],

    只需要在配置中增加一个sass插件就能让snowpack支持sass文件,此外,snowpack也同样支持css module。.module.css或者.module.scss命名的文件就默认开启了css module。此外,css最后的结果都是通过编译成js模块,通过动态创建style标签,插入到body中的。

    //index.module.css文件
    .container{
       padding: 20px;
    }

    snowpack构建处理后的css.proxy.js文件为:

    export let code = "._container_24xje_1 {\n  padding: 20px;\n}";
    let json = {"container":"_container_24xje_1"};
    export default json;

    // [snowpack] add styles to the page (skip if no document exists)
    if (typeof document !== 'undefined') {
     const styleEl = document.createElement("style");
     const codeEl = document.createTextNode(code);
     styleEl.type = 'text/css';

     styleEl.appendChild(codeEl);
     document.head.appendChild(styleEl);
    }

    上述的例子中我们可以看到。最后css的构建结果是一段js代码。在body中动态插入了style标签,就可以让原始的css样式在系统中生效。

    • jpg,png,svg等

    如果处理的是图片类型,那么snowpack同样会将图片编译成js.

    //logo.svg.proxy.js
    export default "../dist/assets/logo.svg";

    snowpack没有对图片做任何的处理,只是把图片的地址,包含到了一个js模块文件导出地址中。值得注意的是在浏览器es module中,import 动作类似一个get请求,import from可以是一个图片地址,浏览器es module自身可以处理图片等形式。因此在.js文件结尾的模块中,export 的可以是一个图片。

    snowpack3.5.0以下的版本在使用css module的时候会丢失hash,需要升级到最新版本。

    2.4 按需加载处理

    snowpack默认是不打包的。只对每一个文件都做一些简单的模块处理(将非js模块转化成js模块)和语法处理,因此天然支持按需加载,snowpack支持React.lazy的写法,在react的项目中,只要正常使用React.Lazy就能实现按需加载。

    2.5 文件hash处理

    在最后构建完成后,在发布构建结果的时候,为了处理缓存,常见的就是跟静态文件增加hash,snowpack也提供了插件机制,插件会处理snowpack构建前的所有文件的内容,做为content转入到插件中,经过插件的处理转换后得到新的content.

    可以通过snowpack-files-hash插件来实现给文件增加hash。

    2.6 公用esm模块托管

    snowpack对于项目构建的bundleless的代码可以直接跑在线上,在bundless的构建结果中,我们想进一步减少构建结果文件大小。以bundleless的方式构建的代码,默认在处理三方npm包依赖的时候,虽然不会打包,snowpack对项目中node_modules中的依赖重新编译成esm形式,然后放在一个新的静态目录下。因此最后构建的代码包含了两个部分:

    项目本身的代码,将node_modules中的依赖处理成esm后的静态文件

    其中node_modules中的依赖处理成esm后的静态文件,可以以cdn或者其他服务形式来托管。这样我们每次都不需要在构建的时候处理node_modules中的依赖。在项目本身的代码中,如果引用了npm包,只需要将其指向一个cdn地址即可。这样处理后的,构建的代码就变成:

    只有项目本身的代码(项目中对于三方插件的引入,直接使用三方插件的cdn地址)

    进一步想,如果我们使用了托管所有npm包(es module形式)的cdn地址之后,那么在本地开发或者线上构建的过程中,我们甚至不需要去维护本地的node_modules目录,以及yarn-lock或者package-lock文件。我们需要做的,仅仅是一个map文件进行版本管理。保存项目中的npm包名和该包相对应的cdn地址。

    比如:

    //config.map.json
    {
     "react": "https://cdn.skypack.dev/react@17.0.2",
     "react-dom": "https://cdn.skypack.dev/react-dom@17.0.2",
    }

    通过这个map文件,不管是在开发还是线上,只要把:

    import React from 'react'

    替换成

    import React from "https://cdn.skypack.dev/react@17.0.2"

    就能让代码在开发环境或者生产环境中跑起来。如此简化之后,我们不论在开发环境还是生产环境都不需要在本地维护node_modules相关的文件,进一步可以减少打包时间。同时包管理也更加清晰,仅仅是一个简单的json文件,一对固定意义的key/value,简单纯粹

    我们提到了一个托管了的npm包的有es module形式的cdn服务,上述以skypack为例,这对比托管了npm包cjs形式的cdn服务unpkg,两者的区别就是,unpkg所托管的npm包,大部分是cjs形式的,cjs形式的npm包,是不能直接用于浏览器的esm中的。skypack所做的事情就是将大部分npm包从cjs形式转化成esm的形式,然后存储和托管esm形式的结果。

    三、snowpack的Streaming Imports

    在2.7中我们提到了在dev开发环境使用了skypack,那么本地不需要node_modules,甚至不需要yarn-lock和package-lock等文件,只需要一个json文件,简单的、纯粹的,只有一对固定意义的key/value。在snowpack3.x就提供了这么一个功能,称之为Streaming Imports。

    3.1 snowpack和skypack

    在snowpack3.x在dev环境支持skypack:

    // snowpack.config.mjs
    export default {
     packageOptions: {
       source: 'remote',
    },
    };

    如此,在dev的webserver过程中,就是直接下载skypack中相应的esm形式的npm包,放在最后的结果中,而不需要在本地做一个cjs到esm的转换。这样做有几点好处:

    • 速度快: 不需要npm install一个npm包,然后在对其进行build转化成esm,Streaming Imports可以直接从一个cdn地址直接下载esm形式的依赖

    • 安全:业务代码中不需要处理公共npm包cjs到esm的转化,业务代码和三方依赖分离,三方依赖交给skypack处理

    3.2 依赖控制

    Streaming Imports自身也实现了一套简单的依赖管理,有点类似go mod。是通过一个叫snowpack.deps.json文件来实现的。跟我们在2.7中提到的一样,如果使用托管cdn,那么本地的pack-lock和yarn-lock,甚至node_modules是不需要存在的,只需要一个简单纯粹的json文件,而snowpack中就是通过snowpack.deps.json来实现包的依赖管理的。

    我们安装一个npm包时,我们以安装ramda为例:

    npx snowpack ramda

    在snowpack.deps.json中会生成:

    {
     "dependencies": {
       "ramda": "^0.27.1",
    },
     "lock": {
       "ramda#^0.27.1": "ramda@v0.27.1-3ePaNsppsnXYRcaNcaWn",
    }
    }

    安装过程的命令行如下所示:

    飞书20210831-211844

    从上图可以看出来,通过npx snowpack安装的依赖是从skypack cdn直接请求的。

    特别的,如果项目需要支持typescript,那么我们需要将相应的npm包的声明文件types下载到本地,skypack同样也支持声明文件的下载,只需要在snowpack的配置文件中增加:

    // snowpack.config.mjs
    export default {
     packageOptions: {
       source: 'remote',
       types:true //增加type=true
    },
    };

    snowpack会把types文件下载到本地的.snowpack目录下,因此在tsc编译的时候需要指定types的查找路径,在tsconfig.json中增加:

    //tsconfig.json
    "paths": {
         "*":[".snowpack/types/*"]
      },

    3.3 build环境

    snowpack的Streaming Imports,在dev可以正常工作,dev的webserver中在请求npm包的时候会将请求代理到skypack,但是在build环境的时候,还是需要其他处理的,在我们的项目中,在build的时候可以用一个插件snowpack-plugin-skypack-replacer,将build后的代码引入npm包的时候,指向skypack。

    build后的线上代码举例如下:

    import * as __SNOWPACK_ENV__ from '../_snowpack/env.271340c8a413.js';
    import.meta.env = __SNOWPACK_ENV__;

    import ReactDOM from "https://cdn.skypack.dev/react-dom@^17.0.2";
    import App from "./App.e1841499eb35.js";
    import React from "https://cdn.skypack.dev/react@^17.0.2";
    import "./index.css.proxy.9c7da16f4b6e.js";

    const start = async () => {
     await ReactDOM.render(/* @__PURE__ */ React.createElement(App, null), document.getElementById("root"));
    };
    start();
    if (undefined /* [snowpack] import.meta.hot */ ) {
     undefined /* [snowpack] import.meta.hot */ .accept();
    }

    从上述可以看出,build之后的代码,通过插件将:

    import React from 'react'
    //替换成了
    import React from "https://cdn.skypack.dev/react@^17.0.2";

    四、性能比较

    4.1 lighthouse对比

    简单的使用lighthouse来对比bundleless和bundle两种不同构建方式网页的性能。

    • bundleless的前端简单性能测试:

    img

    • bundle的前端性能测试:

    img

    对比发现,这里两个网站都是同一套代码,相同的部署环境,一套是构建的时候是bundleless,利用浏览器的esm,另一个是传统的bundle模式,发现性能上并没有明显的区别,至少bundleless简单的性能测试方面没有明显差距。

    4.2构建时间对比

    bundleless构建用于线上,主要是减少了构建的时间,我们传统的bundle的代码,一次编译打包等可能需要几分钟甚至十几分钟。在我的项目中,bundleless的构建只需要4秒。

    飞书20210901-165311

    同一个项目,用webpack构建bundle的情况下需要60秒左右。

    4.3构建产物体积对比

    bundleless构建出的产物,一般来说也只有bundle情况下的1/10.这里不一一举例。

    五、总结

    在没有强兼容性的场景,特别是中后台系统,bundleless的代码直接跑在线上,是一种可以尝试的方案,上线的时间会缩短90%,不过也有一些问题需要解决,首先需要保证托管esm资源的CDN服务的稳定性,且要保障被托管的esm资源在浏览器运行不会出现异常。我们运行了一些常见的npm包,发现并没有异常情况,不过后续需要更多的测试。

    六、附录:snowpack和vite的对比

    6.1 相同点

    snowpack和vite都是bundleless的构建工具,都利用了浏览器的es module来减少对静态文件的打包,从而减少热更新的时间,从而提高开发体验。原理都是将本地安装的依赖重新编译成esm形式,然后放在本地服务的静态目录下。snowpack和vite有很多相似点

    • 在dev环境都将本地的依赖进行二次处理,对于本地node_module目录下的npm包,通过其他构建工具转换成esm。然后将所有转换后的esm文件放在本地服务的静态目录下

    • 都支持css、png等等静态文件,不需要安装其他插件。特别对于css,都默认支持css module

    • 默认都支持jsx,tsx,ts等扩展名的文件

    • 框架无关,都支持react、vue等主流前端框架,不过vite对于vue的支持性是最好的。

    6.2 不同点

    dev构建: snowpack和vite其实大同小异,在dev环境都可以将本地node_modules中npm包,通过esinstall等编译到本地server的静态目录。不同的是在dev环境

    • snowpack是通过rollup来将node_modules的包,重新进行esm形式的编译

    • vite则是通过esbuild来将node_modules的包,重新进行esm形式的编译

    因此dev开发环境来看,vite的速度要相对快一些,因为一个npm包只会重新编译一次,因此dev环境速度影响不大,只是在初始化项目冷启动的时候时间有一些误差,此外snowpack支持Streaming Imports,可以在dev环境直接用托管在cdn上的esm形式的npm包,因此dev环境性能差别不大。

    build构建:

    在生产环境build的时候,vite是不支持unbundle的,在bundle模式下,vite选择采用的是rollup,通过rollup来打包出线上环境运行的静态文件。vite官方支持且仅支持rollup,这样一定程度上可以保持一致性,但是不容易解耦,从而结合非rollup构建工具来打包。而snowpack默认就是unbundle的,这种unbundle的默认形式,对构建工具就没有要求,对于线上环境,即可以使用rollup,也可以使用webpack,甚至可以选择不打包,直接使用unbundle。

    可以用两个表格来总结如上的结论:

    dev开发环境:

    产品dev环境构建工具
    snowpackrollup(或者使用Streaming imports)
    viteesbuild

    build生产环境:

    产品build构建工具
    snowpack1.unbundle(esbuild) 2.rollup 3.webpack...
    viterollup(且不支持unbundle)

    6.3 snowpack支持Streaming Imports

    Streaming Imports是一个新特性,他允许用户,不管是生产环境还是开发环境,都不需要在本地使用npm/yarn来维护一个lock文件,从而下载应用中所使用的npm包到本地的node_module目录下。通过使用Streaming Imports,可以维护一个map文件,该map文件中的key是包名,value直接指向托管该npm包esm文件形式的cdn服务器的地址。

    6.4 vite的一些优点

    vite相对于snowpack有一下几个优点,不过个人以为都不算特别有用的一些优点。

    • 多页面支持,除了根目录的root/index.html外,还支持其他根目录以外的页面,比如nest/index.html

    • 对于css预处理器支持更好(这点个人没发现)

    • 支持css代码的code-splitting

    • 优化了异步请求多个chunk文件(不分场景可以同步进行,从而一定程度下减少请求总时间)

    6.5 总结

    如果想在生产环境使用unbundle,那么vite是不行的,vite对于线上环境的build,是必须要打包的。vite优化的只是开发环境。而snowpack默认就是unbundle的,因此可以作为前提在生产环境使用unbundle.此外,snowpack的Streaming Imports提供了一套完整的本地map的包管理,不需要将npm包安装到本地,很方便我们在线上和线下使用cdn托管公共库。


    作者:yuxiaoliang
    来源:https://juejin.cn/post/7034484346874986533

    收起阅读 »

    重新审视前端模块的调用, 执行和加载之间的关系

    在进入正题之前, 让我们先回顾下前端模块从无到有的一个简短历史 如果你有一定的工作经验, 并且经历过 jQuery 那样的年代, 应该了解早期的前端模块, 只是 window 上的一个局部变量. 在最初的时候前端工程师为了分享自己的代码, 往往会通过 wind...
    继续阅读 »

    在进入正题之前, 让我们先回顾下前端模块从无到有的一个简短历史


    如果你有一定的工作经验, 并且经历过 jQuery 那样的年代, 应该了解早期的前端模块, 只是 window 上的一个局部变量.


    在最初的时候前端工程师为了分享自己的代码, 往往会通过 window 来建立联系, 这种古老的做法至今还被很多人使用, 因为简单. 例如我们编写了一个脚本, 通常我们并不认为这是个模块, 但我们会习惯于将这个脚本包装成一个对象. 例如


    window.myModule = {
    getName(name){
    return `hello ${name}`
    }
    }

    当其他人加载这个脚本后, 就可以便捷的通过 window.myModule 来调用 getName 方法.


    早期的 JavaScript 脚本主要用于开发一些简单的表单和网页的交互功能, 那个年代的前端工程师数量也极少, 通过 window 来实现模块化并没有什么太大的问题.


    直到 ajax 的出现, 将 web 逐步推动到了富客户端的阶段, 随着 spa 的兴起, 前端工程师发现使用 window 模块化代码越来越难以维护, 主要原因有 2 个



    1. 大量的模块加载污染了 window, 导致各种命名冲突和意外覆盖, 这些问题还很难定位.

    2. 模块和模块之间的交互越来越多, 为了保证调用顺序, 需要人为保障 script 标签的加载顺序


    为了解决这个问题, 类似 require seajs 这样的模块 loader 被创造出来, 通过模块 loader, 大大缓解了上述的两个问题.


    但前端技术和互联网发展的速度远超我们的想象, 随着网页越来越像一个真实的客户端, 这对前端的工程能力提出了极大的挑战, 仅靠单纯的脚本开发已经难以满足项目的需要, 于是 gulp 等用于前端工程管理的脚手架开始进入我们的视野, 不过在这个阶段, 模块 loader 和前端工程流之间尚未有机的结合.


    直到 nodejs 问世, 前端拥有了自己的包管理工具 npm, 在此基础上 Webpack 进一步推动了前端工程流和模块之间的整合, 随后前端模块化的进程开始稳固下来, 一直保持至今.


    从这个历史上去回顾, 前端模块化的整个进程包括 es6 关于 module 的标准都是一直围绕这个一个核心命题存在的.


    无论是 require 还是 Webpack 在这个核心命题上并没有区别, 即前端模块遵循


    加载 → 调用 → 执行 这样的一个逻辑关系. 因为模块必须先加载才能调用并执行, 模块加载器和构建工具就必须管理和分析应用中所有模块的依赖关系, 从而确定哪些模块可以拆分哪些可以合并, 以及模块的加载顺序.


    但是随着时间的推移, 前端应用的模块越来越多, 应用越来越庞大, 我们的本地的 node_modules 几百兆起步, Webpack 虽然做了很多优化, 但是 rebuild 的时间在大型应用面前依然显得很慢.


    今年 2 月份, Webpack 5 发布了他们的模块拆解方案, 模块联邦, 这个插件解决了 Webpack 构建的模块无法在多个工程中复用的问题.


    早些时间 yarn 2.0 采用共享 node_moudles 的方法来解决本地模块大量冗余导致的性能问题.
    包括 nodejs 作者在 deno 中放弃了 npm 改用网络化加载模块的方式等等.


    可以看到社区已经意识到了先行的前端模块化机制再次面临瓶颈, 无论是性能还是维护成本都面临诸多挑战, 各个团队都在想办法开辟一个新的方向.


    不过这些努力依然没有超越先行模块化机制中的核心命题, 即模块必须先加载, 后调用执行.


    只要这个核心命题不变, 模块的依赖问题依然是无解的. 为此我们尝试提出了一种新的思路


    模块为什么不能先调用, 后加载执行呢?


    如果 A 模块调用 B 模块, 但并不需要 B 模块立即就绪, 这就意味着, 模块加载器可以不关心模块的依赖关系, 而致力于只解决模块加载的效率和性能问题.


    同时对于构建工具来说, 如果 A 模块的执行并不基于 B 模块立即就绪这件事, 那么构建工具可以放心的将 A 和 B 模块拆成两个文件, 如果模块有很多, 就可以利用 http2 的并行加载能力, 大大提升模块的加载性能.


    在我们的设想中, 一种新的模块加载方式是这样的


    // remoteModule.js 这是一个发布到 cdn 的远程模块, 内部代码是这样

    widnow.rdeco.create({
    name:'remote-module',
    exports:{
    getName(name, next){
    next(`hello ${name}`)
    }
    }
    })


    让我们先不加载这个模块, 而是直接先执行调用端的代码例如这样



    window.rdeco 可以理解成类似 Webpack runtime 一样的存在, 不过 rdeco 是一个独立的库, 其功能远不止于此



    // localModule.js 这个是本地的模块
    window.rdeco.inject('remote-module').getName('world').then(fullName=>{
    console.log(fullName)
    })

    然后我们在 html 中先加载 localModule.js 后加载 remoteModule.js


    <scirpt src="localModule.js"></script>
    <scirpt src="remoteModule.js"></script>


    正常理解, localModule.js 加载完之后会试图去调用 remote-module 的 getName 方法, 但此时 remoteModule 尚未加载, 按照先行的模块化机制, 这种调用会抛出异常. 为了避免这个问题


    模块构建工具需要分析两个文件的代码, 从而发现 localModule.js 依赖 remoteModule.js, 然后保存这个依赖顺序, 同时通知模块加载器, 为了让代码正常执行, 必须先加载 remoteModule.js.


    但如果模块可以先调用后加载, 那么这个复杂的过程就可以完全避免. 目前我们实现了这一机制, 可以看下这个 demo: codesandbox.io/s/tender-ar…


    你可试着先点击 Call remote module's getName method 按钮,


    此时文案不会变化只是显示 hello, 但代码并不会抛出异常, 然后你再点击 Load remote module 按钮, 开始加载 remoteModule, 等待加载完成, getName 才会真实执行, 此时文案变成了 hello world



    作者:掘金泥石流
    链接:https://juejin.cn/post/7034412398261993479

    收起阅读 »

    CSS实现随机不规则圆角头像

     前言 最近真是彻底爱上了 CSS ,我又又又被 CSS 惊艳到了,明明是简单的属性,为啥大佬们稍微一组合,就能形成如此好看的效果啊。本文 给大家带来的是随机不规则圆角头像效果,我们可以把这个效果用于一些人物的展示页面 学习本文章,你可以学到:bor...
    继续阅读 »

     前言


    最近真是彻底爱上了 CSS ,我又又又被 CSS 惊艳到了,明明是简单的属性,为啥大佬们稍微一组合,就能形成如此好看的效果啊。本文


    给大家带来的是随机不规则圆角头像效果,我们可以把这个效果用于一些人物的展示页面


    学习本文章,你可以学到:

    • border-radius 实现椭圆效果
    • border-radius 实现不规则圆角头像
    • animation-delay 设置负值
    • 实现随机不规则圆角

    📃 预备知识


    🎨 border-radius


    border-radius 可以设置外边框的圆角。比如我们经常使用的 border-radius: 50% 可以得到一个圆形头像。


    radius50.png


    border-radius 就只能实现圆形效果吗?当然不是,当使用一个半径是确定圆形,两个半径时则会确定椭圆形。


    光说不练假把式,接下来一起试试



    1. 设置 border-radius: 30% 70%,就可以得到椭圆效果


    radius3070.png


    上面的设置都是针对于四个方向的,也可以只设置一个方向的圆角



    1. 设置 border-top-left-radius: 30% 70%


    radius3070top.png


    从上图其实可以得出,两个值分别设置水平半径和垂直半径的半径,为了更准确我们验证一下


    radiusopa.png


    但为啥设置的圆角与 border-radius: 30% 70% 设置有这么大的差距。别急,下面慢慢道来。



    1. 设置 border-radius: 30%/70%,/ 前后的值分别为水平半径和垂直半径



    border-radius: 30%/70% 相当于给四个方向都设置 30%/70%,而 border-radius: 30% 70% 是给左上右下设置 30% ,左下右上设置 70%



    radius30-70.png



    1. 设置四个方向为四种椭圆角: border-radius: 40% 60% 60% 40% / 60% 30% 70% 40% ,就可以实现简单的不规则圆角效果,小改改的头像是不是看起来舒服了好多。


    radiusdisorder.png


    💞 animation-delay


    animation-delay: 可以定义动画播放的延迟时间。


    但如果给 animation-delay 设置负值会发生什么那?



    MDN 中指出: 定义一个负值会让动画立即开始。但是动画会从它的动画序列中某位置开始。例如,如果设定值为 -1s ,动画会从它的动画序列的第 1 秒位置处立即开始。



    那个,乍看上去,我好像懂了,又好像没懂,咱们还是来自己试一下吧。



    • 创建 div 块,宽高都为 0 ,背景设置为 #000

    • 添加 keyframe 动画,100% 状态宽高都扩展为 1000px


    @keyframes extend {
    0% {
    width: 0;
    height: 0;
    }
    100% {
    width: 1000px;
    height: 1000px;
    }
    }


    • div 添加 animationanimation-delay


    /* 设置 paused 可以使动画暂停 */
    animation: extend 10s linear paused;
    animation-delay: -3s;

    当我打开浏览器时,浏览器出现 300*300 的黑色块,修改 animation-delay-4s ,浏览器出现 400*400 的黑块。我们使用 linear 匀速作为动画播放函数,10s 后 div 会变为 1000px,设置 -3s 起始为 300px-4s 起始为 400px


    这样一对比,我们来把 MDN 的描述翻译一下:
    + animation-delay 设置负值的动画会立即执行
    + 动画起始位置是动画中的一阶段,比如上述案例,定义 10s 的动画,设置 -3s 动画就从 3s 开始执行


    🌊 radius 配合 delay 实现


    有了上面基础知识的配合,不规则圆角的实现就变得很简单了。


    设置 keyframekeyframe 的开始与结束为两种不规则圆角,再使用 :nth-child 进行自然随机设置 animation-delay 的负值延迟时间,就可以得到一组风格各异的不规则圆角效果



    自然随机的算法非常有意思,效果开创者为了更好、更自然的随机性,选取序列为 2n+1 3n+2 5n+3 7n+4 11+5 ...




    1. 设置 keyframe 动画


    @keyframes morph {
    0% {
    border-radius: 40% 60% 60% 40% / 60% 30% 70% 40%;
    transform: rotate(-5deg);
    }
    100% {
    border-radius: 40% 60%;
    transform: rotate(5deg);
    }
    }


    1. 自然随机设置每个头像的 delay


    .avatar:nth-child(n) {
    animation-delay: -3.5s;
    }
    .avatar:nth-child(2n + 1) {
    animation-delay: -1s;
    }
    .avatar:nth-child(3n + 2) {
    animation-delay: -2s;
    }
    .avatar:nth-child(5n + 3) {
    animation-delay: -3s;
    }
    .avatar:nth-child(7n + 5) {
    animation-delay: -4s;
    }
    .avatar:nth-child(11n + 7) {
    animation-delay: -5s;
    }

    当当当当~~~ 效果就实现了! 看着下面这些风格各异的小改改,瞬间心情舒畅了好多。


    avater.png


    不规则圆角头像的功能实现了,但总感觉缺点什么?如果头像能有点动态效果就更好了。


    例如 hover 时,头像圆角会发生变化,用户的体验会更好。


    我首先的想法还是在上面的代码基础上面更改,但由于 @keyframe 定义好了终点时的状态,能变化的效果并不多,而且看起来很单调,显得很呆 🤣。


    那有没有好的实现方案那?有,最终我找到了张鑫旭大佬的实现方案,大佬还是大佬啊。


    🌟 radius 配合 transition 实现


    参考博客: “蝉原则”与CSS3随机多背景随机圆角等效果



    1. 按照自然随机给每个头像赋予不同的不规则圆角


    /* 举两个例子 */
    .list:hover {
    border-radius: 95% 70% 100% 80%;
    transform: rotate(-2deg);
    }
    .list:nth-child(2n+1) {
    border-radius: 59% 52% 56% 59%;
    transform: rotate(-6deg);
    }


    1. 设置 hover 时新的不规则圆角


    .list:nth-child(2n+1):hover {
    border-radius: 51% 67% 56% 64%;
    transform: rotate(-4deg);
    }

    .list:nth-child(3n+2):hover {
    border-radius: 69% 64% 53% 70%;
    transform: rotate(0deg);
    }


    1. list 元素配置 transition


    avatar.gif


    完成上面的步骤,我们就可以得到更灵动的小改改头像了。



    但这种实现方法相比较于 radius 配合 animation-delay 实现具备一定的难点,需要设计多种好看的不规则圆角效果



    🛕 源码仓库


    传送门: 随机不规则圆角




    作者:战场小包
    链接:https://juejin.cn/post/7034396555738251301

    收起阅读 »

    使用 Promise 时的5个常见错误,你占了几个!

    Promise 提供了一种优雅的方法来处理 JS 中的异步操作。这也是避免“回调地狱”的解决方案。然而,并没有多少开发人员了解其中的内容。因此,许多人在实践中往往会犯错误。 在本文中,介绍一下使用 promise 时的五个常见错误,希望大家能够避免这些错误。 ...
    继续阅读 »

    Promise 提供了一种优雅的方法来处理 JS 中的异步操作。这也是避免“回调地狱”的解决方案。然而,并没有多少开发人员了解其中的内容。因此,许多人在实践中往往会犯错误。


    在本文中,介绍一下使用 promise 时的五个常见错误,希望大家能够避免这些错误。


    1.避免 Promise 地狱


    通常,Promise是用来避免回调地狱。但滥用它们也会导致 Promise是地狱。


    userLogin('user').then(function(user){
    getArticle(user).then(function(articles){
    showArticle(articles).then(function(){
    //Your code goes here...
    });
    });
    });

    在上面的例子中,我们对 userLogingetararticleshowararticle 嵌套了三个promise。这样复杂性将按代码行比例增长,它可能变得不可读。


    为了避免这种情况,我们需要解除代码的嵌套,从第一个 then 中返回 getArticle,然后在第二个 then 中处理它。


    userLogin('user')
    .then(getArticle)
    .then(showArticle)
    .then(function(){
    //Your code goes here...
    });

    2. 在 Promise 中使用 try/catch


    通常情况下,我们使用 try/catch 块来处理错误。然而,不建议在 Promise 对象中使用try/catch


    这是因为如果有任何错误,Promise对象会在 catch 内自动处理。


    ew Promise((resolve, reject) => {
    try {
    const data = doThis();
    // do something
    resolve();
    } catch (e) {
    reject(e);
    }
    })
    .then(data => console.log(data))
    .catch(error => console.log(error));

    在上面的例子中,我们在Promise 内使用了 try/catch 块。


    但是,Promise本身会在其作用域内捕捉所有的错误(甚至是打字错误),而不需要 try/catch块。它确保在执行过程中抛出的所有异常都被获取并转换为被拒绝的 Promise。


    new Promise((resolve, reject) => {
    const data = doThis();
    // do something
    resolve()
    })
    .then(data => console.log(data))
    .catch(error => console.log(error));

    **注意:**在 Promise 块中使用 .catch() 块是至关重要的。否则,你的测试案例可能会失败,而且应用程序在生产阶段可能会崩溃。


    3. 在 Promise 块内使用异步函数


    Async/Await 是一种更高级的语法,用于处理同步代码中的多个Promise。当我们在一个函数声明前使用 async 关键字时,它会返回一个 Promise,我们可以使用 await 关键字来停止代码,直到我们正在等待的Promise解决或拒绝。


    但是,当你把一个 Async 函数放在一个 Promise 块里面时,会有一些副作用。


    假设我们想在Promise 块中做一个异步操作,所以使用了 async 关键字,但,不巧的是我们的代码抛出了一个错误。


    这样,即使使用 catch() 块或在 try/catch 块内等待你的Promise,我们也不能立即处理这个错误。请看下面的例子。


    // 此代码无法处理错误
    new Promise(async () => {
    throw new Error('message');
    }).catch(e => console.log(e.message));

    (async () => {
    try {
    await new Promise(async () => {
    throw new Error('message');
    });
    } catch (e) {
    console.log(e.message);
    }
    })();

    当我在Promise块内遇到 async 函数时,我试图将 async 逻辑保持在 Promise 块之外,以保持其同步性。10次中有9次都能成功。


    然而,在某些情况下,可能需要一个 async 函数。在这种情况下,也别无选择,只能用try/catch 块来手动管理。


    new Promise(async (resolve, reject) => {
    try {
    throw new Error('message');
    } catch (error) {
    reject(error);
    }
    }).catch(e => console.log(e.message));


    //using async/await
    (async () => {
    try {
    await new Promise(async (resolve, reject) => {
    try {
    throw new Error('message');
    } catch (error) {
    reject(error);
    }
    });
    } catch (e) {
    console.log(e.message);
    }
    })();

    4.在创建 Promise 后立即执行 Promise 块


    至于下面的代码片断,如果我们把代码片断放在调用HTTP请求的地方,它就会被立即执行。


    const myPromise = new Promise(resolve => {
    // code to make HTTP request
    resolve(result);
    });

    原因是这段代码被包裹在一个Promise构造函数中。然而,有些人可能会认为只有在执行myPromisethen方法之后才被触发。


    然而,真相并非如此。相反,当一个Promise被创建时,回调被立即执行。


    这意味着在建立 myPromise 之后到达下面一行时,HTTP请求很可能已经在运行,或者至少处于调度状态。


    Promises 总是急于执行过程。


    但是,如果希望以后再执行 Promises,应该怎么做?如果现在不想发出HTTP请求怎么办?是否有什么神奇的机制内置于 Promises 中,使我们能够做到这一点?


    答案就是使用函数。函数是一种耗时的机制。只有当开发者明确地用 () 来调用它们时,它们才会执行。简单地定义一个函数还不能让我们得到什么。所以,让 Promise 变得懒惰的最有效方法是将其包裹在一个函数中!


    const createMyPromise = () => new Promise(resolve => {
    // HTTP request
    resolve(result);
    });

    对于HTTP请求,Promise 构造函数和回调函数只有在函数被执行时才会被调用。所以现在我们有一个懒惰的Promise,只有在我们需要的时候才会执行。


    5. 不一定使用 Promise.all() 方法


    如果你已经工作多年,应该已经知道我在说什么了。如果有许多彼此不相关的 Promise,我们可以同时处理它们。


    Promise 是并发的,但如你一个一个地等待它们,会太费时间,Promise.all()可以节省很多时间。



    记住,Promise.all() 是我们的朋友



    const { promisify } = require('util');
    const sleep = promisify(setTimeout);

    async function f1() {
    await sleep(1000);
    }

    async function f2() {
    await sleep(2000);
    }

    async function f3() {
    await sleep(3000);
    }


    (async () => {
    console.time('sequential');
    await f1();
    await f2();
    await f3();
    console.timeEnd('sequential');
    })();

    上述代码的执行时间约为 6 秒。但如果我们用 Promise.all() 代替它,将减少执行时间。


    (async () => {
    console.time('concurrent');
    await Promise.all([f1(), f2(), f3()]);
    console.timeEnd('concurrent');
    })();

    总结


    在这篇文章中,我们讨论了使用 Promise 时常犯的五个错误。然而,可能还有很多简单的问题需要仔细解决。



    作者:前端小智
    链接:https://juejin.cn/post/7034661345148534815

    收起阅读 »