目前,几乎全部业务的开发构建都会用到 webpack 。的确,做为模块加载和打包神器,只需配置几个文件,加载各类 loader 就能够享受无痛流程化开发。但对于 webpack 这样一个复杂度较高的插件集合,它的总体流程及思想对咱们来讲仍是很透明的。那么接下来我会带你了解 webpack 这样一个构建黑盒,首先来谈谈它的流程。javascript
在开始了解以前,必需要能对 webpack 整个流程进行 debug ,配置过程比较简单。
先将 webpack-webstorm-debugger-script 中的 webstorm-debugger.js 置于 webpack.config.js 的同一目录下,搭建好你的脚手架后就能够直接 Debug 这个 webstorm-debugger.js 文件了。css
估计你们对 webpack.config.js 的配置也尝试过很多次了,这里就大体对这个配置文件进行个分析。java
var path = require('path');
var node_modules = path.resolve(__dirname, 'node_modules');
var pathToReact = path.resolve(node_modules, 'react/dist/react.min.js');
module.exports = {
// 入口文件,是模块构建的起点,同时每个入口文件对应最后生成的一个 chunk。
entry: {
bundle: [
'webpack/hot/dev-server',
'webpack-dev-server/client?http://localhost:8080',
path.resolve(__dirname, 'app/app.js')
],
},
// 文件路径指向(可加快打包过程)。
resolve: {
alias: {
'react': pathToReact
}
},
// 生成文件,是模块构建的终点,包括输出文件与输出路径。
output: {
path: path.resolve(__dirname, 'build'),
filename: '[name].js',
},
// 这里配置了处理各模块的 loader ,包括 css 预处理 loader ,es6 编译 loader,图片处理 loader。
module: {
loaders: [
{
test: /\.js$/,
loader: 'babel',
query: {
presets: ['es2015', 'react']
}
}
],
noParse: [pathToReact]
},
// webpack 各插件对象,在 webpack 的事件流中执行对应的方法。
plugins: [
new webpack.HotModuleReplacementPlugin();
]
};
除此以外再大体介绍下 webpack 的一些核心概念:
* loader : 能转换各种资源,并处理成对应模块的加载器。loader 间能够串行使用。
* chunk : code splitting后的产物,也就是按需加载的分块,装载了不一样的module。
对于module和chunk的关系能够参照webpack官方的这张图:
* plugin : webpack 的插件实体,这里以 UglifyJsPlugin 为例。 node
function UglifyJsPlugin(options) {
this.options = options;
}
module.exports = UglifyJsPlugin;
UglifyJsPlugin.prototype.apply = function(compiler) {
compiler.plugin("compilation", function(compilation) {
compilation.plugin("build-module", function(module) {
});
compilation.plugin("optimize-chunk-assets", function(chunks, callback) {
// Uglify 逻辑
});
compilation.plugin("normal-module-loader", function(context) {
});
});
};
在 webpack 中你常常能够看到 compilation.plugin(‘xxx’, callback) ,你能够把它看成是一个事件的绑定,这些事件在打包时由 webpack 来触发。react
在具体流程学习前,能够先经过这幅 webpack总体流程图 了解一下大体流程(建议保存下来查看)。
webpack
每次在命令行输入 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 对象传到了下一个流程的控制对象中。 es6
和 commander 同样,optimist 实现了 node 命令行的解析,其 API 调用很是方便。 web
var optimist = require("optimist");
optimist
.boolean("json").alias("json", "j").describe("json")
.boolean("colors").alias("colors", "c").describe("colors")
.boolean("watch").alias("watch", "w").describe("watch")
...
获取到后缀参数后,optimist 分析参数并以键值对的形式把参数对象保存在 optimist.argv 中,来看看 argv 究竟有什么?shell
// webpack --hot -w
{
hot: true,
profile: false,
watch: true,
...
}
在加载插件以前,webpack 将 webpack.config.js 中的各个配置项拷贝到 options 对象中,并加载用户配置在 webpack.config.js 的 plugins 。接着 optimist.argv 会被传入到 ./node_modules/webpack/bin/convert-argv.js 中,经过判断 argv 中参数的值决定是否去加载对应插件。(至于 webpack 插件运行机制,在以后的运行机制篇会提到) json
ifBooleanArg("hot", function() {
ensureArray(options, "plugins");
var HotModuleReplacementPlugin = require("../lib/HotModuleReplacementPlugin");
options.plugins.push(new HotModuleReplacementPlugin());
});
...
return options;
options 做为最后返回结果,包含了以后构建阶段所需的重要信息。
{
entry: {},//入口配置
output: {}, //输出配置
plugins: [], //插件集合(配置文件 + shell指令)
module: { loaders: [ [Object] ] }, //模块配置
context: //工程路径
...
}
这和 webpack.config.js 的配置很是类似,只是多了一些经 shell 传入的插件对象。插件对象一初始化完毕, options 也就传入到了下个流程中。
var webpack = require("../lib/webpack.js");
var compiler = webpack(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 完成输出
compiler.run 后首先会触发 compile ,这一步会构建出 Compilation 对象:
这个对象有两个做用,一是负责组织整个打包过程,包含了每一个构建环节及输出环节所对应的方法,能够从图中看到比较关键的步骤,如 addEntry() , _addModuleChain() , buildModule() , seal() , createChunkAssets() (在每个节点都会触发 webpack 事件去调用各插件)。二是该对象内部存放着全部 module ,chunk,生成的 asset 以及用来生成最后打包文件的 template 的信息。
在建立 module 以前,Compiler 会触发 make,并调用 Compilation.addEntry 方法,经过 options 对象的 entry 字段找到咱们的入口js文件。以后,在 addEntry 中调用私有方法 _addModuleChain ,这个方法主要作了两件事情。一是根据模块的类型获取对应的模块工厂并建立模块,二是构建模块。
而构建模块做为最耗时的一步,又可细化为三步:
* 调用各 loader 处理模块之间的依赖
webpack 提供的一个很大的便利就是能将全部资源都整合成模块,不只仅是 js 文件。因此须要一些 loader ,好比 url-loader ,jsx-loader , css-loader 等等来让咱们能够直接在源文件中引用各种资源。webpack 调用 doBuild() ,对每个 require() 用对应的 loader 进行加工,最后生成一个 js module。
Compilation.prototype._addModuleChain = function process(context, dependency, onModule, callback) {
var start = this.profile && +new Date();
...
// 根据模块的类型获取对应的模块工厂并建立模块
var moduleFactory = this.dependencyFactories.get(dependency.constructor);
...
moduleFactory.create(context, dependency, function(err, module) {
var result = this.addModule(module);
...
this.buildModule(module, function(err) {
...
// 构建模块,添加依赖模块
}.bind(this));
}.bind(this));
};
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"
});
}
...
};
Compilation.prototype.addModuleDependencies = function(module, dependencies, bail, cacheGroup, recursive, callback) {
// 根据依赖数组(dependencies)建立依赖模块对象
var factories = [];
for(var i = 0; i < dependencies.length; i++) {
var factory = _this.dependencyFactories.get(dependencies[i][0].constructor);
factories[i] = [factory, dependencies[i]];
}
...
// 与当前模块构建步骤相同
}
module 是 webpack 构建的核心实体,也是全部 module的 父类,它有几种不一样子类:NormalModule , MultiModule ,ContextModule , DelegatedModule 等。但这些核心实体都是在构建中都会去调用对应方法,也就是 build() 。来看看其中具体作了什么:
// 初始化module信息,如context,id,chunks,dependencies等。
NormalModule.prototype.build = function build(options, compilation, resolver, fs, callback) {
this.buildTimestamp = new Date().getTime(); // 构建计时
this.built = true;
return this.doBuild(options, compilation, resolver, fs, function(err) {
// 指定模块引用,不经acorn解析
if(options.module && options.module.noParse) {
if(Array.isArray(options.module.noParse)) {
if(options.module.noParse.some(function(regExp) {
return typeof regExp === "string" ?
this.request.indexOf(regExp) === 0 :
regExp.test(this.request);
}, this)) return callback();
} else if(typeof options.module.noParse === "string" ?
this.request.indexOf(options.module.noParse) === 0 :
options.module.noParse.test(this.request)) {
return callback();
}
}
// 由acorn解析生成ast
try {
this.parser.parse(this._source.source(), {
current: this,
module: this,
compilation: compilation,
options: options
});
} catch(e) {
var source = this._source.source();
this._source = null;
return callback(new ModuleParseError(this, source, e));
}
return callback();
}.bind(this));
};
对于每个 module ,它都会有这样一个构建方法。固然,它还包括了从构建到输出的一系列的有关 module 生命周期的函数,咱们经过 module 父类类图其子类类图(这里以 NormalModule 为例)来观察其真实形态:
能够看到不管是构建流程,处理依赖流程,包括后面的封装流程都是与 module 密切相关的。
在全部模块及其依赖模块 build 完成后,webpack 会监听 seal 事件调用各插件对构建后的结果进行封装,要逐次对每一个 module 和 chunk 进行整理,生成编译后的源码,合并,拆分,生成 hash 。 同时这是咱们在开发时进行代码优化和功能添加的关键环节。
Compilation.prototype.seal = function seal(callback) {
this.applyPlugins("seal"); // 触发插件的seal事件
this.preparedChunks.sort(function(a, b) {
if(a.name < b.name) return -1;
if(a.name > b.name) return 1;
return 0;
});
this.preparedChunks.forEach(function(preparedChunk) {
var module = preparedChunk.module;
var chunk = this.addChunk(preparedChunk.name, module);
chunk.initial = chunk.entry = true;
// 整理每一个Module和chunk,每一个chunk对应一个输出文件。
chunk.addModule(module);
module.addChunk(chunk);
}, this);
this.applyPluginsAsync("optimize-tree", this.chunks, this.modules, function(err) {
if(err) {
return callback(err);
}
... // 触发插件的事件
this.createChunkAssets(); // 生成最终assets
... // 触发插件的事件
}.bind(this));
};
在封装过程当中,webpack 会调用 Compilation 中的 createChunkAssets 方法进行打包后代码的生成。 createChunkAssets 流程以下:
* 不一样的 Template
从上图能够看出经过判断是入口 js 仍是须要异步加载的 js 来选择不一样的模板对象进行封装,入口 js 会采用webpack 事件流的 render 事件来触发 Template类 中的 renderChunkModules() (异步加载的 js 会调用 chunkTemplate 中的 render 方法)。
if(chunk.entry) {
source = this.mainTemplate.render(this.hash, chunk, this.moduleTemplate, this.dependencyTemplates);
} else {
source = this.chunkTemplate.render(chunk, this.moduleTemplate, this.dependencyTemplates);
}
在 webpack 中有四个 Template 的子类,分别是 MainTemplate.js , ChunkTemplate.js ,ModuleTemplate.js ,HotUpdateChunkTemplate.js ,前二者先前已大体有介绍,而 ModuleTemplate 是对全部模块进行一个代码生成,HotUpdateChunkTemplate 是对热替换模块的一个处理。
* 模块封装
模块在封装的时候和它在构建时同样,都是调用各模块类中的方法。封装经过调用 module.source() 来进行各操做,好比说 require() 的替换。
MainTemplate.prototype.requireFn = "__webpack_require__";
MainTemplate.prototype.render = function(hash, chunk, moduleTemplate, dependencyTemplates) {
var buf = [];
// 每个module都有一个moduleId,在最后会替换。
buf.push("function " + this.requireFn + "(moduleId) {");
buf.push(this.indent(this.applyPluginsWaterfall("require", "", chunk, hash)));
buf.push("}");
buf.push("");
... // 其他封装操做
};
最后一步,webpack 调用 Compiler 中的 emitAssets() ,按照 output 中的配置项将文件输出到了对应的 path 中,从而 webpack 整个打包过程结束。要注意的是,若想对结果进行处理,则须要在 emit 触发后对自定义插件进行扩展。
webpack 的总体流程主要仍是依赖于 compilation 和 module 这两个对象,但其思想远不止这么简单。最开始也说过,webpack 本质是个插件集合,而且由 tapable 控制各插件在 webpack 事件流上运行,至于具体的思想和细节,将会在后一篇文章中提到。同时,在业务开发中,不管是为了提高构建效率,或是减少打包文件大小,咱们均可以经过编写 webpack 插件来进行流程上的控制,这个也会在以后提到。