蓝染

蓝染 查看完整档案

杭州编辑东华理工大学  |  软件工程 编辑  |  填写所在公司/组织填写个人主网站
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 个人简介什么都没有

个人动态

蓝染 赞了文章 · 2019-04-17

前端面试题 -- Vue

前言

作为前端开发中现行最火的框架之一,Vue 在面试中出现的频率不断增加。基于此,总结了一些 Vue 方面经常出现的面试题,留给自己查看消化,也分享给有需要的小伙伴

感兴趣的小伙伴也可以点击 这里,查看前端方面的其他面试题,欢迎 star 关注

如果文章中有出现纰漏、错误之处,还请看到的小伙伴留言指正,先行谢过

以下 ↓

1. 说一下Vue的双向绑定数据的原理

vue 实现数据双向绑定主要是:采用数据劫持结合发布者-订阅者模式的方式,通过 Object.defineProperty() 来劫持各个属性的 settergetter,在数据变动时发布消息给订阅者,触发相应监听回调

2. 解释单向数据流和双向数据绑定

单向数据流: 顾名思义,数据流是单向的。数据流动方向可以跟踪,流动单一,追查问题的时候可以更快捷。缺点就是写起来不太方便。要使UI发生变更就必须创建各种 action 来维护对应的 state

双向数据绑定:数据之间是相通的,将数据变更的操作隐藏在框架内部。优点是在表单交互较多的场景下,会简化大量与业务无关的代码。缺点就是无法追踪局部状态的变化,增加了出错时 debug 的难度

3. Vue 如何去除url中的 #

vue-router 默认使用 hash 模式,所以在路由加载的时候,项目中的 url 会自带 #。如果不想使用 #, 可以使用 vue-router 的另一种模式 history

new Router({
  mode: 'history',
  routes: [ ]
})
需要注意的是,当我们启用 history 模式的时候,由于我们的项目是一个单页面应用,所以在路由跳转的时候,就会出现访问不到静态资源而出现 404 的情况,这时候就需要服务端增加一个覆盖所有情况的候选资源:如果 URL 匹配不到任何静态资源,则应该返回同一个 index.html 页面

4. 对 MVC、MVVM的理解

MVC

image

特点:

  1. View 传送指令到 Controller
  2. Controller 完成业务逻辑后,要求 Model 改变状态
  3. Model 将新的数据发送到 View,用户得到反馈

所有通信都是单向的

MVVM

image

特点:

  1. 各部分之间的通信,都是双向的
  2. 采用双向绑定:View 的变动,自动反映在 ViewModel,反之亦然

具体请移步 这里

5. 介绍虚拟DOM

参考这里

6. vue生命周期的理解

vue实例有一个完整的生命周期,生命周期也就是指一个实例从开始创建到销毁的这个过程
  • beforeCreate() 在实例创建之间执行,数据未加载状态
  • created() 在实例创建、数据加载后,能初始化数据,dom渲染之前执行
  • beforeMount() 虚拟dom已创建完成,在数据渲染前最后一次更改数据
  • mounted() 页面、数据渲染完成,真实dom挂载完成
  • beforeUpadate() 重新渲染之前触发
  • updated() 数据已经更改完成,dom 也重新 render 完成,更改数据会陷入死循环
  • beforeDestory()destoryed() 前者是销毁前执行(实例仍然完全可用),后者则是销毁后执行

7. 组件通信

父组件向子组件通信

子组件通过 props 属性,绑定父组件数据,实现双方通信

子组件向父组件通信

将父组件的事件在子组件中通过 $emit 触发

非父子组件、兄弟组件之间的数据传递
/*新建一个Vue实例作为中央事件总嫌*/
let event = new Vue();

/*监听事件*/
event.$on('eventName', (val) => {
    //......do something
});

/*触发事件*/
event.$emit('eventName', 'this is a message.')
Vuex 数据管理

8. vue-router 路由实现

路由就是用来跟后端服务器进行交互的一种方式,通过不同的路径,来请求不同的资源,请求不同的页面是路由的其中一种功能

参考 这里

9. v-if 和 v-show 区别

使用了 v-if 的时候,如果值为 false ,那么页面将不会有这个 html 标签生成。

v-show 则是不管值为 true 还是 falsehtml 元素都会存在,只是 CSS 中的 display 显示或隐藏

10. $route和$router的区别

$routerVueRouter 实例,想要导航到不同 URL,则使用 $router.push 方法

$route 为当前 router 跳转对象里面可以获取 namepathqueryparams

11. NextTick 是做什么的

$nextTick 是在下次 DOM 更新循环结束之后执行延迟回调,在修改数据之后使用 $nextTick,则可以在回调中获取更新后的 DOM

具体可参考官方文档 深入响应式原理

12. Vue 组件 data 为什么必须是函数

因为js本身的特性带来的,如果 data 是一个对象,那么由于对象本身属于引用类型,当我们修改其中的一个属性时,会影响到所有Vue实例的数据。如果将 data 作为一个函数返回一个对象,那么每一个实例的 data 属性都是独立的,不会相互影响了

13. 计算属性computed 和事件 methods 有什么区别

我们可以将同一函数定义为一个 method 或者一个计算属性。对于最终的结果,两种方式是相同的

不同点:

computed: 计算属性是基于它们的依赖进行缓存的,只有在它的相关依赖发生改变时才会重新求值

对于 method ,只要发生重新渲染,method 调用总会执行该函数

14. 对比 jQuery ,Vue 有什么不同

jQuery 专注视图层,通过操作 DOM 去实现页面的一些逻辑渲染; Vue 专注于数据层,通过数据的双向绑定,最终表现在 DOM 层面,减少了 DOM 操作

Vue 使用了组件化思想,使得项目子集职责清晰,提高了开发效率,方便重复利用,便于协同开发

15. Vue 中怎么自定义指令

全局注册
// 注册一个全局自定义指令 `v-focus`
Vue.directive('focus', {
  // 当被绑定的元素插入到 DOM 中时……
  inserted: function (el) {
    // 聚焦元素
    el.focus()
  }
})
局部注册
directives: {
  focus: {
    // 指令的定义
    inserted: function (el) {
      el.focus()
    }
  }
}

参考 官方文档-自定义指令

16. Vue 中怎么自定义过滤器

可以用全局方法 Vue.filter() 注册一个自定义过滤器,它接收两个参数:过滤器 ID 和过滤器函数。过滤器函数以值为参数,返回转换后的值
Vue.filter('reverse', function (value) {
  return value.split('').reverse().join('')
})
<!-- 'abc' => 'cba' -->
<span v-text="message | reverse"></span>

过滤器也同样接受全局注册和局部注册

17. 对 keep-alive 的了解

keep-aliveVue 内置的一个组件,可以使被包含的组件保留状态,或避免重新渲染
<keep-alive>
  <component>
    <!-- 该组件将被缓存! -->
  </component>
</keep-alive>
可以使用API提供的props,实现组件的动态缓存

具体参考 官方API

18. Vue 中 key 的作用

key 的特殊属性主要用在 Vue 的虚拟 DOM 算法,在新旧 nodes 对比时辨识 VNodes。如果不使用 keyVue 会使用一种最大限度减少动态元素并且尽可能的尝试修复/再利用相同类型元素的算法。使用 key,它会基于 key 的变化重新排列元素顺序,并且会移除 key 不存在的元素。

有相同父元素的子元素必须有独特的 key。重复的 key 会造成渲染错误

具体参考 官方API

19. Vue 的核心是什么

数据驱动 组件系统

20. vue 等单页面应用的优缺点

优点:
  • 良好的交互体验
  • 良好的前后端工作分离模式
  • 减轻服务器压力
缺点:
  • SEO难度较高
  • 前进、后退管理
  • 初次加载耗时多

21. vue-router 使用params与query传参有什么区别

vue-router 可以通过 paramsquery 进行传参

// 传递
this.$router.push({path: './xxx', params: {xx:xxx}})
this.$router.push({path: './xxx', query: {xx:xxx}})

// 接收
this.$route.params

this.$route.query
  • params 是路由的一部分,必须要有。query 是拼接在 url 后面的参数,没有也没关系
  • params 不设置的时候,刷新页面或者返回参数会丢,query 则不会有这个问题

后记

整理的过程也是重新梳理知识点的过程,途中会发现很多自己理解不是很到位的东西,也算是一种收获吧

感兴趣的小伙伴可以 点击这里 ,也可以扫描下方二维码关注我的微信公众号,查看更多前端小片段,欢迎 star 关注

image

查看原文

赞 386 收藏 301 评论 14

蓝染 发布了文章 · 2019-04-12

javascript 闭包

闭包

一、闭包是什么?

将一个 词法作用域 中的 内部函数 作为一个 一级值类型 到处传递,就形成了闭包。

怎么去理解呢?这里要敲黑板划重点了,上面的概念性文字介绍了三个点:

  • 词法作用域(函数)
  • 内部函数
  • 一级值类型传递

1、先说词法作用域

形成一个作用域最常见的就是函数了,函数内部会形成一个内部作用域,然后还有 let 、const 以及像 try/catch 结构中的 catch 分句形成的块作用域。

let 就是为其声明的变量隐式劫持了所在的块作用域,这个在后面讲 let 和闭包的时候会详细说明 let 和闭包结合的用法。

通过了解可以知道,这里的作用域其实就是函数的内部作用域

2、内部函数

内部函数不用介绍了吧,在词法作用域中定义的函数,传递后具有涵盖自身所在作用域的闭包。

3、一级值类型传递

值类型传递方式有很多种啊,函数里面的一级值传递无非就是:返回值return )、赋值( 赋值给外部变量 )、参数传递( 作为参数传递给外部函数 )。

现在可以画一个基本的闭包出来了:

//三种传递方法①②③分开看,你可以的。

var fn;                //定义全局变量,用于内部赋值        ---②
function foo() {
    var a = 2;
    function bar() {
        console.log(a);
    };
    return bar;        //返回值        ---①
    fn = bar;        //赋值        ---②
    baz(bar);        //参数传递    ---③
};

//定义外部函数,用于使用内部分配给全局变量的函数    ---②
function cat() {
    fn();
};

//定义外部函数,用于内部参数传递        ---③
function baz(func) {
    func();
};

foo();        //2       ---①
cat();        //2        ---②
baz();        //2        ---③

再来一例:

function wait(message) {
    setTimeout(function timer(){
        console.log(message);
    },1000);
}
wait("Hi Baby");

解析一下,按照我们前面的思路可以贯穿下来:

首先 wait(..) 里面的作用域,作用域内部的 timer(..)函数,再将内部函数 timer(..) 传递给内置工具函数 setTimeout(..),setTimeout(...)有参数引用( 也就是我们传递的 timer(...) ),然后调用它。

整个过程行云流水,然后词法作用域在这个过程中保持完璧之身。OK!

二、循环中的闭包

说到循环闭包就要掏出大家耳熟能详的栗子了。

for(var i = 1; i <= 5; i++) {
    setTimeout(function timer() {
        console.log(i);
    },i*1000)
}

666!好!输出了几个6,老铁有点懵逼,不知应该扎心还是双击666。

为何?

你大爷还是你大爷,即使你在每次迭代都定义了函数,但是都在共享全局作用域中,i 还是这个 i

那要怎么解决?

这时候在每个迭代的时候加上一个闭包作用域,并且你得把这个 i 大爷放进作用域中。

//放法可以是传参①,可以是赋值②

for(var i = 1; i <= 5; i++) {
    //这里先搞一个闭包作用域,派出我们的 IIFE
    (function(j) {        // ---①
        setTimeout(function timer() {
            console.log(j);
        },j*1000)
    })(i);        // ---①

    (function() {    
        var j = i;        // ---②
        setTimeout(function timer() {
            console.log(j);
        },j*1000)
    })();    
}

上面这个是用了闭包作用域,每次迭代都生成一个新的作用域,来封闭内部变量。

说到这里,前面提到的 let 应该还有人记得,let 干嘛用的,不就是劫持变量形成块作用域吗? 放在这里不是恰到好处? 来一发。

for(let i = 1; i <= 5; i++) {
    setTimeout(function timer() {
        console.log(i)
    },i*1000)
}

直接在定义 i 大爷的地方就 "绑架" 了他。

或者,你也可以麻烦一点,先让他上迭代车,上车之后再 let 定义一个变量把 i 大爷赋给他,两种都行,简单点好。

三、总结一下闭包应用

定时器、事件监听器、Ajax请求、跨窗口通信、Web Workers 或者其他的异步(或同步)任务中(balabala~~~~),只要使用了回调函数,就是在使用闭包。

还有一处重要的 模块

模块的两个重要特征:

  1. 有外部包装函数(创建内部作用域)且需要被调用。
  2. 外部包装函数返回值至少引用一个内部函数(创建包装函数内部作用域闭包)。
查看原文

赞 0 收藏 0 评论 0

蓝染 赞了文章 · 2019-04-09

【面试篇】寒冬求职季之你必须要懂的原生JS(上)

互联网寒冬之际,各大公司都缩减了HC,甚至是采取了“裁员”措施,在这样的大环境之下,想要获得一份更好的工作,必然需要付出更多的努力。

一年前,也许你搞清楚闭包,this,原型链,就能获得认可。但是现在,很显然是不行了。本文梳理出了一些面试中有一定难度的高频原生JS问题,部分知识点可能你之前从未关注过,或者看到了,却没有仔细研究,但是它们却非常重要。本文将以真实的面试题的形式来呈现知识点,大家在阅读时,建议不要先看我的答案,而是自己先思考一番。尽管,本文所有的答案,都是我在翻阅各种资料,思考并验证之后,才给出的(绝非复制粘贴而来)。但因水平有限,本人的答案未必是最优的,如果您有更好的答案,欢迎给我留言。

本文篇幅较长,但是满满的都是干货!并且还埋伏了可爱的表情包,希望小伙伴们能够坚持读完。

衷心的祝愿大家都能找到心仪的工作。

1. 原始类型有哪几种?null 是对象吗?原始数据类型和复杂数据类型存储有什么区别?

  • 原始类型有6种,分别是undefined,null,bool,string,number,symbol(ES6新增)。
  • 虽然 typeof null 返回的值是 object,但是null不是对象,而是基本数据类型的一种。
  • 原始数据类型存储在栈内存,存储的是值。
  • 复杂数据类型存储在堆内存,存储的是地址。当我们把对象赋值给另外一个变量的时候,复制的是地址,指向同一块内存空间,当其中一个对象改变时,另一个对象也会变化。

2. typeof 是否正确判断类型? instanceof呢? instanceof 的实现原理是什么?

首先 typeof 能够正确的判断基本数据类型,但是除了 null, typeof null输出的是对象。

但是对象来说,typeof 不能正确的判断其类型, typeof 一个函数可以输出 'function',而除此之外,输出的全是 object,这种情况下,我们无法准确的知道对象的类型。

instanceof可以准确的判断复杂数据类型,但是不能正确判断基本数据类型。(正确判断数据类型请戳:https://github.com/YvetteLau/...

instanceof 是通过原型链判断的,A instanceof B, 在A的原型链中层层查找,是否有原型等于B.prototype,如果一直找到A的原型链的顶端(null;即Object.prototype.__proto__),仍然不等于B.prototype,那么返回false,否则返回true.

instanceof的实现代码:

// L instanceof R
function instance_of(L, R) {//L 表示左表达式,R 表示右表达式
    var O = R.prototype;// 取 R 的显式原型
    L = L.__proto__;    // 取 L 的隐式原型
    while (true) { 
        if (L === null) //已经找到顶层
            return false;  
        if (O === L)   //当 O 严格等于 L 时,返回 true
            return true; 
        L = L.__proto__;  //继续向上一层原型链查找
    } 
}

3. for of , for in 和 forEach,map 的区别。

  • for...of循环:具有 iterator 接口,就可以用for...of循环遍历它的成员(属性值)。for...of循环可以使用的范围包括数组、Set 和 Map 结构、某些类似数组的对象、Generator 对象,以及字符串。for...of循环调用遍历器接口,数组的遍历器接口只返回具有数字索引的属性。对于普通的对象,for...of结构不能直接使用,会报错,必须部署了 Iterator 接口后才能使用。可以中断循环。
  • for...in循环:遍历对象自身的和继承的可枚举的属性, 不能直接获取属性值。可以中断循环。
  • forEach: 只能遍历数组,不能中断,没有返回值(或认为返回值是undefined)。
  • map: 只能遍历数组,不能中断,返回值是修改后的数组。

PS: Object.keys():返回给定对象所有可枚举属性的字符串数组。

关于forEach是否会改变原数组的问题,有些小伙伴提出了异议,为此我写了代码测试了下(注意数组项是复杂数据类型的情况)。
除了forEach之外,map等API,也有同样的问题。

let arry = [1, 2, 3, 4];

arry.forEach((item) => {
    item *= 10;
});
console.log(arry); //[1, 2, 3, 4]

arry.forEach((item) => {
    arry[1] = 10; //直接操作数组
});
console.log(arry); //[ 1, 10, 3, 4 ]

let arry2 = [
    { name: "Yve" },
    { age: 20 }
];
arry2.forEach((item) => {
    item.name = 10;
});
console.log(arry2);//[ { name: 10 }, { age: 20, name: 10 } ]

如还不了解 iterator 接口或 for...of, 请先阅读ES6文档: Iterator 和 for...of 循环

更多细节请戳: https://github.com/YvetteLau/...


4. 如何判断一个变量是不是数组?

  • 使用 Array.isArray 判断,如果返回 true, 说明是数组
  • 使用 instanceof Array 判断,如果返回true, 说明是数组
  • 使用 Object.prototype.toString.call 判断,如果值是 [object Array], 说明是数组
  • 通过 constructor 来判断,如果是数组,那么 arr.constructor === Array. (不准确,因为我们可以指定 obj.constructor = Array)
function fn() {
    console.log(Array.isArray(arguments));   //false; 因为arguments是类数组,但不是数组
    console.log(Array.isArray([1,2,3,4]));   //true
    console.log(arguments instanceof Array); //fasle
    console.log([1,2,3,4] instanceof Array); //true
    console.log(Object.prototype.toString.call(arguments)); //[object Arguments]
    console.log(Object.prototype.toString.call([1,2,3,4])); //[object Array]
    console.log(arguments.constructor === Array); //false
    arguments.constructor = Array;
    console.log(arguments.constructor === Array); //true
    console.log(Array.isArray(arguments));        //false
}
fn(1,2,3,4);

5. 类数组和数组的区别是什么?

类数组:

1)拥有length属性,其它属性(索引)为非负整数(对象中的索引会被当做字符串来处理);

