内容简介:对于 webpack 来说每个文件都是一个 module,这篇文章带你来看 webpack 如何从配置中 entry 的定义开始,顺藤摸瓜找到全部的文件,并转化为 module。webpack 入口 entry,entry 参数是单入口字符串、单入口数组、多入口对象还是动态函数,无论是什么都会调用上面是一个粗略的轮廓,接下来我们将细节一一补充进这个轮廓中。首先看编译的总流程控制——编译队列的控制。
对于 webpack 来说每个文件都是一个 module,这篇文章带你来看 webpack 如何从配置中 entry 的定义开始,顺藤摸瓜找到全部的文件,并转化为 module。
总览
webpack 入口 entry,entry 参数是单入口字符串、单入口数组、多入口对象还是动态函数,无论是什么都会调用 compilation.addEntry
方法,这个方法会执行 _addModuleChain
,将入口文件加入需要编译的队列中。然后队列中的文件被一个一个处理,文件中的 import
引入了其他的文件又会通过 addModuleDependencies
加入到编译队列中。最终当这个编译队列中的内容完成被处理完时,就完成了文件到 module 的转化。
上面是一个粗略的轮廓,接下来我们将细节一一补充进这个轮廓中。首先看编译的总流程控制——编译队列的控制。
编译队列控制 —— Semaphore
_addModuleChain 和 addModuleDependencies
函数中都会调用 this.semaphore.acquire
这个函数的具体实现在 lib/util/Semaphore.js
文件中。看一下具体的实现
class Semaphore { constructor(available) { // available 为最大的并发数量 this.available = available; this.waiters = []; this._continue = this._continue.bind(this); } acquire(callback) { if (this.available > 0) { this.available--; callback(); } else { this.waiters.push(callback); } } release() { this.available++; if (this.waiters.length > 0) { process.nextTick(this._continue); } } _continue() { if (this.available > 0) { if (this.waiters.length > 0) { this.available--; const callback = this.waiters.pop(); callback(); } } } } 复制代码
对外暴露的只有两个个方法:
- acquire: 申请处理资源,如果有闲置资源(即并发数量)则立即执行处理,并且闲置的资源减1;否则存入等待队列中。
- release: 释放资源。在 acquire 中会调用 callback 方法,在这里需要使用 release 释放资源,将闲置资源加1。同时会检查是否还有待处理内容,如果有则继续处理
这个 Semaphore 类借鉴了在多线程环境中,对使用资源进行控制的 Semaphore(信号量)的概念。其中并发个数通过 available 来定义,那么默认值是多少呢?在 Compilation.js
中可以找到
this.semaphore = new Semaphore(options.parallelism || 100); 复制代码
默认的并发数是 100,注意这里说的并发只是代码设计中的并发,不要和js的单线程特性搞混了。总的来看编译流程如下图
从入口到 _addModuleChain
webpack 官网配置指南中 entry 可以有下面几种形式:
- string: 字符串,例如
{ entry: './demo.js' } 复制代码
- [string]: string 类型的数组,例如
{ entry: ['./demo1.js', './demo2.js'] } 复制代码
- 对象,例如
{ entry: { app: './demo.js' } } 复制代码
- 函数,动态返回入口,例如
{ entry: () => './demo.js' } // 或者 { entry: () => new Promise((resolve) => resolve('./demo.js')) } 复制代码
这些是哪里处理的呢? webpack 的启动文件 webpack.js 中, 会先对 options 进行处理,有如下一句
compiler.options = new WebpackOptionsApply().process(options, compiler); 复制代码
在 process
的过程中会对 entry
的配置做处理
// WebpackOptionsApply.js 文件中 new EntryOptionPlugin().apply(compiler); compiler.hooks.entryOption.call(options.context, options.entry); 复制代码
先看 EntryOptionsPlugin
做了什么
const SingleEntryPlugin = require("./SingleEntryPlugin"); const MultiEntryPlugin = require("./MultiEntryPlugin"); const DynamicEntryPlugin = require("./DynamicEntryPlugin"); const itemToPlugin = (context, item, name) => { if (Array.isArray(item)) { return new MultiEntryPlugin(context, item, name); } return new SingleEntryPlugin(context, item, name); }; module.exports = class EntryOptionPlugin { apply(compiler) { compiler.hooks.entryOption.tap("EntryOptionPlugin", (context, entry) => { // string 类型则为 new SingleEntryPlugin // array 类型则为 new MultiEntryPlugin if (typeof entry === "string" || Array.isArray(entry)) { itemToPlugin(context, entry, "main").apply(compiler); } else if (typeof entry === "object") { // 对于 object 类型,遍历其中每一项 for (const name of Object.keys(entry)) { itemToPlugin(context, entry[name], name).apply(compiler); } } else if (typeof entry === "function") { // function 类型则为 DynamicEntryPlugin new DynamicEntryPlugin(context, entry).apply(compiler); } return true; }); } }; 复制代码
在 EntryOptionsPlugin
中注册了 entryOption
的事件处理函数,根据 entry
值的不同类型(string/array/object中每一项/functioin)实例化和执行不同的 EntryPlugin
:string 对应 SingleEntryPlugin
; array 对应 MultiEntryPlugin
;function 对应 DynamicEntryPlugin
。而对于 object 类型来说遍历其中的每一个 key,将每一个 key 当做一个入口,并根据类型 string/array 的不同选择 SingleEntryPlugin 或 MultiEntryPlugin。下面我们主要分析:SingleEntryPlugin,MultiEntryPlugin,DynamicEntryPlugin
横向对比一下这三个 Plugin,都做了两件事:
- 注册了 compilation 事件回调(这个事件会在下面 make 事件之前会触发),在 compilation 阶段设置
dependencyFactories
compiler.hooks.compilation.tap('xxEntryPlugin', (compilation, { normalModuleFactory }) => { //... compilation.dependencyFactories.set(...) }) 复制代码
- 注册了 make 事件回调,在 make 阶段的时候调用 addEntry 方法,然后进入
_addModuleChain
进入正式的编译阶段。
compiler.hooks.make.tapAsync('xxEntryPlugin',(compilation, callback) => { // ... compilation.addEntry(...) }) 复制代码
结合 webpack 的打包流程,我们从 Compiler.js 中的 compile 方法开始,看一下 compilation 事件和 make 事件回调起了什么作用
xxxEntryPlugin 在 compilation 事件中回调用来设置 compilation.dependencyFactories
,保证在后面 _addModuleChain
回调阶段可以根据 dependency 获取到对应的 moduleFactory
。
make 事件回调中根据不同的 entry 配置,生成 dependency,然后调用 addEntry
,并将 dependency 传入。
在 _addModuleChain
回调中根据不同 dependency 类型,然后执行 multiModuleFactory.create
或者 normalModuleFacotry.create
。
上面的步骤中不停的提到 dependency,在接下来的文章中将会出现各种 dependency。可见,dependency 是 webpack 中一个很关键的东西,在 webpack/lib/dependencies 文件夹下,你会看到各种各样的 dependency。dependency 和 module 的关系结构如下:
module: { denpendencies: [ dependency: { //... module: // 依赖的 module,也可能为 null } ] } } 复制代码
webpack 中将入口文件也当成入口的依赖来处理,所以上面 xxEntryPlugin 中生成的是 xxEntryDependency。module 中的 dependency 保存了这个 module 对其他文件的依赖信息、自身 export 出去的内容等。后面的文章中,你会看到在生成 chunk 时会依靠 dependency 来得到依赖关系图,生成最终文件时会依赖 dependency 中方法和保存的信息将源文件中的 import
等语句替换成最终输出的可执行的 js 语句。
看完了各个 entryPlugin 的共同点之后,我们纵向深入每个 plugin,对比一下不同之处。
SingleEntryPlugin
SingleEntryPlugin 逻辑很简单:将 SingleEntryDependency 和 normalModuleFactory 关联起来,所以后续的 create 方法会执行 normalModuleFactory.create
方法。
apply(compiler) { compiler.hooks.compilation.tap( "SingleEntryPlugin", (compilation, { normalModuleFactory }) => { // SingleEntryDependency 对应的是 normalModuleFactory compilation.dependencyFactories.set( SingleEntryDependency, normalModuleFactory ); } ); compiler.hooks.make.tapAsync( "SingleEntryPlugin", (compilation, callback) => { const { entry, name, context } = this; const dep = SingleEntryPlugin.createDependency(entry, name); // dep 的 constructor 为 SingleEntryDependency compilation.addEntry(context, dep, name, callback); } ); } static createDependency(entry, name) { const dep = new SingleEntryDependency(entry); dep.loc = name; return dep; } 复制代码
MultiEntryPlugin
与上面 SingleEntryPlugin 相比,
- 在 compilation 中,dependencyFactories 设置了两个对应值
MultiEntryDependency: multiModuleFactory SingleEntryDependency: normalModuleFactory 复制代码
- createDependency: 将 entry 中每一个值作为一个 SingleEntryDependency 处理。
static createDependency(entries, name) { return new MultiEntryDependency( entries.map((e, idx) => { const dep = new SingleEntryDependency(e); // Because entrypoints are not dependencies found in an // existing module, we give it a synthetic id dep.loc = `${name}:${100000 + idx}`; return dep; }), name ); } 复制代码
3.multiModuleFactory.create
在第二步中,由 MultiEntryPlugin.createDependency
生成的 dep,结构如下:
{ dependencies:[] module: MultiModule //... } 复制代码
dependencies 是一个数组,包含多个 SingleEntryDependency。这个 dep 会当做参数传给 multiModuleFactory.create 方法,即下面代码中 data.dependencies[0]
// multiModuleFactory.create create(data, callback) { const dependency = data.dependencies[0]; callback( null, new MultiModule(data.context, dependency.dependencies, dependency.name) ); } 复制代码
create 中生成了 new MultiModule,在 callback 中会执行 MultiModule 中 build 方法,
build(options, compilation, resolver, fs, callback) { this.built = true; // 标记编译已经完成 this.buildMeta = {}; this.buildInfo = {}; return callback(); } 复制代码
这个方法中将编译是否完成的变量值设置为 true,然后直接进入的成功的回调。此时,入口已经完成了编译被转化为一个 module, 并且是一个只有 dependencies 的 module。由于在 createDependency 中每一项都作为一个 SingleEntryDependency 处理,所以 dependencies 中每一项都是一个 SingleEntryDependency。随后进入对这个 module 的依赖处理阶段,我们配置在 entry 中的多个文件就被当做依赖加入到编译链中,被作为 SingleEntryDependency 处理。
总的来看,对于多文件的入口,可以简单理解为 webpack 内部先把入口转化为一个下面的形式:
import './demo1.js' import './demo2.js' 复制代码
然后对其做处理。
DynamicEntryPlugin
动态的 entry 配置中同时支持同步方式和返回值为 Promise 类型的异步方式,所以在处理 addEntry 的时候首先调用 entry 函数,然后根据返回的结果类型的不同,进入 string/array/object 的逻辑。
compiler.hooks.make.tapAsync( "DynamicEntryPlugin", (compilation, callback) => { const addEntry = (entry, name) => { const dep = DynamicEntryPlugin.createDependency(entry, name); return new Promise((resolve, reject) => { compilation.addEntry(this.context, dep, name, err => { if (err) return reject(err); resolve(); }); }); }; Promise.resolve(this.entry()).then(entry => { if (typeof entry === "string" || Array.isArray(entry)) { addEntry(entry, "main").then(() => callback(), callback); } else if (typeof entry === "object") { Promise.all( Object.keys(entry).map(name => { return addEntry(entry[name], name); }) ).then(() => callback(), callback); } }); } ); 复制代码
所以动态入口与其他的差别仅在于多了一层函数的调用。
入口找到了之后,就是将文件转为 module 了。接下来的一篇文章中,将详细介绍转 module 的过程。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- 实战生成对抗网络(二):生成手写数字
- 实战生成对抗网络[2]:生成手写数字
- 020.Python生成器和生成器函数
- faker生成器生成虚拟数据的Python模块
- 利用代码生成工具生成基于ABP框架的代码
- 数据生成工具 ZenData 1.4 发布,内置国家、日期、时间格式,支持文章生成
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
The Practice of Programming
Brian W. Kernighan、Rob Pike / Addison-Wesley / 1999-2-14 / USD 49.99
With the same insight and authority that made their book The Unix Programming Environment a classic, Brian Kernighan and Rob Pike have written The Practice of Programming to help make individual progr......一起来看看 《The Practice of Programming》 这本书的介绍吧!