1. webpack 运行流程图
跟着流程图走, 弄懂编译流程不用愁~
2. 研究 webpack 源码的好处
- 理解 webpack 的运行流程
- 理解 webpack 插件
- 学习 webpack 中的技巧和思想
3. webpack 插件
一个简单的插件
class TestPlugin {
apply(compiler) {
console.log("compiler", compiler);
compiler.hooks.compilation.tap("TestPlugin", function() {
console.log("compilation", compilation);
});
}
}
module.exports = TestPlugin;
- 写插件的关键: 了解 compile, compilation 具体是做什么的, 了解这两个函数的参数,方法, 运行机制,才能写好插件
4. webpack 入口
-
webpack 从入口开始, 输入的是什么, 输出的又是什么呢?
- 输入: 配置文件+ 源代码
- 输出: 打包后的文件
-
webpack 的入口是一个 webpack 的函数, 下面我们来看看这个函数具体做了什么, 主要流程如下:
webpack = options => {
// 1. 整合options
// options就是配置, 包括自己的配置加webpack的默认配置, 指导webpack后续的运行
// 2. 实例化compile
const compile = new Compile(options.context);
// 3. 实例化所有的插件, 调用他们的apply方法
// 4. 返回compile 示例
return compile;
};
- 以下模拟 webpack-cli 中的代码, 相当于在命令行输入 webpack
// webpack函数核心是生成compile函数, 在外部执行它
const options = require("./webpack.config.js"); // 所有配置文件
const compile = webpack(options); // 执行webpack函数
compile.run(); // 执行
- webpack 函数第三步的代码示例
// 插件是否被实例化,是可以进行控制的, 见以下代码
// WebpackOptionsApply.js
// options.optimization.removeAvailableModules为true, 才会实例化插件
if (options.optimization.removeAvailableModules) {
const RemoveParentModulesPlugin = require("./optimize/RemoveParentModulesPlugin");
new RemoveParentModulesPlugin().apply(compiler);
}
// 对于某些插件, 是必须要进行实例化的
// 如NodeEnvironmentPlugin, 在实例化compile后, 会立即执行new NodeEnvironmentPlugin(compile).run()
// 以下为本插件的代码:
class NodeEnvironmentPlugin {
apply(compile) {
compiler.inputFileSystem = new CachedInputFileSystem(
new NodeJsInputFileSystem(),
60000
);
const inputFileSystem = compiler.inputFileSystem;
compiler.outputFileSystem = new NodeOutputFileSystem();
compiler.watchFileSystem = new NodeWatchFileSystem(
compiler.inputFileSystem
);
compiler.hooks.beforeRun.tap("NodeEnvironmentPlugin", compiler => {
if (compiler.inputFileSystem === inputFileSystem)
inputFileSystem.purge();
});
}
}
// 本插件赋予了compile对象文件存储的能力, 这是必须插件
对于webpack插件分为两种, 一种是必须的, 一种是优化插件
5. compile
从 webpack 函数中, 我们知道了实例化 compile 对象后, 会执行 compile.run 方法, 以下我们来看看 compile 函数
// 先看compile中的run方式
// compile.js
class compile extends Tapable {
constructor(context) {
// 注册钩子
this.hooks = {
...beforeRun,
run
};
}
run() {
this.hooks.beforeRun.callAsync(this, err => {
this.hooks.run.callAsync(this, err => {
this.compile(onCompiled); // 传入onCompiled回调
});
});
}
}
执行步骤 beforeRun(此钩子在上例NodeEnvironmentPlugin注册) => run => 调用compile函数, 传入onCompiled回调
// 以下为compolie函数示例
class compile extends Tapable {
createCompilation() {
return new Compilation(this); // 实例化Compilation
}
newCompilation(params) {
const compilation = this.createCompilation();
this.hooks.thisCompilation.call(compilation, params); // thisCompilation钩子
this.hooks.compilation.call(compilation, params); // thisCompilation钩子
return compilation;
}
compile(callback) {
this.hooks.beforeCompile.callAsync(params, err => {
this.hooks.compile.call(params);
const compilation = new newCompilation(params);
this.hooks.make.callAsync(compilation, err => {
compilation.finish(err => {
compilation.seal(err => {
this.hooks.afterCompile.callAsync(compilation, err => {
return callback(null, compilation);
});
});
});
});
});
}
}
执行步骤: beforeCompile钩子 => compile钩子 => 实例化compilation(1. 调用thisCompilation => 2. compilation) => make钩子(执行完成后, 模块转化完成)
=> finish => seal封装 => afterCompile钩子(编译结束)
const onCompiled = (err, compilation) => {
if (this.hooks.shouldEmit.call(compilation) === false) {
this.hooks.done.callAsync(status, err => {});
return;
}
this.emitAssets(compilation, err => {
this.hooks.done.callAsync(status, err => {});
return;
});
};
// 步骤: shouldEmit钩子 ? 成功调用emitAssets => done钩子, 完成一次输出 : 编译不成功调用done钩子
- compile 总结
- compile 的调用栈如下: run => compile => onCompiled
- run 函数中触发的钩子:beforeRun,run
- compile 函数中触发的钩子:beforeCompile,compile,thisCompilation,compilation,make,afterCompile
- onCompiled 函数中触发的钩子: should-emit,emit,done
6. compilation
- 对象职责: 构建模块和 chunk, 并利用插件优化构建过程
- 首先来理解 compilation 对象, moduleFactory, module 三者之间的关系吧
compilation 编译模块的入口
要从 compiler 的 make 钩子看起,从上面的 compile 的方法内看到,实例化 compilation 对象后,并没有对它做什么操作,而是直接调用了 make 钩子,在钩子挂载的入口相关的插件中,操作了 compilation,我们来看一下:
class SingleEntryPlugin {
constructor(context, entry, name) {
this.context = context;
this.entry = entry;
this.name = name;
}
apply(compiler) {
compiler.hooks.compilation.tap(
"SingleEntryPlugin",
(compilation, { normalModuleFactory }) => {
compilation.dependencyFactories.set(
SingleEntryDependency,
normalModuleFactory
);
}
);
compiler.hooks.make.tapAsync(
"SingleEntryPlugin",
(compilation, callback) => {
const { entry, name, context } = this;
const dep = SingleEntryPlugin.createDependency(entry, name);
compilation.addEntry(context, dep, name, callback);
}
);
}
static createDependency(entry, name) {
const dep = new SingleEntryDependency(entry);
dep.loc = { name };
return dep;
}
}
这里使用 SingleEntryPlugin 作为例子,配置单入口时会使用此插件,插件往 make 钩子会挂载了回调函数。它不但挂载了 make 钩子,还挂载了 compilation 钩子,这个钩子先于 make 钩子调用,为 compilation 对象的 dependencyFactories 中添加了值,这值是一个 key-value 对,key 是 SingleEntryDependency,值是 normalModuleFactory,normalModuleFactory 就是一种 modulefactory,我们后面在构建模块中用到。
compilation 编译步骤如下:
执行 make 钩子 => 执行 compilation 下的 addEntry 方法 => _addModuleChain (根据依赖, 递归所有模块, 拿到所有的模块链) => compilation.seal 方法, seal 方法下调用各种钩子, 生成 chunk 对象 => 控制权给到 compile, 执行回调 onCompiled => 调用 done 钩子, 一次构建完成
7. 修改代码后再次编译
当我们修改业务代码再次编译时, webpack-cli 会再次调用 compile.run(), 此时不会再次调用 webpack 函数
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!