作用域是一套用来管理引擎如何在当前作用域以及嵌套的子作用域中根据标识符名称进行变量查找的规则。
{...}
块内部let
和const
定义块作用域,典型应用是for循环。注意const虽然也可以创建块作用域,但有别不let,其值是固定的常量,任何对其值的修改都会引起错误在掌握作用域的前提下,才能真正理解和识别闭包。
import
、export
实现模块应用。闭包的应用示例:
for( var i=0; i<5; i++ ){
!function( j ){
setTimeout(function(){
console.log( j )
}, j*1000)
}( i )
}
// 也可以用 let 通过块作用域实现
for( let i=0; i<5; i++ ){
setTimeout( function(){
console.log( i )
}, i*1000 )
}
当一个函数被调用时,会创建一个活动记录(也叫执行上下文)。这个上下文会包括函数在哪里被调用(调用栈)、函数的调用方法、传入的参数等信息。this就是其中一个属性,会在函数执行过程中用到。而this的指向则取决于函数调用位置而非函数定义位置。也就是说谁调用函数,则函数上下文中的this就指向谁。
this的绑定规则如下。
function
关键字的函数:undefined
,报 TypeError错误var bar = obj.fn()
,但隐式绑定容易造成误导call
或apply
或硬绑定bind
来显式明确具体函数调用时其内部this的指向,如var bar = fn.call(obj1)
;、硬绑定var baz = fn.bind(obj2)
。new
绑定 fn.__proto__ === Fn.prototype
)先查看函数的调用位置,然后再通过绑定规则来判定this指向,如同时存在多种绑定规则的,则按优化级对比。
如果函数内部不关心this指向,可以使用例如call(null)
来忽略函数中的this绑定。
var self = this;
机制。js规范中,定义七种数据类型,分为基本类型和引用类型两大类:
js中还有内置对象:String、Number、Boolean、Object、Function、Array、Date、RegExp、Error等。
其中字符串是基本数据类型,本身不存任何操作方法 。为了方便的对字符串进行操作,ES 提供了一个基本包装类型:String 对象 。它是一种特殊的引用类型,JS引擎每当读取一个字符串的时候,就会在内部创建一个对应的 String 对象,该对象提供了很多操作字符的方法,这就是为什么能对字符串调用方法的原因。
对象Object有4个数据描述符:value(属性值)、writable(可写)、enumerable(可枚举)、configurable(可配置),后三者的默认值均为true;2个访问描述符:getter和setter。
先看4个数据描述符:
在用对象字符量方式创建对象时,对象的属性特性均会使用默认值
如果想自定义属性特性,可以通过Object.defineProperty()来添加一个新属性或者修改一个已有属性,当然想自定义的前提是configurable属性要为true。
var newObj = {};
Object.defineProperty(newObj, 'a', {
value: 10,
writable: true,
enumerable: true,
configurable: true
})
通过Object.defineProperty()来控制对象属性的特性,比较好玩的一个实现就是生成一个真正的常量属性(不可修改、重定义或者删除)
var obj = {};
Object.defineProperty(obj, 'a', {
writable: false,
configurable: false
})
再看2个访问描述符:当对属性定义访问描述符时,js会忽略它们的 value和writable特性,而改为关心 set和get以及configurable和enumerable特性。
var obj = {
//给a定义一个getter
get a(){
return 3;
}
}
Object.defineProperty(obj, 'b', {
//给b设置一个getter
get: function(){return this.a*2;},
//确保b会出现对象的属性列表中
enmuerable: true
})
console.log( obj.a ); // 3
console.log( obj.b ); // 6
对象遍历时需注意以下几点:
手写实现4种继承
function Father () {}
function Child () {}
// 1. 原型继承
Child.prototype = new Father()
// 2. 构造继承
function Child (name) {
Father.call(this, name)
}
// 3. 组合继承
function Child (name) {
Father.call(this, name)
}
Child.prototype = new Father()
// 4. 寄生继承
function cloneObj (o) {
var clone = object.create(o)
clone.sayName = ...
return clone
}
// 5. 寄生组合继承
// 6. ES6 class extend继承
class Child extends Father{
super();
}
Object.create实现(原型式继承,特点:实例的proto指向构造函数本身)
ES6提供的一种异步编程解决方案,Generator函数是一个状态机,封装了多个内部状态。
function* helloWorldGenerator() {
yield 'hello';
yield 'world';
return 'ending';
}
let hw = helloWorldGenerator();
// 调用后返回指向内部状态的指针, 调用next()才会移向下一个状态, 参数:
hw.next()
// { value: 'hello', done: false }
hw.next()
// { value: 'world', done: false }
hw.next()
// { value: 'ending', done: true }
hw.next()
// { value: undefined, done: true }
const myPromise = new Promise((resolve, reject) => {
// 需要执行的代码
// ...
if (/* 异步执行成功 */) {
resolve(value)
} else if (/* 异步执行失败 */) {
reject(error)
}
})
// promise一旦新建就会立即执行
myPromise
.then( value => {
// 成功后调用, 使用value值
},err=>{
// 失败后调用,使用error值
})
.catch( error => {
// 当执行多个then时也可统一捕获失败,使用error值
} )
极简版promise封装:
function Promise () {
this.msg = '' // 存放value和error
this.status = 'pending'
const that = this
const process = arguments[0]
process (function () {
that.status = 'fulfilled'
that.msg = arguments[0]
}, function () {
that.status = 'rejected'
that.msg = arguments[0]
})
return this
}
Promise.prototype.then = function () {
if (this.status === 'fulfilled') {
arguments[0](this.msg)
} else if (this.status === 'rejected' && arguments[1]) {
arguments[1](this.msg)
}
}
又称发布-订阅模式
//定义发布订阅对象
let EventEmitter = function() {
let cacheList = {}, //缓存列表,存放已订阅的事件回调
listen, //订阅命名事件和对应事件回调
emit, //触发命名事件,必传第一个参数为事件的命名,其后参数为选传,数量不限,用于作为事件回调的实参传入
remove; //取消命名事件订阅,并清除该命名事件对应的事件回调
listen = function(key, fn) {
//如果还没有订阅过此命名事件,就给该命名事件创建一个数组型的缓存列表,用于存放对应的事件回调
if (!cacheList[key]) {
cacheList[key] = []
}
//将对应的事件回调传入该命名事件的缓存列表中
cacheList[key].push(fn)
}
emit = function() {
let key = Array.prototype.shift.call(arguments), //取出事件命名
fns = cacheList[key]; //取出该命名事件对应的事件回调缓存列表
//如果没有订阅该命名事件或对应的事件回调缓存列表为空数组,则直接返回false
if (!fns || fns.length == 0) {
return false;
}
// 遍历该命名事件对应的事件回调缓存列表数组,对数组中的每个事件回调传入处理后的实参列表,然后执行
for (let i = 0; i < fns.length; i++) {
// arguments为触发命名事件时传入的参数类数组,此时arguments已被取出索引为0处的事件命名,剩余元素就是要传入事件回调中的所有参数
fns[i].apply(this, arguments)
}
}
remove = function(key, fn) {
// 获取将要被删除的事件命名的事件回调缓存列表
let fns = cacheList[key]
// 如果没有预存事件回调或该命名事件对应的事件回调缓存列表为空数组,直接返回false
if (!fns || fns.length == 0) {
return false;
}
if (!fn) {
// 如果没有显式传入具体的事件回调函数,则清除该命名事件对应的所有事件回调缓存
fns.length = 0
} else {
// 遍历事件命名对应的事件回调缓存列表,如传入要删除的事件回调函数与缓存列表数组中的某项匹配,就删除该项
for (let l = fns.length - 1; l >= 0; l--) {
let _fn = fns[l]
if (_fn == fn) {
fns.splice(l, 1)
}
}
}
}
return {
cacheList,
listen,
emit,
remove
}
}
let emitter = new EventEmitter();
emitter.listen( "event1", function(data){
console.log( data )
} )
emitter.emit( "event1", "数据1" )
Function.prototype.bind = function () {
// 保存原函数
var self = this
// 取出第一个参数作为上下文, 相当于[].shift.call(arguments)
var context = Array.prototype.shift.call(arguments)
// 取剩余的参数作为arg; 因为arguments是伪数组, 所以要转化为数组才能使用数组方法
var arg = Array.prototype.slice.call(arguments)
// 返回一个新函数
return function () {
// 绑定上下文并传参
self.apply(context, Array.prototype.concat.call(arg, Array.prototype.slice.call(arguments)))
}
}
function jsonp ( {url, param, callback} ) {
return new Promise((resolve, reject) => {
// 创建script标签
let script = document.createElement('script')
window.callback = function (data) {
resolve(data)
//移除script标签
document.body.removeChild('script')
}
let param = {...param, callback}
let arr = []
for (let key in param) {
arr.push(`${key}=${param[key]}`)
}
// 拼接url地址
script.src = `${url}?${arr.join('&')}`
// 将创建好的script标签添加到body下面
document.body.appendChild(script)
})
}
JS是单线程, 先执行同步主线程, 再执行异步任务队列
// 函数节流
let throttle = function( fn, interval ){
let timer = null,
firstTime = true;
return function(){
let _me = this;
// 如果是第一次,不需要调用延迟器
if( firstTime ){
fn.apply( _me, arguments );
return firstTime = false;
}
// 如果定时器还在,则前一次延迟还未完成,则忽略该函数的下一次请求
if( timer ){
return false;
}
// 延迟一段时间执行
timer = setTimeout( function(){
clearTimeout( timer );
timer = null;
fn.apply( _me, arguments )
}, interval || 500 )
}
}
let callback = function(){
console.log( 124 );
}
window.onresize = throttle( callback, 2000 );
// 函数防抖
var timer = false;
document.getElementById("debounce").onscroll = function(){
clearTimeout(timer); // 清除未执行的代码,重置回初始化状态
timer = setTimeout(function(){
console.log("函数防抖");
}, 300);
};
这种sleep函数是最简单粗暴的,调用sleep函数会导致CPU占用高升,存在性能问题。执行环境兼容情况下可以另用async/await。
// 这种实现方式是利用一个伪死循环阻塞主线程。因为JS是单线程的。所以通过这种方式可以实现真正意义上的sleep()。
function sleep(delay) {
var start = (new Date()).getTime();
while ((new Date()).getTime() - start < delay) {
continue;
}
}
function test() {
console.log('111');
sleep(2000);
console.log('222');
}
test()
为了避免全局污染,可以用匿名函数包裹起来,这就是最简单的 IIFE 模块
// 定义 IIFE 模块
const iifeCounterModule = (() => {
let count = 0;
return {
increase: () => ++count,
reset: () => {
count = 0;
console.log("Count is reset.");
}
};
})();
// 使用 IIFE 模块
iifeCounterModule.increase();
iifeCounterModule.reset();
// 可以在模块内部直接使用依赖的全局变量,也可以把依赖作为参数传给 IIFE
// 定义带有依赖的 IIFE 模块
const iifeCounterModule = ((dependencyModule1, dependencyModule2) => {
let count = 0;
return {
increase: () => ++count,
reset: () => {
count = 0;
console.log("Count is reset.");
}
};
})(dependencyModule1, dependencyModule2);
CommonJS 最初叫 ServerJS,是由 Node.js 实现的模块化方案。默认情况下,每个 .js 文件就是一个模块,模块内部提供了一个module和exports变量,用于暴露模块的 API。使用 require 加载和使用模块。
// 定义 CommonJS 模块: commonJSCounterModule.js.
const dependencyModule1 = require("./dependencyModule1");
const dependencyModule2 = require("./dependencyModule2");
let count = 0;
const increase = () => ++count;
const reset = () => {
count = 0;
console.log("Count is reset.");
};
exports.increase = increase;
exports.reset = reset;
// 或者这样:
module.exports = {
increase,
reset
};
// -----使用这个模块:
// 使用 CommonJS 模块
const { increase, reset } = require("./commonJSCounterModule");
increase();
reset();
// 或者这样:
const commonJSCounterModule = require("./commonJSCounterModule");
commonJSCounterModule.increase();
commonJSCounterModule.reset();
AMD(异步模块定义)也是一种模块格式,由 RequireJS 这个库实现。它通过define函数定义模块,并接受模块名和依赖的模块名作为参数。
// 定义 AMD 模块
define("amdCounterModule", ["dependencyModule1", "dependencyModule2"],
(dependencyModule1, dependencyModule2) => {
let count = 0;
const increase = () => ++count;
const reset = () => {
count = 0;
console.log("Count is reset.");
};
return {
increase,
reset
};
});
// ------用 require加载和使用模块:
require(["amdCounterModule"], amdCounterModule => {
amdCounterModule.increase();
amdCounterModule.reset();
});
跟 CommonJS 不同,这里的 requrie接受一个回调函数,参数就是加载好的模块对象。
AMD 的define函数还可以动态加载模块,只要给它传一个回调函数,并带上 require参数:
// Use dynamic AMD module.
define(require => {
const dynamicDependencyModule1 = require("dependencyModule1");
const dynamicDependencyModule2 = require("dependencyModule2");
let count = 0;
const increase = () => ++count;
const reset = () => {
count = 0;
console.log("Count is reset.");
};
return {
increase,
reset
};
});
AMD 模块还可以给define传递module和exports,这样就可以在内部使用 CommonJS 代码:
// 定义带有 CommonJS 代码的 AMD 模块
define((require, exports, module) => {
// CommonJS 代码
const dependencyModule1 = require("dependencyModule1");
const dependencyModule2 = require("dependencyModule2");
let count = 0;
const increase = () => ++count;
const reset = () => {
count = 0;
console.log("Count is reset.");
};
exports.increase = increase;
exports.reset = reset;
});
// 使用带有 CommonJS 代码的 AMD 模块
define(require => {
// CommonJS 代码
const counterModule = require("amdCounterModule");
counterModule.increase();
counterModule.reset();
});
UMD(通用模块定义),是一种支持多种环境的模块化格式,可同时用于 AMD 和 浏览器(或者 Node.js)环境。看起来很复杂,其实就是个 IIFE
兼容 AMD 和浏览器全局引入:
((root, factory) => {
// 检测是否存在 AMD/RequireJS 的 define 函数
if (typeof define === "function" && define.amd) {
// 如果是,在 define 函数内调用 factory
define("umdCounterModule", ["deependencyModule1", "dependencyModule2"], factory);
} else {
// 否则为浏览器环境,直接调用 factory
// 导入的依赖是全局变量(window 对象的属性)
// 导出的模块也是全局变量(window 对象的属性)
root.umdCounterModule = factory(root.deependencyModule1, root.dependencyModule2);
}
})(typeof self !== "undefined" ? self : this, (deependencyModule1, dependencyModule2) => {
// 具体的模块代码
let count = 0;
const increase = () => ++count;
const reset = () => {
count = 0;
console.log("Count is reset.");
};
return {
increase,
reset
};
});
下面这个是兼容 AMD 和 CommonJS(Node.js)模块的 UMD:
(define => define((require, exports, module) => {
// 模块代码
const dependencyModule1 = require("dependencyModule1");
const dependencyModule2 = require("dependencyModule2");
let count = 0;
const increase = () => ++count;
const reset = () => {
count = 0;
console.log("Count is reset.");
};
module.export = {
increase,
reset
};
}))(// 判断 CommonJS 里的 module 变量和 exports 变量是否存在
// 同时判断 AMD/RequireJS 的define 函数是否存在
typeof module === "object" && module.exports && typeof define !== "function"
? // 如果是 CommonJS/Node.js,手动定义一个 define 函数
factory => module.exports = factory(require, exports, module)
: // 否则是 AMD/RequireJS,直接使用 define 函数
define);
主要语法就是 import和epxort关键字
// 定义 ES 模块:esCounterModule.js 或 esCounterModule.mjs.
import dependencyModule1 from "./dependencyModule1.mjs";
import dependencyModule2 from "./dependencyModule2.mjs";
let count = 0;
// 具名导出:
export const increase = () => ++count;
export const reset = () => {
count = 0;
console.log("Count is reset.");
};
// 默认导出
export default {
increase,
reset
};
// ----- 浏览器里使用该模块,在 script标签上加上type="module",表明引入的是 ES 模块。在 Node.js 环境中使用时,把扩展名改成 .mjs。
// Use ES module.
//浏览器: <script type="module" src="esCounterModule.js"></script> or inline.
// 服务器:esCounterModule.mjs
import { increase, reset } from "./esCounterModule.mjs";
increase();
reset();
// Or import from default export:
import esCounterModule from "./esCounterModule.mjs";
esCounterModule.increase();
esCounterModule.reset();
// 浏览器如果不支持,可以加个兜底属性:
<script nomodule>
alert("Not supported.");
</script>
// example2.js // 导出默认, 有且只有一个默认
export default const example2 = {
name : 'my name',
age : 'my age',
getName = function(){ return 'my name' }
}
//全部导入 // 名字可以修改
import people from './example2.js'
// -------------------我是一条华丽的分界线---------------------------
// example1.js // 部分导出
export let name = 'my name'
export let age = 'my age'
export let getName = function(){ return 'my name'}
// 导入部分 // 名字必须和 定义的名字一样。
import {name, age} from './example1.js'
//有一种特殊情况,即允许你将整个模块当作单一对象进行导入
//该模块的所有导出都会作为对象的属性存在
import * as example from "./example1.js"
console.log(example.name)
console.log(example.age)
console.log(example.getName())
// -------------------我是一条华丽的分界线---------------------------
// example3.js // 有导出默认, 有且只有一个默认,// 又有部分导出
export default const example3 = {
birthday : '2018 09 20'
}
export let name = 'my name'
export let age = 'my age'
export let getName = function(){ return 'my name'}
// 导入默认与部分
import example3, {name, age} from './example1.js'
// 总结:
// 1.当用 export default people 导出时,就用 import people 导入(不带大括号)
// 2.一个文件里,有且只能有一个 export default。但可以有多个 export。
// 3.当用 export name 时,就用 import { name }导入(记得带上大括号)
// 4.当一个文件里,既有一个 export default people, 又有多个 export name 或者 export age 时,导入就用 import people, { name, age }
// 5.当一个文件里出现 n 多个 export 导出很多模块,导入时除了一个一个导入,也可以用 import * as example
.expand-range {
position: relative;
}
.expand-range:after {
content: '';
position: absolute;
top: -10px; right: -10px; bottom: -10px; left: -10px;
}
/* 推荐使用Scss */
@mixin expand-range($top: -10px, $right: $top, $bottom: $top, $left: $right, $position: relative) {
position: $position;
&:after {
content: '';
position: absolute;
top: $top;
right: $right;
bottom: $bottom;
left: $left;
}
}
//使用:.test { @include expand-range($top: -5px, $position: absolute) }
虚拟DOM可提升性能, 无须整体重新渲染, 而是局部刷新。JS对象, diff算法
addEventListener第三个参数默认false(冒泡阶段执行),true(捕获阶段执行)
DOM本身是一个js对象, 操作这个对象本身不慢, 但是操作后触发了浏览器的行为, 如repaint和reflow等浏览器行为, 使其变慢
判断有无全局对象global和window
正向代理:
反向代理:
raw-loader
:加载文件原始内容(utf-8)file-loader
:把文件输出到一个文件夹中,在代码中通过相对 URL 去引用输出的文件 (处理图片和字体)url-loader
:与 file-loader 类似,区别是用户可以设置一个阈值,大于阈值时返回其 publicPath,小于阈值时返回文件 base64 形式编码 (处理图片和字体)source-map-loader
:加载额外的 Source Map 文件,以方便断点调试svg-inline-loader
:将压缩后的 SVG 内容注入代码中image-loader
:加载并且压缩图片文件json-loader
:加载 JSON 文件(默认包含)handlebars-loader
: 将 Handlebars 模版编译成函数并返回babel-loader
:把 ES6 转换成 ES5ts-loader
: 将 TypeScript 转换成 JavaScriptawesome-typescript-loader
:将 TypeScript 转换成 JavaScript,性能优于 ts-loaderstyle-loader
:将 CSS 代码注入 JavaScript 中,通过 DOM 操作去加载 CSScss-loader
:加载 CSS,支持模块化、压缩、文件导入等特性style-loader
:把 CSS 代码注入到 JavaScript 中,通过 DOM 操作去加载 CSSpostcss-loader
:扩展 CSS 语法,使用下一代 CSS,可以配合 autoprefixer 插件自动补齐 CSS3 前缀eslint-loader
:通过 ESLint 检查 JavaScript 代码tslint-loader
:通过 TSLint检查 TypeScript 代码mocha-loader
:加载 Mocha 测试用例的代码coverjs-loader
:计算测试的覆盖率vue-loader
:加载 Vue.js 单文件组件i18n-loader
: 国际化cache-loader
: 可以在一些性能开销较大的 Loader 之前添加,目的是将结果缓存到磁盘里define-plugin
:定义环境变量 (Webpack4 之后指定 mode 会自动配置)ignore-plugin
:忽略部分文件html-webpack-plugin
:简化 HTML 文件创建 (依赖于 html-loader)web-webpack-plugin
:可方便地为单页应用输出 HTML,比 html-webpack-plugin 好用uglifyjs-webpack-plugin
:不支持 ES6 压缩 (Webpack4 以前)terser-webpack-plugin
: 支持压缩 ES6 (Webpack4)webpack-parallel-uglify-plugin
: 多进程执行代码压缩,提升构建速度mini-css-extract-plugin
: 分离样式文件,CSS 提取为独立文件,支持按需加载 (替代extract-text-webpack-plugin)serviceworker-webpack-plugin
:为网页应用增加离线缓存功能clean-webpack-plugin
: 目录清理ModuleConcatenationPlugin
: 开启 Scope Hoistingspeed-measure-webpack-plugin
: 可以看到每个 Loader 和 Plugin 执行耗时 (整个打包耗时、每个 Plugin 和 Loader 耗时)webpack-bundle-analyzer
: 可视化 Webpack 输出文件的体积 (业务组件、依赖第三方模块)Loader
本质就是一个函数,在该函数中对接收到的内容进行转换,返回转换后的结果。因为 Webpack 只认识 JavaScript,所以 Loader 就成了翻译官,对其他类型的资源进行转译的预处理工作。
Plugin
就是插件,基于事件流框架 Tapable,插件可以扩展 Webpack 的功能,在 Webpack 运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果。
Loader
在 module.rules
中配置,作为模块的解析规则,类型为数组。每一项都是一个 Object,内部包含了 test(类型文件)、loader、options (参数)等属性。
Plugin
在 plugins 中单独配置,类型为数组,每一项是一个 Plugin 的实例,参数都通过构造函数传入。
Webpack 的运行流程是一个串行的过程,从启动到结束会依次执行以下流程:
在以上过程中,Webpack 会在特定的时间点广播出特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用 Webpack 提供的 API 改变 Webpack 的运行结果。
简单说
webpack-dashboard
:可以更友好的展示相关打包信息。webpack-merge
:提取公共配置,减少重复配置代码speed-measure-webpack-plugin
:简称 SMP,分析出 Webpack 打包过程中 Loader 和 Plugin 的耗时,有助于找到构建过程中的性能瓶颈。size-plugin
:监控资源体积变化,尽早发现问题HotModuleReplacementPlugin
:模块热替换source map
是将编译、打包、压缩后的代码映射回源代码的过程。打包压缩后的代码不具备良好的可读性,想要调试源码就需要 soucre map。
map文件只要不打开开发者工具,浏览器是不会加载的。
线上环境一般有三种处理方案:
注意:避免在生产中使用 inline- 和 eval-,因为它们会增加 bundle 体积大小,并降低整体性能。
Webpack 实际上为每个模块创造了一个可以导出和导入的环境,本质上并没有修改代码的执行逻辑,代码执行顺序与模块加载顺序也完全一致。
在发现源码发生变化时,自动重新构建出新的输出文件。
Webpack开启监听模式,有两种方式:
缺点:每次需要手动刷新浏览器
原理:轮询判断文件的最后编辑时间是否变化,如果某个文件发生了变化,并不会立刻告诉监听者,而是先缓存起来,等 aggregateTimeout 后再执行。
module.export = {
// 默认false,也就是不开启
watch: true,
// 只有开启监听模式时,watchOptions才有意义
watchOptions: {
// 默认为空,不监听的文件或者文件夹,支持正则匹配
ignored: /node_modules/,
// 监听到变化发生后会等300ms再去执行,默认300ms
aggregateTimeout:300,
// 判断文件是否发生变化是通过不停询问系统指定文件有没有变化实现的,默认每秒问1000次
poll:1000
}
}
Webpack
的热更新又称热替换(Hot Module Replacement
),缩写为 HMR
。这个机制可以做到不用刷新浏览器而将新变更的模块替换掉旧的模块。
HMR的核心就是客户端从服务端拉去更新后的文件,准确的说是 chunk diff (chunk 需要更新的部分),实际上 WDS 与浏览器之间维护了一个 Websocket,当本地资源发生变化时,WDS 会向浏览器推送更新,并带上构建时的 hash,让客户端与上一次资源进行对比。客户端对比出差异后会向 WDS 发起 Ajax 请求来获取更改内容(文件列表、hash),这样客户端就可以再借助这些信息继续向 WDS 发起 jsonp 请求获取该chunk的增量更新。
后续的部分(拿到增量更新之后如何处理?哪些状态该保留?哪些又需要更新?)由 HotModulePlugin 来完成,提供了相关 API 以供开发者针对自身场景进行处理,像react-hot-loader 和 vue-loader 都是借助这些 API 实现 HMR。
VSCode 中有一个插件 Import Cost 可以帮助我们对引入模块的大小进行实时监测,还可以使用 webpack-bundle-analyzer 生成 bundle 的模块组成图,显示所占体积。
bundlesize 工具包可以进行自动化资源体积监控。
文件指纹是打包后输出的文件名的后缀。
JS的文件指纹设置
设置 output 的 filename,用 chunkhash。
module.exports = {
entry: {
app: './scr/app.js',
search: './src/search.js'
},
output: {
filename: '[name][chunkhash:8].js',
path:__dirname + '/dist'
}
}
CSS的文件指纹设置
设置 MiniCssExtractPlugin 的 filename,使用 contenthash。
module.exports = {
entry: {
app: './scr/app.js',
search: './src/search.js'
},
output: {
filename: '[name][chunkhash:8].js',
path:__dirname + '/dist'
},
plugins:[
new MiniCssExtractPlugin({
filename: `[name][contenthash:8].css`
})
]
}
图片的文件指纹设置
设置file-loader的name,使用hash。
占位符名称及含义
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename:'bundle.js',
path:path.resolve(__dirname, 'dist')
},
module:{
rules:[{
test:/\.(png|svg|jpg|gif)$/,
use:[{
loader:'file-loader',
options:{
name:'img/[name][hash:8].[ext]'
}
}]
}]
}
}
可以使用 enforce 强制执行 loader 的作用顺序,pre 代表在所有正常 loader 之前执行,post 是所有 loader 之后执行。(inline 官方不推荐使用)
代码分割的本质其实就是在源代码直接上线和打包成唯一脚本main.bundle.js这两种极端方案之间的一种更适合实际场景的中间状态。
「用可接受的服务器性能压力增加来换取更好的用户体验。」
源代码直接上线:虽然过程可控,但是http请求多,性能开销大。
打包成唯一脚本:一把梭完自己爽,服务器压力小,但是页面空白期长,用户体验不好。
Loader 支持链式调用,所以开发上需要严格遵循“单一职责”,每个 Loader 只负责自己需要负责的事情。
Loader的API 可以去官网查阅: https://www.webpackjs.com/api/loaders
webpack在运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在特定的阶段钩入想要添加的自定义功能。Webpack 的 Tapable 事件流机制保证了插件的有序性,使得整个系统扩展性良好。
Plugin的API 可以去官网查阅:https://www.webpackjs.com/api/plugins
大多数JavaScript Parser遵循 estree 规范,Babel 最初基于 acorn 项目(轻量级现代 JavaScript 解析器)Babel大概分为三大部分: