一、webpack4核心架构
webpack本质是一个静态模块打包器。
webapck 配置中最重要的有:
- 入口
- 出口
- 各种解析loader
- 优化
- 插件
二、优化
webpack 默认进行 production 模式一些简单的、基本的处理,webpack默认 production 模式就已经处理了,比如箭头函数,专门为 development 处理是没有意义的。但 production 模式不是所有的优化都做了,这就需要手动优化。
一、tree-shaking
引用函数不纯,需要指定纯函数注释 /*pure*/;需要分析scope。
对于 production 模式,默认做 tree-shaking ,但是不会分析函数内的 scope。如果我在一个小函数内倒入一个大包,那么这个大包也会被编译出来。所以这时我需要插件分析 scope。
必须是可以被解构的包,才能进行 tree-shaking。
三、loader
- loader 按照逆序执行:[loader-a,loader-b]先执行 loader-a。
- loader 传入源文件或二进制文件;输出处理后的文件 + sourcemap(可选)。
- 一些 loader 需要标记异步处理。
- loader 和 其他 webpack 处理模式如出一辙,都是拿到源文件然后通过设置的 option 进行处理。
四、编译后的结果
一、同步引入
流程:
- webpack 将会创建一个立即执行匿名函数,如果在 wepback.config.js 中设置了导出模块名,那么这个函数将会把返回值指向这个标识符。
- 匿名函数的参数是一个大的同步执行函数的包,用模块地址→模块内容的形式组织起来。在 dev 环境下,模块内容为 eval,这是为了开发速度快做的简单处理;在 prod 环境下,将会做优化,形成真正的代码。
- 匿名函数的函数体是一个大的闭包函数。它提供真正的执行模块的方法 __webpack_require__,这个方法非常重要!当执行这个方法时,会按照引入的模块名,缓存了所有这些模块的导出状态。默认执行入口模块,并且模块内的引入关系,都是通过在父级模块中通过这个方法调用子模块的方式。
- 任意模块引入同步模块,子模块都会被打入父级模块所在 js 文件。任何模块引入异步模块,子模块都会创建新的模块。
- 无论是单个入口还是多入口,流程都是类似的。多入口引入相同的 js 模块时,会重复打包这个 js 分别到两个入口文件下,这会造成一定的浪费,所以要考虑优化。
假设源文件为:
// sync.js
export default '我是 sync';
// index.js
import syncfrom './sync';
setTimeout(() => {
console.log(sync);
}, 1000);
则编译过的文件大致为:
(function (modules){
function requireFn(moduleId) {
// 如果以前有调用过 moduleId:function 这个函数
if (requireFn.memory[moduleId]) {
console.log('使用缓存');
// 这里默认用了 default
return requireFn.memory[moduleId].default;
}
// 第一次访问该模块
const module = {};
// 做一个缓存
requireFn.memory[moduleId] = module;
modules[moduleId](requireFn.memory, module, requireFn);
}
// 所有文件的缓存
requireFn.memory = {};
// 初始执行,挂在到 requireFn.export,有 export {...} 时才能看得出,它用的是 Object.defineProperty + 闭包
requireFn.export = requireFn('index');
// 暴露给全局,如果设置了的话,整个index.js的函数执行将被付给这个标识符
return requireFn.export;
})({
index: function (modules, exports, requireFn){
// dev源码为eval,为了编译快,prod环境为代码
// 这一步是 import
requireFn('sync');
setTimeout(() => {
console.log(requireFn.memory['sync'].default);
}, 1000);
},
sync: function (modules, exports, requireFn){
// 这里只用 default 为了方便
exports.default = ('我是 sync');
},
});
二、异步引入
流程:
- 每一个异步引入模块的方式,都会创建一个异步文件,这个文件的文件名可以用魔法注释的方法定义 /* webpackChunkName: 'async name' */。
- 在父级 js 中会创建一个专门用于缓存异步 js 模块的列表 webpackJsonp 暴露到全局环境 window 中;还会创建 webpackJsonpCallback 方法 用于设置异步模块的执行缓存;并且设置 webpackJsonp 的 push 方法 为 webpackJsonpCallback
- 在父级 js 中会调用方法 __webpack_require__.e 引入异步 js,__webpack_require__.e 会创建一个 script 标签,src 就是异步 js 文件的地址。
- 新创建的 script 标签会执行它的代码,它会在全局的异步模块列表 webpackJsonp 中 push(会看 2,这个 push 实际是 webpackJsonpCallback) 进这个异步模块的代码。
- webpackJsonpCallback 方法会去执行 __wepback_require__,此时与同步模块类似了,并且,这个异步模块也被缓存到了父级 js 中。
- 更深层的异步引入原理也是类似的,所有父级 js 中提供了所有异步模块引入的方法,所有模块执行都是通过 __webpack_require__ 这个方法,它将所有需要的参数提供给了所有模块。
那个 * webpackChunkName: 'async name' */ ,webpackChunkName 后面的冒号必须紧挨着 webpackChunkName,中间不能有空格。
源文件:
// async.js
(window.webpackJsonp && window.webpackJsonp.webpackJsonpCallback({
'async': function (modules, exports, requireFn) {
exports.default = '我是 async';
}
}))
编译后的文件:
(function (modules){
// 只是提供了一个对象
window.webpackJsonp = [];
// 异步模块执行
webpackJsonp.webpackJsonpCallback = function (obj) {
Object.keys(obj).forEach(moduleId => {
// 绑定异步模块到主模块上
modules[moduleId] = obj[moduleId];
});
}
// 加载异步js
requireFn.e = function (moduleId) {
// 创建 script
const script = document.createElement('script');
// promise是为了延迟到 script 加载完
const promise = new Promise((resolve) => {
// 一些验证
script.onload = function () {
resolve();
}
});
// 设置 src
script.src = `${moduleId}.js`;
// 添加到文档流
document.head.appendChild(script);
// promise.all 是因为有其他情况,比如 async 引入另一个 async
return Promise.all([promise]);
}
function requireFn(moduleId) {
// 如果以前有调用过 moduleId:function 这个函数
if (requireFn.memory[moduleId]) {
console.log('使用缓存');
// 这里默认用了 default
return requireFn.memory[moduleId].default;
}
// 第一次访问该模块
const module = {};
// 做一个缓存
requireFn.memory[moduleId] = module;
modules[moduleId](requireFn.memory, module, requireFn);
}
// 所有文件的缓存
requireFn.memory = {};
// 初始执行,挂在到 requireFn.export,有 export {...} 时才能看得出,它用的是 Object.defineProperty + 闭包
requireFn.export = requireFn('index');
// 暴露给全局,如果设置了的话,整个index.js的函数执行将被付给这个标识符
return requireFn.export;
})({
index: function (modules, exports, requireFn){
// dev源码为eval,为了编译快,prod环境为代码
// 这一步是 import
requireFn('sync');
setTimeout(() => {
console.log(requireFn.memory['sync'].default);
// 在这里异步加载 async 模块
requireFn.e('async')
.then(() => {
requireFn('async');
// 调用后,就有这个模块了
console.log(requireFn.memory['async'].default);
});
}, 1000);
},
sync: function (modules, exports, requireFn){
// 这里只用 default 为了方便
exports.default = ('我是 sync');
},
});
// async.js
(window.webpackJsonp && window.webpackJsonp.webpackJsonpCallback({
'async': function (modules, exports, requireFn){
exports.default = '我是 async';
},
}));