# 模仿webpack编译原理写一个模块打包工具
# 整体思路
- 初始化参数:从配置文件 和 Shell 语句中读取与合并参数,得出最终的参数;
- 开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译;
- 确定入口:根据配置中的 entry 找出所有的入口文件;
- 编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归此步骤直到所有入口依赖的文件都经过了处理;
- 完成模块编译:在经过第4步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系;
- 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会;
- 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统。 在以上过程中,Webpack 会在特定的时间点广播出特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用 Webpack 提供的 API 改变 Webpack 的运行结果。
# webpack 中的 hooks
- entryOption 读取配置文件
- afterPlugins 加载所有的插件
- run 开始执行编译流程
- compile 开始编译
- afterCompile 编译完成
- emit 写入文件
- done 完成整体流程
# 文件介绍
- 1 新建两个目录
sourcepack(自实现打包工具目录) 和usewebpack(模拟项目目录);
usewebpack 的目录结构如下
├── src # 源码目录
│ ├── a # 模块代码
│ ├── loaders # 存放自己实现的loadder文件
│ ├── plugins # 存放自己实现的plugin文件
│ ├── index.js # 入口文件
│ ├── index.less # less文件
├── webpack.config.json # webpack 配置文件
├── package.json # 项目描述
sourcepack 的目录结构如下
├── bin # 主文件目录
│ ├── sourcepack.js # 主文件
├── lib # 工具类目录
│ ├── compiler.js # compiler 类
│ ├── main.ejs # ejs 模版
├── package.json # 项目描述
1, 分别进入sourcepack和usepack文件夹npm install
2, 执行 npm link 建立软连接,是sourcepack命令全局化
npm ls --global sourcepack 可以查看sourcepack命令是否存在
sudo npm rm --global sourcepack 可以删除sourcepack命令
3, 在usewebpack 目录执行sourcepack 命令从而打出bundle包
- 可以在dist目录新建index.html引用此包验证下是否成功
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<h1>sourcePack</h1>
<script src="./bundle.js"></script>
</body>
</html>
# Webpack 核心模块 tapable
在 tapable 解构的 SyncHook 是一个类,注册事件需先创建实例,创建实例时支持传入一个数组,数组内存储事件触发时传入 的参数,实例的 tap 方法用于注册事件,支持传入两个参数,第一个参数为事件名称,在 Webpack 中一般用于存储事件对应的 插件名称(名字随意,只是起到注释作用), 第二个参数为事件处理函数,函数参数为执行 call 方法触发事件时所传入的参数的形参
// SyncHook 钩子的使用
const { SyncHook } = require("tapable");
// 创建实例
let syncHook = new SyncHook(["name", "age"]);
// 注册事件
syncHook.tap("1", (name, age) => console.log("1", name, age));
syncHook.tap("2", (name, age) => console.log("2", name, age));
syncHook.tap("3", (name, age) => console.log("3", name, age));
// 触发事件,让监听函数执行
syncHook.call("panda", 18);
// 1 panda 18
// 2 panda 18
// 3 panda 18
tasks 数组用于存储事件处理函数,call 方法调用时传入参数超过创建 SyncHook 实例传入的数组长度时,多余 参数可处理为 undefined,也可在参数不足时抛出异常,不灵活,后面的例子中就不再这样写了
// 模拟 SyncHook 类
class SyncHook {
constructor(args) {
this.args = args;
this.tasks = [];
}
tap(name, task) {
this.tasks.push(task);
}
call(...args) {
// 也可在参数不足时抛出异常
if (args.length < this.args.length) throw new Error("参数不足");
// 传入参数严格对应创建实例传入数组中的规定的参数,执行时多余的参数为 undefined
args = args.slice(0, this.args.length);
// 依次执行事件处理函数
this.tasks.forEach(task => task(...args));
}
}
# 核心代码实现
// 编译时候就触发此代码
const path = require("path");
const fs = require("fs");
const root = process.cwd();
const configPath = path.join(root,"webpack.config.js");
const config = require(configPath);
const Compiler = require('../lib/Compiler');
const compiler = new Compiler(config);
//发射 entryOption 事件
compiler.hooks.entryOption.call(config);
compiler.run();
看下编译代码的实现
class Compiler{
constructor(options){
//取得当前工作目录
this.root = process.cwd();
//存放着所有的模块 moduleId => 原代码
this.modules = {};
this.options = options;
this.hooks = {
entryOption:new SyncHook(['config']),
afterPlugins:new SyncHook(['afterPlugins']),
run:new SyncHook(['run']),
compile:new SyncHook(['compile']),
afterCompile:new SyncHook(['afterCompile']),
emit:new SyncHook(['emit']),
done:new SyncHook(['done'])
}
let plugins = options.plugins;
if(plugins&&plugins.length>0)
plugins.forEach(plugin=>{
plugin.apply(this);
})
//触发插件挂载完成事件
this.hooks.afterPlugins.call(this);
}
// 找到入口文件 开始编译
run(){
const {
entry,
output:{ path: pathName, filename }
}= this.options;
let _this = this;
const entryPath = path.join(this.root,entry);
this.hooks.compile.call(this);
this.parseModule(entryPath,true);
this.hooks.afterCompile.call(this);
let bundle = ejs.compile(fs.readFileSync(path.join(__dirname,'main.ejs'),"utf8"))({
modules:this.modules,entryId:this.entryId
});
this.hooks.emit.call(this);
fs.writeFileSync(path.join(pathName,filename),bundle);
this.hooks.done.call(this);
}
parseModule(modulePath,isEntry){
const {
module: { rules } ,
resolveLoader:{ modules: loaderPath }
}= this.options;
//取得入口文件内容
let source = fs.readFileSync(modulePath,'utf8');
for (var i =0;i < rules.length; i++) {
let rule = rules[i];
if(rule.test.test(modulePath)){
let loaders = rule.use||rule.loader;
if( Object.prototype.toString.call(loaders)==='[object Array]'){
for(let j = loaders.length-1;j>=0;j--){
let loader = loaders[j];
loader = require(path.join(this.root,loaderPath,loader));
source = loader(source);
}
}else if( Object.prototype.toString.call(loaders)=== "[object Object]"){
loaders = loader.loader;
}
}
}
let parentPath = path.relative(this.root,modulePath);
//TODO 执行loader 进行转换
let result = this.parse(source,path.dirname(parentPath));//用来解析模块内容并返回依赖的模块
this.modules['./'+parentPath] = result.source;
if(isEntry) { this.entryId = './'+parentPath };
let requires = result.requires;
if( requires && requires.length>0){
requires.forEach(function(req){
this.parseModule(path.join(this.root,req));
}.bind(this))
}
}
//对文件内容进行转义。1.处理文件中的路径引用问题 2,生成新的代码
parse(source,parentPath){ // parentPath 相对路径
//生成AST
let ast = esprima.parse(source);
//存放引用文件的路径
const requires = [];
//遍历语法树。1.找到此模块依赖的模块 2,替换掉老的加载路径
estraverse.replace(ast,{
enter(node,parent){
if(node.type == "CallExpression" && node.callee.name == "require"){
let name = node.arguments[0].value;
name += (name.lastIndexOf('.')>0?"":".js");
let moduleId = "./"+path.join(parentPath,name);
requires.push(moduleId);
node.arguments= [{type:"Literal",value:moduleId}];
//返回新节点替换老节点
return node;
}
}
});
source = escodegen.generate(ast);
return { requires, source };
}
}
webpack.config实现
module.exports = {
entry:"./src/index.js",
mode:"development",
output:{
path:path.resolve("dist"),
filename:"bundle.js"
},
resolveLoader:{
modules:'./src/loaders'
},
module:{
rules:[{
test:/\.less$/,
loader:['style-loader','less-loader']
}]
},
plugins:[
new entryOptionPlugin()
]
}
介绍下插件的实现
// compiler钩子参考链接 https://www.webpackjs.com/api/compiler-hooks/
class entryOptionPlugin {
constructor(options){
}
apply(compiler){
compiler.hooks.entryOption.tap('entryOptionPlugin',function(options){
console.log("参数解析完毕...")
});
// 可以打印出所有的钩子,注意这里是我自己实现的钩子只有简单几个,官网上给出了所有钩子
// for (var hook of Object.keys(compiler.hooks)) {
// console.log(hook);
// }
}
}
module.exports = entryOptionPlugin;
1.从表现上看,Compiler暴露了和webpack整个生命周期相关的钩子,通过如下的方式访问:
//基本写法
compiler.hooks.someHook.tap(...)
//如果希望在entry配置完毕后执行某个功能
compiler.hooks.entryOption.tap(...)
//如果希望在生成的资源输出到output指定目录之前执行某个功能
compiler.hooks.emit.tap(...)
2.根据webpack官方文档的说明,一个自定义的plugin需要包含:
一个javascript命名函数
插件函数的prototype上要有一个apply方法
指定一个绑定到webpack自身的事件钩子
注册一个回调函数来处理webpack实例中的指定数据
处理完成后调用webpack提供的回调
官网给出了一个基本的结构示例:
//console-log-on-build-webpack-plugin.js
const pluginName = 'ConsoleLogOnBuildWebpackPlugin';
class ConsoleLogOnBuildWebpackPlugin {
apply(compiler){
compiler.hooks.run.tap(pluginName, compilation=>{
console.log('webpack构建过程开始');
});
}
}
3.比较重要的两个钩子Compilation 实例继承于 compiler例如,compiler.compilation 是对
所有 require 图(graph)中对象的字面上的编译。这个对象可以访问所有的模块和它们的依赖(大部
分是循环依赖)。在编译阶段,模块被加载,封闭,优化,分块,哈希和重建等等。这将是任何编译操作
中,重要的生命周期
compiler 对象代表的是不变的webpack环境,是针对webpack的
compilation 对象针对的是随时可变的项目文件,只要文件有改动,compilation就会被重新创建
介绍下style-loader
module.exports = function(source){
let style = `
let style = document.createElement('style');
style.innerHTML = ${JSON.stringify(source)};
document.head.appendChild(style);
`;
return style;
}
# webpack打包之后的文件分析
# webpack的模块不仅指js,包括css、图片等资源都可以以模块看待,但现在我们只关注js,首先我们创建一个简单入口模块index.js和一个依赖模块bar.js 先使用commonjs规范
//index.js
'use strict';
var bar = require('./bar');
function foo() {
return bar.bar();
}
//bar.js
'use strict';
exports.bar = function () {
return 1;
}
//webpack配置如下
var path = require("path");
module.exports = {
entry: path.join(__dirname, 'index.js'),
output: {
path: path.join(__dirname, 'outs'),
filename: 'index.js'
},
};
这是一个最简单的配置,只指定了模块入口和输出路径,但已经满足了我们的要求。在根目录下执行 webpack,得到经过webpack打包的代码如下
// 浏览器本身不支持模块化,那么webpack就用函数作用域来hack模块化的效果。
// 如果你debug过node代码,你会发现一样的hack方式,node中的模块也是函数,跟模块相关的参数exports、
// require,或者其他参数__filename和__dirname等都是通过函数传值作为模块中的变量,模块与外部模块的
// 访问就是通过这些参数进行的,当然这对开发者来说是透明的
(function(modules) { // webpackBootstrap
// 模块缓存对象
var installedModules = {};
// webpack实现的require
function __webpack_require__(moduleId) {
// 判断是否已缓存模块
if(installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
// 创建新模块并缓存
var module = installedModules[moduleId] = {
i: moduleId,
l: false,
exports: {}
};
// 调用模块函数,注意这里做了一个动态绑定,将模块函数的调用对象绑定为module.exports
// 这是为了保证在模块中的this指向当前模块
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
// 标记模块为已加载
module.l = true;
// 返回module.exports
return module.exports;
}
// expose the modules object (__webpack_modules__)
__webpack_require__.m = modules;
// expose the module cache
__webpack_require__.c = installedModules;
// define getter function for harmony exports
__webpack_require__.d = function(exports, name, getter) {
if(!__webpack_require__.o(exports, name)) {
Object.defineProperty(exports, name, {
configurable: false,
enumerable: true,
get: getter
});
}
};
// getDefaultExport function for compatibility with non-harmony modules
__webpack_require__.n = function(module) {
var getter = module && module.__esModule ?
function getDefault() { return module['default']; } :
function getModuleExports() { return module; };
__webpack_require__.d(getter, 'a', getter);
return getter;
};
// Object.prototype.hasOwnProperty.call
__webpack_require__.o = function(object, property) {
return Object.prototype.hasOwnProperty.call(object, property);
};
// __webpack_public_path__
__webpack_require__.p = "";
// require第一个模块, 就是入口文件模块
return __webpack_require__(__webpack_require__.s = 0);
})
/************************************************************************/
([
/* 0 */
(function(module, exports, __webpack_require__) {
"use strict";
var bar = __webpack_require__(1);
bar.bar();
}),
/* 1 */
// webpack传入的第一个参数module是当前缓存的模块,包含当前模块的信息和exports;第二个参数exports是
// module.exports的引用,这也符合commonjs的规范;第三个__webpack_require__ 则是require的实现
(function(module, exports, __webpack_require__) {
"use strict";
exports.bar = function () {
return 1;
}
})
]);
在我们的模块中,就可以对外使用module.exports或exports进行导出,使用__webpack_require__导入需要的模 块,代码跟commonjs完全一样,这样,就完成了对第一个模块的require,然后第一个模块会根据自己对其他模块的require,依次加载其他模块,最终形成一个依赖网状结构。webpack管理着这些模块的缓存,如果一个模块被require多次,那么只会有一次加载过程,而返回的是缓存的内容,这也是commonjs的规范
# 现在我们使用es6模块化,依然写两个文件,m.js文件用es模块的方式export一个default函数和一个foo函数,index.js import该模块
// m.js
'use strict';
export default function bar () {
return 1;
};
export function foo () {
return 2;
}
// index.js
'use strict';
import bar, {foo} from './m';
bar();
foo();
// webpack配置没有变化,依然以index.js作为入口
var path = require("path");
module.exports = {
entry: path.join(__dirname, 'index.js'),
output: {
path: path.join(__dirname, 'outs'),
filename: 'index.js'
},
};
重新执行webpack后生成的代码如下(只截取IIFE的参数部分)
[
(function(module, __webpack_exports__, __webpack_require__) {
"use strict";
// 定义该模块为es模块
Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
/* harmony import */
var __WEBPACK_IMPORTED_MODULE_0__m__ = __webpack_require__(1);
// 引入的模块属性都会用Object()包装成对象,这是为了保证像Boolean、String、Number这些
// 基本数据类型转换成相应的类型对象
Object(__WEBPACK_IMPORTED_MODULE_0__m__["a" /* default */])();
Object(__WEBPACK_IMPORTED_MODULE_0__m__["b" /* foo */])();
}),
// export default和export都被转换成了类似于commonjs的exports.xxx,这里也已经不区分是不是default
// export了,所有的export对象都是__webpack_exports__的属性
(function(module, __webpack_exports__, __webpack_require__) {
"use strict";
/* harmony export (immutable) */
__webpack_exports__["a"] = bar;
/* harmony export (immutable) */
__webpack_exports__["b"] = foo;
function bar () {
return 1;
};
function foo () {
return 2;
}
})
]
# commonjs与es6 module混用,这里只介绍下es模块对commonjs模块的导入,其他原理一样
下面用具体代码来解释一下,首先修改m.js和index.js代码如下
// m.js
'use strict';
exports.foo = function () {
return 1;
}
// index.js
'use strict';
import m from './m';
m.foo();
重新执行webpack后生成的代码如下(只截取IIFE的参数部分)
[
/* 0 */
(function(module, __webpack_exports__, __webpack_require__) {
"use strict";
Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
/* harmony import */
var __WEBPACK_IMPORTED_MODULE_0__m__ = __webpack_require__(1);
/*
对导出的commonjs模块再做一次包装
__webpack_require__.n会判断module是否为es模块,当__esModule为true的时候,标识module为es模
块,那么module.a默认返回module.default,否则返回module
__webpack_require__.n会判断module是否为es模块,当__esModule为true的时候,标识module为es模
块,那么module.a默认返回module.default,否则返回module
具体实现则是通过 __webpack_require__.d将具体操作绑定到属性a的getter方法上的
*/
var __WEBPACK_IMPORTED_MODULE_0__m___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_0__m__);
__WEBPACK_IMPORTED_MODULE_0__m___default.a.foo();
}),
/* 1 */
(function(module, exports, __webpack_require__) {
"use strict";
exports.foo = function () {
return 1;
}
})
]
# Code Splitting原理
webpack的模块化不仅支持commonjs和es module,还能通过code splitting实现模块的动态加载,首先我们依然 创建一个简单入口模块index.js和两个依赖模块foo.js和bar.js
// index.js
'use strict';
import(/* webpackChunkName: "foo" */ './foo').then(foo => {
console.log(foo());
})
import(/* webpackChunkName: "bar" */ './bar').then(bar => {
console.log(bar());
})
// foo.js
'use strict';
exports.foo = function () {
return 2;
}
// bar.js
'use strict';
exports.bar = function () {
return 1;
}
// webpack配置如下
var path = require("path");
module.exports = {
entry: path.join(__dirname, 'index.js'),
output: {
path: path.join(__dirname, 'outs'),
filename: 'index.js',
chunkFilename: '[name].bundle.js' //不指定会有默认文件名
},
};
在根目录下执行webpack,得到经过webpack打包的代码如下
(function(modules) {
// 省略和和正常打包相同的代码,只留下不同代码
var parentJsonpFunction = window["webpackJsonp"];
// 将每个chunk存入到resolves中,并最后依次执行,另外还行chunk里模块缓存到modules变量。
window["webpackJsonp"] = function webpackJsonpCallback(chunkIds, moreModules,
executeModules) {
// add "moreModules" to the modules object,
// then flag all "chunkIds" as loaded and fire callback
var moduleId, chunkId, i = 0, resolves = [], result;
for(;i < chunkIds.length; i++) {
chunkId = chunkIds[i];
if(installedChunks[chunkId]) {
// 存入每个chunk的resolve方法
resolves.push(installedChunks[chunkId][0]);
}
installedChunks[chunkId] = 0;
}
for(moduleId in moreModules) {
// 缓存对应模块
if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
modules[moduleId] = moreModules[moduleId];
}
}
if(parentJsonpFunction) parentJsonpFunction(chunkIds, moreModules,
executeModules);
// 运行所有resolve
while(resolves.length) {
resolves.shift()();
}
};
// The module cache
var installedModules = {};
// chunks缓存
var installedChunks = {
2: 0
};
// 异步加载方法实现
__webpack_require__.e = function requireEnsure(chunkId) {
var installedChunkData = installedChunks[chunkId];
// 从缓存installedChunks中查找是否有缓存模块,如果缓存标识为0,则表示模块已加载过,直接返回
// promise;如果缓存为数组,表示缓存正在加载中,则返回缓存的promise对象
if(installedChunkData === 0) {
return new Promise(function(resolve) { resolve(); });
}
该值是一个数组第一个值是resolve第二个reject第三个值就是对应的promise
if(installedChunkData) {
return installedChunkData[2];
}
// setup Promise in chunk cache
var promise = new Promise(function(resolve, reject) {
installedChunkData = installedChunks[chunkId] = [resolve, reject];
});
// 赋值promise
installedChunkData[2] = promise;
// 开始chunk loading
var head = document.getElementsByTagName('head')[0];
var script = document.createElement('script');
script.type = 'text/javascript';
script.charset = 'utf-8';
script.async = true;
script.timeout = 120000;
if (__webpack_require__.nc) {
script.setAttribute("nonce", __webpack_require__.nc);
}
script.src = __webpack_require__.p + "" + ({"0":"foo","1":"bar"}[chunkId]||
chunkId) + ".bundle.js";
// 添加script标签onload、onerror事件,如超时或模块加载失败,则调reject返回模块加载失败异常
var timeout = setTimeout(onScriptComplete, 120000);
script.onerror = script.onload = onScriptComplete;
function onScriptComplete() {
// avoid mem leaks in IE.
script.onerror = script.onload = null;
clearTimeout(timeout);
var chunk = installedChunks[chunkId];
if(chunk !== 0) {
if(chunk) {
chunk[1](new Error('Loading chunk ' + chunkId + ' failed.'));
}
installedChunks[chunkId] = undefined;
}
};
head.appendChild(script);
return promise;
};
// on error function for async loading
__webpack_require__.oe = function(err) { console.error(err); throw err; };
// Load entry module and return exports
return __webpack_require__(__webpack_require__.s = 0);
})
([
(function(module, exports, __webpack_require__) {
"use strict";
// 0是chunkId 1是moduleId
__webpack_require__.e/* import() */(0).then(__webpack_require__.bind(null, 1))
.then(foo => {
console.log(foo());
})
__webpack_require__.e/* import() */(1).then(__webpack_require__.bind(null, 2))
.then(bar => {
console.log(bar());
})
})
]);
// 对应其中一个chunk文件内容如下
webpackJsonp([0],{
1,
(function(module, exports, __webpack_require__) {
"use strict";
exports.foo = function () {
return 2;
}
})
});
每个chunkId对应的是一个js文件,每个moduleId对应的是一个个js文件的内容的模块(一个js文件里面可以require多个资源,每个资源分配一个moduleId),所以它两的关系就是一个chunkId可能由很多个moduleId组成
当我们需要动态加载某些组件的时候例如vue-router配合使用时候其实就是,点击到某个路由之后触发该动态加载 方法大概编译后代码如下
exports.default = {
childRoutes: [{
path: 'about',
getComponent: function getComponent(location, cb) {
__webpack_require__.e/* import() */(2).then(__webpack_require__.bind(null, 268)).then(loadRoute(cb)).catch(errorLoading);
}
}
};
由上面代码可以看到,当路径访问about时候,就调取对应的getComponent函数,这个函数里面首先执行 webpack_require.e方法,成功再通过then执行__webpack_require__方法,即先去加载chunk文 件,然后再去加载当前chunk文件里的模块,因此我们可以从这里推断出,上面方法中由两个数字2和268 , 这两个数字肯定就是chunkId和modleId了, 这个2和268肯定就是在about-chunk.js文件中了
// 这只是其中随便一个项目的例子,大概thunk就生成这个样子
// 这个文件里直接调用了webpackJsonp方法,而这个方法第一个参数就是chunkIds 列表,而
// 第二个参数就是一个moduleId与模块的对象
webpackJsonp([2],{
/***/ 268:
/***/ (function(module, exports, __webpack_require__) {
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
var _react = __webpack_require__(13);
var _react2 = _interopRequireDefault(_react);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
var About = function About() {
return _react2.default.createElement(
'div',
null,
'About'
);
};
exports.default = About;
/***/ })
});
# webpack优化
# resolve字段告诉webpack怎么去搜索文件
设置resolve.modules:[path.resolve(__dirname, 'node_modules')]避免层层查找。 resolve.modules告诉webpack去哪些目录下寻找第三方模块,默认值为['node_modules'],会依次查找./node_modules、../node_modules、../../node_modules。
设置resolve.mainFields:['main'],设置尽量少的值可以减少入口文件的搜索步骤 第三方模块为了适应不同的使用环境,会定义多个入口文件,mainFields定义使用第三方模块的哪个入口文件,由于大多数第三方模块都使用main字段描述入口文件的位置,所以可以设置单独一个main值,减少搜索
对庞大的第三方模块设置resolve.alias, 使webpack直接使用库的min文件,避免库内解析 如对于react:
resolve.alias:{
'react':patch.resolve(__dirname, './node_modules/react/dist/react.min.js')
}
这样会影响Tree-Shaking,适合对整体性比较强的库使用,如果是像lodash这类工具类的比较分散的库,比较适合Tree-Shaking,避免使用这种方式
合理配置resolve.extensions,减少文件查找默认值:extensions:['.js', '.json'],当导入语句没带 文件后缀时,Webpack会根据extensions定义的后缀列表进行文件查找,所以:列表值尽量少;频率高的文件类型 的后缀写在前面;源码中的导入语句尽可能的写上文件后缀,如require(./data)要写成require(./data.json)
module.noParse字段告诉Webpack不必解析哪些文件,可以用来排除对非模块化库文件的解析 如jQuery、ChartJS,另外如果使用resolve.alias配置了react.min.js,则也应该排除解析,因为react.min.js经过构建,已经是可以直接运行在浏览器的、非模块化的文件了。noParse值可以是RegExp、[RegExp]、function、module:{ noParse:[/jquery|chartjs/, /react.min.js$/] }
配置loader时,通过test、exclude、include缩小搜索范围
# 使用DllPlugin减少基础模块编译次数
DllPlugin动态链接库插件,其原理是把网页依赖的基础模块抽离出来打包到dll文件中,当需要导入的模块存在于某个dll中时,这个模块不再被打包,而是去dll中获取。为什么会提升构建速度呢?原因在于dll中大多包含的是常用的第三方模块,如react、react-dom,所以只要这些模块版本不升级,就只需被编译一次。我认为这样做和配置resolve.alias和module.noParse的效果有异曲同工的效果
- 使用DllPlugin配置一个webpack_dll.config.js来构建dll文件:
// webpack_dll.config.js
const path = require('path');
const DllPlugin = require('webpack/lib/DllPlugin');
module.exports = {
entry:{
react:['react','react-dom'],
polyfill:['core-js/fn/promise','whatwg-fetch']
},
output:{
filename:'[name].dll.js',
path:path.resolve(__dirname, 'dist'),
library:'_dll_[name]', //dll的全局变量名
},
plugins:[
new DllPlugin({
name:'_dll_[name]', //dll的全局变量名
path:path.join(__dirname,'dist','[name].manifest.json'),//描述生成的manifest文件
})
]
}
需要注意DllPlugin的参数中name值必须和output.library值保持一致,并且生成的manifest文件中会引用output.library值。最终构建出的文件:
|-- polyfill.dll.js
|-- polyfill.manifest.json
|-- react.dll.js
└── react.manifest.json
其中xx.dll.js包含打包的n多模块,这些模块存在一个数组里,并以数组索引作为ID,通过一个变量假设为_xx_dll暴 露在全局中,可以通过window._xx_dll访问这些模块。xx.manifest.json文件描述dll文件包含哪些模块、每个模 块的路径和ID。然后再在项目的主config文件里使用DllReferencePlugin插件引入xx.manifest.json文件 2. 在主config文件里使用DllReferencePlugin插件引入xx.manifest.json文件
//webpack.config.json
const path = require('path');
const DllReferencePlugin = require('webpack/lib/DllReferencePlugin');
module.exports = {
entry:{ main:'./main.js' },
//... 省略output、loader等的配置
plugins:[
new DllReferencePlugin({
manifest:require('./dist/react.manifest.json')
}),
new DllReferenctPlugin({
manifest:require('./dist/polyfill.manifest.json')
})
]
}
# 使用HappyPack开启多进程Loader转换
在整个构建流程中,最耗时的就是Loader对文件的转换操作了,而运行在Node.js之上的Webpack是单线程模型的,也 就是只能一个一个文件进行处理,不能并行处理。HappyPack可以将任务分解给多个子进程,最后将结果发给主进程。 JS是单线程模型,只能通过这种多进程的方式提高性能
const path = require('path');
const HappyPack = require('happypack');
module.exports = {
//...
module:{
rules:[{
test:/\.js$/,
use:['happypack/loader?id=babel']
exclude:path.resolve(__dirname, 'node_modules')
},{
test:/\.css/,
use:['happypack/loader?id=css']
}],
plugins:[
new HappyPack({
id:'babel',
loaders:['babel-loader?cacheDirectory']
}),
new HappyPack({
id:'css',
loaders:['css-loader']
})
]
}
}
除了id和loaders,HappyPack还支持这三个参数:threads、verbose、threadpool,threadpool代表共享进程 池,即多个HappyPack实例都用同个进程池中的子进程处理任务,以防资源占用过多
# 其他小tips
使用UglifyJS插件压缩JS代码时,需要先将代码解析成Object表示的AST(抽象语法树),再去应用各种规则去分析和 处理AST,所以这个过程计算量大耗时较多。ParallelUglifyPlugin可以开启多个子进程,每个子进程使用 UglifyJS压缩代码,可以并行执行,能显著缩短压缩时间,使用也很简单,把原来的UglifyJS插件换成本插件即可
一个中大型应用中,第三方的依赖,庞大得可怕,占据了打包后文件的一半以上。然而,这些依赖模块又是很少变更的资 源,和css 代码分离的逻辑相似,分离第三方依赖库,可以更好的利用浏览器缓存,提升应用性能。因此,将依赖模块从 业务代码中分离是性能优化重要的一环。webpack4.0 中,依赖库的分离只需要通过 optimization.splitChunks 进行配置即可
最主要的一点是我们希望更好的利用浏览器的缓存,当单独修改了样式时,独立的css文件可以不需要应用去加载整个的 脚本文件,提高效率。并且,当遇到多页面的应用时,可以单独将一些公共部分的样式抽离开来,加载一个页面后,接下 来的页面同样可以利用缓存来减少请求。webpack4.0 中提供了抽离css文件的插件,mini-css-extract-plugin, 只需要简单的配置便可以将css文件分离开来
webpack 在构建中提供了不少于7种的sourcemap模式,其中eval模式虽然可以提高构建效率,但是构建后的脚本较 大,因此生产上并不适用。而source-map 模式可以通过生成的 .map 文件来追踪脚本文件的 具体位置,进而缩小脚 本文件的体积,这是生产模式的首选,并且在生产中,我们需要隐藏具体的脚本信息,因此可以使用 cheap 和module 模式来达到目的。综上,在生产的webpack devtool选项中,我们使用 cheap-module-source-map的配置 开发环境下将devtool设置为cheap-module-eval-source-map
热更新、懒加载、压图片,做雪碧图,配置cache:true,是否启用缓存来提升构建速度
配置babel-loader时,use: [‘babel-loader?cacheDirectory’] cacheDirectory用于缓存babel的编译结 果,加快重新编译的速度。另注意排除node_modules文件夹,因为文件都使用了ES5的语法,没必要使用Babel转换