2)不具有数组所具有的方法;

类数组是一个普通对象,而真实的数组是Array类型。

常见的类数组有: 函数的参数 arugments, DOM 对象列表(比如通过 document.querySelectorAll 得到的列表), jQuery 对象 (比如 $("div")).

类数组可以转换为数组:

//第一种方法
Array.prototype.slice.call(arrayLike, start);
//第二种方法
[...arrayLike];
//第三种方法:
Array.from(arrayLike);

PS: 任何定义了遍历器(Iterator)接口的对象,都可以用扩展运算符转为真正的数组。

Array.from方法用于将两类对象转为真正的数组:类似数组的对象(array-like object)和可遍历(iterable)的对象。


6. == 和 === 有什么区别?

=== 不需要进行类型转换,只有类型相同并且值相等时,才返回 true.

== 如果两者类型不同,首先需要进行类型转换。具体流程如下:

  1. 首先判断两者类型是否相同,如果相等,判断值是否相等.
  2. 如果类型不同,进行类型转换
  3. 判断比较的是否是 null 或者是 undefined, 如果是, 返回 true .
  4. 判断两者类型是否为 string 和 number, 如果是, 将字符串转换成 number
  5. 判断其中一方是否为 boolean, 如果是, 将 boolean 转为 number 再进行判断
  6. 判断其中一方是否为 object 且另一方为 string、number 或者 symbol , 如果是, 将 object 转为原始类型再进行判断
let person1 = {
    age: 25
}
let person2 = person1;
person2.gae = 20;
console.log(person1 === person2); //true,注意复杂数据类型,比较的是引用地址
思考: [] == ![]

我们来分析一下: [] == ![] 是true还是false?

  1. 首先,我们需要知道 ! 优先级是高于 == (更多运算符优先级可查看: 运算符优先级)
  2. ![] 引用类型转换成布尔值都是true,因此![]的是false
  3. 根据上面的比较步骤中的第五条,其中一方是 boolean,将 boolean 转为 number 再进行判断,false转换成 number,对应的值是 0.
  4. 根据上面比较步骤中的第六条,有一方是 number,那么将object也转换成Number,空数组转换成数字,对应的值是0.(空数组转换成数字,对应的值是0,如果数组中只有一个数字,那么转成number就是这个数字,其它情况,均为NaN)
  5. 0 == 0; 为true

7. ES6中的class和ES5的类有什么区别?

  1. ES6 class 内部所有定义的方法都是不可枚举的;
  2. ES6 class 必须使用 new 调用;
  3. ES6 class 不存在变量提升;
  4. ES6 class 默认即是严格模式;
  5. ES6 class 子类必须在父类的构造函数中调用super(),这样才有this对象;ES5中类继承的关系是相反的,先有子类的this,然后用父类的方法应用在this上。

8. 数组的哪些API会改变原数组?

修改原数组的API有:

splice/reverse/fill/copyWithin/sort/push/pop/unshift/shift

不修改原数组的API有:

slice/map/forEach/every/filter/reduce/entries/find

注: 数组的每一项是简单数据类型,且未直接操作数组的情况下。


9. let、const 以及 var 的区别是什么?

  • let 和 const 定义的变量不会出现变量提升,而 var 定义的变量会提升。
  • let 和 const 是JS中的块级作用域
  • let 和 const 不允许重复声明(会抛出错误)
  • let 和 const 定义的变量在定义语句之前,如果使用会抛出错误(形成了暂时性死区),而 var 不会。
  • const 声明一个只读的常量。一旦声明,常量的值就不能改变(如果声明是一个对象,那么不能改变的是对象的引用地址)

10. 在JS中什么是变量提升?什么是暂时性死区?

变量提升就是变量在声明之前就可以使用,值为undefined。

在代码块内,使用 let/const 命令声明变量之前,该变量都是不可用的(会抛出错误)。这在语法上,称为“暂时性死区”。暂时性死区也意味着 typeof 不再是一个百分百安全的操作。

typeof x; // ReferenceError(暂时性死区,抛错)
let x;
typeof y; // 值是undefined,不会报错

暂时性死区的本质就是,只要一进入当前作用域,所要使用的变量就已经存在了,但是不可获取,只有等到声明变量的那一行代码出现,才可以获取和使用该变量。


11. 如何正确的判断this? 箭头函数的this是什么?

this的绑定规则有四种:默认绑定,隐式绑定,显式绑定,new绑定.

  1. 函数是否在 new 中调用(new绑定),如果是,那么 this 绑定的是新创建的对象。
  2. 函数是否通过 call,apply 调用,或者使用了 bind (即硬绑定),如果是,那么this绑定的就是指定的对象。
  3. 函数是否在某个上下文对象中调用(隐式绑定),如果是的话,this 绑定的是那个上下文对象。一般是 obj.foo()
  4. 如果以上都不是,那么使用默认绑定。如果在严格模式下,则绑定到 undefined,否则绑定到全局对象。
  5. 如果把 null 或者 undefined 作为 this 的绑定对象传入 call、apply 或者 bind, 这些值在调用时会被忽略,实际应用的是默认绑定规则。
  6. 箭头函数没有自己的 this, 它的this继承于上一层代码块的this。

测试下是否已经成功Get了此知识点(浏览器执行环境):

var number = 5;
var obj = {
    number: 3,
    fn1: (function () {
        var number;
        this.number *= 2;
        number = number * 2;
        number = 3;
        return function () {
            var num = this.number;
            this.number *= 2;
            console.log(num);
            number *= 3;
            console.log(number);
        }
    })()
}
var fn1 = obj.fn1;
fn1.call(null);
obj.fn1();
console.log(window.number);

如果this的知识点,您还不太懂,请戳: 嗨,你真的懂this吗?


12. 词法作用域和this的区别。

  • 词法作用域是由你在写代码时将变量和块作用域写在哪里来决定的
  • this 是在调用时被绑定的,this 指向什么,完全取决于函数的调用位置(关于this的指向问题,本文已经有说明)

13. 谈谈你对JS执行上下文栈和作用域链的理解。

执行上下文就是当前 JavaScript 代码被解析和执行时所在环境, JS执行上下文栈可以认为是一个存储函数调用的栈结构,遵循先进后出的原则。

  • JavaScript执行在单线程上,所有的代码都是排队执行。
  • 一开始浏览器执行全局的代码时,首先创建全局的执行上下文,压入执行栈的顶部。
  • 每当进入一个函数的执行就会创建函数的执行上下文,并且把它压入执行栈的顶部。当前函数执行-完成后,当前函数的执行上下文出栈,并等待垃圾回收。
  • 浏览器的JS执行引擎总是访问栈顶的执行上下文。
  • 全局上下文只有唯一的一个,它在浏览器关闭时出栈。

作用域链: 无论是 LHS 还是 RHS 查询,都会在当前的作用域开始查找,如果没有找到,就会向上级作用域继续查找目标标识符,每次上升一个作用域,一直到全局作用域为止。


题难不难?不难!继续挑战一下难!知道难,就更要继续了!

14. 什么是闭包?闭包的作用是什么?闭包有哪些使用场景?

闭包是指有权访问另一个函数作用域中的变量的函数,创建闭包最常用的方式就是在一个函数内部创建另一个函数。

闭包的作用有:

  1. 封装私有变量
  2. 模仿块级作用域(ES5中没有块级作用域)
  3. 实现JS的模块

15. call、apply有什么区别?call,aplly和bind的内部是如何实现的?

call 和 apply 的功能相同,区别在于传参的方式不一样:

  • fn.call(obj, arg1, arg2, ...),调用一个函数, 具有一个指定的this值和分别地提供的参数(参数的列表)。
  • fn.apply(obj, [argsArray]),调用一个函数,具有一个指定的this值,以及作为一个数组(或类数组对象)提供的参数。
call核心:
  • 将函数设为传入参数的属性
  • 指定this到函数并传入给定参数执行函数
  • 如果不传入参数或者参数为null,默认指向为 window / global
  • 删除参数上的函数
Function.prototype.call = function (context) {
    /** 如果第一个参数传入的是 null 或者是 undefined, 那么指向this指向 window/global */
    /** 如果第一个参数传入的不是null或者是undefined, 那么必须是一个对象 */
    if (!context) {
        //context为null或者是undefined
        context = typeof window === 'undefined' ? global : window;
    }
    context.fn = this; //this指向的是当前的函数(Function的实例)
    let args = [...arguments].slice(1);//获取除了this指向对象以外的参数, 空数组slice后返回的仍然是空数组
    let result = context.fn(...args); //隐式绑定,当前函数的this指向了context.
    delete context.fn;
    return result;
}

//测试代码
var foo = {
    name: 'Selina'
}
var name = 'Chirs';
function bar(job, age) {
    console.log(this.name);
    console.log(job, age);
}
bar.call(foo, 'programmer', 20);
// Selina programmer 20
bar.call(null, 'teacher', 25);
// 浏览器环境: Chirs teacher 25; node 环境: undefined teacher 25
apply:

apply的实现和call很类似,但是需要注意他们的参数是不一样的,apply的第二个参数是数组或类数组.

Function.prototype.apply = function (context, rest) {
    if (!context) {
        //context为null或者是undefined时,设置默认值
        context = typeof window === 'undefined' ? global : window;
    }
    context.fn = this;
    let result;
    if(rest === undefined || rest === null) {
        //undefined 或者 是 null 不是 Iterator 对象,不能被 ...
        result = context.fn(rest);
    }else if(typeof rest === 'object') {
        result = context.fn(...rest);
    }
    delete context.fn;
    return result;
}
var foo = {
    name: 'Selina'
}
var name = 'Chirs';
function bar(job, age) {
    console.log(this.name);
    console.log(job, age);
}
bar.apply(foo, ['programmer', 20]);
// Selina programmer 20
bar.apply(null, ['teacher', 25]);
// 浏览器环境: Chirs programmer 20; node 环境: undefined teacher 25
bind

bind 和 call/apply 有一个很重要的区别,一个函数被 call/apply 的时候,会直接调用,但是 bind 会创建一个新函数。当这个新函数被调用时,bind() 的第一个参数将作为它运行时的 this,之后的一序列参数将会在传递的实参前传入作为它的参数。

Function.prototype.my_bind = function(context) {
    if(typeof this !== "function"){
        throw new TypeError("not a function");
    }
    let self = this;
    let args = [...arguments].slice(1);
    function Fn() {};
    Fn.prototype = this.prototype;
    let bound = function() {
        let res = [...args, ...arguments]; //bind传递的参数和函数调用时传递的参数拼接
        context = this instanceof Fn ? this : context || this;
        return self.apply(context, res);
    }
    //原型链
    bound.prototype = new Fn();
    return bound;
}

var name = 'Jack';
function person(age, job, gender){
    console.log(this.name , age, job, gender);
}
var Yve = {name : 'Yvette'};
let result = person.my_bind(Yve, 22, 'enginner')('female');    

16. new的原理是什么?通过new的方式创建对象和通过字面量创建有什么区别?

new:
  1. 创建一个新对象。
  2. 这个新对象会被执行[[原型]]连接。
  3. 将构造函数的作用域赋值给新对象,即this指向这个新对象.
  4. 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象。
function new(func) {
    lat target = {};
    target.__proto__ = func.prototype;
    let res = func.call(target);
    if (typeof(res) == "object" || typeof(res) == "function") {
        return res;
    }
    return target;
}

字面量创建对象,不会调用 Object构造函数, 简洁且性能更好;

new Object() 方式创建对象本质上是方法调用,涉及到在proto链中遍历该方法,当找到该方法后,又会生产方法调用必须的 堆栈信息,方法调用结束后,还要释放该堆栈,性能不如字面量的方式。

通过对象字面量定义对象时,不会调用Object构造函数。


17. 谈谈你对原型的理解?

在 JavaScript 中,每当定义一个对象(函数也是对象)时候,对象中都会包含一些预定义的属性。其中每个函数对象都有一个prototype 属性,这个属性指向函数的原型对象。使用原型对象的好处是所有对象实例共享它所包含的属性和方法。


18. 什么是原型链?【原型链解决的是什么问题?】

原型链解决的主要是继承问题。

每个对象拥有一个原型对象,通过 proto (读音: dunder proto) 指针指向其原型对象,并从中继承方法和属性,同时原型对象也可能拥有原型,这样一层一层,最终指向 null(Object.proptotype.__proto__ 指向的是null)。这种关系被称为原型链 (prototype chain),通过原型链一个对象可以拥有定义在其他对象中的属性和方法。

构造函数 Parent、Parent.prototype 和 实例 p 的关系如下:(p.__proto__ === Parent.prototype)


19. prototype 和 __proto__ 区别是什么?

prototype是构造函数的属性。

__proto__ 是每个实例都有的属性,可以访问 [[prototype]] 属性。

实例的__proto__ 与其构造函数的prototype指向的是同一个对象。

function Student(name) {
    this.name = name;
}
Student.prototype.setAge = function(){
    this.age=20;
}
let Jack = new Student('jack');
console.log(Jack.__proto__);
//console.log(Object.getPrototypeOf(Jack));;
console.log(Student.prototype);
console.log(Jack.__proto__ === Student.prototype);//true

20. 使用ES5实现一个继承?

组合继承(最常用的继承方式)
function SuperType() {
    this.name = name;
    this.colors = ['red', 'blue', 'green'];
}
SuperType.prototype.sayName = function() {
    console.log(this.name);
}

function SubType(name, age) {
    SuperType.call(this, name);
    this.age = age;
}
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;

SubType.prototype.sayAge = function() {
    console.log(this.age);
}

其它继承方式实现,可以参考《JavaScript高级程序设计》


21. 什么是深拷贝?深拷贝和浅拷贝有什么区别?

浅拷贝是指只复制第一层对象,但是当对象的属性是引用类型时,实质复制的是其引用,当引用指向的值改变时也会跟着变化。

深拷贝复制变量值,对于非基本类型的变量,则递归至基本类型变量后,再复制。深拷贝后的对象与原来的对象是完全隔离的,互不影响,对一个对象的修改并不会影响另一个对象。

实现一个深拷贝:

function deepClone(obj) { //递归拷贝
    if(obj === null) return null; //null 的情况
    if(obj instanceof RegExp) return new RegExp(obj);
    if(obj instanceof Date) return new Date(obj);
    if(typeof obj !== 'object') {
        //如果不是复杂数据类型,直接返回
        return obj;
    }
    /**
     * 如果obj是数组,那么 obj.constructor 是 [Function: Array]
     * 如果obj是对象,那么 obj.constructor 是 [Function: Object]
     */
    let t = new obj.constructor();
    for(let key in obj) {
        //如果 obj[key] 是复杂数据类型,递归
        t[key] = deepClone(obj[key]);
    }
    return t;
}

看不下去了?别人的送分题会成为你的送命题

22. 防抖和节流的区别是什么?防抖和节流的实现。

防抖和节流的作用都是防止函数多次调用。区别在于,假设一个用户一直触发这个函数,且每次触发函数的间隔小于设置的时间,防抖的情况下只会调用一次,而节流的情况会每隔一定时间调用一次函数。

防抖(debounce): n秒内函数只会执行一次,如果n秒内高频事件再次被触发,则重新计算时间
function debounce(func, wait, immediate=true) {
    let timeout, context, args;
        // 延迟执行函数
        const later = () => setTimeout(() => {
            // 延迟函数执行完毕,清空定时器
            timeout = null
            // 延迟执行的情况下,函数会在延迟函数中执行
            // 使用到之前缓存的参数和上下文
            if (!immediate) {
                func.apply(context, args);
                context = args = null;
            }
        }, wait);
        let debounced = function (...params) {
            if (!timeout) {
                timeout = later();
                if (immediate) {
                    //立即执行
                    func.apply(this, params);
                } else {
                    //闭包
                    context = this;
                    args = params;
                }
            } else {
                clearTimeout(timeout);
                timeout = later();
            }
        }
    debounced.cancel = function () {
        clearTimeout(timeout);
        timeout = null;
    };
    return debounced;
};

防抖的应用场景:

  • 每次 resize/scroll 触发统计事件
  • 文本输入的验证(连续输入文字后发送 AJAX 请求进行验证,验证一次就好)
节流(throttle): 高频事件在规定时间内只会执行一次,执行一次后,只有大于设定的执行周期后才会执行第二次。
//underscore.js
function throttle(func, wait, options) {
    var timeout, context, args, result;
    var previous = 0;
    if (!options) options = {};

    var later = function () {
        previous = options.leading === false ? 0 : Date.now() || new Date().getTime();
        timeout = null;
        result = func.apply(context, args);
        if (!timeout) context = args = null;
    };

    var throttled = function () {
        var now = Date.now() || new Date().getTime();
        if (!previous && options.leading === false) previous = now;
        var remaining = wait - (now - previous);
        context = this;
        args = arguments;
        if (remaining <= 0 || remaining > wait) {
            if (timeout) {
                clearTimeout(timeout);
                timeout = null;
            }
            previous = now;
            result = func.apply(context, args);
            if (!timeout) context = args = null;
        } else if (!timeout && options.trailing !== false) {
            // 判断是否设置了定时器和 trailing
            timeout = setTimeout(later, remaining);
        }
        return result;
    };

    throttled.cancel = function () {
        clearTimeout(timeout);
        previous = 0;
        timeout = context = args = null;
    };

    return throttled;
};

