webpack执行机制流程是怎么样的
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了webpack执行机制流程是怎么样的相关的知识,希望对你有一定的参考价值。
几乎所有业务的开发构建都会用到 webpack 。的确,作为模块加载和打包神器,只需配置几个文件,加载各种 loader 就可以享受无痛流程化开发。但对于 webpack 这样一个复杂度较高的插件集合,它的整体流程及思想对我们来说还是很透明的。那么接下来我会带你了解 webpack 这样一个构建黑盒,首先来谈谈它的流程。准备工作
1. webstorm 中配置 webpack-webstorm-debugger-script
在开始了解之前,必须要能对 webpack 整个流程进行 debug ,配置过程比较简单。
先将 webpack-webstorm-debugger-script 中的软件外包企业公司http://www.yingtaow.com?webstorm-debugger.js 置于 webpack.config.js 的同一目录下,搭建好你的脚手架后就可以直接 Debug 这个 webstorm-debugger.js 文件了。
2. webpack.config.js 配置
估计大家对 webpack.config.js 的配置也尝试过不少次了,这里就大致对这个配置文件进行个分析。
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 为例。
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 来触发。
3. 流程总览
在具体流程学习前,可以先通过这幅 webpack整体流程图 了解一下大致流程(建议保存下来查看)。
shell 与 config 解析
每次在命令行输入 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 对象传到了下一个流程的控制对象中。
1. optimist
和 commander 一样,optimist 实现了 node 命令行的解析,其 API 调用非常方便。
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 究竟有什么?
// webpack --hot -w
hot: true,
profile: false,
watch: true,
...
2. config 合并与插件加载
在加载插件之前,webpack 将 webpack.config.js 中的各个配置项拷贝到 options 对象中,并加载用户配置在 webpack.config.js 的 plugins 。接着 optimist.argv 会被传入到 ./node_modules/webpack/bin/convert-argv.js 中,通过判断 argv 中参数的值决定是否去加载对应插件。(至于 webpack 插件运行机制,在之后的运行机制篇会提到)
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 完成输出
1. 核心对象 Compilation
compiler.run 后首先会触发 compile ,这一步会构建出 Compilation 对象:
compilation类图
这个对象有两个作用,一是负责组织整个打包过程,包含了每个构建环节及输出环节所对应的方法,可以从图中看到比较关键的步骤,如 addEntry() , _addModuleChain() , buildModule() , seal() , createChunkAssets() (在每一个节点都会触发 webpack 事件去调用各插件)。二是该对象内部存放着所有 module ,chunk,生成的 asset 以及用来生成最后打包文件的 template 的信息。
2. 编译与构建主流程
在创建 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));
;
调用 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,接着就会重复之前的构建步骤。
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]];
...
// 与当前模块构建步骤相同
3. 构建细节
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 生命周期的函数 参考技术A 正如 lin瑞玉的回答 所说,webpack-dev-middle 把文件放在内存中了。实现过程大致为:webpack 有一个 outputFileSystem 选项,通过这个选项 webpack 在编译时可以使用自定义的 fs 替换 Node 原生 fs,只要 API 兼容。webpack-dev-middle 正是把它替换成
webpack之tapable
webpack
tapable
webpack本质上是一种事件流的机制,它的工作流程就是将各个插件串联起来,而实现这一切的核心就是tapable,核心原理是依赖于发布订阅模式;
tapable注册函数的方法有三种:tap、tapAsync、tapPromise
相对应的执行方法也有三种:call、callAsync、promise
SyncHook
const { SyncLoopHook } = require(‘tapable‘)
class Lesson {
constructor () {
this.index = 0
this.hooks = {
arch: new SyncLoopHook([‘name‘])
}
}
tap () {
let self = this
this.hooks.arch.tap(‘node‘, function (name) {
console.log(‘node‘, name)
return ++self.index >=3 ? undefined :‘23‘
})
this.hooks.arch.tap(‘react‘, function (name) {
console.log(‘react‘, name)
})
}
start () {
this.hooks.arch.call(‘jw‘)
}
}
let l = new Lesson()
l.tap()
l.start()
- SyncBailHook
SyncBailHook同步熔断保险钩子,即return一个非undefined的值,则不再继续执行后面的监听函数
- SyncWaterfallHook
上一个监听函数的返回值会传递给下一个监听函数
- SyncLoopHook
遇到某个不返回undefined的监听函数,就重复执行
AsyncHook
const { AsyncParallelHook } = require(‘tapable‘)
class Lesson {
constructor () {
this.index = 0
this.hooks = {
arch: new AsyncParallelHook([‘name‘])
}
}
tap () {
let self = this
this.hooks.arch.tapAsync(‘node‘, function (name, cb) {
setTimeout(() => {
console.log(‘node‘, name)
cb()
})
})
this.hooks.arch.tapAsync(‘react‘, function (name, cb) {
console.log(‘react‘, name)
cb()
})
}
start () {
this.hooks.arch.callAsync(‘jw‘, function () {
console.log(‘end‘)
})
}
}
let l = new Lesson()
l.tap()
l.start()
结果:
react jw
node jw
end
- AsyncParallelHook
异步 并行
源码:
module.exports = class AsyncParallelHook {
constructor () {
this.tasks = []
}
tapAsync (name, fn) {
this.tasks.push(fn)
}
callAsync (...args) {
const final = args.pop()
let index = 0
const done = () => {
index++
if (index === this.tasks.length) {
final()
}
}
this.tasks.forEach(task => {
task(...args, done)
})
}
}
- AsyncSeriesHook
异步串行
源码:
// callback方式
module.exports = class AsyncSerieslHook {
constructor () {
this.tasks = []
}
tapAsync (name, fn) {
this.tasks.push(fn)
}
callAsync (...args) {
let index = 0
const final = args.pop()
const next = () => {
if (this.tasks.length === index) {
final()
return
}
const firstFn = this.tasks[index++]
firstFn(...args, next)
}
next()
}
}
// promise方式
module.exports = class AsyncSerieslHook {
constructor () {
this.tasks = []
}
tapPromise (name, fn) {
this.tasks.push(fn)
}
promise (...args) {
const [firstFn, ...others] = this.tasks
return others.reduce((n, p) => {
return n.then(_ => p(...args))
}, firstFn(...args))
}
}
调用:
//callback方式调用
const AsyncSeriesHook = require(‘./asyncHook‘)
class Lesson {
constructor () {
this.index = 0
this.hooks = {
arch: new AsyncSeriesHook([‘name‘])
}
}
tap () {
this.hooks.arch.tapAsync(‘node‘, function (name, cb) {
setTimeout(() => {
console.log(‘node‘, name)
cb()
}, 1000)
})
this.hooks.arch.tapAsync(‘react‘, function (name, cb) {
setTimeout(() => {
console.log(‘react‘, name)
cb()
}, 2000)
})
}
start () {
this.hooks.arch.callAsync(‘jw‘, function () {
console.log(‘end‘)
})
}
}
let l = new Lesson()
l.tap()
l.start()
// promise方式调用
const AsyncSeriesHook = require(‘./asyncHook‘)
class Lesson {
constructor () {
this.index = 0
this.hooks = {
arch: new AsyncSeriesHook([‘name‘])
}
}
tap () {
this.hooks.arch.tapPromise(‘node‘, function (name) {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(‘node‘, name)
resolve()
}, 1000)
})
})
this.hooks.arch.tapPromise(‘react‘, function (name) {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(‘react‘, name)
resolve()
}, 2000)
})
})
}
start () {
this.hooks.arch.promise(‘jw‘).then(function () {
console.log(‘end‘)
})
// this.hooks.arch.callAsync(‘jw‘, function () {
// console.log(‘end‘)
// })
}
}
let l = new Lesson()
l.tap()
l.start()
以上是关于webpack执行机制流程是怎么样的的主要内容,如果未能解决你的问题,请参考以下文章