Webpack
1. 背景
实现模块化,更高效管理和维护项目的每一个资源。
- 模块化
最早是将功能及其相关状态数据各自单独放到不同的JS文件中,然后分别引入到页面。一个script对应一个模块。但是模块都是在全局中工作,大量模块成员污染了环境,模块与模块之间没有依赖关系、维护困难、没有私有空间等问题。
如今流行使用CommonJS、ES Modules
2. 问题
- 通过模块化的方式来开发
- 使用一些高级的特性来加快我们的开发效率或者安全性,比如通过ES6+TypeScript开发脚本逻辑,通过sass、less编写样式
- 监听文件的变化来反映到浏览器上,提高开发效率
- JS代码需要模块化,HTML和CSS这些资源文件也会面临需要被模块化的问题
- 开发完成后将代码进行压缩、合并以及其他相关的优化
3. 是什么?
用于现代JS应用程序的静态模块打包工具。
将可以被webpack直接引用的资源打包(bundle.js)
内部构建一个依赖图,反射到各个模块,生成一个或多个bundle。
- 编译代码能力,提高效率,解决浏览器兼容问题
- 模块整合能力,提高性能,可维护性,解决浏览器频繁请求文件的问题
- 万物皆可模块能力,维护性增强
4. 构建流程
4.1 运行流程
运行是串行的过程,将各个插件串联起来。插件监听他关心的事件,就能加入webpack机制,改变webpack运作。
启动到结束会依次执行以下三大步骤:
- 初始化流程:从配置文件和Shell语句中读取与合并参数,并初始化需要使用的插件和配置插件等执行环境所需要的的参数
- 编译构建流程:从Entry发出,针对每个Module串行调用对应的Loader去翻译文件内容,再找到该Module依赖的Module,递归地进行编译处理
- 输出流程:对编译后的Module组合成Chunk,把Chunk转换成文件,输出到文件系统
4.2 初始化流程
从配置文件和Shell语句中读取与合并参数,得出最终的参数。
配置文件默认为webpack.config.js,或通过命令的形式指定配置文件,主要作用:激活webpack的加载项和插件
1 | var path = require('path'); |
webpack将配置文件中的各个配置项拷贝到options对象中,并加载用户配置的plugins,然后开始初始化Compiler编译对象,该对象掌控webpack生命周期,不执行具体的任务,进行调度工作。
1 | class Compiler extends Tapable { //Complier对象继承Tapable,初始化时定义了钩子函数 |
4.3 编译构建流程
1 | module.exports = { |
初始化完成后调用Compiler的run来启动webpack构建流程,如下:
- compile开始编译
run后首先触发compile,主要是构建一个Compilation对象。
该对象是编译阶段的主要执行者,主要会依次下述流程:执行模块创建、依赖收集、分块、打包等主要任务的对象 - make从入口点分析模块及其依赖的模块,创建这些模块对象
从entry开始读取,执行_addModuleChain()函数该函数接收参数dependency传入的入口依赖,使用对应的create生成空的module对象,然后回调将module存入compilation.modules对象和dependencies.modules对象中,compilation.entrire也会保存,然后执行buildModule进行真正的构建模块module内容1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25_addModuleChain(context, dependency, onModule, callback) {
...
// 根据依赖查找对应的工厂函数
const Dep = /** @type {DepConstructor} */ (dependency.constructor);
const moduleFactory = this.dependencyFactories.get(Dep);
// 调用工厂函数NormalModuleFactory的create来生成一个空的NormalModule对象
moduleFactory.create({
dependencies: [dependency]
...
}, (err, module) => {
...
const afterBuild = () => {
this.processModuleDependencies(module, err => {
if (err) return callback(err);
callback(null, module);
});
};
this.buildModule(module, false, null, null, err => {
...
afterBuild();
})
})
} - build-module构建模块
调用配置的loaders,将模块转成标准的JS模块。
在用Loader对一个模块转换完后,使用acorn解析转换后的内容,输出对应的抽象语法树(AST),以方便webpack后面对代码的分析
从配置的入口模块开始,分析其AST,当遇到require等导入其他模块语句时,便将其加入到依赖的模块列表,同时对新找出的依赖模块递归分析,最终搞清所有模块的依赖关系 - seal封装构建结果
这步开始进行输出,seal主要生成chunks,对chunks进行一系列的优化操作,并生成要输出的代码。(chunk可以理解为配置在entry中的模块,或者是动态引入的模块)根据入口和模块之间的依赖关系,组成一个个包含多个模块的Chunk,再把每个Chunk转换成一个单独的文件加入到到输出列表。 - emit把各个chunk输出到结果文件
确定好输出内容后,根据配置确定输出的路径和文件名在Compiler开始生成文件前,钩子emit会被执行,这是修改最终文件的最后一个机会。打包过程结束。1
2
3
4output: {
path: path.resolve(__dirname, 'build'),
filename: '[name].js'
}
4.4 小结
5. loader
5.1 是什么?
loader用于对模块的源代码进行转换,在import或加载模块时预处理文件。
默认情况下只打包js文件,css、sass、png等类型文件,需要配置对应的loader进行解析,在碰到该类型文件则会在配置中找解析规则。
配置loader方式:
- 配置方式(推荐):在webpack.config.js文件中指定loader
rules是一个数组的形式,我们可以配置很多个loader
每一个loader对应一个对象的形式,对象属性test为匹配的规则,一般情况为正则表达式
属性use针对匹配到文件类型,调用对应的loader进行处理1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: [
{ loader: 'style-loader' },
{
loader: 'css-loader',
options: {
modules: true
}
},
{ loader: 'sass-loader' }
]
}
]
}
}; - 内联方式:在每个import语句中显式指定loader
- CLI方式:在shell命令中指定它们
5.2 特性
loader支持链式调用,链中的每个loader会处理之前已处理过的资源,最终变为JS代码。顺序为相反的顺序执行(sass-css-style)。
- loader可以是同步的,也可以是异步的
- loader运行在node.js中,并且能够执行任何操作
- 除了常见的通过package.json的main来讲一个npm模块导出为loader,还可以在module.rules中使用loader字段直接引用一个模块
- 插件(plugin)可以为loader带来更多特性
- loader能够产生额外的任意文件
5.3 常见的loader
- style-loader:将css添加到DOM的内联样式标签style里
- css-loader:允许将css文件通过require的方式引入,并返回css代码
- less-loader:处理less
- sass-loader:处理sass
- postcss-loader:用postcss来处理CSS
- autoprefixer-loader:处理CSS3属性前缀,已弃用
- file-loader:分发文件到output目录,并返回相对路径
- url-loader:和file-loader相似,文件小于设定可以返回一个Data url
- html-minify-loader:压缩HTML
- babel-loader:用babel来转换ES6到ES
- 具体例子详看loader篇
6. Plugin
6.1 是什么?
Plugin是一种计算机应用程序,和主应用程序互相交互,以提供特定的功能。
遵循一定规范的应用程序编写的程序,运行在程序规定的系统下,调用原系统提供的函数库或者数据。用于解决loader无法实现的事。
6.2 配置方式
1 | const HtmlWebpackPlugin = require('html-webpack-plugin'); // 通过 npm 安装 |
6.3 特性
本质是一个具有apply方法的js对象,apply被webpack compiler调用,并且在整个编译生命周期都可以访问compiler对象
1 | const pluginName = 'ConsoleOnBuildWebpackPlugin'; |
编译声明周期钩子:
- entry-option:初始化option
- run
- compile:真正开始的编译,在创建compilation对象之前
- compilation:生成好了compilation
- make从entry开始递归分析依赖,准备对每个模块进行build
- after-emit:在将内存中assets内容写到磁盘文件夹之后
- done:完成所有的编译过程
- failed:编译失败的时候
6.4 常见Plugin
常见用法详看plugin篇
7. Loader与Plugin的区别
7.1 区别
Loader:文件加载器,能够加载资源文件,并对这些文件进行一些处理,诸如编译、压缩等,最终一起打包到指定的文件中
Plugin:赋予了webpack各种灵活的功能,例如打包优化、资源管理、环境变量注入等,目的是解决loader无法实现的其他事
7.2 编写loader
loader本质为函数,this作为上下文会被webpack填充,不能将loader设为一个箭头函数,loader接收一个source参数,为文件源内容。this是webpack提供的对象,能获取当前loader所需要的各种信息,函数中有异步或同步操作,异步操作通过this.callback返回,返回值要求为string或者Buffer
1 | module.exports = function(source) { |
一般编写loader,保持功能单一,避免做多种功能,如less文件转换成css也是经过几个loader的链式调用才能完成转换
7.3 编写plugin
webpack基于发布订阅模式,在生命周期通过监听执行插件任务。
webpack编译会创建两个核心对象:
compiler:包含了环境所有的配置信息
compilation:plugin内置事件回调函数的参数,包含了模块资源等,检测到变化则创建新的compilation
实现plugin也需要一定规范:
插件必须是一个函数或者是一个包含apply方法的对象,这样才能访问compiler实例
传给每个插件的compiler和compilation对象都是同一个引用,不建议修改
异步的事件需要在插件处理完任务时调用回调函数进入下一个流程
1 | class MyPlugin { |
在emit事件发生时,代表源文件的转换和组装已经完成,可以读取到最终将输出的资源、代码块、模块及其依赖,并且可以修改输出资源的内容