1. 首页

Webpack那些你知道或不知道的事儿

随着前端工程化的不断发展,构建工具也在不断完善。Webpack藉由它强大的扩展能力及万物皆模块的概念,逐渐成为前端构建工具中的小王子,随着webpack4的不断迭代,我们享受着构建效率不断提升带来的快感,配置不断减少的舒适,也许你已经可以熟练使用Webpack进行项目构建,但是在编写自己的Plugin的时候不知道如何下手,也许你想阅读Webpack源码,但是当自己看的时候感到十分复杂而寸步难行。请不用担心,此篇文章会详细的为你讲解Webpack中的插件机制原理,以及事件流机制原理工作流程原理,帮助你轻松探索Webpack中的那些未解之谜。

在阅读本文之前,我们希望你已经掌握了Webpack的基本配置,能够独立搭建一款基于Webpack的前端自动化构建体系,所以这篇文章不会教你如何配置或者使用Webpack,基本概念我们就不做介绍了,直面主题,开始讲解Webpack原理。

Plugin

介绍

首先呢,说下plugin,我觉得plugin就是用来扩展webpack的功能的。不过相对于loader,它仿佛更加的“无所不能”且非常的灵活。下面我们从plugin的配置入手,分析一下plugin的是如何嵌入到webpack中的,先动手做一个非常简易的plugin,在实践中学习。最后从这个小小的例子中,深入到webpack的事件流,来看看这个驱动的webpack的”核心引擎” => tapable

先从基础开始看看,怎么配置一个plugin


plugins: [ new WebpackPluginXXXXX({ //...param }), ] 复制代码

非常的简单易懂,创建一个plugin的对象,然后放到数组中。。。
那么plugin内部是怎样的规则才能嵌入到webpack中执行呢?请看下面。

写一个简单plugin

官网文档:教你如何写插件 webpack.js.org/contribute/…

先写一个最最最简单的plugin,让他run起来。


class MyPlugin { apply(compiler) { compiler.hooks.run.tap("myPlugin", compiler=> { console.log("我写的插件正在运行!!!"); }); } } module.exports = { MyPlugin }; 复制代码

然后我们新建个项目测试下

注意:我的 node版本 10.16.0 ,webpack版本4.41.2

然后放到配置文件中。


const { MyPlugin } = require("./plugin/MyPlugin"); module.exports = { entry: { app: "./src/index.js" }, plugins: [new MyPlugin()] }; 复制代码

ok 看下效果。

配置效果

发现我写的已经在运行了~
通过官网学习和实践,我得出这里面最重要的是apply方法,这是与webpack内部运行接轨的方法。从apply中得到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钩子和compilationoptimizeChunkAssets钩子,我们看下官方文档。

官方文档-compilation

在run钩子中Compilation 对象包含了当前的模块资源、编译生成资源、变化的文件等。Compilation 对象也提供了很多事件回调供插件做扩展。每当检测到文件变化,一次新的Compilation 将被创建。

当我想写一些自己的插件的时候,看文档是必不可少的,这时我对上图中红框的内容产生了疑问,这是什么?要了解这些,就要深入学习webpack的比较核心的库,tapable,处理webpack复杂交通的指挥枢纽。

Tapable

经过了很长一段时间的学习,我发现我之前眼中高大上的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"); 复制代码

SyncHook

这个是最普通的同步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

Sync为同步函数,下面我们来分别看看这些函数的功能。

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。

AsyncHook

asyncHook

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热点~

carefree.jd.com/#/

通过对webpack事件流的理解和对tapable用法学习之后,在开发webpack插件的时候可能对各种hook的理解更深入一点,下面我们来看下webpack的功能流程解析,看下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执行过程其实是一个串行的过程,这里先大概了解下。如下图:

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; }; 复制代码

1.初始化参数:

Webpack最开始运行的时候,会先执行createCompiler并传入用户自定义配置参数options,然后会从我们写的配置文件和Shell中读取与合并参数,得出最终的参数options

精简伪代码(lib/webpack.js):


const createCompiler = options => { // 初始化参数:将用户本地的配置文件拼接上webpack内置的参数 options = new WebpackOptionsDefaulter().process(options); ... } 复制代码

2.实例化Compiler:

用上一步得到的参数初始化Compiler实例,CompilerWebpack的指挥官,负责并贯穿了整个打包生产线。在Compiler实例中包含了完整的Webpack环境信息,全局只有一个Compiler实例。

精简伪代码(lib/webpack.js):


const createCompiler = options => { ... // 用options参数实例化compiler,负责文件监听和启动编译; const compiler = new Compiler(options.context); ... } 复制代码

3.挂载用户自定义插件:

开始挂载我们在配置文件中使用的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); } } } ... } 复制代码

4.挂载内置插件和处理入口文件:

挂载完用户自定义的插件之后,开始挂载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(在文件系统中寻找指定路径的文件)。


二、编译阶段:

1.开跑:

执行compile.run让编译阶段跑起来,run函数里边会定义一个编译完成之后的回调函数,这个函数的作用就是将编译后的内容生成文件。我们可以看到首先是判断是否编译成功,未成功则直接触发done事件结束编译。成功则开始打包文件。然后先后触发了beforeRunrun事件,这两个事件会绑定文件读取对象和开启缓存插件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(); ... } } 复制代码
2.初始化compilation:

等等,我们好像还缺少一位非常重要的对象,那就是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; } } 复制代码
3.开始编译:

编译阶段此时进入主场,首先会把每一个入口文件交给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插件(编译缓存,提高编译效率)。


三、输出阶段:

1.重组源码:

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); } } 复制代码
2.输出完成:

在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的工程化应用

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也恰好是针对模块化开发的自动化构建工具,再加上它强大的扩展性使它使用场景非常的广泛,不火也不行啊!

作者:京东设计中心JDC
链接:https://juejin.im/post/5e4c9ddb6fb9a07cd614cc89

看完两件小事

如果你觉得这篇文章对你挺有启发,我想请你帮我两个小忙:

  1. 关注我们的 GitHub 博客,让我们成为长期关系
  2. 把这篇文章分享给你的朋友 / 交流群,让更多的人看到,一起进步,一起成长!
  3. 关注公众号 「画漫画的程序员」,公众号后台回复「资源」 免费领取我精心整理的前端进阶资源教程

JS中文网是中国领先的新一代开发者社区和专业的技术媒体,一个帮助开发者成长的社区,目前已经覆盖和服务了超过 300 万开发者,你每天都可以在这里找到技术世界的头条内容。欢迎热爱技术的你一起加入交流与学习,JS中文网的使命是帮助开发者用代码改变世界

本文著作权归作者所有,如若转载,请注明出处

转载请注明:文章转载自「 Js中文网 · 前端进阶资源教程 」https://www.javascriptc.com

标题:Webpack那些你知道或不知道的事儿

链接:https://www.javascriptc.com/3936.html

« 如何在常见业务场景中使用React Hook
滴滴正式发布开源客户端研发助手 DoKit 3.0,新特性解读»
Flutter 中文教程资源

相关推荐

QR code