webpack 设计模式
一切资源皆 Module
Module
(模块)是 webpack 的中的关键实体。Webpack 会从配置的 Entry 开始递归找出所有依赖的模块. 通过Loaders
(模块转换器),用于把模块原内容按照需求转换成新模块内容.
事件驱动架构
webpack 整体是一个事件驱动架构,所有的功能都以 Plugin
(插件) 的方式集成在构建流程中,通过发布订阅事件来触发各个插件执行。webpack 核心使用 tapable 来实现 Plugin
(插件) 的注册和调用,Tapable 是一个事件发布 (tap) 订阅 (call) 库
概念
Graph 模块之间的 Dependency
(依赖关系) 构成的依赖图
Compiler(Tapable
实例)订阅了 webpack 最顶层的生命周期事件
Complilation(Tapable
实例)该对象由 Compiler
创建, 负责构建 Graph,Seal,Render… 是整个工作流程的核心生命周期, 包含 Dep Graph 遍历算法, 优化(optimize),tree shaking…
Compiler 和 Compilation 的区别在于:Compiler 代表了整个 Webpack 从启动到关闭的生命周期,而 Compilation 只是代表了一次新的编译。
Resolver(Tapable
实例)资源路径解析器
ModuleFactory(Tapable
实例)被 Resolver
成功解析的资源需要被这个工厂类被实例化成Module
Parser(Tapable
实例)负责将 Module
(ModuleFactory
实例化来的)转 AST 的解析器 (webpack 默认用 acorn), 并解析出不同规范的 require/import 转成 Dependency(依赖)
Template 模块化的模板. Chunk,Module,Dependency 都有各自的模块模板, 来自各自的工厂类的实例
bundle
和 chunk
区别:https://github.com/webpack/we…
bundle
: 由多个不同的模块打包生成生成最终的 js 文件,一个 js 文件即是 1 个 bundle。
chunk
: Graph 的组成部分。一般有 n 个入口 = n 个 bundle=graph 中有 n 个 chunk。但假设由于 n 个入口有 m 个公共模块会被重复打包,需要分离,最终 =n+ m 个 bundle=graph 中有 n + m 个 chunk
有 3 类 chunk:
- Entry chunk: 包含 runtime code 的,就是开发模式下编译出的有很长的
/******/
的部分(是 bundle) - Initial chunk: 同步加载,不包含 runtime code 的。(可能和 entry chunk 打包成一个 bundle, 也可能分离成多个 bundle)
- Normal chunk:延迟加载 / 异步 的 module
chunk 的依赖图算法
https://medium.com/webpack/th…
整个工作流程
-
Compiler
读取配置,创建Compilation
-
Compiler
创建Graph
的过程:-
Compilation
读取资源入口 -
NMF(normal module factory)
-
Resolver
解析 - 输出 NM
-
-
Parser
解析 AST- js json 用 acorn
- 其他用
Loader
(执行 loader runner)
- 如果有依赖, 重复步骤 2
-
-
Compilation
优化Graph
-
Compilation
渲染Graph
-
根据 Graph 上的各类模块用各自的 Template 渲染
- chunk template
- Dependency template
- …
- 合成 IIFE 的最终资源
-
Tapable
钩子列表
钩子名 | 执行方式 | 要点 |
---|---|---|
SyncHook | 同步串行 | 不关心监听函数的返回值 |
SyncBailHook | 同步串行 | 只要监听函数中有一个函数的返回值不为null , 则跳过剩下所有的逻辑 |
SyncWaterfallHook | 同步串行 | 上一个监听函数的返回值可以传给下一个监听函数 |
SyncLoopHook | 同步循环 | 当监听函数被触发的时候, 如果该监听函数返回 true 时则这个监听函数会反复执行, 如果返回 undefined 则表示退出循环 |
AsyncParallelHook | 异步并发 | 不关心监听函数的返回值 |
AsyncParallelBailHook | 异步并发 | 只要监听函数的返回值不为 null , 就会忽略后面的监听函数执行, 直接跳跃到callAsync 等触发函数绑定的回调函数, 然后执行这个被绑定的回调函数 |
AsyncSeriesHook | 异步串行 | 不关心 callback 的参数 |
AsyncSeriesBailHook | 异步串行 |
callback() 的参数不为 null , 就会直接执行callAsync 等触发函数绑定的回调函数 |
AsyncSeriesWaterfalllHook | 异步串行 | 上一个监听函数中的 callback(err,data) 的第二个参数, 可以作为下一个监听函数的参数 |
示例
// 创建一个发布订阅中心
let Center=new TapableHook()
// 注册监听事件
Center.tap('eventName',callback)
// 触发事件
Center.call(...args)
// 注册拦截器
Center.intercept({
context,// 事件回调和拦截器的共享数据
call:()=>{},// 钩子触发前
register:()=>{},// 添加事件时
tap:()=>{},// 执行钩子前
loop:()=>{},// 循环钩子
})
Module
它有很多子类:RawModule, NormalModule ,MultiModule,ContextModule,DelegatedModule,DllModule,ExternalModule 等
ModuleFactory: 使用工厂模式创建不同的 Module, 有四个主要的子类:NormalModuleFactory,ContextModuleFactory , DllModuleFactory,MultiModuleFactory
Template
-
mainTemplate 和 chunkTemplate
if(chunk.entry) {source = this.mainTemplate.render(this.hash, chunk, this.moduleTemplate, this.dependencyTemplates); } else {source = this.chunkTemplate.render(chunk, this.moduleTemplate, this.dependencyTemplates); }
-
不同模块规范封装
MainTemplate.prototype.requireFn = "__webpack_require__"; MainTemplate.prototype.render = function(hash, chunk, moduleTemplate, dependencyTemplates) {var buf = []; // 每一个 module 都有一个 moduleId, 在最后会替换。buf.push("function" + this.requireFn + "(moduleId) {"); buf.push(this.indent(this.applyPluginsWaterfall("require", "", chunk, hash))); buf.push("}"); buf.push(""); ... // 其余封装操作 };
-
- ModuleTemplate 是对所有模块进行一个代码生成
- HotUpdateChunkTemplate 是对热替换模块的一个处理
webpack_require
function __webpack_require__(moduleId) {
// 1. 首先会检查模块缓存
if(installedModules[moduleId]) {return installedModules[moduleId].exports;
}
// 2. 缓存不存在时,创建并缓存一个新的模块对象,类似 Node 中的 new Module 操作
var module = installedModules[moduleId] = {
i: moduleId,
l: false,
exports: {},
children: []};
// 3. 执行模块,类似于 Node 中的:// result = compiledWrapper.call(this.exports, this.exports, require, this, filename, dirname);
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
// 需要引入模块时,同步地将模块从暂存区取出来执行,避免使用网络请求导致过长的同步等待时间。module.l = true;
// 4. 返回该 module 的输出
return module.exports;
}
异步模块加载
__webpack_require__.e = function requireEnsure(chunkId) {var promises = [];
var installedChunkData = installedChunks[chunkId];
// 判断该 chunk 是否已经被加载,0 表示已加载。installChunk 中的状态:// undefined:chunk 未进行加载,
// null:chunk preloaded/prefetched
// Promise:chunk 正在加载中
// 0:chunk 加载完毕
if(installedChunkData !== 0) {
// chunk 不为 null 和 undefined,则为 Promise,表示加载中,继续等待
if(installedChunkData) {promises.push(installedChunkData[2]);
} else {
// 注意这里 installChunk 的数据格式
// 从左到右三个元素分别为 resolve、reject、promise
var promise = new Promise(function(resolve, reject) {installedChunkData = installedChunks[chunkId] = [resolve, reject];
});
promises.push(installedChunkData[2] = promise);
// 下面代码主要是根据 chunkId 加载对应的 script 脚本
var head = document.getElementsByTagName('head')[0];
var script = document.createElement('script');
var onScriptComplete;
script.charset = 'utf-8';
script.timeout = 120;
if (__webpack_require__.nc) {script.setAttribute("nonce", __webpack_require__.nc);
}
// jsonpScriptSrc 方法会根据传入的 chunkId 返回对应的文件路径
script.src = jsonpScriptSrc(chunkId);
onScriptComplete = function (event) {
script.onerror = script.onload = null;
clearTimeout(timeout);
var chunk = installedChunks[chunkId];
if(chunk !== 0) {if(chunk) {var errorType = event && (event.type === 'load' ? 'missing' : event.type);
var realSrc = event && event.target && event.target.src;
var error = new Error('Loading chunk' + chunkId + 'failed.\n(' + errorType + ':' + realSrc + ')');
error.type = errorType;
error.request = realSrc;
chunk[1](error);
}
installedChunks[chunkId] = undefined;
}
};
var timeout = setTimeout(function(){onScriptComplete({ type: 'timeout', target: script});
}, 120000);
script.onerror = script.onload = onScriptComplete;
head.appendChild(script);
}
}
return Promise.all(promises);
};
异步模块缓存
// webpack runtime chunk
function webpackJsonpCallback(data) {var chunkIds = data[0];
var moreModules = data[1];
var executeModules = data[2];
var moduleId, chunkId, i = 0, resolves = [];
// webpack 会在 installChunks 中存储 chunk 的载入状态,据此判断 chunk 是否加载完毕
for(;i < chunkIds.length; i++) {chunkId = chunkIds[i];
if(installedChunks[chunkId]) {resolves.push(installedChunks[chunkId][0]);
}
installedChunks[chunkId] = 0;
}
// 注意,这里会进行“注册”,将模块暂存入内存中
// 将 module chunk 中第二个数组元素包含的 module 方法注册到 modules 对象里
for(moduleId in moreModules) {if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {modules[moduleId] = moreModules[moduleId];
}
}
if(parentJsonpFunction) parentJsonpFunction(data);
// 先根据模块注册时的 chunkId,取出 installedChunks 对应的所有 loading 中的 chunk,最后将这些 chunk 的 promise 进行 resolve 操作
while(resolves.length) {resolves.shift()();}
deferredModules.push.apply(deferredModules, executeModules || []);
return checkDeferredModules();};
保证 chunk 加载后才执行模块
function checkDeferredModules() {
var result;
for(var i = 0; i < deferredModules.length; i++) {var deferredModule = deferredModules[i];
var fulfilled = true;
// 第一个元素是模块 id,后面是其所需的 chunk
for(var j = 1; j < deferredModule.length; j++) {var depId = deferredModule[j];
// 这里会首先判断模块所需 chunk 是否已经加载完毕
if(installedChunks[depId] !== 0) fulfilled = false;
}
// 只有模块所需的 chunk 都加载完毕,该模块才会被执行(__webpack_require__)if(fulfilled) {deferredModules.splice(i--, 1);
result = __webpack_require__(__webpack_require__.s = deferredModule[0]);
}
}
return result;
}
Module 被 Loader 编译的主要步骤
-
webpack 的配置 options
- 在 Compiler.js 中会为将用户配置与默认配置 (WebpackOptionsDefaulter) 合并,其中就包括了 loader 的默认配置 module.defaultRules (OptionsDefaulter 则是一个封装的配置项存取器,封装了一些特殊的方法来操作配置对象)
//lib/webpack.js options = new WebpackOptionsDefaulter().process(options); compiler = new Compiler(options.context); compiler.options = options; /*options:{entry: {},// 入口配置 output: {}, // 输出配置 plugins: [], // 插件集合(配置文件 + shell 指令) module: {loaders: [ [Object] ] }, // 模块配置 context: // 工程路径 ... }*/
-
创建 Module
- 根据配置创建 Module 的工厂类 Factory(Compiler.js)
- 通过 loader 的 resolver 来解析 loader 路径
- 使用 Factory 创建 NormalModule 实例
- 使用 loaderResolver 解析 loader 模块路径
- 根据 rule.modules 创建 RulesSet 规则集
-
Loader 编译过程 ( 详见 Loader 章节)
- NormalModule 实例.build() 进行模块的构建
- loader-runner 执行编译 module
Compiler
Compiler 源码
compiler.hooks
class Compiler extends Tapable {constructor(context) {super();
this.hooks = {shouldEmit: new SyncBailHook(["compilation"]),// 此时返回 true/false。done: new AsyncSeriesHook(["stats"]),// 编译 (compilation) 完成。additionalPass: new AsyncSeriesHook([]),
beforeRun: new AsyncSeriesHook(["compiler"]),//compiler.run() 执行之前,添加一个钩子。run: new AsyncSeriesHook(["compiler"]),// 开始读取 records 之前,钩入(hook into) compiler。emit: new AsyncSeriesHook(["compilation"]),// 输出到 dist 目录
afterEmit: new AsyncSeriesHook(["compilation"]),// 生成资源到 output 目录之后。thisCompilation: new SyncHook(["compilation", "params"]),// 触发 compilation 事件之前执行(查看下面的 compilation)。compilation: new SyncHook(["compilation", "params"]),// 编译 (compilation) 创建之后,执行插件。normalModuleFactory: new SyncHook(["normalModuleFactory"]),//NormalModuleFactory 创建之后,执行插件。contextModuleFactory: new SyncHook(["contextModulefactory"]),//ContextModuleFactory 创建之后,执行插件。beforeCompile: new AsyncSeriesHook(["params"]),// 编译 (compilation) 参数创建之后,执行插件。compile: new SyncHook(["params"]),// 一个新的编译 (compilation) 创建之后,钩入(hook into) compiler。make: new AsyncParallelHook(["compilation"]),// 从入口分析依赖以及间接依赖模块
afterCompile: new AsyncSeriesHook(["compilation"]),// 完成构建,缓存数据
watchRun: new AsyncSeriesHook(["compiler"]),// 监听模式下,一个新的编译 (compilation) 触发之后,执行一个插件,但是是在实际编译开始之前。failed: new SyncHook(["error"]),// 编译 (compilation) 失败。invalid: new SyncHook(["filename", "changeTime"]),// 监听模式下,编译无效时。watchClose: new SyncHook([]),// 监听模式停止。}
}
}
compiler 其他属性
this.name /** @type {string=} */
this.parentCompilation /** @type {Compilation=} */
this.outputPath = /** @type {string} */
this.outputFileSystem
this.inputFileSystem
this.recordsInputPath /** @type {string|null} */
this.recordsOutputPath /** @type {string|null} */
this.records = {};
this.removedFiles //new Set();
this.fileTimestamps /** @type {Map<string, number>} */
this.contextTimestamps /** @type {Map<string, number>} */
this.resolverFactory /** @type {ResolverFactory} */
this.options = /** @type {WebpackOptions} */
this.context = context;
this.requestShortener
this.running = false;/** @type {boolean} */
this.watchMode = false;/** @type {boolean} */
this._assetEmittingSourceCache /** @private @type {WeakMap<Source, { sizeOnlySource: SizeOnlySource, writtenTo: Map<string, number>}>} */
this._assetEmittingWrittenFiles/** @private @type {Map<string, number>} */
compiler.prototype.run(callback)执行过程
- compiler.hooks.beforeRun
- compiler.hooks.run
-
compiler.compile
-
params=this.newCompilationParams 创建 NormalModuleFactory,contextModuleFactory 实例。
- NMF.hooks.beforeResolve
- NMF.hooks.resolve 解析 loader 模块的路径(例如 css-loader 这个 loader 的模块路径是什么)
- NMF.hooks.factory 基于 resolve 钩子的返回值来创建 NormalModule 实例。
- NMF.hooks.afterResolve
- NMF.hooks.createModule
- compiler.hooks.compile.call(params)
-
compilation = new Compilation(compiler)
- this.hooks.thisCompilation.call(compilation, params)
- this.hooks.compilation.call(compilation, params)
- compiler.hooks.make
compilation.hooks.finish
compilation.hooks.seal
- compiler.hooks.afterCompile
return callback(null, compilation)
-
Compilation
Compilation 源码
Compilation 对象包含了当前的模块资源、编译生成资源、变化的文件等。当 Webpack 以开发模式运行时,每当检测到一个文件变化,一次新的 Compilation 将被创建。Compilation 对象也提供了很多事件回调供插件做扩展。通过 Compilation 也能读取到 Compiler 对象。
承接上文的compilation = new Compilation(compiler)
-
负责组织整个打包过程,包含了每个构建环节及输出环节所对应的方法
- 如 addEntry() , _addModuleChain() , buildModule() , seal() , createChunkAssets() (在每一个节点都会触发 webpack 事件去调用各插件)。
- 该对象内部存放着所有 module,chunk,生成的 asset 以及用来生成最后打包文件的 template 的信息。
compilation.addEntry()主要执行过程
-
comilation._addModuleChain()
- moduleFactory = comilation.dependencyFactories.get(Dep)
-
moduleFactory.create()
- comilation.addModule(module)
-
comilation.buildModule(module)
- afterBuild()
compilation.seal()主要执行过程
- comilation.hooks.optimizeDependencies
- 创建 chunks
- 循环 comilation.chunkGroups.push(entrypoint)
- comilation.processDependenciesBlocksForChunkGroups(comilation.chunkGroups.slice())
- comilation.sortModules(comilation.modules);
- 优化 modules
- comilation.hooks.optimizeModules
- 优化 chunks
- comilation.hooks.optimizeChunks
- 优化 tree
-
comilation.hooks.optimizeTree
- comilation.hooks.optimizeChunkModules
- comilation.sortItemsWithModuleIds
- comilation.sortItemsWithChunkIds
- comilation.createHash
- comilation.createModuleAssets 添加到 compildation.assets[fileName]
- comilation.hooks.additionalChunkAssets
- comilation.summarizeDependencies
-
comilation.hooks.additionalAssets
- comilation.hooks.optimizeChunkAssets
- comilation.hooks.optimizeAssets
- comilation.hooks.afterSeal
Plugin
插件可以用于执行范围更广的任务。包括:打包优化,资源管理,注入环境变量
plugin
: 一个具有 apply 方法的 JavaScript 对象。apply 方法会被 compiler 调用,并且 compiler 对象可在整个编译生命周期访问。这些插件包通常以某种方式扩展编译功能。
编写 Plugin 示例
class MyPlugin{apply(compiler){compiler.hooks.done.tabAsync("myPlugin",(stats,cb)=>{const assetsNames=[]
for(let assetName in stats.compilation.assets)
assetNames.push(assetName)
console.log(assetsNames.join("\n"))
cb()})
compiler.hooks.compilation.tap("MyPlugin",(compilation,params)=>{new MyCompilationPlugin().apply(compilation)
})
}
}
class MyCompilationPlugin{apply(compilation){
compilation.hooks.additionalAssets.tapAsync('MyPlugin', callback => {download('https://img.shields.io/npm/v/webpack.svg', function(resp) {if(resp.status === 200) {compilation.assets['webpack-version.svg'] = toAsset(resp);
callback()}
else
callback(new Error('[webpack-example-plugin] Unable to download the image'))
});
});
}
}
module.exports=MyPlugin
其他声明周期 hooks 和示例 https://webpack.docschina.org…
Resolver
在 NormalModuleFactory.js 的 resolver.resolve
中触发
hooks 在 WebpackOptionsApply.js 的 compiler.resolverFactory.hooks
中。
可以完全被替换,比如注入自己的 fileSystem
Parser
在 CommonJSPulgin.js 的 new CommonJsRequireDependencyParserPlugin(options).appply(parser)
触发,调用 CommonJsRequireDependencyParserPlugin.js 的apply(parser)
,负责添加 Dependency,Template…
hooks 在 CommonJsPlugin.js 的 normarlModuleFactory.hooks.parser
中
Loader
在 make 阶段 build 中会调用 doBuild 去加载资源,doBuild 中会传入资源路径和插件资源去调用 loader-runner 插件的 runLoaders 方法去加载和执行 loader。执行完成后会返回如下图的 result 结果,根据返回数据把源码和 sourceMap 存储在 module 的_source 属性上;doBuild 的回调函数中调用 Parser 类生成 AST,并根据 AST 生成依赖后回调 buildModule 方法返回 compilation 类。
Loader 的路径
NormalModuleFactory 将 loader 分为 preLoader、postLoader 和 loader 三种
对 loader 文件的路径解析分为两种:inline loader 和 config 文件中的 loader。
require 的 inline loader 路径前面的感叹号作用:
!
禁用 preLoaders (代码检查和测试, 不生成 module)!!
禁用所有 Loaders-!
禁用 preLoaders 和 loaders,但不是 postLoaders
前面提到 NormalModuleFactory 中的 resolver 钩子中会先处理 inline loader。
最终 loader 的顺序:post
、inline
、normal
和pre
然而 loader 是从右至左执行的,真实的 loader 执行顺序是倒过来的,因此 inlineLoader 是整体后于 config 中 normal loader 执行的。
路径解析之 inline loader
-
正则解析 loader 和参数
//NormalModuleFactory.js let elements = requestWithoutMatchResource .replace(/^-?!+/, "") .replace(/!!+/g, "!") .split("!");
- 将“解析模块的 loader 数组”与“解析模块本身”一起并行执行,用到了 neo-async 这个库(和 async 库类似,都是为异步编程提供一些工具方法,但是会比 async 库更快。)
-
解析返回结果:
[ // 第一个元素是一个 loader 数组 [ { loader: '/workspace/basic-demo/home/node_modules/html-webpack-plugin/lib/loader.js', options: undefined } ], // 第二个元素是模块本身的一些信息 { resourceResolveData: {context: [Object], path: '/workspace/basic-demo/home/public/index.html', request: undefined, query: '', module: false, file: false, descriptionFilePath: '/workspace/basic-demo/home/package.json', descriptionFileData: [Object], descriptionFileRoot: '/workspace/basic-demo/home', relativePath: './public/index.html', __innerRequest_request: undefined, __innerRequest_relativePath: './public/index.html', __innerRequest: './public/index.html' }, resource: '/workspace/basic-demo/home/public/index.html' } ]
路径解析之 config loader
-
NormalModuleFactory 中有一个
ruleSet
的属性,相当于一个规则过滤器,会将 resourcePath 应用于所有的 module.rules 规则, 它可以根据模块路径名,匹配出模块所需的 loader。webpack 编译会根据用户配置与默认配置,实例化一个 RuleSet, 它包含:- 类静态方法
normalizeRule()
将配置值转换为标准化的 test 对象, 其上还会存储一个 this.references 属性 - 实例方法
exec()
每次创建一个新的 NormalModule 时都会调用 RuleSet 实例的.exec() 方法,只有当通过了各类测试条件,才会将该 loader push 到结果数组中。
- 类静态方法
-
references {map}
key 是 loader 在配置中的类型和位置,例如,ref- 2 表示 loader 配置数组中的第三个。
pitch & normal
同一匹配 (test) 资源有多 loader 的时候:(类似先捕获, 再冒泡)
- 先顺序
loader.pitch()
(源码里是 PitchingLoaders 不妨称为 pitch 阶段) - 再倒序
loader()
(源码里是 NormalLoaders 不妨称为 normal 阶段).
这两个阶段(pitch
和 normal
)就是 loader-runner 中对应的iteratePitchingLoaders()
和iterateNormalLoaders()
两个方法。
如果某个 loader 在 pitch 方法中 return 结果,会跳过剩下的 loader。那么 pitch 的递归就此结束,开始从当前位置从后往前执行 normal
normal loaders 结果示例(apply-loader, pug-loader)
//webpack.config.js
test: /\.pug/,
use: [
'apply-loader',
'pug-loader',
]
先执行 pug-loader, 得到 Module pug-loader/index.js!./src/index.pug
的 js 代码:
var pug = __webpack_require__(/*! pug-runtime/index.js */ "pug-runtime/index.js");
function template(locals) {var pug_html = "", pug_mixins = {}, pug_interp;pug_html = pug_html +"\\u003Cdiv class=\"haha\"\\u003Easd\\u003C\\u002Fdiv\\u003E";return pug_html;};
module.exports = template;
//# sourceURL=webpack:///./src/index.pug?pug-loader
再执行 apply-loader, 得到 Module "./src/index.pug"
的 js 代码:
var req = __webpack_require__(/*! !pug-loader!./src/index.pug */ "pug-loader/index.js!./src/index.pug");
module.exports = (req['default'] || req).apply(req, [])
//# sourceURL=webpack:///./src/index.pug?
此时假设在入口文件 ./src/index.js
引用
var html =__webpack_require__('./index.pug')
console.log(html)
//<div class="haha">asd</div>
这个入口文件 Module 的 js 代码:
module.exports = __webpack_require__(/*! ./src/index.js */"./src/index.js");
//# sourceURL=webpack:///multi_./src/index.js?
build 后可看到控制台输出的 1 个 Chunk,2 个 Module(1 个 fs 忽略),3 个中间 Module 和一些隐藏 Module
Asset Size Chunks Chunk Names
main.js 12.9 KiB main [emitted] main
Entrypoint main = main.js
[0] multi ./src/index.js 28 bytes {main} [built]
[1] fs (ignored) 15 bytes {main} [optional] [built]
[pug-loader/index.js!./src/index.pug] pug-loader!./src/index.pug 288 bytes {main} [built]
[./src/index.js] 51 bytes {main} [built]
[./src/index.pug] 222 bytes {main} [built]
pitching loaders 结果示例(style-loader, css-loader)
pitch: 顺序执行 loader.pitch,例:
//webpack.config.js
test: /\.css/,
use: [
'style-loader',
'css-loader',
]
style-loader(负责添加 <style>
到页面)
得到 Module ./src/a.css
的 js 代码:
// Load styles
var content = __webpack_require__(/*! !css-loader/dist/cjs.js!./a.css */ "css-loader/dist/cjs.js!./src/a.css");
if(typeof content === 'string') content = [[module.i, content, '']];
// Transform styles
var options = {"hmr":true}
options.transform = undefined
options.insertInto = undefined;
// Add styles to the DOM
var update = __webpack_require__(/*! style-loader/lib/addStyles.js */ "style-loader/lib/addStyles.js")(content, options);
module.exports = content.locals;
//# sourceURL=webpack:///./src/a.css?
build 后可看到控制台输出的 1 个 Chunk,1 个最终 Module,3 个中间 Module, 和一些隐藏 Module
Asset Size Chunks Chunk Names
main.js 24.3 KiB main [emitted] main
Entrypoint main = main.js
[0] multi ./src/index.js 28 bytes {main} [built]
[./node_modules/_css-loader@2.1.1@css-loader/dist/cjs.js!./src/a.css] 170 bytes {main} [built]
[./src/a.css] 1.12 KiB {main} [built]
[./src/index.js] 16 bytes {main} [built]
+ 3 hidden modules
其他 loader 解析:bundle loader , style-loader , css-loader , file-loader, url-loader
happypack
Loader 编译过程
loader 的内部处理流程: 流水线机制,即挨个处理每个 loader,前一个 loader 的结果会传递给下一个 loader。
loader 有一些主要的特性:同步 & 异步; pitch&normal; context
runLoaders 方法调用 iteratePitchingLoaders 去递归查找执行有 pich 属性的 loader;若存在多个 pitch 属性的 loader 则依次执行所有带 pitch 属性的 loader,执行完后逆向执行所有带 pitch 属性的 normal 的 normal loader 后返回 result,没有 pitch 属性的 loader 就不会再执行;若 loaders 中没有 pitch 属性的 loader 则逆向执行 loader;执行正常 loader 是在 iterateNormalLoaders 方法完成的,处理完所有 loader 后返回 result;
用 loader 编译 Module 的主要步骤
-
compilation.addEntry()
方法中调用的_addModuleChain()
会执行一系列的模块方法,其中对于未 build 过的模块,最终会调用到NormalModule.doBuild()
方法。 - loader 中的 this 其实是一个叫
loaderContext
的对象 -
doBuild()
run Loaders 后将 js 代码通过 acorn 转为 AST (源码) Parser 中生产 AST 语法树后调用 walkStatements 方法分析语法树,根据 AST 的 node 的 type 来递归查找每一个 node 的类型和执行不同的逻辑,并创建依赖。- loadLoader.js 一个兼容性的模块加载器
-
LoaderRunner.js 核心
runLoaders()
-
iteratePitchingLoaders()
递归执行,并记录 loader 的 pitch 状态;loaderIndex++; 当达到最大的 loader 序号时,处理实际的 module(源码):
// 递归执行每个 loader 的 pitch 函数,并在所有 pitch 执行完后调用 processResource if(loaderContext.loaderIndex >= loaderContext.loaders.length) return processResource(options, loaderContext, callback);
-
processResource()
将目标 module 当做 loaderContext 的一个依赖, 添加该模块为依赖和读取模块内容 -
iterateNormalLoaders()
递归执行 normal,和 pitch 的流程大同小异,需要注意的是顺序是反过来的,从后往前。,loaderIndex–
-
在 pitch 中返回值 除了跳过余下 loader 外,不仅会阻止
.addDependency()
触发(不将该模块资源添加进依赖),而且无法读取模块的文件内容。loader 会将 pitch 返回的值作为“文件内容”来处理,并返回给 webpack。- pitch 与 loader 本身方法的执行顺序
-
runSyncOrAsync()
pitch 与 normal 的实际执行 (源码)往
context
上添加了async
和callback
函数.当我们编写 loader 调用
this.async()
或this.callback()
时,会将 loader 变为一个异步的 loader,并返回一个异步回调,还可以直接返回一个 Promise。只有 isSync 标识为 true 时,才会在 loader function 执行完毕后立即(同步)回调 callback 来继续 loader-runner。
Loader 的 this 对象(LoaderContext)属性清单
version:number 2// 版本
emitWarning(warning: Error)// 发出一个警告
emitError(error: Error)// 发出一个错误
resolve(context: String, request: String, callback: function(err, result: string)),// 像 require 表达式一样解析一个 request
getResolve(),//?
emitFile(name: string, content: Buffer|string, sourceMap: {...}),// 产生一个文件
rootContext:'/home/seasonley/workplace/webpack-demo',// 从 webpack 4 开始,原先的 this.options.context 被改进为 this.rootContext
webpack:true,// 如果是由 webpack 编译的,这个布尔值会被设置为真(loader 最初被设计为可以同时当 Babel transform 用)
sourceMap:false,// 是否生成 source map
_module:[Object:NormalModule],
_compilation:[Object:Compilation],
_compiler:[Object:Compiler],
fs:[Object:CachedInputFileSystem],// 用于访问 compilation 的 inputFileSystem 属性。target:'web',// 编译的目标。从配置选项中传递过来的。示例:"web", "node"
loadModule(request: string, callback: function(err, source, sourceMap, module))],// 解析给定的 request 到一个模块,应用所有配置的 loader,并且在回调函数中传入生成的 source、sourceMap 和 模块实例(通常是 NormalModule 的一个实例)。如果你需要获取其他模块的源代码来生成结果的话,你可以使用这个函数。context: '/home/seasonley/workplace/webpack-demo/src',// 模块所在的目录。可以用作解析其他模块路径的上下文。loaderIndex: 0,// 当前 loader 在 loader 数组中的索引。loaders:Array
[ { path: '/home/seasonley/workplace/webpack-demo/src/myloader.js',
query: '',
options: undefined,
ident: undefined,
normal: [Function],
pitch: undefined,
raw: undefined,
data: null,
pitchExecuted: true,
normalExecuted: true,
request: [Getter/Setter] } ],// 所有 loader 组成的数组。它在 pitch 阶段的时候是可以写入的。resourcePath: '/home/seasonley/workplace/webpack-demo/src/index.js',// 资源文件的路径。resourceQuery: '',// 资源的 query 参数。async(),// 告诉 loader-runner 这个 loader 将会异步地回调。返回 this.callback。callback(err,content,sourceMap,meta),/* 一个可以同步或者异步调用的可以返回多个结果的函数。如果这个函数被调用的话,你应该返回 undefined 从而避免含糊的 loader 结果。this.callback(
err: Error | null,
content: string | Buffer,
sourceMap?: SourceMap,
meta?: any
);
可以将抽象语法树 AST(例如 ESTree)作为第四个参数(meta),如果你想在多个 loader 之间共享通用的 AST,这样做有助于加速编译时间。*/
cacheable(flag),/* 设置是否可缓存标志的函数:cacheable(flag = true: boolean)
默认情况下,loader 的处理结果会被标记为可缓存。调用这个方法然后传入 false,可以关闭 loader 的缓存。一个可缓存的 loader 在输入和相关依赖没有变化时,必须返回相同的结果。这意味着 loader 除了 this.addDependency 里指定的以外,不应该有其它任何外部依赖。*/
addDependency(file),// 加入一个文件作为产生 loader 结果的依赖,使它们的任何变化可以被监听到。例如,html-loader 就使用了这个技巧,当它发现 src 和 src-set 属性时,就会把这些属性上的 url 加入到被解析的 html 文件的依赖中。dependency(file),// addDependency 的简写
addContextDependency(directory),//(directory: string)把文件夹作为 loader 结果的依赖加入。getDependencies(),//
getContextDependencies(),//
clearDependencies(),// 移除 loader 结果的所有依赖。甚至自己和其它 loader 的初始依赖。考虑使用 pitch。resource: [Getter/Setter],//request 中的资源部分,包括 query 参数。示例:"/abc/resource.js?rrr"
request: [Getter],/* 被解析出来的 request 字符串。"/abc/loader1.js?xyz!/abc/node_modules/loader2/index.js!/abc/resource.js?rrr"*/
remainingRequest: [Getter],//
currentRequest: [Getter],//
previousRequest: [Getter],//
query: [Getter],/**
如果这个 loader 配置了 options 对象的话,this.query 就指向这个 option 对象。如果 loader 中没有 options,而是以 query 字符串作为参数调用时,this.query 就是一个以 ? 开头的字符串。使用 loader-utils 中提供的 getOptions 方法 来提取给定 loader 的 option。*/
data: [Getter]// 在 pitch 阶段和正常阶段之间共享的 data 对象。/*
Object.defineProperty(loaderContext, "data", {
enumerable: true,
get: function() {return loaderContext.loaders[loaderContext.loaderIndex].data;
}
});
*/
编写 Loader
function myLoader(resource) {if(/\.js/.test(this.resource))
return resource+';console.log(`wa js`);';
};
module.exports = myLoader
//webpack.config.js
var path = require('path');
module.exports = {
mode: 'production',
entry: ['./src/index.js'],
output: {path: path.resolve(__dirname, './dist'),
filename: '[name].js'
},
module: {
rules: [
{
test: /index\.js$/,
use: 'bundle-loader'
}
]
},
resolveLoader: {modules: ['./src/myloader/'],
}
};
webpack 源码分析方法
inspect-brk 启动的时候自动在第一行自动加上断点
- node –inspect-brk ./node_modules/webpack/bin/webpack.js –config ./webpack.config.js
- chrome 输入 chrome://inspect/
Tree Shaking
webpack 通过静态语法分析,找出了不用的 export,把他们改成 free variable(只是把 exports 关键字删除了,变量的声明并没有删除)
Uglify 通过静态语法分析,找出了不用的变量声明,直接把他们删了。
Watch
webpack-dev-server
当配置了 watch 时 webpack-dev-middleware 将 webpack 原本的 outputFileSystem 替换成了 MemoryFileSystem(memory-fs 插件)实例。
MemoryFileSystem 是个抽象的文件系统库,webpack 将该部分解耦,可进一步设置 redis 或 mongodb 作为文件系统,在多个 webpack 实例中共享资源
监控
当执行 watch 时会实例化一个 Watching 对象,监控和构建打包都是 Watching 实例来控制;在 Watching 构造函数中设置变化延迟通知时间(默认 200),然后调用_go 方法;webpack 首次构建和后续的文件变化重新构建都是_执行_go 方法,在__go 方法中调用 this.compiler.compile 启动编译。webpack 构建完成后会触发 _done 方法,在 _done 方法中调用 this.watch 方法,传入 compilation.fileDependencies 和 compilation.contextDependencies 需要监控的文件夹和目录;在 watch 中调用 this.compiler.watchFileSystem.watch 方法正式开始创建监听。
Watchpack
在 this.compiler.watchFileSystem.watch 中每次会重新创建一个 Watchpack 实例,创建完成后监控 aggregated 事件和触发 this.watcher.watch(files.concat(missing), dirs.concat(missing), startTime)方法,并且关闭旧的 Watchpack 实例;在 watch 中会调用 WatcherManager 为每一个文件所在目录创建的文件夹创建一个 DirectoryWatcher 对象,在 DirectoryWatcher 对象的 watch 构造函数中调用 chokidar 插件进行文件夹监听,并且绑定一堆触发事件并返回 watcher;Watchpack 会给每一个 watcher 注册一个监听 change 事件,每当有文件变化时会触发 change 事件。
在 Watchpack 插件监听的文件变化后设置一个定时器去延迟触发 change 事件,解决多次快速修改时频繁触发问题。
触发
当文件变化时 NodeWatchFileStstem 中的 aggregated 监听事件根据 watcher 获取每一个监听文件的最后修改时间,并把该对象存放在 this.compiler.fileTimestamps 上然后触发 _go 方法去构建。
在 compile 中会把 this.fileTimestamps 赋值给 compilation 对象,在 make 阶段从入口开始,递归构建所有 module,和首次构建不同的是在 compilation.addModule 方法会首先去缓存中根据资源路径取出 module,然后拿 module.buildTimestamp(module 最后修改时间)和 fileTimestamps 中的该文件最后修改时间进行比较,若文件修改时间大于 buildTimestamp 则重新 bulid 该 module,否则递归查找该 module 的的依赖。
在 webpack 构建过程中是文件解析和模块构建比较耗时,所以 webpack 在 build 过程中已经把文件绝对路径和 module 已经缓存起来,在 rebuild 时只会操作变化的 module,这样可以大大提升 webpack 的 rebuild 过程。
模块热更新 (HMR) 机制
https://github.com/lihongxun9…
当完成编译的时候,就通过 websocket 发送给客户端一个消息(一个 hash 和 一个 ok)
向 client 发送一条更新消息 当有文件发生变动的时候,webpack 编译文件,并通过 websocket 向 client 发送一条更新消息
//webpack-dev-server/lib/Server.js
compiler.plugin('done', (stats) => {// 当完成编译的时候,就通过 websocket 发送给客户端一个消息(一个 `hash` 和 一个 `ok`)
this._sendStats(this.sockets, stats.toJson(clientStats));
});
回顾 webpack 整体详细流程
webpack 主要是使用 Compiler 和 Compilation 类来控制 webpack 的整个生命周期,定义执行流程;他们都继承了 tabpable 并且通过 tabpable 来注册了生命周期中的每一个流程需要触发的事件。
webpack 内部实现了一堆 plugin,这些内部 plugin 是 webpack 打包构建过程中的功能实现,订阅感兴趣的事件,在执行流程中调用不同的订阅函数就构成了 webpack 的完整生命周期。
其中:[event-name]
代表 事件名
[— 初始化阶段 —]
- 初始化参数:webpack.config.js / shell+yargs(optimist) 获取配置
options
-
初始化 Compiler 实例 (全局只有一个, 继承自 Tapable, 大多数面向用户的插件,都是首先在 Compiler 上注册的)
-
Compiler
:存放输入输出配置+编译器 Parser 对象 -
Watching()
:监听文件变化
-
- 初始化 complier 上下文,loader 和 file 的输入输出环境
- 初始化础插件
WebpacOptionsApply()
(根据 options) - [entry-option] : 读取配置的 Entrys,为每个 Entry 实例化一个对应的 EntryPlugin,为后面该 Entry 的递归解析工作做准备
- [after-plugins] : 调用完所有内置的和配置的插件的 apply 方法。
- [after-resolvers] : 根据配置初始化完 resolver,resolver 负责在文件系统中寻找指定路径的文件。
- [environment] : 开始应用 Node.js 风格的文件系统到 compiler 对象,以方便后续的文件寻找和读取。
- [after-environment]
[—- 构建 Graph 阶段 1—-]
入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理
- [before-run]
-
[run]启动一次新的编译
- 使用信息 `Compiler.readRecords(cb)` - 触发 `Compiler.compile(onCompiled)` (开始构建 options 中模块) - 创建参数 `Compiler.newCompilationParams()`
- [normal-module-factory] 引入
NormalModule
工厂函数 - [context-module-factory] 引入
ContextModule
工厂函数 - [before-compile]执行一些编译之前需要处理的插件
-
[compile]
- 实例化 `compilation` 对象 - `Compiler.newCompilation(params)` - `Compiler.createCompilation()`
该对象负责组织整个编译过程,包含了每个构建环节对应的方法。对象内部保留了对 `compile` 的引用,供 plugin 使用,并存放所有 modules,chunks,assets(对应 entry),template。根据 test 正则找到导入,并分配唯一 id
- [this-compilation]触发 compilation 事件之前
- [compilation]通知订阅的插件,比如在 compilation.dependencyFactories 中添加依赖工厂类等操作
[—- 构建 Graph 阶段 2—-]
-
[make]是 compilation 初始化完成触发的事件
- 通知在 WebpackOptionsApply 中注册的 EntryOptionPlugin 插件
- EntryOptionPlugin 插件使用 entries 参数创建一个单入口(
SingleEntryDependency
)或者多入口(MultiEntryDependency
)依赖,多个入口时在 make 事件上注册多个相同的监听,并行执行多个入口 - tapAsync 注册了一个
DllEntryPlugin
, 就是将入口模块通过调用compilation.addEntry()
方法将所有的入口模块添加到编译构建队列中,开启编译流程。 - 随后在 addEntry 中调用
_addModuleChain
开始编译。在_addModuleChain
首先会生成模块,最后构建。在_addModuleChain
中根据依赖查找对应的工厂函数,并调用工厂函数的 create 来生成一个空的MultModule
对象,并且把MultModule
对象存入 compilation 的 modules 中后执行MultModule.build
,因为是入口 module,所以在 build 中没处理任何事直接调用了afterBuild
;在afterBuild
中判断是否有依赖,若是叶子结点直接结束,否则调用processModuleDependencies
方法来查找依赖 - 上面讲述的 afterBuild 肯定至少存在一个依赖,
processModuleDependencies
方法就会被调用;processModuleDependencies
根据当前的module.dependencies
对象查找该 module 依赖中所有需要加载的资源和对应的工厂类,并把 module 和需要加载资源的依赖作为参数传给addModuleDependencies
方法;在addModuleDependencies
中异步执行所有的资源依赖,在异步中调用依赖的工厂类的 create 去查找该资源的绝对路径和该资源所依赖所有 loader 的绝对路径,并且创建对应的 module 后返回;然后根据该 module 的资源路径作为 key 判断该资源是否被加载过,若加载过直接把该资源引用指向加载过的 module 返回;否则调用this.buildModule
方法执行module.build
加载资源;build 完成就得到了 loader 处理过后的最终 module 了,然后递归调用afterBuild
,直到所有的模块都加载完成后 make 阶段才结束。 - 在 make 阶段 webpack 会根据模块工厂(normalModuleFactory)的 create 去实例化 module;实例化 moduel 后触发 this.hooks.module 事件,若构建配置中注册了 DllReferencePlugin 插件,DelegatedModuleFactoryPlugin 会监听 this.hooks.module 事件,在该插件里判断该 moduel 的路径是否在 this.options.content 中,若存在则创建代理 module(DelegatedModule)去覆盖默认 module;DelegatedModule 对象的 delegateData 中存放 manifest 中对应的数据(文件路径和 id),所以 DelegatedModule 对象不会执行 bulled,在生成源码时只需要在使用的地方引入对应的 id 即可。
- make 结束后会把所有的编译完成的 module 存放在 compilation 的 modules 数组中,通过单例模式保证同样的模块只有一个实例,modules 中的所有的 module 会构成一个图。
- [before-resolve]准备创建 Module
- [factory]根据配置创建 Module 的工厂类 Factory(Compiler.js) 使用 Factory 创建 NormalModule 实例 根据 rule.modules 创建 RulesSet 规则集
- [resolver]通过 loader 的 resolver 来解析 loader 路径
- [resolve]使用 loaderResolver 解析 loader 模块路径
- [resolve-step]
- [file]
- [directory]
- [resolve-step]
- [result]
- [after-resolve]
- [create-module]
- [module]
- [build-module] NormalModule 实例.build() 进行模块的构建
- [normal-build-loader] acron 对 DSL 进行 AST 分析
- [program] 遇到 require 创建依赖收集; 异步处理依赖的 module,循环处理依赖的依赖
- [statement]
- [succeed-module]
[—- 优化 Graph—-]
-
compilation.seal(cb)
根据之前收集的依赖,决定生成多少文件,每个文件的内容是什么. 对每个 module 和 chunk 整理,生成编译后的源码,合并, 拆分, 生成 hash, 保存在 compilation.assets,compilation.chunk- [seal]密封已经开始。不再接受任何 Module
-
[optimize] 优化编译. 触发 optimizeDependencies 类型的一些事件去 优化依赖(比如 tree shaking 就是在这个地方执行的)
- 根据入口 module 创建 chunk,如果是单入口就只有一个 chunk,多入口就有多个 chunk;
- 根据 chunk 递归分析查找 module 中存在的异步导 module,并以该 module 为节点创建一个 chunk,和入口创建的 chunk 区别在于后面调用模版不一样。
- 所有 chunk 执行完后会触发 optimizeModules 和 optimizeChunks 等优化事件通知感兴趣的插件进行优化处理。
-
createChunkAssets 生产 assets给 chunk 生成 hash 然后调用 createChunkAssets 来根据模版生成源码对象. 所有的 module,chunk 任然保存的是通过一个个 require 聚合起来的代码,需要通过 template 产生最后带有
__webpack__reuqire()
的格式。- createChunkAssets.jpg
- 根据 chunks 生产 sourceMap使用 summarizeDependencies 把所有解析的文件缓存起来,最后调用插件生成 soureMap 和最终的数据
- 把 assets 中的对象生产要输出的代码assets 是一个对象,以最终输出名称为 key 存放的输出对象,每一个输出文件对应着一个输出对象
- [after-optimize-assets]资产已经优化
- [after-compile] 一次 Compilation 执行完成。
[—- 渲染 Graph—-]
- [should-emit] 所有需要输出的文件已经生成好,询问插件哪些文件需要输出,哪些不需要。
Compiler.emitAssets()
-
[emit]
- 按照 output 中的配置项异步将将最终的文件输出到了对应的 path 中
- output:plugin 结束前,在内存中生成一个 compilation 对象文件模块 tree,枝叶节点就是所有的 module(由 import 或者 require 为标志,并配备唯一 moduleId), 主枝干就是所有的 assets,也就是我们最后需要写入到 output.path 文件夹里的文件内容。
-
MainTemplate.render()
和ChunkTemplate.render()
处理入口文件的 module 和 非首屏需异步加载的 module -
MainTemplate.render()
- 处理不同的模块规范 Commonjs,AMD…
- 生成好的 js 保存在 compilation.assets 中
[asset-path]
[after-emit]
[done]
-
if needAdditionalPass
-
needAdditionalPass()
- 回到 compiler.run
-
- else
this.emitRecords(cb)
- 调用户自定义 callback
[failed] 如果在编译和输出流程中遇到异常导致 Webpack 退出时,就会直接跳转到本步骤,插件可以在本事件中获取到具体的错误原因。
参考资料
webpack loader 机制源码解析
【webpack 进阶】你真的掌握了 loader 么?- loader 十问
webpack 源码解析
webpack tapable 原理详解
webpack4 源码分析
随笔分类 – webpack 源码系列
webpack the confusing parts
细说 webpack 之流程篇