函数节流的应用场景有:

  • DOM 元素的拖拽功能实现(mousemove)
  • 射击游戏的 mousedown/keydown 事件(单位时间只能发射一颗子弹)
  • 计算鼠标移动的距离(mousemove)
  • Canvas 模拟画板功能(mousemove)
  • 搜索联想(keyup)
  • 监听滚动事件判断是否到页面底部自动加载更多:给 scroll 加了 debounce 后,只有用户停止滚动后,才会判断是否到了页面底部;如果是 throttle 的话,只要页面滚动就会间隔一段时间判断一次

23. 取数组的最大值(ES5、ES6)

// ES5 的写法
Math.max.apply(null, [14, 3, 77, 30]);

// ES6 的写法
Math.max(...[14, 3, 77, 30]);

// reduce
[14,3,77,30].reduce((accumulator, currentValue)=>{
    return accumulator = accumulator > currentValue ? accumulator : currentValue
});

24. ES6新的特性有哪些?

  1. 新增了块级作用域(let,const)
  2. 提供了定义类的语法糖(class)
  3. 新增了一种基本数据类型(Symbol)
  4. 新增了变量的解构赋值
  5. 函数参数允许设置默认值,引入了rest参数,新增了箭头函数
  6. 数组新增了一些API,如 isArray / from / of 方法;数组实例新增了 entries(),keys() 和 values() 等方法
  7. 对象和数组新增了扩展运算符
  8. ES6 新增了模块化(import/export)
  9. ES6 新增了 Set 和 Map 数据结构
  10. ES6 原生提供 Proxy 构造函数,用来生成 Proxy 实例
  11. ES6 新增了生成器(Generator)和遍历器(Iterator)

25. setTimeout倒计时为什么会出现误差?

setTimeout() 只是将事件插入了“任务队列”,必须等当前代码(执行栈)执行完,主线程才会去执行它指定的回调函数。要是当前代码消耗时间很长,也有可能要等很久,所以并没办法保证回调函数一定会在 setTimeout() 指定的时间执行。所以, setTimeout() 的第二个参数表示的是最少时间,并非是确切时间。

HTML5标准规定了 setTimeout() 的第二个参数的最小值不得小于4毫秒,如果低于这个值,则默认是4毫秒。在此之前。老版本的浏览器都将最短时间设为10毫秒。另外,对于那些DOM的变动(尤其是涉及页面重新渲染的部分),通常是间隔16毫秒执行。这时使用 requestAnimationFrame() 的效果要好于 setTimeout();


26. 为什么 0.1 + 0.2 != 0.3 ?

0.1 + 0.2 != 0.3 是因为在进制转换和进阶运算的过程中出现精度损失。

下面是详细解释:

JavaScript使用 Number 类型表示数字(整数和浮点数),使用64位表示一个数字。

图片说明:

  • 第0位:符号位,0表示正数,1表示负数(s)
  • 第1位到第11位:储存指数部分(e)
  • 第12位到第63位:储存小数部分(即有效数字)f

计算机无法直接对十进制的数字进行运算, 需要先对照 IEEE 754 规范转换成二进制,然后对阶运算。

1.进制转换

0.1和0.2转换成二进制后会无限循环

0.1 -> 0.0001100110011001...(无限循环)
0.2 -> 0.0011001100110011...(无限循环)

但是由于IEEE 754尾数位数限制,需要将后面多余的位截掉,这样在进制之间的转换中精度已经损失。

2.对阶运算

由于指数位数不相同,运算时需要对阶运算 这部分也可能产生精度损失。

按照上面两步运算(包括两步的精度损失),最后的结果是

0.0100110011001100110011001100110011001100110011001100

结果转换成十进制之后就是 0.30000000000000004。

27. promise 有几种状态, Promise 有什么优缺点 ?

promise有三种状态: fulfilled, rejected, pending.

Promise 的优点:
  1. 一旦状态改变,就不会再变,任何时候都可以得到这个结果
  2. 可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数
Promise 的缺点:
  1. 无法取消 Promise
  2. 当处于pending状态时,无法得知目前进展到哪一个阶段

28. Promise构造函数是同步还是异步执行,then中的方法呢 ?promise如何实现then处理 ?

Promise的构造函数是同步执行的。then中的方法是异步执行的。

promise的then实现,详见: Promise源码实现


29. Promise和setTimeout的区别 ?

Promise 是微任务,setTimeout 是宏任务,同一个事件循环中,promise总是先于 setTimeout 执行。


30. 如何实现 Promise.all ?

要实现 Promise.all,首先我们需要知道 Promise.all 的功能:

  1. 如果传入的参数是一个空的可迭代对象,那么此promise对象回调完成(resolve),只有此情况,是同步执行的,其它都是异步返回的。
  2. 如果传入的参数不包含任何 promise,则返回一个异步完成.

promises 中所有的promise都“完成”时或参数中不包含 promise 时回调完成。

  1. 如果参数中有一个promise失败,那么Promise.all返回的promise对象失败
  2. 在任何情况下,Promise.all 返回的 promise 的完成状态的结果都是一个数组
Promise.all = function (promises) {
    return new Promise((resolve, reject) => {
        let index = 0;
        let result = [];
        if (promises.length === 0) {
            resolve(result);
        } else {
            setTimeout(() => {
                function processValue(i, data) {
                    result[i] = data;
                    if (++index === promises.length) {
                        resolve(result);
                    }
                }
                for (let i = 0; i < promises.length; i++) {
                    //promises[i] 可能是普通值
                    Promise.resolve(promises[i]).then((data) => {
                        processValue(i, data);
                    }, (err) => {
                        reject(err);
                        return;
                    });
                }
            })
        }
    });
}

如果想了解更多Promise的源码实现,可以参考我的另一篇文章:Promise的源码实现(完美符合Promise/A+规范)


31.如何实现 Promise.finally ?

不管成功还是失败,都会走到finally中,并且finally之后,还可以继续then。并且会将值原封不动的传递给后面的then.

Promise.prototype.finally = function (callback) {
    return this.then((value) => {
        return Promise.resolve(callback()).then(() => {
            return value;
        });
    }, (err) => {
        return Promise.resolve(callback()).then(() => {
            throw err;
        });
    });
}

32. 什么是函数柯里化?实现 sum(1)(2)(3) 返回结果是1,2,3之和

函数柯里化是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。

function sum(a) {
    return function(b) {
        return function(c) {
            return a+b+c;
        }
    }
}
console.log(sum(1)(2)(3)); // 6

引申:实现一个curry函数,将普通函数进行柯里化:

function curry(fn, args = []) {
    return function(){
        let rest = [...args, ...arguments];
        if (rest.length < fn.length) {
            return curry.call(this,fn,rest);
        }else{
            return fn.apply(this,rest);
        }
    }
}
//test
function sum(a,b,c) {
    return a+b+c;
}
let sumFn = curry(sum);
console.log(sumFn(1)(2)(3)); //6
console.log(sumFn(1)(2, 3)); //6

如果您在面试中遇到了更多的原生JS问题,或者有一些本文未涉及到且有一定难度的JS知识,请给我留言。您的问题将会出现在后续文章中~

本文的写成耗费了非常多的时间,在这个过程中,我也学习到了很多知识,谢谢各位小伙伴愿意花费宝贵的时间阅读本文,如果本文给了您一点帮助或者是启发,请不要吝啬你的赞和Star,您的肯定是我前进的最大动力。https://github.com/YvetteLau/...

后续写作计划

1.《寒冬求职季之你必须要懂的原生JS》(中)(下)

2.《寒冬求职季之你必须要知道的CSS》

3.《寒冬求职季之你必须要懂的前端安全》

4.《寒冬求职季之你必须要懂的一些浏览器知识》

5.《寒冬求职季之你必须要知道的性能优化》

针对React技术栈:

1.《寒冬求职季之你必须要懂的React》系列

2.《寒冬求职季之你必须要懂的ReactNative》系列

参考文章:
  1. https://www.ibm.com/developer...
  2. https://juejin.im/post/5c7736...
  3. 选用了面试之道上的部分面试题
  4. 选用了木易杨说文中提及的部分面试题: https://juejin.im/post/5bc92e...
  5. 特别说明: 0.1 + 0.2 !== 0.3 此题答案大量使用了此篇文章的图文: https://juejin.im/post/5b90e0...
  6. 选用了朋友面试大厂时遇到的一些面试题
  7. 《你不知道的JavaSctipt》
  8. 《JavaScript高级程序设计》
  9. https://github.com/hanzichi/u...
推荐关注本人公众号:

clipboard.png

查看原文

赞 213 收藏 167 评论 40

蓝染 赞了文章 · 2019-04-02

web前端性能优化总结

概括

涉及到的分类

  • 网络层面
  • 构建层面
  • 浏览器渲染层面
  • 服务端层面

涉及到的功能点

  • 资源的合并与压缩
  • 图片编解码原理和类型选择
  • 浏览器渲染机制
  • 懒加载预加载
  • 浏览器存储
  • 缓存机制
  • PWA
  • Vue-SSR

资源合并与压缩

http请求的过程及潜在的性能优化点

  • 理解减少http请求数量减少请求资源大小两个优化要点
  • 掌握压缩合并的原理
  • 掌握通过在线网站fis3两种实现压缩与合并的方法

浏览器的一个请求从发送到返回都经历了什么

动态的加载静态的资源

  • dns是否可以通过缓存减少dns查询时间
  • 网络请求的过程走最近的网络环境
  • 相同的静态资源是否可以缓存
  • 能否减少http请求大小
  • 能否减少http请求数量
  • 服务端渲染

资源的合并与压缩设计到的性能点

  • 减少http请求的数量
  • 减少请求的大小

html压缩

HTML代码压缩就是压缩这些在文本文件中有意义,但是在HTML中不显示的字符,包括空格,制表符,换行符等,还有一些其他意义的字符,如HTML注释也可以被压缩

意义

  • 大型网站意义比较大

如何进行html的压缩

  • 使用在线网站进行压缩(走构建工具多,公司级在线网站手动压缩小)
  • node.js提供了html-minifier工具
  • 后端模板引擎渲染压缩

cssjs压缩

css的压缩

  • 无效代码删除

    • 注释、无效字符
  • css语义合并

css压缩的方式

  • 使用在线网站进行压缩
  • 使用html-minifierhtml中的css进行压缩
  • 使用clean-csscss进行压缩

js的压缩语混乱

  • 无效字符的删除

    • 空格、注释、回车等
  • 剔除注释
  • 代码语意的缩减和优化

    • 变量名缩短(a,b)等
  • 代码保护

    • 前端代码是透明的,客户端代码用户是可以直接看到的,可以轻易被窥探到逻辑和漏洞

js压缩的方式

  • 使用在线网站进行压缩
  • 使用html-minifierhtml中的js进行压缩
  • 使用uglifyjs2js进行压缩

不合并文件可能存在的问题

  • 文件与文件有插入之间的上行请求,又增加了N-1个网络延迟
  • 受丢包问题影响更严重
  • 经过代理服务器时可能会被断开

文件合并缺点

  • 首屏渲染问题

    • 文件合并之后的js变大,如果首页的渲染依赖这个js的话,整个页面的渲染要等js请求完才能执行
    • 如果首屏只依赖a.js,只要等a.js完成后就可执行
    • 没有通过服务器端渲染,现在框架都需要等合并完的文件请求完才能执行,基本都需要等文件合并后的js
  • 缓存失效问题

    • 标记 js`md5`戳
    • 合并之后的js,任何一个改动都会导致大面积的缓存失效

文件合并对应缺点的处理

  • 公共库合并
  • 不同页面的合并

    • 不同页面js单独打包
  • 见机行事,随机应变

文件合并对应方法

  • 使用在线网站进行合并
  • 构建阶段,使用nodejs进行文件合并

图片相关优化

一张JPG的解析过程


jpg有损压缩:虽然损失一些信息,但是肉眼可见影响并不大

png8/png24/png32之间的区别

  • png8   ----256色 + 支持透明
  • png24 ----2^24 + 不支持透明
  • png32  ---2^24 +支持透明

文件大小 + 色彩丰富程度

png32是在png24上支持了透明,针对不同的业务场景选择不同的图片格式很重要

不同的格式图片常用的业务场景

不同格式图片的特点

  • jpg有损压缩,压缩率高,不支持透明
  • png支持透明,浏览器兼容性好
  • webp压缩程度更好,在ios webview中有兼容性问题
  • svg矢量图,代码内嵌,相对较小,图片样式相对简单的场景(尽量使用,绘制能力有限,图片简单用的比较多)

不同格式图片的使用场景

  • jpg:大部分不需要透明图片的业务场景
  • png:大部分需要透明图片的业务场景
  • webpandroid全部(解码速度和压缩率高于jpgpng,但是iossafari还没支持)
  • svg:图片样式相对简单的业务场景

图片压缩的几种情况

  • 针对真实图片情况,舍弃一些相对无关紧要的色彩信息
  • CSS雪碧图:把你的网站用到的一些图片整合到一张单独的图片中

    • 优点:减少HTTP请求的数量(通过backgroundPosition定位所需图片)
    • 缺点:整合图片比较大时,加载比较慢(如果这张图片没有加载成功,整个页面会失去图片信息)facebook官网任然在用,主要pc用的比较多,相对性能比较强
  • Image-inline:将图片的内容嵌到html中(减少网站的HTTP请求)

    • base64信息,减少网站的HTTP请求,如果图片比较小比较多,时间损耗主要在请求的骨干网络
  • 使用矢量图

    • 使用SVG进行矢量图的绘制
    • 使用icon-font解决icon问题
  • 在android下使用webp

    • webp的优势主要体现在它具有更优的图像数据压缩算法,能带来更小的图片体积,而且拥有肉眼识别无差异的图像质量;
    • 同时具备了无损和有损的压缩模式、Alpha透明以及动画的特性,在JPEGPNG上的转化效果都非常优秀、稳定和统一

cssjs的装载与执行

HTML页面加载渲染的过程

一个网站在浏览器端是如何进行渲染的

HTML渲染过程中的一些特点

  • 顺序执行,并发加载

    • 词法分析:从上到下依次解析

      • 通过HTML生成Token对象(当前节点的所有子节点生成后,才会通过next token获取到当前节点的兄弟节点),最终生成Dom Tree
    • 并发加载:资源请求是并发请求的
    • 并发上限

      • 浏览器中可以支持并发请求,不同浏览器所支持的并发数量不同(以域名划分),以Chrome为例,并发上限为6个
      • 优化点: 把CDN资源分布在多个域名下
  • 是否阻塞

    • css阻塞

      • csshead中通过link引入会阻塞页面的渲染

        • 如果我们把css代码放在head中去引入的话,那么我们整个页面的渲染实际上就会等待headcss加载并生成css树,最终和DOM整合生成RanderTree之后才会进行渲染
        • 为了浏览器的渲染,能让页面显示的时候视觉上更好。

避免某些情况,如:假设你放在页面最底部,用户打开页面时,有可能出现,页面先是显示一大堆文字或图片,自上而下,丝毫没有排版和样式可言。最后,页面又恢复所要的效果

    - `css`不阻塞`js`的加载,但阻塞`js`的执行
    - `css`不阻塞外部脚步的加载(`webkit preloader 预资源加载器`)
- `js`阻塞
    -  直接通过`<script src>`引入会阻塞后面节点的渲染
        -  `html parse`认为`js`会动态修改文档结构(`document.write`等方式),没有进行后面文档的变化
        -  `async`、`defer`(`async`放弃了依赖关系)
            - `defer`属性(`<script data-original="" defer></script>`) 

