随着前端工程化的不断发展,构建工具也在不断完善。Webpack藉由它强大的扩展能力及万物皆模块的概念,逐渐成为前端构建工具中的小王子,随着webpack4的不断迭代,咱们享受着构建效率不断提高带来的快感,配置不断减小的温馨,也许你已经能够熟练使用Webpack进行项目构建,可是在编写本身的Plugin的时候不知道如何下手,也许你想阅读Webpack源码,可是当本身看的时候感到十分复杂而步履维艰。请不用担忧,此篇文章会详细的为你讲解Webpack中的插件机制原理,以及事件流机制原理和工做流程原理,帮助你轻松探索Webpack中的那些未解之谜。javascript
在阅读本文以前,咱们但愿你已经掌握了Webpack的基本配置,可以独立搭建一款基于Webpack的前端自动化构建体系,因此这篇文章不会教你如何配置或者使用Webpack,基本概念咱们就不作介绍了,直面主题,开始讲解Webpack原理。css
首先呢,说下plugin,我以为plugin就是用来扩展webpack的功能的。不过相对于loader,它仿佛更加的“无所不能”且很是的灵活。下面咱们从plugin的配置入手,分析一下plugin的是如何嵌入到webpack中的,先动手作一个很是简易的plugin,在实践中学习。最后从这个小小的例子中,深刻到webpack的事件流,来看看这个驱动的webpack的"核心引擎" => tapable。前端
plugins: [
new WebpackPluginXXXXX({
//...param
}),
]
复制代码
很是的简单易懂,建立一个plugin的对象,而后放到数组中。。。
那么plugin内部是怎样的规则才能嵌入到webpack中执行呢?请看下面。vue
官网文档:教你如何写插件 webpack.js.org/contribute/…java
先写一个最最最简单的plugin,让他run起来。node
class MyPlugin {
apply(compiler) {
compiler.hooks.run.tap("myPlugin", compiler=> {
console.log("我写的插件正在运行!!!");
});
}
}
module.exports = { MyPlugin };
复制代码
而后咱们新建个项目测试下react
注意:个人 node版本 10.16.0 ,webpack版本4.41.2webpack
而后放到配置文件中。git
const { MyPlugin } = require("./plugin/MyPlugin");
module.exports = {
entry: {
app: "./src/index.js"
},
plugins: [new MyPlugin()]
};
复制代码
ok 看下效果。github
compiler
对象,
compiler
对象可在整个编译生命周期访问,经过compiler.hooks来访问各类各样的钩子,好比run方法就是其中的hook之一,官网定义以下图,经过tap方法来注册到该事件中,来监听事件响应。compiler为
tapable
的实例,tap就是
tapable
中注册同步执行的钩子,类型为
AsyncSeriesHook
(下面会有对tapable,以及相应hook类型的详解)。
插件必定是class吗?因而我又尝试下两种其余写法。
const MyPlugin2 = {
apply(compiler) {
compiler.hooks.run.tap("myPlugin", compilation => {
console.log("我写的第二个插件也正在运行!!!");
});
}
};
function MyPlugin3() {}
MyPlugin3.prototype.apply = function(compiler) {
compiler.hooks.run.tap("myPlugin", compilation => {
console.log("我写的第三个插件也正在运行!!!");
});
};
module.exports = { MyPlugin, MyPlugin2, MyPlugin3 };
复制代码
而后在配置文件中添加以下,而后运行。
plugins: [new MyPlugin(), MyPlugin2, new MyPlugin3()]
复制代码
结果发现,3个插件居然都能顺利运行,不过确定是不推荐第二种。
ok,下面咱们来写一个稍微有点实用价值的插件,这个功能是:在打包完成以后插入一段注释。
const { ConcatSource } = require("webpack-sources");
class MyPlugin {
apply(compiler) {
//使用compilation hook 编译(compilation)建立以后,执行插件。
compiler.hooks.compilation.tap("BannerPlugin", compilation => {
//优化全部 chunk 资源(asset)。资源(asset)会被存储在 compilation.assets。
// 每一个 Chunk 都有一个 files 属性,指向这个 chunk 建立的全部文件。
//附加资源(asset)被存储在 compilation.additionalChunkAssets 中。
compilation.hooks.optimizeChunkAssets.tap("BannerPlugin", chunks => {
for (const chunk of chunks) {
for (const file of chunk.files) {
compilation.updateAsset(file, old => {
return new ConcatSource("/*!我在这里加入一行注释*/","\n", old);
});
}
}
});
});
}
}
module.exports = { MyPlugin };
复制代码
而后就能够在打包中看到效果了。
ConcatSource
这个是一个webpack 合并资源的方法 查看更多=> www.npmjs.com/package/web…
在上面的例子中,咱们用了compiler的compilation
钩子和compilation
的optimizeChunkAssets
钩子,咱们看下官方文档。
Compilation
对象包含了当前的模块资源、编译生成资源、变化的文件等。
Compilation
对象也提供了不少事件回调供插件作扩展。每当检测到文件变化,一次新的
Compilation
将被建立。
当我想写一些本身的插件的时候,看文档是必不可少的,这时我对上图中红框的内容产生了疑问,这是什么?要了解这些,就要深刻学习webpack的比较核心的库,tapable,处理webpack复杂交通的指挥枢纽。
通过了很长一段时间的学习,我发现我以前眼中高大上的tapable其实原理并不复杂,其设计模式是前端最经常使用的设计模式之一,观察者模式。有点像nodejs的Events,注册一个事件,而后到了适当的时候触发,下面events,你们回顾下,一个作监听,一个作触发。
const EventEmitter = require('events');
const myEmitter = new EventEmitter();
//on的第一个参数是事件名,以后emit能够经过这个事件名,从而触发这个方法。
//on的第二个参数是回掉函数,也就是此事件的执行方法
myEmitter.on('newListener', (param1,param2) => {
console.log("newListener",param1,param2)
});
//emit的第一个参数是触发的事件名
//emit的第二个之后的参数是回调函数的参数。
myEmitter.emit('newListener',111,222);
复制代码
可是webpack的需求可能不只仅是一个Events能够支撑的,必定有更复杂的需求,那么这个升级版的Events到底提供了哪些功能呢?
咱们找到了npm 库中的tapable,而后发现tapable库暴露了不少Hook(钩子)类,为插件提供挂载的钩子。
const {
SyncHook,
SyncBailHook,
SyncWaterfallHook,
SyncLoopHook,
AsyncParallelHook,
AsyncParallelBailHook,
AsyncSeriesHook,
AsyncSeriesBailHook,
AsyncSeriesWaterfallHook
} = require("tapable");
复制代码
这个是最普通的同步hook,也具备表明性,由于全部的钩子构造函数都采用一个可选参数,即做为字符串数组的参数名列表。 tap为绑定该hook的方法,第一个参数为事件名称,第二参数为事件回调函数。
const hook = new SyncHook(["arg1", "arg2", "arg3"]);
//绑定事件到webapck事件流
hook.tap('plugin1', (arg1, arg2, arg3) => console.log(arg1, arg2, arg3))
hook.tap('plugin2', (arg1, arg2, arg3) => console.log(arg1, arg2, arg3))
//执行绑定的事件
hook.call(1,2,3)
//1,2,3
//1,2,3
复制代码
SyncHook
:顺序执行,最基础也是最经常使用的hook。
SyncBailHook
:该hook容许提早退出,当任何挂载的钩子返回任何函数的时候,则下面的hook都将中止运行。
WaterfallHook
:相似于 reduce,若是前一个 Hook 函数的结果 result !== undefined,则 result 会做为后一个 Hook 函数的第一个参数。
LoopHook
:不停的循环执行 Hook,直到全部函数结果 result === undefined。
下面举个SyncBailHook的使用例子,其余的就不在一一尝试了,有兴趣的同窗能够本身尝试下。
const hook1 = new SyncBailHook(["arg1"]);
hook1.tap("A", arg => {console.log(`A函数 param:${arg}`)});
hook1.tap("B", arg => arg);
hook1.tap("C", arg => {console.log(`C函数 param:${arg}`)});
hook1.tap("D", arg => {console.log(`D函数 param:${arg}`)});
hook1.call("sync");
//A函数 param:sync
复制代码
上面的是同步的,下面咱们来看下异步hook。
AsyncParalle
* 表明异步并行执行钩子
AsyncSeries
* 表明异步串行执行钩子
而上图就是一个异步串行钩子,下面咱们来详细的说明一下它的使用方法
异步hook比同步接口增长了不少功能。
1.从消息发布者方法来看能够获取到全部订阅者执行结束后的回调。
hook.callAsync(name,callback)
复制代码
2.从消息接收方来看增长了几种监听的方式
//同步方式执行
hook.tap(name)
//异步方式执行callback方式
hook.tapAsync(name,callback)
//异步方式执行Promise方式
hook.tapPromise(name)
复制代码
学习了tapable的基本用法思考下面的运行,看看本身对tapable插件的理解~
const testHook = new AsyncSeriesHook(["name"]);
testHook.tap("plugin1", function(name) {
console.log(name + "我是plugin1");
});
testHook.tapAsync("plugin2", function(name, cb) {
console.log(name + "我是plugin2");
setTimeout(() => {
console.log("plugin2的异步回调开始执行");
cb();
}, 2000);
});
testHook.tapPromise("plugin3", function(name, cb) {
console.log(name + "我是plugin3");
return new Promise(resolve => setTimeout(resolve, 1000));
});
testHook.callAsync("hello", () => {
console.log("over");
});
复制代码
答案:
hello我是plugin1
hello我是plugin2
plugin2的异步回调开始执行
hello我是plugin3
over
1.首先AsyncSeriesHook
是一个串行异步函数,支持三种监听事件方法
2.tap为同步方法,那么首先打印出来的应该是 "hello我是plugin1"
3.接来下打印hello我是plugin2,走到第二个hook中,等待两秒触发回调打印plugin2的异步回调开始执行
4.紧接着走到第三个hook "hello我是plugin3",一秒以后Promise resolve方法执行
5.这时全部监听者完成执行显示 "over"
给你们介绍咱们这边的一个小工具carefree,基于webpack插件和服务端Whistle的一套web真机测试解决方案,且套不依赖Wifi热点~
经过对webpack事件流的理解和对tapable用法学习以后,在开发webpack插件的时候可能对各类hook的理解更深刻一点,下面咱们来看下webpack的功能流程解析,看下webpack是怎样实现的
Webpack的启动方式有两种:
- 既能够在
Terminal
终端中直接运行,这种方式最快捷,开箱即用。- 也能够经过
require('webpack')
引入的方式执行,这种方式最灵活,咱们能够控制Webpack启动的时机,也能够经过Webpack暴露出的钩子在它的生命周期中作一些事情。
这里咱们为了方便对源码进行调试和理解,使用了第二种方式来启动Webpack(注意这里咱们使用的webpack版本为5.0.0-beta.9
,因此后面源码也是这个版本),咱们在根目录新建一个启动文件index.js:
const webpack = require(webpack);
const config = require("./webpack.config.js"); // 咱们本身定义的webpack配置
const compiler = webpack(config);
// 因为启动webpack的时候没有传第二个参数callback,因此须要咱们手动执行run开始编译
compiler.run((err, stats) => {
if (err) {
console.error(err);
} else {
console.log(stats);
}
});
复制代码
Webpack执行过程实际上是一个串行
的过程,这里先大概了解下。以下图:
咱们能够看到整个运行流程可简单分为三个大阶段,分别是
初始化
、编译
、输出
,那么这里详细的介绍下这三个阶段会发生什么事件:
一切从const compiler = webpack(config)
开始。
webpack函数源码(lib/webpack.js
):
const webpack = (options, callback) => {
// options参数就是本地配置文件的参数
let compiler;
// 初始化阶段开始
compiler = createCompiler(options);
// 若是传入callback函数,则自启动,不然须要用户手动执行run
if (callback) {
compiler.run((err, stats) => {
compiler.close(err2 => {
callback(err || err2, stats);
});
});
}
return compiler;
};
复制代码
Webpack
最开始运行的时候,会先执行createCompiler
并传入用户自定义配置参数options
,而后会从咱们写的配置文件和Shell中读取与合并参数,得出最终的参数options
。
精简伪代码(lib/webpack.js
):
const createCompiler = options => {
// 初始化参数:将用户本地的配置文件拼接上webpack内置的参数
options = new WebpackOptionsDefaulter().process(options);
...
}
复制代码
用上一步获得的参数初始化Compiler
实例,Compiler
是Webpack的指挥官,负责并贯穿了整个打包生产线。在Compiler
实例中包含了完整的Webpack环境信息,全局只有一个Compiler
实例。
精简伪代码(lib/webpack.js
):
const createCompiler = options => {
...
// 用options参数实例化compiler,负责文件监听和启动编译;
const compiler = new Compiler(options.context);
...
}
复制代码
开始挂载咱们在配置文件中使用的plugins
,这里会判断是否为函数,若是是函数直接调用,反之则会调用对象的apply
方法(这就是为何Webpack官方限制咱们的插件只能用这两种方式调用的缘由)。同时向插件传入compiler
实例的引用,以方便在插件内部经过Compiler
调用Hook
,使插件在任意事件节点执行,还能获取Webpack环境的配置。
精简伪代码(lib/webpack.js
):
const createCompiler = options => {
...
// 挂载咱们本身配置的插件
if (Array.isArray(options.plugins)) {
for (const plugin of options.plugins) {
if (typeof plugin === "function") {
plugin.call(compiler, compiler);
} else {
plugin.apply(compiler);
}
}
}
...
}
复制代码
挂载完用户自定义的插件以后,开始挂载Webpack内置的插件,将内置的插件注册到不一样的Hook
上。
精简伪代码(lib/webpack.js
):
const createCompiler = options => {
...
// 挂载webpack内置插件
compiler.options = new WebpackOptionsApply().process(options, compiler);
...
}
复制代码
WebpackOptionsApply
类的工做就是初始化内置插件,里边会判断不少不一样的状况来加载不一样的插件。
精简伪代码(lib/WebpackOptionsApply.js
):
class WebpackOptionsApply extends OptionsApply {
constructor() {
super();
}
process(options, compiler) {
// 当传入的配置信息知足要求,处理与配置项相关的逻辑
if(options.target) {
new OnePlugin().apply(compiler);
}
if(options.devtool) {
new AnotherPlugin().apply(compiler);
}
new JavascriptModulesPlugin().apply(compiler);
new JsonModulesPlugin().apply(compiler);
...
// 注册entryoption插件
new EntryOptionPlugin().apply(compiler);
// 触发entry-option: 读取entry的配置,找出全部入口文件,而后为每一个入口文件挂上make的Hook
compiler.hooks.entryOption.call(options.context, options.entry);
...
// 触发afterPlugins: 调用完全部的内置和自定义插件的apply方法
compiler.hooks.afterPlugins.call(compiler);
...
// 触发afterResolvers:根据配置初始化resolver:resolver负责在文件系统中寻找指定路径的文件
compiler.hooks.afterResolvers.call(compiler);
return options;
}
}
复制代码
而后这里还会根据entry
配置找出全部的入口文件,若是entry
是数组说明是多入口,会循环遍历每个入口处理,若是是函数,说明是异步加载入口,那么使用异步加载的plugin
处理,DynamicEntryPlugin
其实就比EntryPlugin
多了个使用Promise
异步加载入口文件的操做。
精简伪代码(lib/EntryOptionPlugin.js
):
module.exports = class EntryOptionPlugin {
apply(compiler) {
compiler.hooks.entryOption.tap("EntryOptionPlugin", (context, entry) => {
const applyEntryPlugins = (entry, name) => {
if (typeof entry === "string") {
new EntryPlugin(context, entry, name).apply(compiler);
} else if (Array.isArray(entry)) {
// 若是是多入口会遍历全部入口
for (const item of entry) {
applyEntryPlugins(item, name);
}
}
};
if (typeof entry === "string" || Array.isArray(entry)) {
applyEntryPlugins(entry, "main");
} else if (typeof entry === "object") {
for (const name of Object.keys(entry)) {
applyEntryPlugins(entry[name], name);
}
} else if (typeof entry === "function") {
// 若是是异步加载入口,则使用异步加载处理
new DynamicEntryPlugin(context, entry).apply(compiler);
}
return true;
});
}
};
复制代码
入口文件使用EntryPlugin
进行处理,给每一个入口文件挂上compilation
钩子,而且给入口文件绑定上模块工厂,而后还给每一个入口文件挂上make
钩子,等待编译阶段使用模块工厂将入口文件及其依赖转换为JS模块。
精简伪代码(lib/EntryPlugin.js
):
class EntryPlugin {
constructor(context, entry, name) {
this.context = context;
this.entry = entry;
this.name = name;
}
apply(compiler) {
// 给每一个入口文件注册compilation钩子
compiler.hooks.compilation.tap(
"EntryPlugin",
(compilation, { normalModuleFactory }) => {
compilation.dependencyFactories.set(
EntryDependency,
normalModuleFactory
);
}
);
// 给每一个入口文件注册make钩子
compiler.hooks.make.tapAsync("EntryPlugin", (compilation, callback) => {
const { entry, name, context } = this;
const dep = EntryPlugin.createDependency(entry, name);
compilation.addEntry(context, dep, name, err => {
callback(err);
});
});
}
}
复制代码
process函数执行完,Webpack将全部它关心的Hook消息都注册完成,等待后续编译过程当中挨个触发。
以上就是Webpack的初始化阶段,这个阶段的主要任务是整合配置参数options,初始化
Compiler
对象,挂载全部的Plugin
,注册全部的Hook。另外还有一些小插曲好比引入了Node文件系统fs
插件(主要是为了接下来输出打包文件作准备)、初始化resolver(在文件系统中寻找指定路径的文件)。
执行compile.run
让编译阶段跑起来,run
函数里边会定义一个编译完成以后的回调函数,这个函数的做用就是将编译后的内容生成文件。咱们能够看到首先是判断是否编译成功,未成功则直接触发done
事件结束编译。成功则开始打包文件。而后前后触发了beforeRun
和run
事件,这两个事件会绑定文件读取对象和开启缓存插件CachePlugin
,而后会开始读取入口文件,得到入口文件内容以后就开始执行this.compile()
开始编译了。
精简伪代码(lib/Compiler.js
):
class Compiler {
...
// 整个run的过程
run(callback) {
const onCompiled = (err, compilation) => {
// 编译失败
if (this.hooks.shouldEmit.call(compilation) === false) {
const stats = new Stats(compilation);
stats.startTime = startTime;
stats.endTime = Date.now();
this.hooks.done.callAsync(stats, () => {
this.hooks.afterDone.call(stats);
});
return;
}
process.nextTick(() => {
// 输出文件
this.emitAssets(compilation, () => {
// 触发done事件
this.hooks.done.callAsync(stats, () => {
// 触发咱们手动执行run函数的时候传入的回调
callback(stats);
// 触发afterDone事件
this.hooks.afterDone.call(stats);
});
});
});
};
// 触发beforeRun事件,绑定文件读取对象
this.hooks.beforeRun.callAsync(this, () => {
// 触发run事件,会启动编译缓存插件CachePlugin,提升编译效率
this.hooks.run.callAsync(this, () => {
// 读取入口文件内容(readFile)
this.readRecords(() => {
// 开始编译,并传入编译完成后的回调函数
this.compile(onCompiled);
})
})
})
}
}
复制代码
this.compile()
会先初始化模块工厂ModuleFactory
并存入loader的配置,为解析、转换模块作准备。而后会触发compile
事件告诉插件一次新的编译即将开始。
精简伪代码(lib/Compiler.js
):
class Compiler {
newCompilationParams() {
const params = {
normalModuleFactory: this.createNormalModuleFactory(),
contextModuleFactory: this.createContextModuleFactory()
};
return params;
}
compile(callback) {
// 初始化模块工厂
const params = this.newCompilationParams();
...
}
}
复制代码
等等,咱们好像还缺乏一位很是重要的对象,那就是Compilation
了,前面咱们提到了Compiler
,它是负责整条生产线,就像是一位指挥官,指挥着Webpack的各项工做,那么Compilation
就专一于一个产品的生产,咱们每编译一次,都会从新初始化一个Compilation
,它包含了当前编译的环境信息,接着触发make
钩子,真正开始编译。
精简伪代码(lib/Compiler.js
):
class Compiler {
compile(callback) {
...
// 触发compile事件告诉插件一次新的编译将要启动
this.hooks.compile.call(params);
// 初始化compilation,compilation对象表明了一次单一的版本构建和生成资源过程
const compilation = this.newCompilation(params);
// 触发make事件,开始编译
this.hooks.make.callAsync(compilation, () => {
...
})
}
createCompilation() {
return new Compilation(this);
}
newCompilation(params) {
const compilation = this.createCompilation();
compilation.name = this.name;
compilation.records = this.records;
this.hooks.thisCompilation.call(compilation, params);
this.hooks.compilation.call(compilation, params);
return compilation;
}
}
复制代码
编译阶段此时进入主场,首先会把每个入口文件交给Compilation
,而后执行addEntry
。
精简伪代码(lib/EntryPlugin.js
):
compiler.hooks.make.tapAsync("EntryPlugin", (compilation, callback) => {
const { entry, name, context } = this; // 这里的this是入口文件
const dep = EntryPlugin.createDependency(entry, name);
// 把入口文件交给compilation
compilation.addEntry(context, dep, name, err => {
callback(err);
});
});
复制代码
在addEntry中会执行addModuleChain
处理每一个入口文件。而后会使用模块工厂把入口文件转换成模块实例(NormalModule
),这个过程调用链很是长,为了方便理解,把执行过程进行了精简。
精简伪代码(lib/EntryPlugin.js
):
addEntry(context, entry) {
...
this._addModuleChain(context, entry);
}
_addModuleChain(context, dependency) {
...
const Dep = dependency.constructor;
// 建立入口文件的模块工厂
const moduleFactory = this.dependencyFactories.get(Dep);
// 建立入口模块
moduleFactory.create(
{
contextInfo: {
issuer: "",
compiler: this.compiler.name
},
context: context,
dependencies: [dependency]
},
({ module }) => {
// 建立完毕以后运行loader
this.buildModule(module);
}
);
}
buildModule(module) {
...
// 开始编译,build会执行dubuild来运行各个loader
module.build(
this.options,
this,
this.resolverFactory.get("normal", module.resolveOptions),
this.inputFileSystem
);
}
复制代码
入口模块构建完毕以后,会执行doBuild
,其实doBuild
就是选用合适的loader
去加载resource
。目的是为了将这份resource
转换为JS模块(缘由是webpack只识别JS模块)。最后返回加载后的源文件source
,以便接下来继续处理。
精简伪代码(lib/NormalModule.js
):
const { runLoaders } = require("loader-runner");
doBuild(options, compilation, resolver, fs, callback) {
// runLoader从包'loader-runner'引入的方法
runLoaders({
resource: this.resource, // 这里的resource多是js文件,多是css文件,多是img文件
loaders: this.loaders
}, (err, result) => {
const source = result[0];
const sourceMap = result.length >= 1 ? result[1] : null;
const extraInfo = result.length >= 2 ? result[2] : null;
...
})
}
复制代码
咱们处理完入口文件以后,还有入口文件的依赖以及依赖的依赖没有处理,这个时候就须要使用Parser(acorn)
将入口文件JS代码转换为标准的AST(抽象语法树)
,而后对这个AST
进行分析,当遇到require
或者import
等导入其余模块的语句的时候,便将这个模块加入到module.dependencies
中,同时对新找出的依赖进行递归分析,最终弄清楚全部模块的依赖关系,这样就能生成一颗完整的依赖树(依赖图集moduleGraph
)。
精简伪代码(lib/javascript/JavascriptParser.js
):
parse(code, options) {
// 调用第三方插件'acorn'解析js模块
let ast = acorn.parse(code);
// 省略部分代码
if(this.hooks.program.call(ast, comments) === undefined) {
this.detectStrictMode(ast.body);
this.prewalkStatements(ast.body);
this.blockPrewalkStatements(ast.body);
// 这里webpack会遍历一次ast.body,其中会手机这个模块的全部依赖项,最后写入到'module.dependencies'中
this.walkStatements(ast.body);
}
}
复制代码
至此,全部源码已经编译完成,而且保存在内存中(compilation.modules
),等待打包并输出。
以上就是Webpack的编译阶段,这个阶段的主要任务是初始化模块工厂,初始化
compilation
,而后调用loader进行编译,转换AST,生成依赖图集。另外还有一些小插曲好比绑定文件读取对象,调用了Cache插件(编译缓存,提升编译效率)。
make
事件结束后,开始执行回调compilation.seal()
,开始打包封装模块,这里会执行compilation.createChunkAssets
方法(在执行的时候会优先读取cache中是否已经有了相同hash的资源,若是有,则直接返回内容,不然才会继续执行模块生成的逻辑,并存入cache中)生成须要进行输出的chunk资源。 这里会先调用getRenderManifest
获取输出列表,里边每一项都包含一个须要打包输出的资源及信息。而后会将AST转换回JS代码并使用对应的模板进行拼接,而后把拼接好的内容根据文件名保存在Compilation.assets
中,以备以后进行文件输出。
精简伪代码(lib/Compilation.js
):
createChunkAssets(callback) {
// 获取输出列表,包含每个须要输出的资源信息
let manifest = this.getRenderManifest();
for (const fileManifest of manifest) {
// 将AST转换回JS代码而后根据模板拼接好代码
source = fileManifest.render();
// 将最后的代码内容放到compilation.assets中,准备生成文件
this.emitAsset(file, source, assetInfo);
}
}
复制代码
在seal执行结束后,全部模块打包完毕并保存在内存中(Compilation.assets
),是时候将它们输出为文件了。接下来就是一连串的callback回调,最后咱们到达了compiler.emitAssets
方法体中,而后会先触发emit
事件,根据webpack.config.js
文件的output
配置的path
属性,将文件输出到指定的文件夹。至此,你就能够在dist
中查看到打包后的文件了。
精简伪代码(lib/Compiler.js
):
emitAssets(compilation, callback) {
let outputPath;
this.hooks.emit.callAsync(compilation, () => {
// 找到输出文件路径
outputPath = compilation.getPath(this.outputPath, {});
// 将compilation.assets输出到指定路径
mkdirp(this.outputFileSystem, outputPath, compilation.getAssets());
})
}
复制代码
以上就是Webpack的输出阶段,这个阶段的主要任务是拿到转换后的结果和依赖关系以后,将模块组合成一个个Chunk,而后会根据Chunk类型使用对应的模板生成最终要输出的文件内容,最后将内容输出到硬盘里。
webpack专一于模块化编译,现在众多vue、react项目都是基于它进行打包编译,你能够为你的团队搭建一个针对vue、react技术栈且具备开发、测试、上线等工做流的脚手架,可是从零开始搭建可能须要花费你几天的时间。 如今已经诞生了不少前端脚手架构建工具,这里以Gaea-cli为例,它是基于webpack、Node.js实现的脚手架搭建工具,也包含了开发、测试、打包等完整的前端工做流,具备合理的webpack默认配置,同时暴露出webpack配置文件让用户本身配置额外的插件。经过命令行初始化项目的时候还能选择你喜欢的UI框架,好比NutUI、ElementUI等,也能够经过配置不一样的webpack插件来为你的脚手架提供更多的功能,好比CareFree、图片压缩、PWA、Smock等,还能选择是否须要支持Ts、Vuex、Eslint等配置。 这些构建工具均可以经过简单选择、零配置搭建起来一个vue、react脚手架,这样你能够专一在撰写应用上,而没必要花好几天去纠结配置的问题了,赶快用起来吧,真香!
Webpack
的总体架构是一个插件的形势,经过Tapable
实现事件流,它的整个工做流程都是经过事件拼接起来的,而事件流可使插件在不一样的流程阶段执行,Webpack的事件流机制保证了插件的有序性,使得整个系统扩展性很好。
介于node.js
单线程的壁垒,Webpack
构建慢一直是它的短板(这也是Happypack
之因此能大火的缘由,这里Happypack是利用node.js原生的cluster模块去开辟多进程进行构建,webpack4已经集成)。由于每个模块,都会通过Loader -> js(String) -> AST -> js(String)
的过程,在Webapck里,只有模块!
现在前端项目都使用模块化的思想来开发,Webpack也刚好是针对模块化开发的自动化构建工具,再加上它强大的扩展性使它使用场景很是的普遍,不火也不行啊!