
是什么
Webpack 是一个基于事件流的插件集合。它的工作流程就是将各个插件串联起来,核心是Tapable,它主要控制钩子函数的发布与订阅,控制着 Webpack 的插件系统。Webpack 中最核心的负责编译的Compiler和负责创建捆绑包的Compilation都是 Tapable 的实例。
Tapable 库
API
const {
SyncHook, // 同步钩子
SyncBailHook, // 同步熔断钩子
SyncWaterfallHook, // 同步流水钩子
SyncLoopHook, // 同步循环钩子
AsyncParallelHook, // 异步并发钩子
AsyncParallelBailHook, // 异步并发熔断钩子
AsyncSeriesHook, // 异步串行钩子
AsyncSeriesBailHook, // 异步串行熔断钩子
AsyncSeriesWaterfallHook // 异步串行流水钩子
} = require("tapable")
const hook = new SyncHook(["arg1", "arg2", "arg3"])
use
const { SyncHook } = require("tapable");
let queue = new SyncHook(['name']); //所有的构造函数都接收一个可选的参数,这个参数是一个字符串的数组。
// 订阅
queue.tap('1', function (name, name2) {// tap 的第一个参数是用来标识订阅的函数的
console.log(name, name2, 1);
return'1'
});
queue.tap('2', function (name) {
console.log(name, 2);
});
queue.tap('3', function (name) {
console.log(name, 3);
});
// 发布
queue.call('webpack', 'webpack-cli');// 发布的时候触发订阅的函数 同时传入参数
// 执行结果:
/*
webpack undefined 1 // 传入的参数需要和new实例的时候保持一致,否则获取不到多传的参数
webpack 2
webpack 3
*/
工作流程
- 参数解析;
- 找到入口文件;
- 调用 loader 编译文件;
- 遍历 AST,收集依赖;
- 生成 trunk;
- 输出文件。
loader
处理任意类型的文件,把他们转化为一个 webpack 可以处理的有效模块。
Loader 的配置
1. 在 webpack.config.js 配置
Loader 可以在 webpack.config.js 里配置,定义在 module.rules 里:
// webpack.config.js
module.exports = {
mudule: {
rules:[
{
test: /\.js$/,
use: 'babel-loader'
},
{
test: /\.css$/,
use: [
{ loader: 'style-loader' },
{ loader: 'css-loader' },
{ loader: 'postcss-loader' },
]
}
]
}
}
- module.rules 是个数组;
- 每个 rules 都有两个属性:test 和 use;
- test 是个正则表达式;
- use 可以是 string | array | object | function
- string: 只有一个 Loader 时,直接声明 Loader,比如 babel-loader。
- array: 声明多个 Loader 时,使用数组形式声明,比如上文声明 .css 的 Loader。执行顺序是从右到左,从下到上。
- object: 只有一个 Loader 时,需要有额外的配置项时。
- function: use 也支持回调函数的形式。
2. 内联
在 import 等语句里指定 Loader,使用 ! 来将 Loader分开:
import style from 'style-loader!css-loader?modules!./styles.css'
内联时,通过 query 来传递参数,例如 ?key=value。 内联形式多出现于 Loader 内部,比如 style-loader 会在自身代码里引入 css-loader:
require("!!../../node_modules/css-loader/dist/cjs.js!./styles.css")
Loader 类型
同步 Loader
一般来说,Loader 都是同步的,通过 return 或者 this.callback 来同步地返回 source转换后的结果。
module.exports = function(source) {
const result = someSyncOperation(source); // 同步逻辑
return result;
}
异步 Loader
有的时候,我们需要在 Loader 里做一些异步的事情,比如说需要发送网络请求。如果同步地等着,网络请求就会阻塞整个构建过程,这个时候我们就需要进行异步 Loader,可以这样做:
module.exports = function(source) {
// 告诉 webpack 这次转换是异步的
const callback = this.async();
// 异步逻辑
someAsyncOperation(content, function(err, result) {
if (err) return callback(err);
// 通过 callback 来返回异步处理的结果
callback(null, result, map, meta);
});
};
Pitching Loader
每个 Loader 都支持一个 pitch 属性,通过 module.exports.pitch 声明。
{
test: /\.js$/,
use: [
{ loader: 'aa-loader' },
{ loader: 'bb-loader' },
{ loader: 'cc-loader' },
]
}
我们知道,Loader 总是从右到左被调用。上面配置的 Loader,就会按照以下顺序执行:
cc-loader -> bb-loader -> aa-loader
如果该 Loader 声明了 pitch,则该方法会优先于 Loader 的实际方法先执行,官方也给出了执行顺序:
|- aa-loader `pitch`
|- bb-loader `pitch`
|- cc-loader `pitch`
|- requested module is picked up as a dependency
|- cc-loader normal execution
|- bb-loader normal execution
|- aa-loader normal execution
Raw Loader
我们在 url-loader 里和 file-loader 最后都见过这样一句代码:
export const raw = true
默认情况下,webpack 会把文件进行 UTF-8 编码,然后传给 Loader。通过设置 raw,Loader 就可以接受到原始的 Buffer 数据。
API
所谓 Loader,也只是一个符合 commonjs 规范的 node 模块,它会导出一个可执行函数。loader runner 会调用这个函数,将文件的内容或者上一个 Loader 处理的结果传递进去。同时,webpack 还为 Loader 提供了一个上下文 this,其中有很多有用的 api,我们找几个典型的来看看。
this.callback()
在 Loader 中,通常使用 return 来返回一个字符串或者 Buffer。如果需要返回多个结果值时,就需要使用 this.callback,定义如下:
this.callback(
// 无法转换时返回 Error,其余情况都返回 null
err: Error | null,
// 转换结果
content: string | Buffer,
// source map,方便调试用的
sourceMap?: SourceMap,
// 可以是任何东西。比如 ast
meta?: any
);
一般来说如果调用该函数的话,应该手动 return,告诉 webpack 返回的结果在 this.callback 中,以避免含糊不清的结果:
module.exports = function(source) {
this.callback(null, source, sourceMaps);
return;
};
this.async()
同上,异步 Loader
this.cacheable()
有些情况下,有些操作需要耗费大量时间,每一次调用 Loader 转换时都会执行这些费时的操作。
在处理这类费时的操作时, webapck 会默认缓存所有 Loader 的处理结果,只有当被处理的文件发生变化时,才会重新调用 Loader 去执行转换操作。
webpack 是默认可缓存的,可以执行 this.cacheable(false) 手动关闭缓存。
this.resource
当前处理文件的完整请求路径,包括 query,比如 /src/App.vue?type=templpate
this.resourcePath
当前处理文件的路径,不包括 query,比如 /src/App.vue。
this.resourceQuery
当前处理文件的 query 字符串,比如 ?type=template。我们在 vue-loader 里有见过如何使用它:
const qs = require('querystring');
const { resourceQuery } = this;
const rawQuery = resourceQuery.slice(1); // 删除前面的 ?
const incomingQuery = qs.parse(rawQuery); // 解析字符串成对象
// 取 query
if (incomingQuery.type) {}
this.emitFile
让 webpack 在输出目录新建一个文件,我们在 file-loader 里有见过:
if (typeof options.emitFile === 'undefined' || options.emitFile) {
this.emitFile(outputPath, content);
}
Loader 特点
- Loader 是一个 node 模块;
- Loader 可以处理任意类型的文件,转换成 webpack 可以处理的模块;
- Loader 可以在 webpack.config.js 里配置,也可以在 require 语句里内联;
- Loader 可以根据配置从右向左链式执行;
- Loader 接受源文件内容字符串或者 Buffer;
- Loader 分为多种类型:同步、异步和 pitching,他们的执行流程不一样;
- webpack 为 Loader 提供了一个上下文,有一些 api 可以使用。
Loader 工作流程
- webpack.config.js 里配置了一个 js 的 Loader;
- 遇到 js 文件时,触发了 js-loader;
- js-loader 接受了一个表示该 js 文件内容的 source;
- js-loader 使用 webapck 提供的一系列 api 对 source 进行转换,得到一个 result;
- 将 result 返回或者传递给下一个 Loader,直到处理完毕。
如何编写一个 Loader
单一任务和链式调用。 loader结构很简单,接收一个参数,并且return一个内容就ok了。 例如:
- 处理.txt文件
- 对字符串做反转操作
- 首字母大写
1)首先创建两个loader
// reverseLoader.js
module.exports = function(src) {
if (src) {
console.log('-----------reverseLoader start-------')
console.log(src)
src = src.split('').reverse().join('')
console.log('-----------reverseLoader end-------')
console.log(src)
}
return src
}
// uppercaseLoader.js
module.exports = function(src) {
if (src) {
console.log('-----------uppercaseLoader start-------')
console.log(src)
src = `${src.charAt(0).toUpperCase()}${src.slice(1)}`
console.log('-----------uppercaseLoader end-------')
console.log(src)
}
// 这里为什么要这么写?因为直接返回转换后的字符串会报语法错误,
// 这么写import后转换成可以使用的字符串
return `module.exports = ${src}`
}
2) 配置webpack
// ...
module: {
rules: [
...,
{
test: /\.txt$/,
use: [
'./loader/uppercaseLoader.js',
'./loader/reverseLoader.js'
]
}
]
}
3)在入口文件引入我们的test.txt 文件 4)执行 npm run build

Plugins
在 Webpack 运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果。
对于loader,它就是一个转换器,将A文件进行编译形成B文件,这里操作的是文件,比如将A.scss或A.less转变为B.css,单纯的文件转换过程。
plugin是一个扩展器,它丰富了wepack本身,针对webpack打包的整个过程,它并不直接操作文件,而是基于事件机制工作,会监听webpack打包过程中的某些节点,执行广泛的任务。
实现一个Plugin
// MyPlugin.js
class MyPlugin {
constructor(options) {
console.log(`MyPlugin constructor: ${options}`)
}
// 应用函数
apply (compiler) {
// 绑定钩子事件
compiler.plugin('compilation', compilation => {
console.log('compilation')
})
}
}
module.exports = MyPlugin
webpack.config.js
const MyPlugin = require('./plugins/MyPlugin')
//...
module.exports = {
// ...
plugins: [
new MyPlugin()
]
}

这就是一个最简单的插件(虽然我们什么都没干)
- webpack 启动后,在读取配置的过程中会先执行 new MyPlugin(options) 初始化一个 MyPlugin 获得其实例。
- 在初始化 compiler 对象后,再调用 myPlugin.apply(compiler) 给插件实例传入 compiler 对象。
- 插件实例在获取到 compiler 对象后,就可以通过 compiler.plugin(事件名称, 回调函数) 监听到 Webpack 广播出来的事件。
- 并且可以通过 compiler 对象去操作 webpack。
Compiler 和 Compilation
Compiler 对象包含了 Webpack 环境所有的的配置信息,包含 options,loaders,plugins 这些信息,这个对象在 Webpack 启动时候被实例化,它是全局唯一的,可以简单地把它理解为 Webpack 实例;
Compilation 对象包含了当前的模块资源、编译生成资源、变化的文件等。当 Webpack 以开发模式运行时,每当检测到一个文件变化,一次新的 Compilation 将被创建。Compilation 对象也提供了很多事件回调供插件做扩展。通过 Compilation 也能读取到 Compiler 对象。
Compiler 和 Compilation 的区别在于: Compiler 代表了整个 Webpack 从启动到关闭的生命周期,而 Compilation 只是代表了一次新的编译。
tips
- 只要能拿到 Compiler 或 Compilation 对象,就能广播出新的事件,所以在新开发的插件中也能广播出事件,给其它插件监听使用。
- 传给每个插件的 Compiler 和 Compilation 对象都是同一个引用。也就是说在一个插件中修改了 Compiler 或 Compilation 对象上的属性,会影响到后面的插件。
- 有些事件是异步的,这些异步的事件会附带两个参数,第二个参数为回调函数,在插件处理完任务时需要调用回调函数通知 webpack,才会进入下一处理流程。例如:
compiler.plugin('emit',function(compilation, callback) {
...
// 处理完毕后执行 callback 以通知 Webpack
// 如果不执行 callback,运行流程将会一直卡在这不往下执行
callback();
});
Tree-Shaking
它是一个性能优化的范畴。
在 webpack 项目中,有一个入口文件,入口文件有很多依赖的模块,实际情况中,虽然依赖了某个模块,但其实只使用了其中的某些功能,通过 tree-shaking 将没有使用的模块删除,这样来达到删除无用代码的目的。
支持 Tree-shaking 的构建工具
- Rollup
- Webpack 2
- Closure Compiler
原理
- ES6 模块引入是静态分析的,因此可以在编译阶段正确判断加载了什么代码;
- 分析程序流,判断哪些变量未被使用、引用,进而删除此代码。
使用rollup打包JavaScript库
好处
- 它支持导出ES模块的包。
- 它支持程序流分析,能更加正确的判断项目本身的代码是否有副作用。
通过rollup打出两份文件,一份umd版,一份ES模块版,它们的路径分别设为main,module的值。这样就能方便使用者进行tree-shaking。
package.json
{
"name": "my-package",
"main": "dist/my-package.umd.js",
"module": "dist/my-package.esm.js"
}