(这是延迟执行引入的js脚本(即脚本加载是不会导致解析停止,等到document全部解析完毕后,defer-script也加载完毕后,在执行所有的defer-script加载的js代码,再触发Domcontentloaded

            - `async`属性(`<script data-original="" async></script>`) 
                - 这是异步执行引入的`js`脚本文件 
                - 与`defer`的区别是`async`会在加载完成后就执行,但是不会影响阻塞到解析和渲染。但是还是会阻塞`load`事件,所以`async-script`会可能在`DOMcontentloaded`触发前或后执行,但是一定会在`load`事件前触发。



懒加载与预加载

懒加载

  • 图片进入可视区域之后请求图片资源
  • 对于电商等图片很多,页面很长的业务场景适用
  • 减少无效资源的加载
  • 并发加载的资源过多会会阻塞js的加载,影响网站的正常使用

img src被设置之后,webkit解析到之后才去请求这个资源。所以我们希望图片到达可视区域之后,img src才会被设置进来,没有到达可视区域前并不现实真正的src,而是类似一个1px的占位符。

场景:电商图片

预加载

  • 图片等静态资源在使用之前的提前请求
  • 资源使用到时能从缓存中加载,提升用户体验
  • 页面展示的依赖关系维护

场景:抽奖

懒加载原生jszepto.lazyload

原理

先将img标签中的src链接设为同一张图片(空白图片),将其真正的图片地址存储再img标签的自定义属性中(比如data-src)。当js监听到该图片元素进入可视窗口时,即将自定义属性中的地址存储到src属性中,达到懒加载的效果。

注意问题:
  • 关注首屏处理,因为还没滑动
  • 占位,图片大小首先需要预设高度,如果没有设置的话,会全部显示出来

var viewheight = document.documentElement.clientHeight   //可视区域高度

function lazyload(){
    var eles = document.querySelectorAll('img[data-original][lazyload]')

    Array.prototype.forEach.call(eles,function(item,index){
        var rect;
        if(item.dataset.original === '') return;
        rect = item.getBoundingClientRect(); //返回元素的大小及其相对于视口的

        if(rect.bottom >= 0 && rect.top < viewheight){
            !function(){
                var img = new Image();
                img.src = item.dataset.url;
                img.onload = function(){
                    item.src = img.src
                }
                item.removeAttribute('data-original');
                item.removeAttribute('lazyload');
            }()
        }
    })
}

lazyload()
document.addEventListener('scroll',lazyload)

预加载原生jspreloadJS实现

预加载实现的几种方式

  • 第一种方式:直接请求下来
<img data-original="https://user-gold-cdn.xitu.io/2019/2/21/1690d1b216cbfa18" style="display: none"/>
<img data-original="https://user-gold-cdn.xitu.io/2019/2/21/1690d1b21b70c8d2" style="display: none"/>
<img data-original="https://user-gold-cdn.xitu.io/2019/2/21/1690d1b216e17e26" style="display: none"/>
<img data-original="https://user-gold-cdn.xitu.io/2019/2/21/1690d1b217b3ae59" style="display: none"/>
  • 第二种方式:image对象
var image = new Image();
image.src = "www.pic26.com/dafdafd/safdas.jpg";
  • 第三种方式:xmlhttprequest

    • 缺点:存在跨域问题
    • 优点:好控制
var xmlhttprequest = new XMLHttpRequest();

xmlhttprequest.onreadystatechange = callback;

xmlhttprequest.onprogress = progressCallback;

xmlhttprequest.open("GET","http:www.xxx.com",true);

xmlhttprequest.send();

function callback(){
    if(xmlhttprequest.readyState == 4 && xmlhttprequest.status == 200){
        var responseText = xmlhttprequest.responseText;
    }else{
        console.log("Request was unsuccessful:" + xmlhttprequest.status);
    }
}

function progressCallback(){
    e = e || event;
    if(e.lengthComputable){
        console.log("Received"+e.loaded+"of"+e.total+"bytes")
    }
}   

 

PreloadJS模块

  • 本质权衡浏览器加载能力,让它尽可能饱和利用起来

重绘与回流

css性能让javascript变慢

要把css相关的外部文件引入放进head中,加载css时,整个页面的渲染是阻塞的,同样的执行javascript代码的时候也是阻塞的,例如javascript死循环。

一个线程   =>  javascript解析
一个线程   =>  UI渲染

这两个线程是互斥的,当UI渲染的时候,javascript的代码被终止。当javascript代码执行,UI线程被冻结。所以css的性能让javascript变慢。

频繁触发重绘与回流,会导致UI频繁渲染,最终导致js变慢

什么是重绘和回流

回流

  • render tree中的一部分(或全部)因为元素的规模尺寸布局隐藏等改变而需要重新构建。这就成为回流(reflow)
  • 页面布局和几何属性改变时,就需要回流

重绘

  • render tree中的一些元素需要更新属性,而这些属性只是影响元素的外观风格,而不影响布局,比如background-color。就称重绘

关系

用到chrome 分析 performance

回流必将引起重绘,但是重绘不一定会引起回流

避免重绘、回流的两种方法

触发页面重布局的一些css属性

  • 盒子模型相关属性会触发重布局

    • width
    • height
    • padding
    • margin
    • display
    • border-width
    • border
    • min-height
  • 定位属性及浮动也会触发重布局

    • top
    • bottom
    • left
    • right
    • position
    • float
    • clear
  • 改变节点内部文字结构也会触发重布局
  • text-align
  • overflow-y
  • font-weight
  • overflow
  • font-family
  • line-height
  • vertical-align
  • white-space
  • font-size

优化点:使用不触发回流的方案替代触发回流的方案

只触发重绘不触发回流

  • color
  • border-styleborder-radius
  • visibility
  • text-decoration
  • backgroundbackground-imagebackground-positionbackground-repeatbackground-size
  • outlineoutline-coloroutline-styleoutline-width
  • box-shadow

新建DOM的过程

  • 获取DOM后分割为多个图层
  • 对每个图层的节点计算样式结果(Recalculate style 样式重计算)
  • 为每个节点生成图形和位置(Layout 回流和重布局)
  • 将每个节点绘制填充到图层位图中(Paint SetupPaint重绘)
  • 图层作为纹理上传至gpu
  • 符合多个图层到页面上生成最终屏幕图像(Composite Layers 图层重组)

浏览器绘制DOM的过程是这样子的:

  • 获取 DOM 并将其分割为多个层(layer),将每个层独立地绘制进位图(bitmap)中
  • 将层作为纹理(texture)上传至 GPU,复合(composite)多个层来生成最终的屏幕图像
  • left/top/margin之类的属性会影响到元素在文档中的布局,当对布局(layout)进行动画时,该元素的布局改变可能会影响到其他元素在文档中的位置,就导致了所有被影响到的元素都要进行重新布局,浏览器需要为整个层进行重绘并重新上传到 GPU,造成了极大的性能开销。
  • transform 属于合成属性(composite property),对合成属性进行 transition/animation 动画将会创建一个合成层(composite layer),这使得被动画元素在一个独立的层中进行动画。
  • 通常情况下,浏览器会将一个层的内容先绘制进一个位图中,然后再作为纹理(texture)上传到 GPU,只要该层的内容不发生改变,就没必要进行重绘(repaint),浏览器会通过重新复合(recomposite)来形成一个新的帧。

chrome创建图层的条件

将频繁重绘回流的DOM元素单独作为一个独立图层,那么这个DOM元素的重绘和回流的影响只会在这个图层中

  • 3D或透视变换
  • CSS 属性使用加速视频解码的 <video> 元素
  • 拥有 3D (WebGL) 上下文或加速的
  • 2D 上下文的 <canvas> 元素
  • 复合插件(如 Flash)
  • 进行 opacity/transform 动画的元素拥有加速
  • CSS filters 的元素元素有一个包含复合层的后代节点(换句话说,就是一个元素拥有一个子元素,该子元素在自己的层里)
  • 元素有一个 z-index 较低且包含一个复合层的兄弟元素(换句话说就是该元素在复合层上面渲染)
总结:对布局属性进行动画,浏览器需要为每一帧进行重绘并上传到 GPU 中对合成属性进行动画,浏览器会为元素创建一个独立的复合层,当元素内容没有发生改变,该层就不会被重绘,浏览器会通过重新复合来创建动画帧

gif图

总结

  • 尽量避免使用触发回流重绘CSS属性
  • 重绘回流的影响范围限制在单独的图层(layers)之内
  • 图层合成过程中消耗很大页面性能,这时候需要平衡考虑重绘回流的性能消耗

实战优化点总结

  • translate替代top属性

    • top会触发layout,但translate不会
  • opacity代替visibility

    • opacity不会触发重绘也不会触发回流,只是改变图层alpha值,但是必须要将这个图片独立出一个图层
    • visibility会触发重绘
  • 不要一条一条的修改DOM的样式,预先定义好class,然后修改DOMclassName
  • 把DOM离线后修改,比如:先把DOMdisplay:none(有一次reflow),然后你修改100次,然后再把它显示出来
  • 不要把DOM节点的属性值放在一个循环里当成循环的变量

    • offsetHeightoffsetWidth每次都要刷新缓冲区,缓冲机制被破坏
    • 先用变量存储下来
  • 不要使用table布局,可能很小的一个小改动会造成整个table的重新布局

    • div只会影响后续样式的布局
  • 动画实现的速度的选择

    • 选择合适的动画速度
    • 根据performance量化性能优化
  • 对于动画新建图层

    • 启用gpu硬件加速(并行运算),gpu加速意味着数据需要从cpu走总线到gpu传输,需要考虑传输损耗.

      • transform:translateZ(0)
      • transform:translate3D(0)

浏览器存储

cookies

多种浏览器存储方式并存,如何选择?

  • 因为http请求无状态,所以需要cookie去维持客户端状态
  • cookie的生成方式:

    • http-->response header-->set-cookie
    • js中可以通过document.cookie可以读写cookie
    • cookie的使用用处:

      • 用于浏览器端和服务器端的交互(用户状态)
      • 客户端自身数据的存储
  • expire:过期时间
  • cookie的限制:

    • 作为浏览器存储,大小4kb左右
    • 需要设置过期时间 expire
  • 重要属性:httponly 不支持js读写(防止收到模拟请求攻击)
  • 不太作为存储方案而是用于维护客户关系
  • 优化点:cookie中在相关域名下面

    • cdn的流量损耗
    • 解决方案:cdn的域名和主站域名要分开

localStorage

localstorage

  • HTML5设计出来专门用于浏览器存储的
  • 大小为5M左右
  • 仅在客户端使用,不和服务端进行通信
  • 接口封装较好
  • 浏览器本地缓存方案

sessionstorage

  • 会话级别的浏览器存储
  • 大小为5M左右
  • 仅在客户端使用,不和服务器端进行通信
  • 接口封装较好
  • 对于表单信息的维护

indexedDB

  • IndexedDB是一种低级API,用于客户端存储大量结构化数据。该API使用索引来实现对该数据的高性能搜索。虽然Web
  • Storage对于存储叫少量的数据很管用,但对于存储更大量的结构化数据来说,这种方法不太有用。IndexedDB提供了一个解决方案。

为应用创建离线版本

  • cdn域名不要带cookie
  • localstorage存库、图片

cookie种在主站下,二级域名也会携带这个域名,造成流量的浪费

Service Worker产生的意义

PWAService Worker

  • PWA(Progressive Web Apps)是一种Web App新模型,并不是具体指某一种前言的技术或者某一个单一的知识点,我们从英文缩写来看就能看出来,这是一个渐进式的Web App,是通过一系列新的Web特性,配合优秀的UI交互设计,逐步增强Web App的用户体验

PWAService worker

chrome 插件 lighthouse

检测是不是一个渐进式web app
  • 当前手机在弱网环境下能不能加载出来
  • 离线环境下能不能加载出来
特点
  • 可靠:没有网络的环境中也能提供基本的页面访问,而不会出现“未连接到互联网”的页面
  • 快速:针对网页渲染及网络数据访问有较好的优化
  • 融入(Engaging):应用可以被增加到手机桌面,并且和普通应用一样有全屏、推送等特性

service worker

service worker是一个脚本,浏览器独立于当前页面,将其在后台运行,为实现一些不依赖页面的或者用户交互的特性打开了一扇大门。在未来这些特性将包括消息推送,背景后台同步,geofencing(地理围栏定位),但他将推出的第一个首要的特性,就是拦截和处理网络请求的能力,包括以编程方式来管理被缓存的响应。

案例分析

Service Worker学习与实践

了解servie worker

chrome://serviceworker-internals/

chrome://inspect/#service-worker/

service worker网络拦截能力,存储Cache Storage,实现离线应用

indexedDB

callback && callback()写法
相当于 
if(callback){
   callback();
}

cookiesessionlocalStoragesessionStorage基本操作

indexedDB基本操作

object store:对象存储
本身就是结构化存储
 function openDB(name, callback) {
            //建立打开indexdb  indexedDB.open
            var request = window.indexedDB.open(name)
            request.onerror = function(e) {
                console.log('on indexedDB error')
            }
            request.onsuccess = function(e) {
                    myDB.db = e.target.result
                    callback && callback()
                }
                //from no database to first version,first version to second version...
            request.onupgradeneeded = function() {
                console.log('created')
                var store = request.result.createObjectStore('books', {
                    keyPath: 'isbn'
                })
                console.log(store)
                var titleIndex = store.createIndex('by_title', 'title', {
                    unique: true
                })
                var authorIndex = store.createIndex('by_author', 'author')

                store.put({
                    title: 'quarry memories',
                    author: 'fred',
                    isbn: 123456
                })
                store.put({
                    title: 'dafd memories',
                    author: 'frdfaded',
                    isbn: 12345
                })
                store.put({
                    title: 'dafd medafdadmories',
                    author: 'frdfdsafdafded',
                    isbn: 12345434
                })
            }
        }
        var myDB = {
            name: 'tesDB',
            version: '2.0.1',
            db: null
        }

        function addData(db, storeName) {

        }

        openDB(myDB.name, function() {
            // myDB.db = e.target.result
            // window.indexedDB.deleteDatabase(myDB.name)
        });

        //删除indexedDB

indexDB事务

transcationobject store建立关联关系来操作object store
建立之初可以配置

 var transcation = db.transcation('books', 'readwrite')
 var store = transcation.objectStore('books')

 var data =store.get(34314)
 store.delete(2334)
 store.add({
     title: 'dafd medafdadmories',
     author: 'frdfdsafdafded',
     isbn: 12345434
 })

Service Worker离线应用

serviceworker需要https协议

如何实现ServiceWorker与主页面之间的通信

lavas

缓存

期望大规模数据能自动化缓存,而不是手动进行缓存,需要浏览器端和服务器端协商一种缓存机制

  • Cache-Control所控制的缓存策略
  • last-modified 和 etage以及整个服务端浏览器端的缓存流程
  • 基于node实践以上缓存方式

httpheader

可缓存性

  • public:表明响应可以被任何对象(包括:发送请求的客户端,代理服务器,等等)缓存。
  • private:表明响应只能被单个用户缓存,不能作为共享缓存(即代理服务器不能缓存它)。
  • no-cache:强制所有缓存了该响应的缓存用户,在使用已存储的缓存数据前,发送带验证器的请求到原始服务器
  • only-if-cached:表明如果缓存存在,只使用缓存,无论原始服务器数据是否有更新

到期

  • max-age=<seconds>:设置缓存存储的最大周期,超过这个时间缓存被认为过期(单位秒)。与 Expires相反,时间是相对于请求的时间。
  • s-maxage=<seconds>:覆盖max-age 或者 Expires 头,但是仅适用于共享缓存(比如各个代理),并且私有缓存中它被忽略。cdn缓存
  • max-stale[=<seconds>]

表明客户端愿意接收一个已经过期的资源。 可选的设置一个时间(单位秒),表示响应不能超过的过时时间。

  • min-fresh=<seconds>

表示客户端希望在指定的时间内获取最新的响应。

重新验证重新加载

重新验证
  • must-revalidate:缓存必须在使用之前验证旧资源的状态,并且不可使用过期资源。
  • proxy-revalidate:与must-revalidate作用相同,但它仅适用于共享缓存(例如代理),并被私有缓存忽略。
  • immutable :表示响应正文不会随时间而改变。资源(如果未过期)在服务器上不发生改变,因此客户端不应发送重新验证请求头(例如If-None-MatchIf-Modified-Since)来检查更新,即使用户显式地刷新页面。在Firefox中,immutable只能被用在 https:// transactions.
重新加载
  • no-store:缓存不应存储有关客户端请求或服务器响应的任何内容。
  • no-transform:不得对资源进行转换或转变。Content-Encoding, Content-Range, Content-TypeHTTP头不能由代理修改。例如,非透明代理可以对图像格式进行转换,以便节省缓存空间或者减少缓慢链路上的流量。 no-transform指令不允许这样做。

Expires

  • 缓存过期时间,用来指定资源到期的时间,是服务器端的时间点
  • 告诉浏览器在过期时间前浏览器可以直接从浏览器缓存中存取数据,而无需再次请求
  • expireshttp1.0的时候的
  • http1.1时候,我们希望cache的管理统一进行,max-age优先级高于expires,当有max-age在的时候expires可能就会被忽略。
  • 如果没有设置cache-control时候会使用expires

Last-modifiedIf-Modified-since

  • 基于客户端和服务器端协商的缓存机制
  • last-modified --> response header

    if-modified-since --> request header
  • 需要与cache-control共同使用
last-modified有什么缺点?
  • 某些服务端不能获取精确的修改时间
  • 文件修改时间改了,但文件的内容却没有变

EtagIf-none-match

  • 文件内容的hash值
  • etag -->reponse header

    if-none-match -->request header
  • 需要与cache-control共同使用

好处:

  • if-modified-since更加准确
  • 优先级比etage更高

流程图


enter image description here

服务端性能优化

服务端用的node.js因为和前端用的同一种语言,可以利用服务端运算能力来进行相关的运算而减少前端的运算

  • vue渲染遇到的问题
  • vue-ssr和原理和引用

vue渲染面临的问题

    先加载vue.js
=>  执行vue.js代码
=>  生成html
以前没有前端框架时,
  • jsp/php在服务端进行数据的填充,发送给客户端就是已经填充好数据`的html
  • 使用jQuery异步加载数据
  • 使用ReactVue前端框架

    • 代价:需要框架全部加载完,才能把页面渲染出来,页面的首屏性能不好

多层次的优化方案

  • 构建层的模板编译。runtime,compile拆开,构建层做模板编译工作。webpack构建时候,统一,直接编译成runtime可以执行的代码
  • 数据无关的prerender的方式
  • 服务端渲染

查看原文

赞 239 收藏 196 评论 1

蓝染 赞了文章 · 2019-04-02

vue开发项目完全指南

这篇文章总结了vue项目的所遇到的问题,包括跨域、用户认证、接口统一管理、路由配置、兼容性处理,性能优化等内容。

项目github地址 :

image-20190318090248419

一、环境依赖安装

1. node环境

1.1 node和npm环境的安装

根据以下教程安装,然后设置好环境变量

http://www.runoob.com/nodejs/...

视频教程 http://101.110.118.22/github....

centos如果装不上看这里:https://www.rosehosting.com/b...

1.2 为npm更改源

npm默认使用的源的服务器在国外下载速度慢,所以需要更换源

以下两种方法任选一种

1.2.1使用cnpm代替npm

参考链接:https://npm.taobao.org/
# 安装
npm install -g cnpm --registry=https://registry.npm.taobao.org

#安装完cnpm,之后再按照依赖就要使用cnpm
cnpm install [包名]

1.2.2为npm更换源

参考链接 https://segmentfault.com/a/11...

修改源为淘宝的源

npm config set registry http://registry.npm.taobao.org/

我们在发布自己包的时候需要将官方的源改回来

npm config set registry https://registry.npmjs.org/

1.3 管理(更新)nodejs的版本

切换nodejs版本有两种方式,分别是nvmn,n更简单推荐使用

使用n管理nodejs版本

参考链接 https://www.jianshu.com/p/c64...

官网 https://github.com/tj/n

#安装
npm install -g n

#使用n下载所需node版本
n 版本号
#下载最新版本
n latest
# 切换版本
输入 n,
然后选中所需版本
#以指定的版本来执行版本
n use 7.4.0 index.js

linux使用n安装新版本nodejs之后,如果node -v还是原来的版本,那么就需要改变一下环境变量

vim .bash_profile

export NODE_HOME=/usr/local     #NODE_HOME改成新版本nodejs安装的目录,如果找不到,find / -name node
export PATH=$NODE_HOME/bin:$PATH
export NODE_PATH=$NODE_HOME/lib/node_modules:$PATH

修改环境变量参考:https://blog.csdn.net/yi412/a...

1.4 package.json文件详解

参考文档 http://javascript.ruanyifeng....

2. vue脚手架

vue-cli目前已经更新到3版本,vue-cli3把webpack相关的配置隐藏起来了,所有的配置都在vue.config.js文件夹中,所以使用vue-cli3需要的webpack水平较高,建议使用vue-cli2

3.1 vue-cli2.x安装

参考链接:https://github.com/vuejs/vue-...

安装:

npm install -g vue-cli

用法:

$ vue init < template-name >  < project-name >

例:

$ vue init webpack my-project

目前可用的模块包括:

  • webpack - 一个功能齐全的Webpack + vue-loader设置,具有热重载,linting,测试和css提取功能。
  • webpack-simple - 一个简单的Webpack + vue-loader设置,用于快速原型设计。
  • browserify -全功能Browserify + vueify设置用热重装载,linting&单元测试。
  • browserify -simple - 一个简单的Browserify + vueify设置,用于快速原型设计。
  • pwa - 基于webpack模板的vue-cli的PWA模板
  • simple - 单个HTML文件中最简单的Vue设置

3.2 vue-cli3.x安装及配置(仅供参考)

vue-cli3x的官方文档:https://cli.vuejs.org/

Vue-cli3 中vue.config.js文件配置参考文档:https://cli.vuejs.org/zh/conf...

Vue CLI 的包名称由 vue-cli 改成了 @vue/cli。 如果你已经全局安装了旧版本的 vue-cli(1.x 或 2.x),你需要先通过 npm uninstall vue-cli -gyarn global remove vue-cli 卸载它。

安装

npm install -g @vue/cli

安装了vue-cli3如果还想使用vue-cli2的init功能,需要安装一个桥接功能

npm install -g @vue/cli-init
// vue.config.js 配置说明
//官方vue.config.js 参考文档 https://cli.vuejs.org/zh/config/#css-loaderoptions
// 这里只列一部分,具体配置参考文档
module.exports = {
  // 部署生产环境和开发环境下的URL。
  // 默认情况下,Vue CLI 会假设你的应用是被部署在一个域名的根路径上
  //例如 https://www.my-app.com/。如果应用被部署在一个子路径上,你就需要用这个选项指定这个子路径。例如,如果你的应用被部署在 https://www.my-app.com/my-app/,则设置 baseUrl 为 /my-app/。
  baseUrl: process.env.NODE_ENV === "production" ? "./" : "/",
 
  // outputDir: 在npm run build 或 yarn build 时 ,生成文件的目录名称(要和baseUrl的生产环境路径一致)
  outputDir: "dist",
  //用于放置生成的静态资源 (js、css、img、fonts) 的;(项目打包之后,静态资源会放在这个文件夹下)
  assetsDir: "assets",
  //指定生成的 index.html 的输出路径  (打包之后,改变系统默认的index.html的文件名)
  // indexPath: "myIndex.html",
  //默认情况下,生成的静态资源在它们的文件名中包含了 hash 以便更好的控制缓存。你可以通过将这个选项设为 false 来关闭文件名哈希。(false的时候就是让原来的文件名不改变)
  filenameHashing: false,
 
  //   lintOnSave:{ type:Boolean default:true } 问你是否使用eslint
  `lintOnSave`: true,
  //如果你想要在生产构建时禁用 eslint-loader,你可以用如下配置
  // lintOnSave: process.env.NODE_ENV !== 'production',
 
  //是否使用包含运行时编译器的 Vue 构建版本。设置为 true 后你就可以在 Vue 组件中使用 template 选项了,但是这会让你的应用额外增加 10kb 左右。(默认false)
  // runtimeCompiler: false,
 
  /**
   * 如果你不需要生产环境的 source map,可以将其设置为 false 以加速生产环境构建。
   *  打包之后发现map文件过大,项目文件体积很大,设置为false就可以不输出map文件
   *  map文件的作用在于:项目打包后,代码都是经过压缩加密的,如果运行时报错,输出的错误信息无法准确得知是哪里的代码报错。
   *  有了map就可以像未加密的代码一样,准确的输出是哪一行哪一列有错。
   * */
  productionSourceMap: false,
 
  // 它支持webPack-dev-server的所有选项
  devServer: {
    host: "localhost",
    port: 1111, // 端口号
    https: false, // https:{type:Boolean}
    open: true, //配置自动启动浏览器
    // proxy: 'http://localhost:4000' // 配置跨域处理,只有一个代理
 
    // 配置多个代理
    proxy: {
      "/api": {
        target: "<url>",
        ws: true,
        changeOrigin: true
      },
      "/foo": {
        target: "<other_url>"
      }
    }
  }
};

二、开发

以下内容依赖环境为 : vue-cli 版本2.9.x

项目github地址 :

安装完以上依赖后,就可以开始一个项目了,我们先看下后端api的定义

前后端交互报文定义以及数据api接口

前后端交互报文定义

请求

http request header{ //除登录注册以外的请求,发起请求时要在请求头中加入token
    authorization:jwt
}
http request body{
    
}

返回

http response header{
    
}
http response body{
    code:业务处理状态码
    msg:业务处理描述
    token:jwt token
    data:业务数据
}

项目中使用的后台api定义如下

注:服务器端的host为118.24.85.97,端口为22222

1.测试api是否可用

  1. uri: http://118.24.85.97:22222/api
  2. 描述:测试接口是否能用,能用的话返回 'API WORDS'字符串
  3. 请求类型 GET
  4. 请求参数 无
  5. 返回值 {'Api Works'}

2.注册

  1. uri: http://118.24.85.97:22222/api/users/reg
  2. 描述:注册
  3. 请求类型 POST
  4. 请求参数
序号参数名是否必填描述
1namey用户名
2passy密码
  1. 返回参数 不重要

3.登录

  1. uri: http://118.24.85.97:22222/api/users/login
  2. 描述:登录
  3. 请求类型 POST
  4. 请求参数
序号参数名是否必填描述
1namey用户名
2passy密码
  1. 返回参数
序号参数名描述
1msgok
2token用于验证用户身份的token

4.获取当前用户信息

  1. uri: http://118.24.85.97:22222/api/users/current
  2. 描述:获取用户信息
  3. 请求类型 GET
  4. 请求参数 无
  5. 返回参数
序号参数名描述
1id用户id
2token用于验证用户身份的token

0.初始化项目

在终端中输入

vue init webpack vue2_template

然后会有一些选项让你选,按照项目需求选择,例如我不需要eslint,unit test,就可以选No,现在选no将来如果需要的话也可以自己安装

image-20190301151747514

安装完成之后,按照提示切换到相应目录,执行相应指令,然后在浏览器打开网址,这样一个简单的vue项目就启动起来了

image-20190301152115255

1. 项目文件介绍

整个文件介绍:

image-20190301153205422

注意:

  1. 开发主要使用src文件夹
  2. webpack的配置文件配置文件详解看这里:https://segmentfault.com/a/11...
  3. package.json配置详解 http://javascript.ruanyifeng....

src目录介绍

首先在src目录下新建一个文件夹views,用来放我们的主要页面,然后在assets文件夹中建立fonts styles imgs,用来存放相应的资源,建完之后,文件夹如下

image-20190301155249665

2. 跨域、axios配置与api管理

在这个项目中,我们使用axios进行数据请求

axios中文文档: https://www.kancloud.cn/yunye...
# 安装axios
npm/cnpm i axios -S      # -S 指安装到package.json中的dependencies中

安装完成后,我们要在main.js中引入,然后测试一下是否成功引入

//main.js文件
import axios from 'axios'

axios.get('https://api.github.com/users?since=10')   //使用github接口做一下测试
  .then(res=>console.log(res))
  .catch(err=>console.log(err))

浏览器显示以下信息,说明引入成功image-20190301160510216

github提供的接口配置了cors,所以我们能够能够在浏览器正常访问到,但cors兼容性最低到ie10,而且后台不一定会配置cors,所以在开发时我们需要配置一下跨域

参考链接:

  1. cors详解 http://www.ruanyifeng.com/blo...

2.1配置跨域

参考文档:https://segmentfault.com/a/11...

先找个没有设置cors的api使用axios访问一下

axios.get('http://118.24.85.97:22222/api')
.then(res=>console.log(res))
.catch(err=>console.log(err))

浏览器会因为同源策略报错

image-20190307094529285

下面进行跨域的配置

配置目录 config/index.js 13行
proxyTable: {
  '/apis':{
    target:'http://118.24.85.97:22222',//后台地址 proxyTable  把/apis映射成target 即 /apis=http://118.24.85.97:22222
    changeOrigin:true,//是否跨域
    pathRewrite:{
      '^/apis':''
    }
  }

}

再进行访问数据时就要在接口前面加上/apis(/apis就相当于http://118.24.85.97:22222)

axios.get('/apis/api')
.then(res=>console.log(res))
.catch(err=>console.log(err))

然后就发现浏览器访问成功了

image-20190307095002857

proxyTable原理:跨域是浏览器禁止的,服务端并不禁止跨域 ,所以浏览器可以发给自己的服务端然后,由自己的服务端再转发给要跨域的服务端,做一层代理。proxyTable使用的是http-proxy-middleware中间件,内部用的是http-proxy

以上配置的跨域是开发环境下的,在生产环境就自动失效了,而且这样配置我们开发时访问接口时,都要写成/apis/xxx/xxx格式,在部署到服务器中时,我们要把/apis拿掉,才能访问到正确的url。有两种方法,一种是在开发环境中设置(通过axios的baseURL),另一种是在服务器上修改nginx的配置设置。

2.2生产环境去除/apis前缀

在这里详细说下第一种方式,原理是这样的:

通过检测是开发环境和生产环境,设置不同的baseURL,使生产环境和开发环境都能正确访问url

在src目录下新建一个apis目录,然后在apis目录下新建一个api.config.js文件

//判断是否是生产环境
//webpack在开发环境和生产环境分别执行不同的js文件,process.env.NODE_ENV设置了不同的值,process.env.NODE_ENV在生产环境中值为'production'(这个值是在build/build.js中第4行设置的)
var isPro = process.env.NODE_ENV=== 'production'
// 如果是生产环境 我们就使用服务器的uri,如果是开发环境,我们就添加/apis前缀
module.exports = {
    baseUrl: isPro ? 'http://118.24.85.97:22222' : '/apis'
}

在main.js中引入这个文件,然后设置axios的baseURL

//引入api.config.js文件,然后设置axios的baseURL
import apiConfig from './apis/api.config'
axios.defaults.baseURL=apiConfig.baseUrl

再来测试一下不加/apis的接口

axios.get('/api')
.then(res=>console.log(res))
.catch(err=>console.log(err))

浏览器显示是ok的。这样我们以后使用axios访问接口就可以不加/apis了,打包后访问也不用手动去除/apis

2.3 api统一管理

在vue项目开发过程中,会涉及到很多接口的处理,当项目足够大时,就需要统一管理接口。

具体方法应该挺多的,这里只介绍一种:使用axios+async/await进行接口的统一管理

一般来说,后台的接口是分模块的,例如我们后台的测试接口

  • 身份认证 /api/login /api/reg
  • 用户信息 /v1/api/user

我们首先在src目录下新建一个apis文件夹,后台提供的所有接口都在这里定义

第二步,按照后台提供的模块新建js文件,我们新建user.jsauth.js

第三步,引入axios,做相应的配置

在apis目录下新建一个http.js,在里面做axios相应的配置

  1. 我们上文中是在main.js文件引入的axios,设置的baseURL,以上代码可以去除,改为在http.js中引入
  2. 我们做的主要是:引入axios,创建一个axios的实例(实例的功能和axios一样)
import axios from 'axios'
import apiConfig from './api.config'
//创建axios的一个实例
var instance = axios.create({
    baseURL:apiConfig.baseUrl,
    timeout: 6000
})


//------------------- 一、请求拦截器 后面介绍
instance.interceptors.request.use(function (config) {

    return config;
}, function (error) {
    // 对请求错误做些什么
    
    return Promise.reject(error);
});

//----------------- 二、响应拦截器 后面介绍
instance.interceptors.response.use(function (response) {
    
    return response.data;
}, function (error) {
    // 对响应错误做点什么
    return Promise.reject(error);
});

/**
 * 使用es6的export default导出了一个函数,导出的函数代替axios去帮我们请求数据,
 * 函数的参数及返回值如下:
 * @param {String} method  请求的方法:get、post、delete、put
 * @param {String} url     请求的url:
 * @param {Object} data    请求的参数
 * @returns {Promise}     返回一个promise对象,其实就相当于axios请求数据的返回值
 */
export default function (method, url, data = null) {
    method = method.toLowerCase();
    if (method == 'post') {
        return instance.post(url, data)
    } else if (method == 'get') {
        return instance.get(url, { params: data })
    } else if (method == 'delete') {
        return instance.delete(url, { params: data })
    }else if(method == 'put'){
        return instance.put(url,data)
    }else{
        console.error('未知的method'+method)
        return false
    }
}

第四步,在apis/xxx.js文件中引入http.js导出的函数,拿其中一个文件auth.js说明

//auth.js 用于定义用户的登录、注册、注销等

import req from './http.js'

//定义接口

//在这里定义了一个登陆的接口,把登陆的接口暴露出去给组件使用
export const LOGIN =params=>req('post','/api/users/login',params)
//这里使用了箭头函数,转换一下写法:
// export const LOGIN=function(params){
//   return req('post','/api/login',params)
// }

//定义注册接口
export const REG =params=>req('post','/api/users/reg',params)


最后一步,在需要用的该api的组件中引入并调用,我们在App.vue文件中测试下

<template>
  <div>
    <h2>登录</h2>
    用户名<input type="text" v-model="user">
    密码<input type="password" v-model="pass">
    <input type="button" @click="reg" value="注册">
    <input type="button" @click="login" value="登录">
  </div>
</template>
<script>
import {LOGIN,REG} from '../../apis/auth.js'
export default {
  data() {
    return {
      user:'',
      pass:'',
      err:[]
    }
  },
  methods: {
    async reg(){
      try {
        const data = await REG({ name: this.user,pass: this.pass })
        console.log(data)
        alert(JSON.stringify(data))
        this.cleanForm()


      } catch (error) {
        console.log(error)
      }

    },
    async login(){
      try {
        const data = await LOGIN({ name: this.user,pass: this.pass })
        alert(JSON.stringify(data))
        this.cleanForm()
      } catch (error) {
        console.log(error)
      }
    },
    cleanForm(){
      this.user=''
      this.pass=''
    }
  },

}
</script>

注:如果要打开Login.vue,需要配置对应的路由

上面的代码引入了auth.js定义的api,并在对应的方法中使用。代码中用到了async/await,其实很简单,可以假设async是个标识,说明这个函数中有异步请求,await翻译为'等',后面接一个异步请求,等后面的异步请求执行完成之后,会把结果赋给=左边的值

参考链接 http://www.runoob.com/w3cnote...

总结一下,像上面那样定义接口虽然麻烦点,但有两个好处:

  1. 代码看起来规范,所有的接口都在一个文件夹定义,不用分散的各个组件,维护起来简单,例如后台的一些url变了,改起来也方便
  2. 可以做到接口一次定义,到处使用

3. 路由配置

Vue Router官方文档 https://router.vuejs.org/zh/

前端路由原理:https://segmentfault.com/a/11...

3.1 最简配置

路由的配置文件在router/index.js文件中

先引入文件,再进行配置

首先在views目录中新建以下页面,主页(Home/Home.vue),登录页(Login/Login.vue),测试页(Test/Test.vue)

然后配置下路由

import Vue from 'vue'
import Router from 'vue-router'
//@表示 src目录 webpack的配置在webpack.base.conf.js第29行 alias{'@':resolve('src')}
import Home from '@/views/Home/Home.vue'
import Login from '@/views/Login/Login.vue'
import Test from '@/views/Test/Test.vue'

Vue.use(Router)

export default new Router({
  routes: [//路由规则
    {
      path: '/',
      name: 'Home',
      component: Home
    },
    {
      path:'/login',
      name:'Login',
      component:Login
    },
    {
      path:'/test',
      name:'Test',
      component:Test
    }
  ]
})

路由规则在routes中进行配置,routes是一个数组,接受一系列路由规则,每个路由规则是一个对象,包括路径、路由名字,和路径匹配的组件,建议给每个路由加个名字,在后面可能会用到。

打开浏览器,输入相应的url查看配置的路由是否正确,不正确的话检查下自己的配置

3.2配置路由懒加载

参考文档:

路由懒加载官方文档:https://router.vuejs.org/zh/g...

webpack之mainfest解读:https://github.com/younth/blo...

当打包构建应用时,Javascript 包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就更加高效了。所以,懒加载的含义是当路由被访问时再去加载对应的js代码。

首先,不做路由懒加载的情况下,我们打包一下(切换到项目目录,执行npm run build),然后会发现项目下生产了3个js文件
image-20190304110812440
简单介绍一下作用:

  1. vendor.js 第三方库,一般是 node_modules里面的依赖进行打包 体积最大
  2. app.js 入口js打包的结果,即我们编写的所有代码都会打包进去
  3. manifest.js 主要是一些异步加载的实现方法(通过建立script方式动态引入js),内容上包含异步js的文件名和路径。

然后我们实现一下路由懒加载 @/router/router.js

import Vue from 'vue'
import Router from 'vue-router'
// import Home from '@/views/Home/Home.vue'
// import Login from '@/views/Login/Login.vue'
// import Test from '@/views/Test/Test.vue'
// 懒加载方式
const Home=()=>import('@/views/Home/Home.vue')
const Login=()=>import('@/views/Login/Login.vue')
const Test=()=>import('@/views/Test/Test.vue')
Vue.use(Router)

export default new Router({
  routes: [
    {
      path: '/',
      name: 'Home',
      component: Home
    },
    {
      path:'/login',
      name:'Login',
      component:Login
    },
    {
      path:'/test',
      name:'Test',
      component:Test
    }
  ]
})

懒加载只是改变了一下组件的引用方式,由原来的直接引入变成异步引入,当我们访问对应的路由path时,才会加载相应的路由组件。

配置完成后再执行一次打包,结果如下:
image-20190304112607087

我们会发现目录中多出来3个js文件,并且app.js文件变小了。这说明配置了懒加载之后,app.js中其他组件的内容被抽离出来,分配到各自的js文件中。配置懒加载之后,刚开始打开页面只会加载app.js文件,只有在用户点击相应路由时,才会加载对应的js代码。当我们的业务代码非常多时,懒加载是个很好的选择。

3.3 配置history模式

官方文档:https://router.vuejs.org/zh/g...

配置history模式有两个原因,一是因为hash模式看很丑,二是因为预加载要用到History模式,配置非常简单,只需要配置属性mode的值为'history'

const router = new VueRouter({
  mode: 'history',
  routes: [...]
})

不过这种方式需要后台的支持,当匹配不到url时,返回url/index.html页面

nginx配置如下

location / {
  try_files $uri /index.html;
}

4. 权限管理

参考链接:

json web token入门教程 http://www.ruanyifeng.com/blo...

jwt官网 https://jwt.io/

4.1 token验证

我们通过jwt进行用户认证,jwt的原理是:服务器认证以后,生成一个json对象,发回给用户.

{
    "id":"001",
    "姓名":"小明",
    "角色":"管理员",
    "到期时间":"2019年3月3日12时30分"
}

以后用户与服务端通信的时候,都要发回这个json对象。服务器完全靠这个对象认定用户身份(一般是通过这个对象的中id去数据库请求数据)。为了防止用户篡改数据,服务器会在生成这个对象的时候,加上签名。就像这种形式:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

关于JWT保存更新的业务流程如下:

  1. 保存:登录后保存token
  2. 添加:每次发送请求之前检查token是否存在,存在,添加到请求头中,发送请求
  3. 更新:每次发送请求服务器返回数据之后更新token

主要逻辑包括:

  1. 登录之后,在localStorage中保存token
  2. 每次发送请求之前,使用axios请求拦截器将token放到请求头中
  3. 每次发送请求服务器返回数据之后在axios的响应拦截器中更新token
//1.登录之后保存token login.vue
async login(){
    const data = await LOGIN({ name: this.user,pass: this.pass })
    //保存token
    localStorage.setItem('token',data.token)
    //查看是否保存成功
    console.log(localStorage.getItem('token'))
}
//每次发送请求之前,讲token放到请求头中 api/http.js
//---使用axios的请求拦截器,每次发送请求之前拦截一下
instance.interceptors.request.use(function (config) {
    // 给头添加token
    if (localStorage.getItem('token')){//存在token,加入头
        config.headers.authorization=localStorage.getItem('token')
    }
    return config;
}, function (error) {
    // 对请求错误做些什么

    return Promise.reject(error);
});
//完成之后,记得发送一个请求,看看是否正确添加token

//---响应拦截器,服务器响应后先到达这里
instance.interceptors.response.use(function (response) {
    if(response.data.code=='2000'){//成功响应,更新token
      if(response.data.token){
        localStorage.setItem('token',response.data.token)
      }
    }else{
        //错误处理 根据不同的状态码,进行错误处理  
    }
    return response.data;
}, function (error) {
    // 对响应错误做点什么
    return Promise.reject(error);
});

4.2 对页面的访问权限

除了对token的操作,我们还要判断用户有没有权限访问这个页面(有些页面是用户必须登录才能访问的),具体配置要使用Vue Router的导航守卫

参考链接:https://router.vuejs.org/zh/g...

在全局前置守卫中进行验证


//在router/index.js进行配置
//在每次进行路由跳转之前进行
router.beforeEach((to,from,next)=>{//增加登录验证
  const isLogin=localStorage.getItem('token')?true:false;
  if(to.path=='/login'){//如果是登录页面,不需要token
    next();
  }else{//如果不是登录页面就要判断是否登录
    isLogin?next():next('/login');
  }

})

5. 将界面交给第三方UI库

iview官网:https://www.iviewui.com/

为节省开发时间,我们往往会使用一些第三方ui库,比如iview elementui等

我们在这里只介绍iview,其他ui库大同小异

iview的安装与引入

安装

cnpm i iview --save

按需引入组件

官网说,需要下载插件才能按需引入,官网说明,但是不下好像也可以正常引入

//在main.js文件中引入项目需要的组件
import {Button,Table,Message} from 'iview'
//然后注册组件
Vue.component('Button',Button)
Vue.component('Table',Table)
Vue.component('Message',Message)

这样注册的话太繁琐,所以需要优化一下

//main.js
import {Button,Table,Message} from 'iview'
const iviewComs={Button,Table,Message}
Object.keys(iviewComs).forEach(key=>{Vue.component(key,component[key])})

代码都写在main.js中显得太拥挤,我们可以把代码拿出去,写成一个插件

我们在components文件夹中新建一个文件iview-coms,用来放iview中引入的组件

//components/iview-coms.js  

import {Button,Table,Message} from 'iview'
const components={Button,Table,Message}
const install = function(Vue, opts = {}){
  Object.keys(components).forEach(key=>{
    Vue.component(key,components[key])
  })
}

export default install

然后在main.js中引入,use这个插件

import iviewComs from './components/iview-coms'
Vue.use(iviewComs)

ok了,接下来看自定义主题

自定义主题

官网链接:https://www.iviewui.com/docs/...

原理很简单,就是把ivew的less文件引入,并且覆盖掉,然后在main.js文件中引入自己的less文件

首先,我们需要下载解析less文件的loader ,lessless-loader,这里有个坑,下载less的时候要下载3版本以下的,不然会报一堆错误

cnpm i less@2.7.2 less-loader -D

下载完就ok了,不需要在webpack中进行配置,因为已经配置好了

然后,在assets/styles/base.less(没有需要自己新建)中,引入iview的样式文件,并且覆盖掉

默认变量列表:https://github.com/iview/ivie...

//assets/styles/base.less
//------ 引入iview样式
@import '~iview/src/styles/index.less';
//------ 覆盖iview的样式
@primary-color: #E91E63;
@error-color : #FF3300;

最后在main.js引入该less文件

//main.js
import './assets/styles/base.less'

此时,引入的组件就可以在.vue文件中使用了,看一下效果:

image-20190307150805499

ok了。最后还要补充一下,在项目开发过程中,不可避免的要覆盖iview默认的样式,我们分为两种情况,一种是全局覆盖,一种是局部覆盖。

全局覆盖的话我们要新建一个less文件,比如叫cover-iview.less所有覆盖iview样式的代码都放在这里,然后在base.less中引入这个文件。

局部覆盖的话要注意不要影响到别的样式,所以要充分利用less的作用域,例如我们只需要改home页面下的iview按钮样式,我们可以这样:

.home{
    .ivu-btn{
        
    }
}

6.开发中注意问题

6.1编写自己的工具库插件

参考文档:

vue插件说明:https://cn.vuejs.org/v2/guide...

项目中往往会使用一些通用的函数,比如获取当前时间、时间格式转化,防抖,节流等,我们可以把这个公用的部分封装成插件,在main.js中引入。

首先,在src目录下新建utils文件夹,在里面新建index.js,utils.js文件

我们在utils.js中编写自己的工具库,然后导出

class Utils{
    constructor(){
        this.d=new Date();//date对象
        this.instance=null;
    }
    static getInstance(){//单例模式
        if(!this.instance){
            this.instance = new Utils();
        }
        return this.instance;
    }

    pick(obj,arr){//pick({ a: 1, b: '2', 'c': 3 }, ['a', 'c'])  =>{a:1,c:3}
       return arr.reduce((acc,curr)=>{
            return (curr in obj && (acc[curr] = obj[curr]), acc)
        },{})
    }

    dateFormat(datetime,pattern=""){
        let vWeek = ["星期天","星期一","星期二","星期三","星期四","星期五","星期六"];
        let dt=new Date(datetime);
        let y=dt.getFullYear();
        let m=(dt.getMonth()+1).toString().padStart(2,'0');
        let d=dt.getDate().toString().padStart(2,'0');
        let hh=dt.getHours().toString().padStart(2,'0');
        let mm=dt.getMinutes().toString().padStart(2,'0');
        let ss=dt.getSeconds().toString().padStart(2,'0');
        let vWeek_s = dt.getDay();//星期
        if(pattern.toLowerCase() === 'yyyy-mm-dd'){
            return `${y}-${m}-${d}`
        }else if(pattern.toLowerCase() === 'mm-dd'){
            return `${m}-${d}`
        }else if(pattern.toLowerCase() === 'yyyymmddhhmmss'){
            return `${y}${m}${d}${hh}${mm}${ss}`
        }else {
            return `${y}-${m}-${d} ${hh}:${mm}:${ss} ${vWeek[vWeek_s]}`
        }

    }

}

const UTIL = Utils.getInstance();

// console.log(UTIL.dateFormat(new Date(),'yyyymmddhhmmss')) //=>20190312110722
// console.log(UTIL.dateFormat(new Date()))//=>2019-03-12 11:07:22 星期二
// console.log(UTIL.pick({ a: 1, b: '2', 'c': 3 }, ['a', 'c']))//=>{a:1,c:3}

export default UTIL;

然后在index.js中编写插件,导出

//utils/index.js

import UTIL from './utils.js'

const UtilPlugin={}

UtilPlugin.install=function(Vue,options){//插件必须有install方法,接受两个参数,一个是Vue构造器,一个是参数
  Vue.prototype.$utils=UTIL//在vue prototype上添加实例方法
}
export default UtilPlugin

最后在main.js中引入并use插件

// utils
import Util from './utils/index'
Vue.use(Util)
console.log(Vue.prototype.$util)//打印下是否引入成功

之后就可以在组件中通过使用this.$utils调用方法了

7. 兼容性处理

我们的目标是兼容到ie9,对ie8及以下的浏览器做相应的跳转处理(跳转到浏览器下载界面)

兼容性对一个程序来说是非常重要的,兼容性测试越早越好

image-20190307151841810

7.1 对ie8及以下浏览器的跳转处理

在项目根目录下中的html中head中加入下面代码

<!--[if lte IE 8]><script>window.location.href="https://support.dmeng.net/upgrade-your-browser.html?referrer="+encodeURIComponent(window.location.href);</script><![endif]-->

目的是检测ie浏览器的版本,如果低于<=ie8,就跳转到下面这个页面

image-20190307153138889

7.2 兼容ie9

参考链接:https://juejin.im/post/5b2868...

7.2.1 ES6兼容

我们把浏览器调到ie9,然后看控制台报错信息

image-20190307154807782

报这个错的原因是es6的新对象,新表达式,ie9不支持,为解决这个问题,我们需要引入babel-polyfill

cnpm i babel-polyfill -D

安装完成之后,在main.js文件中引入

import 'babel-polyfill'

在项目使用 vue-cli 生成的代码中,根目录有一个 .babelrc 文件,这是项目使用 babel 的配置文件。在默认生成的模板内容中,增加 "useBuiltIns": "entry" 的设置内容,这是一个指定哪些内容需要被 polyfill(兼容) 的设置

useBuiltIns 有三个设置选项

  • false - 不做任何操作
  • entry - 根据浏览器版本的支持,将 polyfill 需求拆分引入,仅引入有浏览器不支持的polyfill
  • usage - 检测代码中 ES6/7/8 等的使用情况,仅仅加载代码中用到的 polyfill

7.2.2建立自己的polyfill

加入这些代码后,工程中大部分代码已可以兼容到ie9版本,但还是会有少部分不兼容的特性,例如requestAnimationFrameclassList等。对于这些内容,我们需要自己定义polyfill来解决,在src目录下新建一个文件夹polyfill,然后在polyfill文件夹下面建一个polyfill.js,我们在polyfill.js中加入我们的兼容代码

然后在main.js中引入这个文件

import './polyfill/polyfill'

解决兼容方式的正确姿势是:拿到ie9浏览器下的报错信息,去goole或者baidu搜索,得到polyfill,然后加入到自己的polyfill.js文件中

三、优化

1. webpack3.x优化打包速度

我们执行一下npm run build,结果如下:

image-20190307161705933

整个打包过程花了32s左右,现在我们的项目只是引入了相关的依赖,一些业务逻辑还没有写,打包速度就那么慢了,等到我们写完整个项目,打包速度还会继续变长,所以我们需要优化一下。

优化打包速度,我们修改的主要是webpack.prod.conf.js文件

替换代码压缩工具

Webpack 默认提供的 UglifyJS 插件,由于采用单线程压缩,速度慢 ;

webpack-parallel-uglify-plugin 插件可以并行运行 UglifyJS 插件,更加充分而合理的使用 CPU 资源,这可以大大减少的构建时间;

//安装
cnpm i webpack-parallel-uglify-plugin -D
//配置 webpack.prod.conf.js

//首先删除项目中的 UglifyJsPlugin插件及配置,第二次打包时提高速度,要把.cache文件加入到gitignore中
// new webpack.optimize.UglifyJsPlugin({
//   compress: {
//     warnings: false,
//     drop_console: true
//   },
//   sourceMap: true
// }),

//然后引入并使用我们刚才装的插件

==注意:版本控制工具提交时,要忽略.cache文件==

配置完后我们执行npm run build,发现打包速度降到了23s

image-20190307162957635

再执行一次npm run build,发现打包速度降到了12s

image-20190307164513348

时间降低那么多是因为文件没有改动,直接利用了缓存中的js文件

happypack开启多核构建项目

一般node.js是单线程执行编译,而happypack则是启动node的多线程进行构建,大大提高了构建速度。

首先安装,

修改webpack.base.conf.js

const HappyPack = require('happypack');
const os = require('os');
const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length });
...
...
// 增加plugins
 plugins: [
  new HappyPack({
    id: 'happy-babel-js',
    loaders: ['babel-loader?cacheDirectory=true'],
    threadPool: happyThreadPool,
  })
]
...
...
// 修改对应loader
{
  test: /\.js$/,
  loader: 'happypack/loader?id=happy-babel-js',
  include: [resolve('src'), resolve('test')],
}

配置完成,执行npm run build

image-20190307165549102

what??并没有提高速度 不要用这个鬼东西了

hardSourceWebpackPlugin节省70%的时间

https://github.com/mzgoddard/...
#安装
cnpm install --save-dev hard-source-webpack-plugin

使用,在webpack.prod.conf.js中引入并使用

const HardSourceWebpackPlugin = require('hard-source-webpack-plugin');

module.exports = {
  context: // ...
  entry: // ...
  output: // ...
  plugins: [
    new HardSourceWebpackPlugin()
  ]
}

结果:

image-20190307171310562

注:要第二次打包才生效

总结下,使用了三个插件,我们的打包速度从30s降低到4s,awesome!

2. webpack3.x优化首屏加载速度

首先要说明一下,首屏加载速度优化针对的是打包后dist文件。我们如果要在本地进行测试的话,需要本地有个服务器,我们在这里使用nginx。

2.1本地安装nginx

下载地址: http://nginx.org/en/download....

在官网上找到自己系统适合的nginx版本,下载到本地

2.1.1window安装

  1. 解压文件
  2. 双击运行nginx.exe,在任务管理器中出现nginx的进程,则表示安装成功

2.1.2 mac/linux安装

#1.解压文件
tar -xzf nginx-1.14.0.tar.gz  #mac可以使用解压缩工具解压,不必用命令行

#2. 配置安装路径   --prefix指定安装路径  假设我要装到/usr/local/nginx文件夹中
./configure --prefix=/Users/best9/local/nginx

#编译
make

##安装
make install

安装完成后进入到—prefix指定的文件夹中,执行ll,会发现文件夹下有以下目录

image-20190308144717721

我们要关心就是我上面标出来的三个目录

进到sbin目录中,启动nginx程序

image-20190308145219037

cd sbin
#需要使用root权限,否则会报错  报错信息可以在日志中查看到,错误日志目录 /logs/error.log
sudo ./nginx     

正常的话,nginx会默认在localhost:80端口启动,在浏览器访问localhost,就会显示默认界面

image-20190308145304691

如果电脑的80端口被占用的话,在conf/nginx.conf文件中修改端口

2.2 nginx常用命令

nginx使用-s发送信号操作运行中的进程,常用命令如下:

注意:使用命令需要在sbin目录下

#启动nginx
./nginx
#立即停止服务 -s stop
./nginx -s stop
#优雅地停止服务 -s quit
./nginx -s quit
#重启服务 -s reload
./nginx -s reload

2.3 nginx配置静态文件服务器

我们在这里使用nginx配置一个最简单的静态文件服务器,更复杂的配置稍后再讲

nginx的配置文件地址:conf/nginx.conf

使用vim或者其他编辑器打开该文件,修改配置文件第43-45行:

vim conf/nginx.conf

image-20190308150902046

location / {
  alias /Users/best9/github/vue2_template/dist;  #访问/相当于访问alias配置的目录    
}

配置完成后保存,然后重启服务

sudo ./sbin/nginx -s reload 要使用root权限重启

打开浏览器访问localhost

image-20190308151058193

因为没有登录,会自动跳转到登录界面

到这里静态文件服务器就配置好了,但我们刷新下页面,会报错404

image-20190308151213416

这是因为我们使用了vue router的history模式,我们需要在nginx中加入以下配置

image-20190308151523068

location / {
  try_files $uri $uri/  /index.html;
}

然后重启nginx,再刷新页面就没问题了

2.4 优化首屏加载速度

以上步骤就绪后,我们就可以来优化加载速度了

打开chrome的devTools面板,切换到Network,禁用浏览器缓存,刷新测试下加载速度,发现整个应用加载大约需要1.97s,如下图:

image-20190308152853851

把网络环境切换到Fast 3G,再测试一次,发现加载用了7.56s,白屏时间6.89s

image-20190308165101522

我们使用预渲染插件进行优化

2.4.1 预渲染

使用插件:prerender-spa-plugin

参考链接:https://juejin.im/post/59d49d...

首先,安装 prerender-spa-plugin,安装时件略长,因为其依赖了 phantomjs

cnpm install prerender-spa-plugin --save-dev

我们只在生产环境中进行预渲染,修改 build/webpack.prod.conf.js,在配置插件的地方加入如下代码。

//引入 预渲染插件
const PrerenderSpaP=require('prerender-spa-plugin')

//在plugins中配置
new PrerenderSpaP(
  // 输出目录的绝对路径
  path.join(__dirname,'../dist'),
  //预渲染路由
  ['/home','/login']
)

再次执行打包,然后再进行测试:

image-20190308165347855

发现白屏时间为4.10s,在弱网环境下,使用预渲染,大约能缩减2.5秒的白屏时间

预渲染注意事项
  • 预渲染的路由不能是动态加载的,否则会报webpackJsonp is not define的错误,要想解决这个错误,可以看这里 https://juejin.im/entry/5911a...
  • 预渲染的路由不能是需要权限才能访问的页面。预渲染的机制是在本地跑一个chromium浏览器,然后去爬取你预渲染页面的Html,如果你的页面需要权限(登录)才能进入,就爬不到,也不会报错,最终只会渲染不需要权限的页面

举个例子:

插件配置如下:

new PrerenderPlugin({
    staticDir:path.join(__dirname,'../dist')
    routes:['/','/about','/login']
})

路由配置如下:

image-20190314164834830

2.4.2 配置gzip压缩

gzip官方文档 http://nginx.org/en/docs/http...

nginx默认是关闭gzip的,我们需要自己打开,并进行一些配置:

image-20190311103518953

gzip:on;  #打开gzip,关闭为off
gzip_min_length 1;  #小于gzip_min_length,不进行压缩(默认单位为byte)
gzip_comp_level 2;  #压缩级别
gzip_types text/plain text/css application/javascript text/javascript image/jpeg image/gif image/png;#指定类型进行gzip压缩

配置完成后,我们再测试一下加载速度:

image-20190311103446777

发现白屏时间为1.95s,加载文件的体积也变小了

四、部署

1. nginx配置反向代理

我们要在本地部署测试,所以后台的地址是127.0.0.1:22222

项目开发完成后需要部署到服务器,因为是前后端分离,所以前端的应用部署到nginx,后端的应用部署到自己对应的服务器,所以我们需要配置一下,把后端的服务器变成上游服务,nginx做反向代理服务器

反向代理:服务器根据客户端的请求,从其关系的一组或多组后端服务器上获取资源,然后将这些资源返回给客户端。

由于上游服务器(后台服务器)要处理非常复杂的逻辑,所以性能不怎么样,我们使用nginx作为反向代理服务器后,可以将请求按照负载均衡算法代理给多台上游服务器。配置如下:

image-20190311112112788

以上配置是将所有的请求转发给上游服务器,但如果我们只想将动态请求转发给上游服务器,静态资源由nginx自己处理,就可以这样做:

判断是否是后台api(根据location的匹配规则),如果是的话,就进行转发

匹配规则看这里:https://stackoverflow.com/que...

upstream local{
    server 127.0.0.1:22222;  #假设在本地部署
}
server{
    listen:80;
    server_name localhost;
    location ~ /api/ {  #以`/api/`开头的uri就行转发,否则不转发 ~代表正则表达式匹配
        proxy_set_header: Host $host;
        proxy_set_header: X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_pass http://local;
    }    
    location / {
        #... alias index等配置 
          
    }
}

这里需要注意一个问题:proxy_pass是转发请求的模块,当你访问localhost:80/api/users/login时,会被转发到local的地址,即127.0.0.1:22222/api/users/login,所以开发环境下访问后台接口的URI要写你部署到nginx的URI,而不是真正的后台地址(因为被转发了)

前端配置

//apis/api.config.js
//判断是否是生产环境
var isPro = process.env.NODE_ENV=== 'production'
module.exports = {
    baseUrl: isPro ? 'http://localhost:80' : '/apis'//生产环境下的baseURl是nginx的hoost:port
}

2. 持续部署

项目做完需要发布到服务器,但每次手动打包,然后ftp传上去的话就太麻烦了,所以我们的需求是:git或者svn提交后,自动打包发布到服务器。使用的工具是jenkins.

参考文档:https://juejin.im/post/5ad198...

官网:https://jenkins.io/

jenkins安装与启动

jenkins一般情况下会装在服务器,但如果是同一个局域网的话,装在本机也可以

linux:

  1. https://blog.csdn.net/fenglai...
  2. https://www.jianshu.com/p/8a7... (centos)
  3. 配置文件地址 /etc/sysconfig/jenkins
  4. 工作空间 /var/lib/jenkins

windows下:

  1. 从Jenkins官网下载最新war文件。
  2. 运行java -jar jenkins.war即可。

mac:

  1. 从官网下载pkg文件
  2. 双击安装,安装之后自己就会启动

jenkins初始化

  1. jenkins的默认端口是8080,启动成功后在浏览器打开。
  2. 进入后会让我们输管理员密码,打开网页上提示路径下的文件,复制密码粘贴输入即可。
  3. 然后会让安装需要的插件,此处选默认即可,等待安装完成。
  4. 创建一个管理员账户。
  5. 上面都完成后会看到这个界面。

image-20190314171915326

创建任务

在主页上点击创建

image-20190314172049214

直接点保存,然后去安装插件

image-20190314172224487

安装插件

首先返回主页,然后点击左侧菜单 系统管理->插件管理

image-20190315091230194

需要安装的插件有:

  • Generic Webhook Trigger 实现git提交触发更新功能
  • Publish Over SSH 实现服务器部署功能
  • nvm wrapper 引入node

安装插件的方式:

image-20190315091837845

安装完插件之后重启一下jenkins(安装完插件后,有个重启的选项,勾选即可)

实现git钩子功能

当我们向github/码云等远程仓库push我们的代码时,jenkins能知道我们提交了代码,这是自动构建自动部署的前提,钩子的实现原理是在远端仓库上配置一个Jenkins服务器的接口地址,当本地向远端仓库发起push时,远端仓库会向配置的Jenkins服务器的接口地址发起一个带参数的请求,jenkins收到后开始工作

打开创建的项目(进入工程->点击配置)

image-20190315092840660

构建触发器

勾选 Generic Webhook Trigger

image-20190315094851281

github仓库配置钩子:

进入github项目中该项目页面,点击setting->webhooks,添加payload URL,

URL格式为 http://<User ID>:<API Token>@<Jenkins IP地址>:端口/generic-webhook-trigger/invoke userid和api token在jenkins的系统管理-管理用户-选择你的用户点进去-左侧设置

image-20190315095453871

实现自动化构建

自动化构建:jenkins实现安装依赖,打包(npm install && npm run build),此外还可以执行一些测试行为

点击构建环境,勾选nvm,输入node版本

image-20190315103055020

点击构建,选择执行shell,输入执行命令,多个命令使用&&分开

npm config set registry http://registry.npm.taobao.org/ &&
npm install && 
npm run build
查看原文

赞 322 收藏 239 评论 7

蓝染 赞了文章 · 2019-04-02

前端面试:谈谈 JS 垃圾回收机制

个人专栏 ES6 深入浅出已上线,深入ES6 ,通过案例学习掌握 ES6 中新特性一些使用技巧及原理,持续更新中,←点击可订阅。

点赞再看,养成习惯

本文 GitHubhttps://github.com/qq44924588... 上已经收录,更多往期高赞文章的分类,也整理了很多我的文档,和教程资料。欢迎Star和完善,大家面试可以参照考点复习,希望我们一起有点东西。


为了保证的可读性,本文采用意译而非直译。

最近看到一些面试的回顾,不少有被面试官问到谈谈JS 垃圾回收机制,说实话,面试官会问这个问题,说明他最近看到一些关于 JS 垃圾回收机制的相关的文章,为了 B 格,就会顺带的问问。

想阅读更多优质文章请猛戳GitHub博客,一年百来篇优质文章等着你!

最近看到一篇讲 JS 垃圾回收的国外文章,觉得讲得明白,所以就翻译过来了,希望对你们有所帮助。

垃圾回收

JavaScript 中的内存管理是自动执行的,而且是不可见的。我们创建基本类型、对象、函数……所有这些都需要内存。

当不再需要某样东西时会发生什么? JavaScript 引擎是如何发现并清理它?

可达性

JavaScript 中内存管理的主要概念是可达性。

简单地说,“可达性” 值就是那些以某种方式可访问或可用的值,它们被保证存储在内存中。

1. 有一组基本的固有可达值,由于显而易见的原因无法删除。例如:

  • 本地函数的局部变量和参数
  • 当前嵌套调用链上的其他函数的变量和参数
  • 全局变量
  • 还有一些其他的,内部的

这些值称为根。

2. 如果引用或引用链可以从根访问任何其他值,则认为该值是可访问的。

例如,如果局部变量中有对象,并且该对象具有引用另一个对象的属性,则该对象被视为可达性, 它引用的那些也是可以访问的,详细的例子如下。

JavaScript 引擎中有一个后台进程称为垃圾回收器,它监视所有对象,并删除那些不可访问的对象。

一个简单的例子

下面是最简单的例子:

// user 具有对象的引用
let user = {
  name: "John"
};

图片描述

这里箭头表示一个对象引用。全局变量“user”引用对象 {name:“John”} (为了简洁起见,我们将其命名为John)。John 的 “name” 属性存储一个基本类型,因此它被绘制在对象中。

如果 user 的值被覆盖,则引用丢失:

user = null;

图片描述

现在 John 变成不可达的状态,没有办法访问它,没有对它的引用。垃圾回收器将丢弃 John 数据并释放内存。

两个引用

现在让我们假设我们将引用从 user 复制到 admin:

// user具有对象的引用
let user = {
  name: "John"
};

let admin = user;

图片描述

现在如果我们做同样的事情:

user = null;

该对象仍然可以通过 admin 全局变量访问,所以它在内存中。如果我们也覆盖admin,那么它可以被释放。

相互关联的对象

现在来看一个更复杂的例子, family 对象:

function marry (man, woman) {
  woman.husban = man;
  man.wife = woman;

  return {
    father: man,
    mother: woman
  }
}

let family = marry({
  name: "John"
}, {
  name: "Ann"
})

函数 marry 通过给两个对象彼此提供引用来“联姻”它们,并返回一个包含两个对象的新对象。

产生的内存结构:

图片描述

到目前为止,所有对象都是可访问的。

现在让我们删除两个引用:

delete family.father;
delete family.mother.husband;

图片描述

仅仅删除这两个引用中的一个是不够的,因为所有对象仍然是可访问的。

但是如果我们把这两个都删除,那么我们可以看到 John 不再有传入的引用:

图片描述

输出引用无关紧要。只有传入的对象才能使对象可访问,因此,John 现在是不可访问的,并将从内存中删除所有不可访问的数据。

垃圾回收之后:

图片描述

无法访问的数据块

有可能整个相互连接的对象变得不可访问并从内存中删除。

源对象与上面的相同。然后:

family = null;

内存中的图片变成:

图片描述

这个例子说明了可达性的概念是多么重要。

很明显,John和Ann仍然链接在一起,都有传入的引用。但这还不够。

“family”对象已经从根上断开了链接,不再有对它的引用,因此下面的整个块变得不可到达,并将被删除。

内部算法

基本的垃圾回收算法称为“标记-清除”,定期执行以下“垃圾回收”步骤:

  • 垃圾回收器获取根并“标记”(记住)它们。
  • 然后它访问并“标记”所有来自它们的引用。
  • 然后它访问标记的对象并标记它们的引用。所有被访问的对象都被记住,以便以后不再访问同一个对象两次。
  • 以此类推,直到有未访问的引用(可以从根访问)为止。
  • 除标记的对象外,所有对象都被删除。

例如,对象结构如下:

图片描述

我们可以清楚地看到右边有一个“不可到达的块”。现在让我们看看“标记并清除”垃圾回收器如何处理它。

第一步标记根

图片描述

然后标记他们的引用

图片描述

以及子孙代的引用:

图片描述

现在进程中不能访问的对象被认为是不可访问的,将被删除:

图片描述

这就是垃圾收集的工作原理。JavaScript引擎应用了许多优化,使其运行得更快,并且不影响执行。

一些优化:

  • 分代回收——对象分为两组:“新对象”和“旧对象”。许多对象出现,完成它们的工作并迅速结 ,它们很快就会被清理干净。那些活得足够久的对象,会变“老”,并且很少接受检查。
  • 增量回收——如果有很多对象,并且我们试图一次遍历并标记整个对象集,那么可能会花费一些时间,并在执行中会有一定的延迟。因此,引擎试图将垃圾回收分解为多个部分。然后,各个部分分别执行。这需要额外的标记来跟踪变化,这样有很多微小的延迟,而不是很大的延迟。
  • 空闲时间收集——垃圾回收器只在 CPU 空闲时运行,以减少对执行的可能影响。

面试怎么回答

1)问什么是垃圾

一般来说没有被引用的对象就是垃圾,就是要被清除, 有个例外如果几个对象引用形成一个环,互相引用,但根访问不到它们,这几个对象也是垃圾,也要被清除。

2)如何检垃圾

一种算法是标记 标记-清除 算法,还想说出不同的算法可以参考这里

更深入一些的讲解 http://newhtml.net/v8-garbage...

还有一种牛逼的答法就是说看我的博客,当然是要自己总结的博客。

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

你的点赞是我持续分享好东西的动力,欢迎点赞!

交流

干货系列文章汇总如下,觉得不错点个Star,欢迎 加群 互相学习。

https://github.com/qq44924588...

我是小智,公众号「大迁世界」作者,对前端技术保持学习爱好者。我会经常分享自己所学所看的干货,在进阶的路上,共勉!

关注公众号,后台回复福利,即可看到福利,你懂的。

clipboard.png

查看原文

赞 191 收藏 118 评论 4

蓝染 赞了文章 · 2019-04-02

发布订阅模式与观察者模式

背景

设计模式并非是软件开发的专业术语,实际上,“模式”最早诞生于建筑学。

设计模式的定义是:在面向对象软件设计过程中针对特定问题的简洁而优雅的解决方案。通俗一点说,设计模式是在某种场合下对某个问题的一种解决方案。如果再通俗一点说,设计模式就是给面向对象软件开发中的一些好的设计取个名字。

这些“好的设计”并不是谁发明的,而是早已存在于软件开发中。一个稍有经验的程序员也许在不知不觉中数次使用过这些设计模式。GoF(Gang of Four--四人组,《设计模式》几位作者)最大的功绩是把这些“好的设计”从浩瀚的面向对象世界中挑选出来,并且给予它们一个好听又好记的名字。

设计模式并不直接用来完成代码的编写,而是描述在各种不同情况下,要怎么解决问题的一种方案,他不是一个死的机制,他是一种思想,一种写代码的形式。每种语言对于各种设计模式都有他们自己的实现方式,对于某些设计模式来说,可能在某些语言下并不适用,比如工厂方法模式对于javascript。模式应该用在正确的地方。而哪些才算正确的地方,只有在我们深刻理解了模式的意图之后,再结合项目的实际场景才会知道。。

模式的社区一直在发展。GoF在1995年提出了23种设计模式,但模式不仅仅局限于这23种,后面增加到了24种。在这20多年的时间里,也许有更多的模式已经被人发现并总结了出来,比如一些JavaScript 图书中会提到模块模式、沙箱模式等。这些“模式”能否被世人公认并流传下来,还有待时间验证。

观察者模式(Observer Pattern)

观察者模式定义了对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知,并自动更新。观察者模式属于行为型模式,行为型模式关注的是对象之间的通讯,观察者模式就是观察者和被观察者之间的通讯。

观察者模式有一个别名叫“发布-订阅模式”,或者说是“订阅-发布模式”,订阅者和订阅目标是联系在一起的,当订阅目标发生改变时,逐个通知订阅者。我们可以用报纸期刊的订阅来形象的说明,当你订阅了一份报纸,每天都会有一份最新的报纸送到你手上,有多少人订阅报纸,报社就会发多少份报纸,报社和订报纸的客户就是上面文章开头所说的“一对多”的依赖关系。

发布订阅模式(Pub-Sub Pattern)

其实24种基本的设计模式中并没有发布订阅模式,上面也说了,他只是观察者模式的一个别称。

但是经过时间的沉淀,似乎他已经强大了起来,已经独立于观察者模式,成为另外一种不同的设计模式。

在现在的发布订阅模式中,称为发布者的消息发送者不会将消息直接发送给订阅者,这意味着发布者和订阅者不知道彼此的存在。在发布者和订阅者之间存在第三个组件,称为消息代理或调度中心或中间件,它维持着发布者和订阅者之间的联系,过滤所有发布者传入的消息并相应地分发它们给订阅者。

举一个例子,你在微博上关注了A,同时其他很多人也关注了A,那么当A发布动态的时候,微博就会为你们推送这条动态。A就是发布者,你是订阅者,微博就是调度中心,你和A是没有直接的消息往来的,全是通过微博来协调的(你的关注,A的发布动态)。

观察者模式和发布订阅模式有什么区别?

我们先来看下这两个模式的实现结构:
图片描述

观察者模式:观察者(Observer)直接订阅(Subscribe)主题(Subject),而当主题被激活的时候,会触发(Fire Event)观察者里的事件。

发布订阅模式:订阅者(Subscriber)把自己想订阅的事件注册(Subscribe)到调度中心(Topic),当发布者(Publisher)发布该事件(Publish topic)到调度中心,也就是该事件触发时,由调度中心统一调度(Fire Event)订阅者注册到调度中心的处理代码。

我们再来看下这两个模式的代码案例:(猎人发布与订阅任务)

观察者模式:

    //有一家猎人工会,其中每个猎人都具有发布任务(publish),订阅任务(subscribe)的功能
    //他们都有一个订阅列表来记录谁订阅了自己
    //定义一个猎人类
    //包括姓名,级别,订阅列表
    function Hunter(name, level){
        this.name = name
        this.level = level
        this.list = []
    }
    Hunter.prototype.publish = function (money){
        console.log(this.level + '猎人' + this.name + '寻求帮助')
        this.list.forEach(function(item, index){
            item(money)
        })
    }
    Hunter.prototype.subscribe = function (targrt, fn){
        console.log(this.level + '猎人' + this.name + '订阅了' + targrt.name)
        targrt.list.push(fn)
    }
    
    //猎人工会走来了几个猎人
    let hunterMing = new Hunter('小明', '黄金')
    let hunterJin = new Hunter('小金', '白银')
    let hunterZhang = new Hunter('小张', '黄金')
    let hunterPeter = new Hunter('Peter', '青铜')
    
    //Peter等级较低,可能需要帮助,所以小明,小金,小张都订阅了Peter
    hunterMing.subscribe(hunterPeter, function(money){
        console.log('小明表示:' + (money > 200 ? '' : '暂时很忙,不能') + '给予帮助')
    })
    hunterJin.subscribe(hunterPeter, function(){
        console.log('小金表示:给予帮助')
    })
    hunterZhang.subscribe(hunterPeter, function(){
        console.log('小张表示:给予帮助')
    })
    
    //Peter遇到困难,赏金198寻求帮助
    hunterPeter.publish(198)
    
    //猎人们(观察者)关联他们感兴趣的猎人(目标对象),如Peter,当Peter有困难时,会自动通知给他们(观察者)

发布订阅模式:

    //定义一家猎人工会
    //主要功能包括任务发布大厅(topics),以及订阅任务(subscribe),发布任务(publish)
    let HunterUnion = {
        type: 'hunt',
        topics: Object.create(null),
        subscribe: function (topic, fn){
            if(!this.topics[topic]){
                  this.topics[topic] = [];  
            }
            this.topics[topic].push(fn);
        },
        publish: function (topic, money){
            if(!this.topics[topic])
                  return;
            for(let fn of this.topics[topic]){
                fn(money)
            }
        }
    }
    
    //定义一个猎人类
    //包括姓名,级别
    function Hunter(name, level){
        this.name = name
        this.level = level
    }
    //猎人可在猎人工会发布订阅任务
    Hunter.prototype.subscribe = function (topic, fn){
        console.log(this.level + '猎人' + this.name + '订阅了狩猎' + topic + '的任务')
        HunterUnion.subscribe(topic, fn)
    }
    Hunter.prototype.publish = function (topic, money){
        console.log(this.level + '猎人' + this.name + '发布了狩猎' + topic + '的任务')
        HunterUnion.publish(topic, money)
    }
    
    //猎人工会走来了几个猎人
    let hunterMing = new Hunter('小明', '黄金')
    let hunterJin = new Hunter('小金', '白银')
    let hunterZhang = new Hunter('小张', '黄金')
    let hunterPeter = new Hunter('Peter', '青铜')
    
    //小明,小金,小张分别订阅了狩猎tiger的任务
    hunterMing.subscribe('tiger', function(money){
        console.log('小明表示:' + (money > 200 ? '' : '不') + '接取任务')
    })
    hunterJin.subscribe('tiger', function(money){
        console.log('小金表示:接取任务')
    })
    hunterZhang.subscribe('tiger', function(money){
        console.log('小张表示:接取任务')
    })
    //Peter订阅了狩猎sheep的任务
    hunterPeter.subscribe('sheep', function(money){
        console.log('Peter表示:接取任务')
    })
    
    //Peter发布了狩猎tiger的任务
    hunterPeter.publish('tiger', 198)
    
    //猎人们发布(发布者)或订阅(观察者/订阅者)任务都是通过猎人工会(调度中心)关联起来的,他们没有直接的交流。

观察者模式和发布订阅模式最大的区别就是发布订阅模式有个事件调度中心。

观察者模式由具体目标调度,每个被订阅的目标里面都需要有对观察者的处理,这种处理方式比较直接粗暴,但是会造成代码的冗余。

而发布订阅模式中统一由调度中心进行处理,订阅者和发布者互不干扰,消除了发布者和订阅者之间的依赖。这样一方面实现了解耦,还有就是可以实现更细粒度的一些控制。比如发布者发布了很多消息,但是不想所有的订阅者都接收到,就可以在调度中心做一些处理,类似于权限控制之类的。还可以做一些节流操作。

观察者模式是不是发布订阅模式

网上关于这个问题的回答,出现了两极分化,有认为发布订阅模式就是观察者模式的,也有认为观察者模式和发布订阅模式是真不一样的。

其实我不知道发布订阅模式是不是观察者模式,就像我不知道辨别模式的关键是设计意图还是设计结构(理念),虽然《JavaScript设计模式与开发实践》一书中说了分辨模式的关键是意图而不是结构

如果以结构来分辨模式,发布订阅模式相比观察者模式多了一个中间件订阅器,所以发布订阅模式是不同于观察者模式的;如果以意图来分辨模式,他们都是实现了对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知,并自动更新,那么他们就是同一种模式,发布订阅模式是在观察者模式的基础上做的优化升级。

不过,不管他们是不是同一个设计模式,他们的实现方式确实有差别,我们在使用的时候应该根据场景来判断选择哪个。

查看原文

赞 112 收藏 76 评论 9

蓝染 赞了文章 · 2019-04-01

JavaScript 中常见设计模式整理

开发中,我们或多或少地接触了设计模式,但是很多时候不知道自己使用了哪种设计模式或者说该使用何种设计模式。本文意在梳理常见设计模式的特点,从而对它们有比较清晰的认知。

JavaScript 中常见设计模式

各设计模式关键词

看完了上述设计模式后,把它们的关键词特点罗列出来,以后提到某种设计模式,进而联想相应的关键词和例子,从而心中有数。

设计模式特点案例
单例模式一个类只能构造出唯一实例创建菜单对象
策略模式根据不同参数可以命中不同的策略动画库里的算法函数
代理模式代理对象和本体对象具有一致的接口图片预加载
迭代器模式能获取聚合对象的顺序和元素each([1, 2, 3], cb)
发布-订阅模式PubSub瀑布流库
命令模式不同对象间约定好相应的接口按钮和命令的分离
组合模式组合模式在对象间形成一致对待的树形结构扫描文件夹
模板方法模式父类中定好执行顺序咖啡和茶
享元模式减少创建实例的个数男女模具试装
职责链模式通过请求第一个条件,会持续执行后续的条件,直到返回结果为止if else 优化
中介者模式对象和对象之间借助第三方中介者进行通信测试结束告知结果
装饰者模式动态地给函数赋能天冷了穿衣服,热了脱衣服
状态模式每个状态建立一个类,状态改变会产生不同行为电灯换挡
适配者模式一种数据结构改成另一种数据结构枚举值接口变更
观察者模式当观察对象发生变化时自动调用相关函数vue 双向绑定

参考文献

*《JavaScript设计模式与开发实践》

查看原文

赞 151 收藏 160 评论 0

蓝染 发布了文章 · 2019-03-31

观察者模式与发布-订阅模式

观察者模式(Observer)

观察者模式:一个对象(主体)根据它维护的一个对象列表(观察者),自动通知它们状态的任何变化。(举例说明,电商平台关注(订阅)一家店铺(发布者)的鞋子,当鞋子上架之后店铺就会发送消息给用户(订阅者);用户(订阅者)可以通过取消订阅来取消推送消息接收。)

实际上是:主体对观察者传递消息通知,观察者必须将该消息通知订阅到触发事件对象上。

关系链: 目标 <--- 继承 --- 具体目标(发布者)< -- 订阅 -- 调度观察者 --- > 具体观察者(订阅者) --- 继承 ---> 观察者

具体代码实现:

//观察者列表
function ObserverList(){
  this.observerList = [];
}
ObserverList.prototype.add = function( obj ){
  return this.observerList.push( obj );
};
ObserverList.prototype.count = function(){
  return this.observerList.length;
};
ObserverList.prototype.get = function( index ){
  if( index > -1 && index < this.observerList.length ){
    return this.observerList[ index ];
  }
};
ObserverList.prototype.indexOf = function( obj, startIndex ){
  var i = startIndex;
  while( i < this.observerList.length ){
    if( this.observerList[i] === obj ){
      return i;
    }
    i++;
  }
  return -1;
};
ObserverList.prototype.removeAt = function( index ){
  this.observerList.splice( index, 1 );
};

//目标
function Subject(){
  this.observers = new ObserverList();
}
Subject.prototype.addObserver = function( observer ){
  this.observers.add( observer );
};
Subject.prototype.removeObserver = function( observer ){
  this.observers.removeAt( this.observers.indexOf( observer, 0 ) );
};
Subject.prototype.notify = function( context ){
  var observerCount = this.observers.count();
  for(var i=0; i < observerCount; i++){
    this.observers.get(i).update( context );
  }
};

//观察者
function Observer(){
  this.update = function(){
    // 自定义行为
  };
}

发布-订阅模式

发布订阅模式:观察者模式也可以说是发布订阅模式,然而发布订阅模式与观察者模式不同之处就在于,添加了一个中介(调度中心)来避免发布者和订阅者之间产生依赖关系。

关系链: 订阅者(数量不限) --- 订阅 ---> 调度中心 <--- 发布 --- 发布者(不直接发布)

两种模式的区别:观察两种模式其实发现,模式的基本思想是一致的,仅仅是在调度是否是直接调度上有所不同。观察者模式是由具体目标直接调度的(eg: dom操作);而发布订阅模式是在调度中心调度,发布者与订阅者不产生依赖。

具体代码实现:

var pubsub = {};
(function(myObject) {
    // Storage for topics that can be broadcast
    // or listened to
    var topics = {};
    // An topic identifier
    var subUid = -1;
    // Publish or broadcast events of interest
    // with a specific topic name and arguments
    // such as the data to pass along
    myObject.publish = function( topic, args ) {
        if ( !topics[topic] ) {
            return false;
        }
        var subscribers = topics[topic],
            len = subscribers ? subscribers.length : 0;
        while (len--) {
            subscribers[len].func( topic, args );
        }
        return this;
    };
    // Subscribe to events of interest
    // with a specific topic name and a
    // callback function, to be executed
    // when the topic/event is observed
    myObject.subscribe = function( topic, func ) {
        if (!topics[topic]) {
            topics[topic] = [];
        }
        var token = ( ++subUid ).toString();
        topics[topic].push({
            token: token,
            func: func
        });
        return token;
    };
    // Unsubscribe from a specific
    // topic, based on a tokenized reference
    // to the subscription
    myObject.unsubscribe = function( token ) {
        for ( var m in topics ) {
            if ( topics[m] ) {
                for ( var i = 0, j = topics[m].length; i < j; i++ ) {
                    if ( topics[m][i].token === token ) {
                        topics[m].splice( i, 1 );
                        return token;
                    }
                }
            }
        }
        return this;
    };
}( pubsub ));

参考文献:《Learning JavaScript Design Patterns》

查看原文

赞 0 收藏 0 评论 0

蓝染 发布了文章 · 2019-03-26

跨域的解决方式

同源政策:协议、域名、端口均相同。

非同源限制:

  1. cookie、localStorage、indexDB无法读取。
  2. DOM无法获取。
  3. AJAX请求无法发送。

解决方式:

一、JSONP

原理:通过动态添加一个<script>元素,向服务器请求JSON数据。服务器接收请求返回到指定具名回调函数。

eg:

function addScript(src) {
    var script = document.createElement('script');
    script.setAttribute("type", "text/javscript");
    script.src = src;
    document.body.appendChild(script);
}

window.onload = function() {
    addScript("https://segmentfault.com/data?callback=getData");
}

function getData(data) {
    console.log(data)
}

注意:
1、查询的Url中callback需要指定回调函数的名字。
2、<script>在浏览器作为代码运行,定义的getData函数会被立即调用。
3、返回的JSON参数作为javascript对象,不是字符串,不需要进行JSON转换。
4、jquery库的 $.getJSON()也可以实现。

$.getJSON("https://segmentfault.com/data?callback=?", function(data) {
    console.log(data)
})

缺陷:是GET方式获取,不支持 POST。

二、window.postMessage

window.postMessage 无论是否同源都允许跨窗口通信。 postMessage 参数一是传递内容,参数二是协议+域名+端口或者(*表示不限制域名)

页面一:"https://www.segmentfault.com/page1.html"    //传递页面
<script>
    window.onload = function () {
        if (typeof window.postMessage === undefined) {
            alert("浏览器不支持postMessage!");
        } else {
            window.open.postMessage({data: "Hello World"}, "https://www.example.com/page2.html");
        }
    }
</script>
页面二:"https://www.example.com/page2.html"    //接收页面
<script>
    window.addEventListener('message', function(e) {
        console.log(e.data);
    },false);
</script>

事件接收window.addEventListener('message', function(){});中的message事件对象event有三个属性:
1、event.source:发送消息的窗口
2、event.origin: 消息发向的网址
3、event.data: 消息内容

<script>
    //引用父窗口发送信息给下一个窗口
    window.addEventListener('message', receiveMessage);
    function receiveMessage(event) {
      event.source.postMessage('Nice to see you!', '*');
    }
</script>
<script>
    //过滤不是发给本窗口的信息
    window.addEventListener('message', receiveMessage);
    function receiveMessage(event) {
      if (event.origin !== 'http://www.segmentfault.com/page1.html') return;
      if (event.data === 'Hello World') {
          event.source.postMessage('Hello', event.origin);
      } else {
        console.log(event.data);
      }
    }
</script>

三、iframe

iframe载入页面和src里面的目标域是同一个域,是能够发起ajax请求(父子窗口)。//前提是同源,不同源就不可以发起ajax请求。

不同窗口同源之间是可以获取window对象,但是不能获取window对象的属性和方法。//不同源会报错

1、document.domain + iframe(同源可用 -- 跨子域)

document.domain属性:一级域名相同,二级域名不同可以实现window对象获取。

页面一:"https://segmentfault.com/page1.html"
<script>
    window.onload = function() {
        document.domain = "https://segmentfault.com/";        //设置domain
        window.getData = function() {
            //ajax请求
        }
    }
</script>
页面二:"https://segmentfault.com/page2.html"
<iframe id="iframe" data-original="https://segmentfault.com/page1.html" onload="test()"></iframe>
<script>
    //动态创建iframe最佳,获取完数据销毁。
    //document.domain设置成自身或更高一级的父域,主域必须相同。
    document.domain = "https://segmentfault.com/"        //设置domain
    function test() {
        var win = document.getElementById("iframe").contentWindow;
        win.getData("https://segmentfault.com/json_domain.php", function() {})
    }
</script>

缺陷:主域名得一致

2、window.name + iframe(非同源可用)

window.name属性:在一个窗口的生命周期内,无论是否同源,同一个窗口的载入页面window.name属性是共享的,每个页面都可以操作。

页面一:"https://segmentfault.com/page1.html"
<script>
    window.name = "this is data!"
</script>
页面二:"https://segmentfault.com/page2.html"
<iframe id="iframe" data-original="https://segmentfault.com/page1.html" onload="test()"></iframe>
<script>
    //动态创建iframe最佳,获取完数据销毁。
    //获取window.name
    function test() {
        var winName = document.getElementById("iframe").contentWindow.name;
        winName.src = "https://segmentfault.com/data.html";        //最后需要将iframe的src设置成当前域的一个页面地址
    }
</script>

缺陷:兼容性不好

3、location.hash + iframe(非同源可用)

片段标识符:片段标识符是指url#号后面的部分。只是改变片段标识符页面不刷新。

页面一:"https://www.segmentfault.com/page1.html"
<script>  
    function startRequest(){
        var ifr = document.createElement('iframe');
        ifr.style.display = 'none';
        ifr.src = 'https://www.example.com/page2.html#messgae';
        document.body.appendChild(ifr);
    }
    
    function checkHash() {
        var data = location.hash ? location.hash.substring(1) : '';
    }
    setInterval(checkHash, 2000);
</script>
页面二:"https://www.example.com/page2.html#messgae"
<script>
    function callBack(){
        try {
            parent.location.hash = 'somedata';
        } catch (e) {
            // ie、chrome的安全机制无法修改parent.location.hash,
            // 所以要利用一个中间的example域下的代理iframe
            var ifrproxy = document.createElement('iframe');
            ifrproxy.style.display = 'none';
            ifrproxy.src = 'https:/www.segmentfault.com/page3html#somedata';    // 注意该文件在"segmentfault.com"域下
            document.body.appendChild(ifrproxy);
        }
    }
</script>
页面三:"ttps:/www.segmentfault.com/page3html#somedata"
<script>
    //因为parent.parent和自身属于同一个域,所以可以改变其location.hash的值
    parent.parent.location.hash = self.location.hash.substring(1);
</script>

缺点:数据暴露在url,长度也有限制。

四、WebSocket

WebSocket:浏览器通过 JavaScript 向服务器发出建立 WebSocket 连接的请求,连接建立以后,客户端和服务器端就可以通过 TCP 连接直接交换数据。

设置WebSocket请求头信息,服务器支持就可以进行。

Origin: http://example.com        //根据域名是否在白名单内来判断是否可以通信

缺点:实现成本高。

五、CORS

cors是跨域资源分享。现CORS通信的关键是服务器。只要服务器实现了CORS接口,就可以跨源通信。

缺点:服务器配置,占用主域带宽。

查看原文

赞 2 收藏 2 评论 0

认证与成就

  • 获得 3 次点赞
  • 获得 1 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 1 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2016-10-16
个人主页被 387 人浏览