桔妹导读:对于 webpack 来说每个文件都是一个 module,这篇文章带你来看 webpack 如何从配置中 entry 的定义开始,顺藤摸瓜找到全部的文件,并转化为 module。

1.

总览


webpack 入口 entry,entry 参数是单入口字符串、单入口数组、多入口对象还是动态函数,无论是什么都会调用 compilation.addEntry 方法,这个方法会执行 _addModuleChain,将入口文件加入需要编译的队列中。然后队列中的文件被一个一个处理,文件中的 import 引入了其他的文件又会通过 addModuleDependencies 加入到编译队列中。最终当这个编译队列中的内容完成被处理完时,就完成了文件到 module 的转化。

上面是一个粗略的轮廓,接下来我们将细节一一补充进这个轮廓中。首先看编译的总流程控制——编译队列的控制。

2.

编译队列控制 —— 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的单线程特性搞混了。总的来看编译流程如下图:

3.

从入口到 _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 MultiEntryPluginif (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 类型则为 DynamicEntryPluginnew 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 对应的是 normalModuleFactorycompilation.dependencyFactories.set(SingleEntryDependency,normalModuleFactory);});compiler.hooks.make.tapAsync("SingleEntryPlugin",(compilation, callback) => {const { entry, name, context } = this;const dep = SingleEntryPlugin.createDependency(entry, name);// dep 的 constructor 为 SingleEntryDependencycompilation.addEntry(context, dep, name, callback);});
}static createDependency(entry, name) {const dep = new SingleEntryDependency(entry);dep.loc = name;return dep;
}

▌MultiEntryPlugin

与上面 SingleEntryPlugin 相比,

  • 1. 在 compilation 中,dependencyFactories 设置了两个对应值

MultiEntryDependency: multiModuleFactory
SingleEntryDependency: normalModuleFactory
  • 2. 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 iddep.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 的过程。

本文作者▬崔 静
滴滴 | 高级软件开发工程师
2015年,因为颜控,掉入前端深坑无法自拔,也是同年加入滴滴,正式踏上前端之路。喜欢技术和挑战,爱潜水,要做一条徜徉大海的前端鱼。
同时,我们的团队正在招聘高级/资深前端开发工程师,欢迎投递简历至:cuijing@didiglobal.com。
推荐阅读
▬更多推荐
▬
滴滴开源 / Open Source
Levin | AoE | Delta | Mpx | Booster | Chameleon | DDMQ | DroidAssist | Rdebug | Doraemonkit | Kemon | Mand Moblie | virtualApk | 获取更多项目技术干货 / Recommended article
重磅!滴滴跨端框架Chameleon 1.0正式发布 | Android 性能优化之 Activity 启动耗时分析 | HDFS 源码解读:HadoopRPC 实现细节的探究| 内容 

webpack系列之五module 生成1相关推荐

  1. Webpack系列-第一篇基础杂记

    系列文章 Webpack系列-第一篇基础杂记 Webpack系列-第二篇插件机制杂记 Webpack系列-第三篇流程杂记 前言 公司的前端项目基本都是用Webpack来做工程化的,而Webpack虽然 ...

  2. 手写webpack系列一:了解认识loader-utils

    Created By JishuBao on 2019-03-29 12:38:22 Recently revised in 2019-04-01 12:38:22   欢迎大家来到技术宝的掘金世界, ...

  3. webpack系列之优化策略

    1.打包速度优化 1.1.速度监控插件-speedMeasureWebpackPlugin speed-measure-webpack-plugin是一款能检测Loader和Plugin耗时的插件,具 ...

  4. 带你深度解锁Webpack系列(优化篇)

    带你深度解锁Webpack系列(优化篇) 本文罗列出了十多种优化方式,大家可以结合自己的项目,选择适当的方式进行优化.这些 Webpack 插件的源码我大多也没有看过,主要是结合 Webpack 官方 ...

  5. 作业——Windows核心编程学习手札系列之五

    作业 --Windows核心编程学习手札系列之五 Windows提供作业内核对象,可以将进程组合在千毫 ,并创建一个"沙框"以便限制进程能够进行的操作.作业可视为进程的容器,win ...

  6. 隐马尔科夫模型(Hidden Markov Models) 系列之五

    隐马尔科夫模型(Hidden Markov Models) 系列之五 介绍(introduction) 生成模式(Generating Patterns) 隐含模式(Hidden Patterns) ...

  7. 数字化转型知识方法系列之五:数字化转型战略

    数字化转型知识方法系列之五:数字化转型战略 人工智能技术与咨询 一.数字化转型是信息时代企业级核心战略   新一轮科技革命和产业变革迅猛发展,企业发展环境日益复杂多变,机遇挑战并存.全球经济从增量发展 ...

  8. webpack打包压缩混淆_细说webpack系列 3. webpack-cli 零配置打包

    大家好!我是萝卜,webpack 4 带来了大量的更新,其中一个就是webpack 4 默认不需要配置文件,下面就带大家体验一下! 初始化项目 首先创建项目,创建一个名为webpack的文件夹,进入文 ...

  9. r语言中mpg数据_R语言数据分析系列之五

    R语言数据分析系列之五 本节来讨论一下R语言的基本图形展示,先来看一张效果图吧. 这是一张用R语言生成的,虚拟的wordcloud云图,详细实现细节请參见我的github项目:https://gith ...

最新文章

  1. PHP微信公众号开发插件,基于ThinkCMF1.5.0开发的微信公众号插件
  2. Centos7中yum安装jdk及配置环境变量
  3. 使用jackson转换类型时报Unrecognized field
  4. c 结构体 不允许使用不完整的类型_.NET Core 基础类型介绍
  5. 一男子安装lua开发环境傻逼操作,惊呆所有人
  6. c++如何获取文件时间_3分钟短文 | PHP 如何优雅地获取文件扩展名?别再explode了
  7. 线段树(Segment Tree)
  8. Servlet线程安全问题
  9. python如何上传文件_python请求文件上传
  10. FIT2CLOUD飞致云正式启用CloudExplorer多云管理平台产品品牌
  11. ideaIU-2019.2.4版安装
  12. 2016 server win 假死_Windows Server下Apache假死堵塞卡死无响应解决方法
  13. 计算机无法识别新u盘,电脑新装win7系统就无法识别u盘怎么办
  14. mac linux 笔记
  15. 张爱玲经典爱情语录大全
  16. 亚利桑那大学在线计算机硕士,亚利桑那大学计算机工程硕士排名第33(2020年TFE Times排名)...
  17. 一些完整的Android开源app项目
  18. 1.去色快捷键,2.怎么复制图层,并且偏移呢
  19. 前端 项目 中英文切换无感刷新
  20. 从gitee中复制文件并且解决issues

热门文章

  1. Python 内置函数功能汇总
  2. ubuntu18.04 (rk3399pro) U盘内容不能识别
  3. 趣图:等等,我还没备份呢
  4. 【蓝桥杯】【嵌入式组别】第十二节:USART串口通讯
  5. 用linux下的C语言编程万年历,C语言 万年历程序
  6. 视频转换为GIF的方法有哪些
  7. 神经网络模型简介及常见的损失函数
  8. 我的两次UFO目击经历
  9. Electron.js指南——macOS Dock
  10. python处理excel文件,python xlsxwriter 一文初掌握