前言
这是这个系列的最后一篇,讲述的是一些 JavaScript
的进阶知识,也算是复习学习重点了。 中篇发布的时候审核不通过,感兴趣的同学可以移步JavaScript核心知识总结(中)观看,主要讲述函数、作用域、闭包、原型this等内容。
异常处理
为什么要处理异常
- 增强用户体验;
- 远程定位问题;
- 未雨绸缪,及早发现问题;
- 无法复现问题,尤其是移动端,机型,系统都是问题;
- 完善的前端方案,前端监控系统;
需要处理哪些异常
JS
语法错误、代码异常AJAX
请求异常- 静态资源加载异常
Promise
异常Iframe
异常- 跨域
Script error
- 崩溃和卡顿
Try-catch
try-catch
只能捕获到同步的运行时错误,对语法和异步错误却无能为力,捕获不到。
- 同步运行时错误(可):
try { let name = 'jartto'; console.log(nam); } catch(e) { console.log('捕获到异常:',e); }
输出:
捕获到异常: ReferenceError: nam is not defined at <anonymous>:3:15
- 语法错误(不可)
我们修改一下代码,删掉一个单引号:
try { let name = 'jartto; console.log(nam); } catch(e) { console.log('捕获到异常:',e); }
输出:
Uncaught SyntaxError: Invalid or unexpected token
- 异步错误(不可)
try { setTimeout(() => { undefined.map(v => v); }, 1000) } catch(e) { console.log('捕获到异常:',e); }
我们看看日志:
Uncaught TypeError: Cannot read property 'map' of undefined at setTimeout (<anonymous>:3:11)
并没有捕获到异常,这是需要我们特别注意的地方。
window.onerror
当 JS
运行时错误发生时,window
会触发一个 ErrorEvent
接口的 error
事件,并执行 window.onerror()
。
js
复制代码
/** * @param {String} message 错误信息 * @param {String} source 出错文件 * @param {Number} lineno 行号 * @param {Number} colno 列号 * @param {Object} error Error对象(对象) */ window.onerror = function(message, source, lineno, colno, error) { console.log('捕获到异常:',{message, source, lineno, colno, error}); }
可以捕获的异常:
- 同步运行时错误
- 异步运行时错误
不可捕获的异常
- 语法错误
- 请求错误
- 静态资源错误
到这里基本就清晰了:在实际的使用过程中,
onerror
主要是来捕获预料之外的错误,而try-catch
则是用来在可预见情况下监控特定的错误,两者结合使用更加高效。
window.addEventListener
当一项资源(如图片或脚本)加载失败,加载资源的元素会触发一个 Event
接口的 error
事件,并执行该元素上的onerror()
处理函数。这些 error
事件不会向上冒泡到 window
,不过(至少在 Firefox
中)能被单一的window.addEventListener
捕获。
<script> window.addEventListener('error', (error) => { console.log('捕获到异常:', error); }, true) </script> <img src="./jartto.png">
控制台输出:
由于网络请求异常不会事件冒泡,因此必须在捕获阶段将其捕捉到才行,但是这种方式虽然可以捕捉到网络请求的异常,但是无法判断 HTTP
的状态是 404
还是其他比如 500
等等,所以还需要配合服务端日志才进行排查分析才可以。
Promise Catch
在 promise
中使用 catch
可以非常方便的捕获到异步 error
,这个很简单。
没有写 catch
的 Promise
中抛出的错误无法被 onerror
或 try-catch
捕获到,所以我们务必要在 Promise
中不要忘记写 catch
处理抛出的异常。
解决方案: 为了防止有漏掉的 Promise
异常,建议在全局增加一个对 unhandledrejection
的监听,用来全局监听Uncaught Promise Error
。使用方式:
window.addEventListener("`unhandledrejection`", function(e){ console.log(e); });
模块化
CommonJs
CommonJS API
定义很多普通应用程序(主要指非浏览器的应用)使用的API
,从而填补了这个空白。它的终极目标是提供一个类似Python
,Ruby
和Java
标准库。这样的话,开发者可以使用CommonJS API
编写应用程序,然后这些应用可以运行在不同的JavaScript
解释器和不同的主机环境中。
node.js
的模块系统,就是参照CommonJS
规范实现的。在CommonJS
中,有一个全局性方法require()
,用于加载模块。例如,要加载一个math.js
,可以如下这样加载
var math = require('math') //然后就可以调用math中的方法 math.add(2,3)//5
CommonJS
定义的模块分为:
- 模块引用---
require
---用来引入外部模块 - 模块定义---
exports
---用于导出当前模块的方法或变量 - 模块标识---
module-
--代表模块本身
CommonJS的原理以及简单实现
简单例子
var module = { exports: {} }; (function(module, exports) { exports.multiply = function (n) { return n * 1000 }; }(module, module.exports)) var f = module.exports.multiply; f(5) // 5000
上面代码向一个立即执行函数提供 module
和 exports
两个外部变量,模块就放在这个立即执行函数里面。模块的输出值放在 module
.exports
之中,这样就实现了模块的加载。
Browserify 的实现
Browserify
是目前最常用的 CommonJS
格式转换的工具。
请看一个例子,main.js
模块加载 foo.js
模块。
// foo.js module.exports = function(x) { console.log(x); }; // main.js var foo = require("./foo"); foo("Hi");
使用下面的命令,就能将main.js转为浏览器可用的格式。
$ browserify main.js > compiled.js
Browserify
到底做了什么?安装一下browser-unpack
,就能看清楚了。
$ npm install browser-unpack -g
然后,将前面生成的compile.js
解包。
$ browser-unpack < compiled.js
[ { "id":1, "source":"module.exports = function(x) {\n console.log(x);\n};", "deps":{} }, { "id":2, "source":"var foo = require(\"./foo\");\nfoo(\"Hi\");", "deps":{"./foo":1}, "entry":true } ]
可以看到,browserify
将所有模块放入一个数组,id
属性是模块的编号,source
属性是模块的源码,deps
属性是模块的依赖。
因为 main.js
里面加载了 foo.js
,所以 deps
属性就指定 ./foo
对应1号模块。执行的时候,浏览器遇到 require('./foo')
语句,就自动执行1号模块的 source
属性,并将执行后的 module.exports
属性值输出。
AMD
基于commonJS
规范的nodeJS
出来以后,服务端的模块概念已经形成,很自然地,大家就想要客户端模块。而且最好两者能够兼容,一个模块不用修改,在服务器和浏览器都可以运行。但是,由于一个重大的局限,使得CommonJS
规范不适用于浏览器环境。还是上面的代码,如果在浏览器中运行,会有一个很大的问题,你能看出来吗?
var math = require('math'); math.add(2, 3);
第二行math.add(2, 3)
,在第一行require('math')
之后运行,因此必须等math.js
加载完成。也就是说,如果加载时间很长,整个应用就会停在那里等。您会注意到 require
是同步的。
这对服务器端不是一个问题,因为所有的模块都存放在本地硬盘,可以同步加载完成,等待时间就是硬盘的读取时间。但是,对于浏览器,这却是一个大问题,因为模块都放在服务器端,等待时间取决于网速的快慢,可能要等很长时间,浏览器处于"假死"状态。
因此,浏览器端的模块,不能采用"同步加载"(synchronous),只能采用"异步加载"(asynchronous)。这就是AMD
规范诞生的背景。
CommonJS
是主要为了JS
在后端的表现制定的,他是不适合前端的,AMD
(异步模块定义)出现了,它就主要为前端JS的表现制定规范。
AMD
是"Asynchronous Module Definition"的缩写,意思就是"异步模块定义"。它采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。
AMD
也采用require()
语句加载模块,但是不同于CommonJS
,它要求两个参数:
require([module], callback);
第一个参数[module]
,是一个数组,里面的成员就是要加载的模块;第二个参数callback
,则是加载成功之后的回调函数。如果将前面的代码改写成AMD形式,就是下面这样:
require(['math'], function (math) { math.add(2, 3); });
以RequireJS为例说明AMD规范
一、为什么要用require.js?
最早的时候,所有Javascript代码都写在一个文件里面,只要加载这一个文件就够了。后来,代码越来越多,一个文件不够了,必须分成多个文件,依次加载。下面的网页代码,相信很多人都见过。
<script src="1.js"></script> <script src="2.js"></script> <script src="3.js"></script> <script src="4.js"></script> <script src="5.js"></script> <script src="6.js"></script>
这段代码依次加载多个js文件。
这样的写法有很大的缺点。首先,加载的时候,浏览器会停止网页渲染,加载文件越多,网页失去响应的时间就会越长;其次,由于js文件之间存在依赖关系,因此必须严格保证加载顺序(比如上例的1.js要在2.js的前面),依赖性最大的模块一定要放到最后加载,当依赖关系很复杂的时候,代码的编写和维护都会变得困难。
require.js
的诞生,就是为了解决这两个问题:
1.实现js文件的异步加载,避免网页失去响应
2.管理模块之间的依赖性,便于代码的编写和维护
AMD && CMD
对于依赖的模块,AMD
是提前执行,CMD
是延迟执行。不过 RequireJS
从 2.0 开始,也改成可以延迟执行(根据写法不同,处理方式不同)。CMD 推崇 as lazy as possible.
CMD 推崇依赖就近,AMD 推崇依赖前置。看代码:
// CMD define(function(require, exports, module) { var a = require('./a') a.doSomething() // 此处略去 100 行 var b = require('./b') // 依赖可以就近书写 b.doSomething() // ... }) // AMD 默认推荐的是 define(['./a', './b'], function(a, b) { // 依赖必须一开始就写好 a.doSomething() // 此处略去 100 行 b.doSomething() ... })
虽然 AMD
也支持 CMD
的写法,同时还支持将 require
作为依赖项传递,但 RequireJS
的作者默认是最喜欢上面的写法,也是官方文档里默认的模块定义写法。
AMD 的 API 默认是一个当多个用,CMD 的 API 严格区分,推崇职责单一。比如 AMD 里,require 分全局 require 和局部 require,都叫 require。CMD 里,没有全局 require,而是根据模块系统的完备性,提供 seajs.use 来实现模块系统的加载启动。CMD 里,每个 API 都简单纯粹。
ES6 Module
- 命令
export
:规定模块对外接口
- 默认导出:
export default Person
(导入时可指定模块任意名称,无需知晓内部真实名称) - 单独导出:
export const name = "Bruce"
- 按需导出:
export { age, name, sex }
(推荐) - 改名导出:
export { name as newName }
import
:导入模块内部功能
- 默认导入:
import Person from "person"
- 整体导入:
import * as Person from "person"
- 按需导入:
import { age, name, sex } from "person"
- 改名导入:
import { name as newName } from "person"
- 自执导入:
import "person"
- 复合导入:
import Person, { name } from "person"
- 复合模式:
export
命令和import
命令结合在一起写成一行,变量实质没有被导入当前模块,相当于对外转发接口,导致当前模块无法直接使用其导入变量
- 默认导入导出:
export { default } from "person"
- 整体导入导出:
export * from "person"
- 按需导入导出:
export { age, name, sex } from "person"
- 改名导入导出:
export { name as newName } from "person"
- 具名改默认导入导出:
export { name as default } from "person"
- 默认改具名导入导出:
export { default as name } from "person"
CommonJS和ESM的区别
CommonJS
输出值的拷贝,ESM
输出值的引用
CommonJS
一旦输出一个值,模块内部的变化就影响不到这个值ESM
是动态引用且不会缓存值,模块里的变量绑定其所在的模块,等到脚本真正执行时,再根据这个只读引用到被加载的那个模块里去取值
CommonJS
是运行时加载,ESM
是编译时加载
- CommonJS加载模块是对象(即
module.exports
),该对象只有在脚本运行完才会生成 - ESM加载模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成
循环加载
- 定义:脚本A的执行依赖脚本B,而脚本A的执行又依赖脚本B
- 加载原理
CommonJS
:require()
首次加载脚本就会执行整个脚本,在内存里生成一个对象缓存下来,二次加载脚本时直接从缓存中获取ESM
:import
命令加载变量不会被缓存,而是成为一个指向被加载模块的引用- 循环加载
CommonJS
:只输出已经执行的部分,还未执行的部分不会输出ESM
:需开发者自己保证真正取值时能够取到值(可把变量写成函数形式,函数具有提升作用)
性能优化
性能优化的方向主要分为以下几点:
- 编写高性能的
JavaScript
- 浏览器渲染
- 网络优化
- 打包工具的优化,以
webpack
为例
编写高性能的JavaScript
常见编码规范
- 将
js
脚本放在页面底部,加快渲染页面 - 将
js
脚本将脚本成组打包,减少请求 - 使用非阻塞方式下载
js
脚本 - 尽量使用局部变量来保存全局变量
- 遵循严格模式:
"use strict"
; - 尽量减少使用闭包
- 减少对象成员嵌套
- 缓存
DOM
节点访问 - 避免使用
eval()
和Function()
构造器 - 尽量使用直接量取创建对象和数组
- 最小化重绘(
repaint
)和回流(reflow
)
为什么JS要放到body尾部?
如果JS
需要绑定操作DOM
,那么放在header
中如果处理不当就不会绑定到DOM
**JS 引擎是独立于渲染引擎存在的。**我们的 JS
代码在文档的何处插入,就在何处执行。当 HTML
解析器遇到一个 script
标签时,它会暂停渲染过程,将控制权交给 JS
引擎。JS
引擎对内联的 JS
代码会直接执行,对外部 JS
文件还要先获取到脚本、再进行执行。等 JS
引擎运行完毕,浏览器又会把控制权还给渲染引擎,继续 CSSOM
和 DOM
的构建。
浏览器之所以让 JS
阻塞其它的活动,是因为它不知道 JS
会做什么改变,担心如果不阻止后续的操作,会造成混乱。
结论:
- 如果JS在header中,浏览器会阻塞并等待JS加载完毕并执行
- 如果JS在body尾部,浏览器会进行一次提前渲染,从而提前首屏出现时间
参考demo: 执行/性能优化/testDemo/slowServer/index.js
,注意查看终端
非核心代码的异步加载
- 动态脚本加载
- 使用
JS
创建一个script
标签再插入到页面中
- defer(IE)
- 整个HTML解析完后才会执行,如果是多个,按照加载顺序依次执行
- async
- 加载完之后立即执行,如果是多个,执行和加载顺序无关
header中meta
兼容性配置,让IE
使用最高级的Edge
渲染,如果有chrome
就使用chrome
渲染。
<meta http-equiv="X-UA-Compatible" content="IE=Edge,chrome=1">
如果是双核浏览器,优先使用webkit
引擎
<meta name="render" content="webkit">
懒加载
懒加载的原理就是只加载自定义区域(通常是可视区域,但也可以是即将进入可视区域)内需要加载的东西。 对于图片来说,先设置图片标签的 src
属性为一张占位图或为空,将真实的图片资源放入一个自定义属性中,当进入自定义区域时,就将自定义属性替换为 src
属性,这样图片就会去下载资源,实现了图片懒加载。
浏览器渲染
HTML
经过解析生成 DOM
树; CSS
经过解析生成 Style Rules
。 二者一结合生成了Render Tree
。 通过layout
计算出DOM
要显示的宽高、位置、颜色。 最后渲染在界面上,用户就看到了
浏览器的渲染过程:
- 解析HTML构建DOM(树),并行请求css/image/js
- CSS文件下载完成,开始构建CSSOM(CSS树)
- CSSOM构建结束后,和DOM一起生成render tree(渲染树)
- 布局(Layout):计算出每个节点在屏幕中的位置
- 显示(Painting):通过显卡把页面画到屏幕上
DOM
树与渲染树的区别
DOM
树与HTML
标签一一对应,包括head
和隐藏元素- 渲染树不包括
head
和隐藏元素,大段文本的每一个行都是独立节点,每一个节点都有对应的css
属性
CSS会阻塞DOM解析吗
对于一个HTML
文档来说,不管是内联还是外链的CSS
,都会阻碍后续的DOM
渲染,但是不会阻碍后续的DOM
解析
当css文件放在中时,虽然css解析也会阻碍后续DOM的渲染,但是在解析CSS的同时也在解析DOM
,所以等到css解析完毕就会逐步的渲染页面了
重绘和回流(重排)的区别和关系?
- 重绘:当渲染树中的元素外观(如:颜色)发生改变,不影响布局时,产生重绘
- 回流:当渲染树中的元素的布局(如:尺寸、位置、隐藏/状态状态)发生改变时,产生重绘回流
- 注意:JS 获取
Layout
属性值(如:offsetLeft
、scrollTop
、getComputedStyle
等)也会引起回流。因为浏览器需要通过回流计算最新值 - 回流必将引起重绘,而重绘不一定会引起回流
DOM结构中的各元素都有自己的盒子,这些都需要浏览器根据各种样式来计算并更具结果将元素放到它该出现的位置,这个过程叫 reflow
触发reflow
- 添加或删除可见的DOM元素。
- 元素位置改变。
- 元素的尺寸改变(包括:内外边距、边框厚度、宽度、高度等属性的改变)。
- 内容改变。
- 页面渲染器初始化。
- 浏览器窗口尺寸改变。
解决方法:
- 需要要对DOM元素进行复杂的操作时,可以先隐藏(
display:"none"
),操作完成后再显示 - 需要创建多个 DOM 节点时,使用
DocumentFragment
创建完后一次性的加入document
,或使用字符串拼接方式构建好对应HTML后再使用innerHTML来修改页面 - 缓存
Layout
属性值,如:var left = elem.offsetLeft
; 这样,多次使用left
只产生一次回流 - 避免用
table
布局(table
元素一旦触发回流就会导致table
里所有的其它元素回流) - 避免使用
css
表达式(expression
),因为每次调用都会重新计算值(包括加载页面) - 尽量使用
css
属性简写,如:用 border 代替 border-width, border-style, border-color - 批量修改元素样式:
elem.className
和elem.style.cssText
代替elem.style.xxx
网络优化
合并资源文件,减少HTTP请求
浏览器并发的HTTP
请求是由数量限制的(比如桌面浏览器并发请求可能是8个,手机浏览器是6个),如果一下子并发的几十个请求那么会有很多请求会停下来等,等前面的请求好了下一个再进去,这样就延长了整个页面的加载时间
压缩资源文件减小请求大小
文件大小越小当然加载速度就越快。 可对代码进行压缩,去掉空格、注释、变量替换,在传输时,使用gzip
等压缩方式也可以降低资源文件的大小。
缓存分类
- 强缓存
- 直接从浏览器缓存中读取,不去后台查询是否过期
Expire
过期时间Cache-Control:max-age=3600
过期秒数
- 协商缓存
- 每次使用缓存之前先去后台确认一下
Last-Modified
If-Modified-Since
上次修改时间Etag
If-None-Match
- 如何区别
- 是否设置了
no-cache
利用缓存机制,尽可能使用缓存减少请求
浏览器是有缓存机制的,在返回资源的时候设置一个cache-control
设置过期时间,在过期时间内浏览器会默认使用本地缓存。
但缓存机制也存在一定的问题,因为网站开发是阶段性的,隔一段时间会发布一个新的版本。因为HTTP
请求是根据url
来定位的,如果资源文件名的url
没有发生更改那么浏览器还是会使用缓存,这个时候怎么办那? 这时就需要一个缓存更新机制来让修改过的文件具有一个新的名字。 最简单的方法就是在url
后加一个时间戳,但是这会导致只要有新的版本发布就会重新获取所有的新资源。 一个现代流行的方法就是根据文件计算一个hash
值,这个hash值是根据文件的更新变化而变化的。 当浏览器获取文件时如果这个文件名有更新那么就会请求新的文件。
DNS预解析
现代浏览器在 DNS Prefetch
上做了两项工作:
html
源码下载完成后,会解析页面的包含链接的标签,提前查询对应的域名- 对于访问过的页面,浏览器会记录一份域名列表,当再次打开时,会在
html
下载的同时去解析DNS
自动解析
浏览器使用超链接的href属性来查找要预解析的主机名。当遇到a标签,浏览器会自动将href中的域名解析为IP地址,这个解析过程是与用户浏览网页并行处理的。但是为了确保安全性,在HTTPS页面中不会自动解析
手动解析
预解析某域名 <link rel="dns-prefetch" href="//img.alicdn.com"> 强制开启HTTPS下的DNS预解析 <meta http-equiv="x-dns-prefetch-control" content="on">
CDN
CDN
的原理是尽可能的在各个地方分布机房缓存数据。
因此,我们可以将静态资源尽量使用 CDN
加载,由于浏览器对于单个域名有并发请求上限,可以考虑使用多个 CDN
域名。并且对于 CDN
加载静态资源需要注意 CDN
域名要与主站不同,否则每次请求都会带上主站的 Cookie
,平白消耗流量。
预加载
在开发中,可能会遇到这样的情况。有些资源不需要马上用到,但是希望尽早获取,这时候就可以使用预加载。
预加载其实是声明式的 fetch
,强制浏览器请求资源,并且不会阻塞 onload
事件,可以使用以下代码开启预加载。
预加载可以一定程度上降低首屏的加载时间,因为可以将一些不影响首屏但重要的文件延后加载,唯一缺点就是兼容性不好。
<link rel="preload" href="http://example.com">
预渲染
可以通过预渲染将下载的文件预先在后台渲染,可以使用以下代码开启预渲染。
图片优化
- 不用图片。很多时候会使用到很多修饰类图片,其实这类修饰图片完全可以用
CSS
去代替。 - 对于移动端按理说,图片不需要加载原图,可请求裁剪好的图片
- 小图使用
base64
格式 - 将多个图标文件整合到一张图中(雪碧图)
- 采用正确的图片格式
- 对于能够显示
WebP
格式的浏览器尽量使用WebP
格式。因为WebP
格式具有更好的图像数据压缩算法,能带来更小的图片体积,而且拥有肉眼识别无差异的图像质量,缺点就是兼容性并不好 - 色彩很多的使用
JPEG
- 色彩种类少的使用
PNG
,有的可用SVG
代替
webpack中优化
主要优化思路是以下两点:
- 有哪些方式可以减少
Webpack
的打包时间 - 有哪些方式可以让
Webpack
打出来的包更小
减小打包后文件体积
按需加载
如果我们将页面全部打包进一个 JS
文件的话,虽然将多个请求合并了,但是同样也加载了很多并不需要的代码,耗费了更长的时间。那么为了首页能更快地呈现给用户,我们肯定是希望首页能加载的文件体积越小越好,这时候我们就可以使用按需加载,将每个路由页面单独打包为一个文件。
Tree Shaking
Tree Shaking
可以实现删除项目中未被引用的代码,比如
// test.js export const a = 1 export const b = 2 // index.js import { a } from './test.js'
对于以上情况,test 文件中的变量 b 如果没有在项目中使用到的话,就不会被打包到文件中。
如果你使用 Webpack 4
的话,开启生产环境就会自动启动这个优化功能。
Scope Hoisting
Scope Hoisting
会分析出模块之间的依赖关系,尽可能的把打包出来的模块合并到一个函数中去。 比如我们希望打包两个文件
// test.js export const a = 1 // index.js import { a } from './test.js'
对于这种情况,我们打包出来的代码会类似这样
[ /* 0 */ function (module, exports, require) { //... }, /* 1 */ function (module, exports, require) { //... } ]
但是如果我们使用 Scope Hoisting
的话,代码就会尽可能的合并到一个函数中去,也就变成了这样的类似代码
[ /* 0 */ function (module, exports, require) { //... } ]
样的打包方式生成的代码明显比之前的少多了。如果在 Webpack4 中你希望开启这个功能,只需要启用 optimization.concatenateModules 就可以了。
module.exports = { optimization: { concatenateModules: true } }
加快打包速度
优化 Loader
对于 Loader
来说,影响打包效率首当其冲必属 Babel
了。因为 Babel
会将代码转为字符串生成 AST(抽象语法树),然后对 AST 继续进行转变最后再生成新的代码,项目越大,转换代码越多,效率就越低。当然了,我们是有办法优化的。
首先我们可以减小 Loader 的文件搜索范围
module.exports = { module: { rules: [ { // js 文件才使用 babel test: /\.js$/, loader: 'babel-loader', // 只在 src 文件夹下查找 include: [resolve('src')], // 不会去查找的路径 exclude: /node_modules/ } ] } }
还可以将 Babel 编译过的文件缓存起来,下次只需要编译更改过的代码文件即可,这样可以大幅度加快打包时间。
loader: 'babel-loader?cacheDirectory=true'
HappyPack
受限于 Node
是单线程运行的,所以 Webpack
在打包的过程中也是单线程的,特别是在执行 Loader
的时候,长时间编译的任务很多,这样就会导致等待的情况。
HappyPack 可以将 Loader 的同步执行转换为并行的,这样就能充分利用系统资源来加快打包效率了
module: { loaders: [ { test: /\.js$/, include: [resolve('src')], exclude: /node_modules/, // id 后面的内容对应下面 loader: 'happypack/loader?id=happybabel' } ] }, plugins: [ new HappyPack({ id: 'happybabel', loaders: ['babel-loader?cacheDirectory'], // 开启 4 个线程 threads: 4 }) ]
DllPlugin
DllPlugin
可以将特定的类库提前打包然后引入。这种方式可以极大的减少打包类库的次数,只有当类库更新版本才有需要重新打包,并且也实现了将公共代码抽离成单独文件的优化方案。
// 单独配置在一个文件中 // webpack.dll.conf.js const path = require('path') const webpack = require('webpack') module.exports = { entry: { // 想统一打包的类库 vendor: ['react'] }, output: { path: path.join(__dirname, 'dist'), filename: '[name].dll.js', library: '[name]-[hash]' }, plugins: [ new webpack.DllPlugin({ // name 必须和 output.library 一致 name: '[name]-[hash]', // 该属性需要与 DllReferencePlugin 中一致 context: __dirname, path: path.join(__dirname, 'dist', '[name]-manifest.json') }) ] }
然后我们需要执行这个配置文件生成依赖文件,接下来我们需要使用 DllReferencePlugin 将依赖文件引入项目中
// webpack.conf.js module.exports = { // ...省略其他配置 plugins: [ new webpack.DllReferencePlugin({ context: __dirname, // manifest 就是之前打包出来的 json 文件 manifest: require('./dist/vendor-manifest.json'), }) ] }
代码压缩
在 Webpack3
中,我们一般使用 UglifyJS
来压缩代码,但是这个是单线程运行的,为了加快效率,我们可以使用 webpack-parallel-uglify-plugin
来并行运行 UglifyJS
,从而提高效率。
在 Webpack4
中,我们就不需要以上这些操作了,只需要将 mode
设置为 production
就可以默认开启以上功能。代码压缩也是我们必做的性能优化方案,当然我们不止可以压缩 JS 代码,还可以压缩 HTML、CSS
代码,并且在压缩 JS
代码的过程中,我们还可以通过配置实现比如删除 console.log
这类代码的功能。
一些小的优化点
resolve.extensions
用来表明文件后缀列表,默认查找顺序是 ['.js', '.json'],如果你的导入文件没有添加后缀就会按照这个顺序查找文件。我们应该尽可能减少后缀列表长度,然后将出现频率高的后缀排在前面resolve.alias
可以通过别名的方式来映射一个路径,能让 Webpack 更快找到路径module.noParse
如果你确定一个文件下没有其他依赖,就可以使用该属性让 Webpack 不扫描该文件,这种方式对于大型的类库很有帮助
web安全
web中常见的安全问题有以下数种:
- 同源策略
- XSS 跨站脚本攻击
- CSRF 跨站请求伪造
同源策略
最初,它的含义是指,A
网页设置的 Cookie
,B
网页不能打开,除非这两个网页"同源"。所谓"同源"指的是"三个相同"。
- 协议相同
- 域名相同
- 端口相同
目的
同源政策的目的,是为了保证用户信息的安全,防止恶意的网站窃取数据。
设想这样一种情况:A网站是一家银行,用户登录以后,又去浏览其他网站。如果其他网站可以读取A网站的 Cookie
,会发生什么?
很显然,如果 Cookie
包含隐私(比如存款总额),这些信息就会泄漏。更可怕的是,Cookie
往往用来保存用户的登录状态,如果用户没有退出登录,其他网站就可以冒充用户,为所欲为。因为浏览器同时还规定,提交表单不受同源政策的限制。
由此可见,"同源政策"是必需的,否则 Cookie
可以共享,互联网就毫无安全可言了。
限制范围
(1) Cookie``、LocalStorage
和 IndexDB
无法读取。
(2) DOM
无法获得。
(3) AJAX
请求不能发送。
跨域方案
- CORS
- JSONP
- document.domain+iframe
- location.hash+iframe
- window.name+iframe
- postMessage
- WebSocket
- node中间件
- nginx代理
CORS跨域资源请求
CORS(Cross-origin resource sharing)
跨域资源请求
浏览器在请求一个跨域资源的时候,如果是跨域的Ajax
请求,他会在请求头中加一个origin
字段,但他是不知道这个资源服务端是否允许跨域请求的。浏览器会发送到服务端,如果服务器返回的头中没有'Access-Control-Allow-Origin': '对应网址或 * '
的话,那么浏览器就会把请求内容给忽略掉,并且在控制台报错
CORS限制
允许的请求方法
GET
POST
HEAD
允许的Content-Type
text/plain
multipart/form-data
application/x-www-form-urlencoded
其他类型的请求方法和Content-Type
需要通过预请求验证后然后才能发送
CORS预请求
跨域资源共享标准新增了一组 HTTP
首部字段,允许服务器声明哪些源站有权限访问哪些资源。另外,规范要求,对那些可能对服务器数据产生副作用的 HTTP
请求方法(特别是 GET
以外的 HTTP
请求,或者搭配某些 MIME
类型的 POST
请求),浏览器必须首先使用 OPTIONS
方法发起一个预检请求。
服务器在HTTP header
中加入允许请求的方法和Content-Type
后,其他指定的方法和Content-Type
就可以成功请求了
a
'Access-Control-Allow-Headers': '允许Content-Type' 'Access-Control-Allow-Methods': '允许的请求方法' 'Access-Control-Max-Age': '预请求允许其他方法和类型传输的时间'
JSONP跨域
浏览器上虽然有同源限制,但是像 script标签、link标签、img标签、iframe标签,这种在标签上通过src地址来加载一些内容的时候浏览器是允许进行跨域请求的。
所以JSONP的原理就是:
- 创建一个
script
标签,这个script
标签的src
就是请求的地址; - 这个
script
标签插入到DOM
中,浏览器就根据src
地址访问服务器资源 - 返回的资源是一个文本,但是因为是在script标签中,浏览器会执行它
- 而这个文本恰好是函数调用的形式,即函数名(数据),浏览器会把它当作JS代码来执行即调用这个函数
- 只要提前约定好这个函数名,并且这个函数存在于
window
对象中,就可以把数据传递给处理函数。
仅限主域相同,子域不同
父窗口:www.domain.com/a.html
<iframe id="iframe" src="http://child.domain.com/b.html"></iframe> <script> document.domain = 'domain.com'; var user = 'admin'; </script>
子窗口:child.domain.com/b.html
<script> document.domain = 'domain.com'; // 获取父窗口中变量 alert('get js data from parent ---> ' + window.parent.user); </script>
location.hash + iframe
实现原理: a
欲与b
跨域相互通信,通过中间页c
来实现。 三个页面,不同域之间利用iframe
的location.hash
传值,相同域之间直接js
访问来通信。
具体实现:A域:a.html -> B域:b.html -> A域:c.html,a与b不同域只能通过hash值单向通信,b与c也不同域也只能单向通信,但c与a同域,所以c可通过parent.parent访问a页面所有对象。
1.)a.html:(www.domain1.com/a.html)
<iframe id="iframe" src="http://www.domain2.com/b.html" style="display:none;"></iframe> <script> var iframe = document.getElementById('iframe'); // 向b.html传hash值 setTimeout(function() { iframe.src = iframe.src + '#user=admin'; }, 1000); // 开放给同域c.html的回调方法 function onCallback(res) { alert('data from c.html ---> ' + res); } </script>
2.)b.html:(www.domain2.com/b.html)
<iframe id="iframe" src="http://www.domain1.com/c.html" style="display:none;"></iframe> <script> var iframe = document.getElementById('iframe'); // 监听a.html传来的hash值,再传给c.html window.onhashchange = function () { iframe.src = iframe.src + location.hash; }; </script>
3.)c.html:(www.domain1.com/c.html)
<script> // 监听b.html传来的hash值 window.onhashchange = function () { // 再通过操作同域a.html的js回调,将结果传回 window.parent.parent.onCallback('hello: ' + location.hash.replace('#user=', '')); }; </script>
JavaScript核心知识总结(下)二https://developer.aliyun.com/article/1495085