每次在命令行输入 webpack 后,操作系统都会去调用 ./node_modules/.bin/webpack
这个 shell 脚本。这个脚本会去调用 ./node_modules/webpack/bin/webpack.js
并追加输入的参数,如 -p , -w 。(图中 webpack.js 是 webpack 的启动文件,而 $@ 是后缀参数)
在 webpack.js 这个文件中 webpack 通过 optimist 将用户配置的 webpack.config.js 和 shell 脚本传过来的参数整合成 options 对象传到了下一个流程的控制对象中。
获取到后缀参数后,optimist 分析参数并以键值对的形式把参数对象保存在 optimist.argv 中,来看看 argv 究竟有什么?
在加载插件之前,webpack 将 webpack.config.js 中的各个配置项拷贝到 options 对象中,并加载用户配置在 webpack.config.js 的 plugins 。接着 optimist.argv 会被传入到 ./node_modules/webpack/bin/convert-argv.js
中,通过判断 argv 中参数的值决定是否去加载对应插件。options
作为最后返回结果,包含了之后构建阶段所需的重要信息。
在加载配置文件和 shell 后缀参数申明的插件,并传入构建信息 options 对象后,开始整个 webpack 打包最漫长的一步。而这个时候,真正的 webpack 对象才刚被初始化,具体的初始化逻辑在 lib/webpack.js
中,如下:
function webpack(options) {
var compiler = new Compiler();
...// 检查options,若watch字段为true,则开启watch线程
return compiler;
}
...
webpack 的实际入口是 Compiler 中的 run 方法,run 一旦执行后,就开始了编译和构建流程 ,其中有几个比较关键的 webpack 事件节点。
compile
开始编译make
从入口点分析模块及其依赖的模块,创建这些模块对象build-module
构建模块after-compile
完成构建seal
封装构建结果emit
把各个chunk输出到结果文件after-emit
完成输出webpack 提供的一个很大的便利就是能将所有资源都整合成模块,不仅仅是 js 文件。所以需要一些 loader ,比如 url-loader
, jsx-loader
, css-loader
等等来让我们可以直接在源文件中引用各类资源。webpack 调用 doBuild()
,对每一个 require() 用对应的 loader 进行加工,最后生成一个 js module。
调用 acorn 解析经 loader 处理后的源文件生成抽象语法树 AST
Parser.prototype.parse = function parse(source, initialState) {
var ast;
if (!ast) {
// acorn以es6的语法进行解析
ast = acorn.parse(source, {
ranges: true,
locations: true,
ecmaVersion: 6,
sourceType: "module"
});
}
...
};
遍历 AST,构建该模块所依赖的模块
对于当前模块,或许存在着多个依赖模块。当前模块会开辟一个依赖模块的数组,在遍历 AST 时,将 require() 中的模块通过 addDependency()
添加到数组中。当前模块构建完成后,webpack 调用 processModuleDependencies
开始递归处理依赖的 module,接着就会重复之前的构建步骤。
在所有模块及其依赖模块 build 完成后,webpack 会监听 seal
事件调用各插件对构建后的结果进行封装,要逐次对每个 module 和 chunk 进行整理,生成编译后的源码,合并,拆分,生成 hash 。 同时这是我们在开发时进行代码优化和功能添加的关键环节。
webpack 的整体流程主要还是依赖于 compilation
和 module
这两个对象,但其思想远不止这么简单。最开始也说过,webpack 本质是个插件集合,并且由 tapable
控制各插件在 webpack 事件流上运行,至于具体的思想和细节,将会在后一篇文章中提到。同时,在业务开发中,无论是为了提升构建效率,或是减小打包文件大小,我们都可以通过编写 webpack 插件来进行流程上的控制,这个也会在之后提到。
webpack整体是一个插件架构,所有的功能都以插件的方式集成在构建流程中,通过发布订阅事件来触发各个插件执行。webpack核心使用Tapable 来实现插件(plugins
)的binding和applying.
其中有几个关键节段对应的事件分别是:
entry-option
初始化option
run
开始编译
make
从entry开始递归的分析依赖,对每个依赖模块进行build
before-resolve
- after-resolve
对其中一个模块位置进行解析
build-module
开始构建 (build) 这个module,这里将使用文件对应的loader加载
normal-module-loader
对用loader加载完成的module(是一段js代码)进行编译,用 acorn 编译,生成ast抽象语法树。
program
开始对ast进行遍历,当遇到require等一些调用表达式时,触发call require
事件的handler执行,收集依赖,并。如:AMDRequireDependenciesBlockParserPlugin等
seal
所有依赖build完成,下面将开始对chunk进行优化,比如合并,抽取公共模块,加hash
bootstrap
生成启动代码
emit
把各个chunk输出到结果文件
Module
是webpack的中的核心实体,要加载的一切和所有的依赖都是Module,总之一切都是Module。它有很多子类:RawModule
,NormalModule
,MultiModule
,ContextModule
,DelegatedModule
,DllModule
,ExternalModule
等
每一个依赖(Dependency)的实体都包含一个module字段,指向被依赖的Module. 这样通过Module的dependencies数组成员就能找出该模块所依赖的其它模块。 webpack使用不同的Dependency子类,如AMDRequireDependency
,AMDDefineDependency
,AMDRequireArrayDependency
,CommonJsRequireDependency
,SystemImportDependency
来表式不同的模块加载规范, 通过对应的DependencyParserPlugin
来加载 AMD
或CMD
的模块。 后面会专门讲不同DependencyParserPlugin
的实现方式 。
webpack的实际入口是Compiler
类的run
方法, 在run方法里调用compile
方法开始编译。在编译的时候会使用一个核心对象:Compilation
.
webpack使用acorn解析每一个经loader处理过的source,并且成AST,然后遍历所有节点,当遇到require调用时,会分析是AMD的还是CMD的调用,或者是require.ensure
. 我们不再分析AST的遍历过程了。
webpack官网对 loader 已经介绍的非常详细了,不再多说。你只需要记住:
webpack在build模块时 (`调用doBuild方法`),要先调用相应的loader对resource进行加工,生成一段js代码后交给acorn解析生成AST.所以不管是css文件,还是jpg文件,还是html模版,
最终经过loader处理会变成一个module:一段js代码。