前言
插件plugin,webpack重要的组成部分。它以事件流的方式让用户可以直接接触到webpack的整个编译过程。plugin在编译的关键地方触发对应的事件,极大的增强了webpack的扩展性。它的出现让webpack从一个面向过程的打包工具,变成了一套完整的打包生态系统。
功能分析
Tapable
既然说到了事件流,那么就得介绍Tapable了,Tapable是webpack里面的一个小型库,它允许你自定义一个事件,并在触发后访问到触发者的上下文。当然他也支持异步触发,多个事件同步,异步触发。本次实现用的是较早的v0.1.9版,具体文档可查看tapable v0.19文档
在webpack内使用,如SingleEntryPlugin中
compiler.plugin("make",function(compilation,callback){
compilation.addEntry(this.context, new SingleEntryDependency({request: this.entry}), this.name, callback);
})
在compiler内部触发。
this.applyPluginsParallel('make',compilation, err => {
/* do something */
})
解析入口文件时,通过EntryOptionPlugin解析entry类型并实例化SingleEntryPlugin, SingleEntryPlugin在调用compilation的addEntry函数开启编译。这种观察者模式的设计,解耦了compiler, compilation,并使它们提供的功能更加纯粹,进而增加扩展性。
流程划分
纵观整个打包过程,可以流程划分为四块。
- 初始化
- 构建
- 封装
- 文件写入
模块划分
接入plugin后,webpack对parse,resolve,build,writeSource等功能的大规模重构。
目前拆分模块为
- Parser模块,负责编译module。
- Resolver模块,负责对文件路径进行解析。
- ModuleFactory模块,负责完成module的实例化。
- Module模块,负责解析出modules依赖,chunk依赖。构建出打包后自身module的源码。
- Template模块,负责提供bundle,chunk模块文件写入的模版。
- Compilation模块,负责文件编译细节,构建并封装出assets对象供Compiler模块进行文件写入。
- Compiler模块,负责实例化compilation,bundle文件的写入。监听modules的变化,并重新编译。
核心类关系图
功能实现
Parser模块
通过exprima将源码解析为AST树,并拆分statements,以及expression直至Identifier基础模块。
- 解析到CallExpression时触发call事件。
- 解析到MemberExpression,Identifier时触发expression事件。
- 提供evaluateExpression函数,订阅Literal,ArrayExpression,CallExpression,ConditionalExpression等颗粒化的事件供evaluateExpression调用。
case 'CallExpression':
//do something
this.applyPluginsBailResult('call ' + calleeName, expression);
//do something
break;
case 'MemberExpression':
//do something
this.applyPluginsBailResult('expression ' + memberName, expression);
//do something
break;
case 'Identifier':
//do something
this.applyPluginsBailResult('expression ' + idenName, expression);
//do something
break;
this.plugin('evaluate Literal', (expr) => {})
this.plugin('evaluate ArrayExpression', (expr) => {})
this.plugin('evaluate CallExpression', (expr) => {})
...
如需要解析require(“a”),require.ensure([“b”],function(){})的时候,注册plugin去订阅”call require”,以及”call require.ensure”,再在回调函数调用evaluateExpression解析expression。
Resolver模块
封装在enhanced-resolve库,提供异步解析文件路径,以及可配置的filestream能力。在webpack用于缓存文件流以及以下三种类型模块的路径解析。
- 普通的module模块
- 带context的module模块
- loader模块
用法如
ResolverFactory.createResolver(Object.assign({
fileSystem: compiler.inputFileSystem,
resolveToContext: true
}, options.resolve));
具体配置可去查看github文档
ModuleFactory模块
子类有NormalModuleFactory,ContextModuleFactory。常用的NormalModuleFactory功能如下
- 实例化module之前,调用Resolver模块解析出module和preloaders的绝对路径。
- 通过正则匹配module文件名,匹配出rules内的loaders,并和preloaders合并。
- 实例化module
这里主要是使用async库的parallel函数并行的解析loaders和module的路径,并整合运行结果。
async.parallel([
(callback) => {
this.requestResolverArray( context, loader, resolver, callback)
},
(callback) => {
resolver.normal.resolve({}, context, req, function (err, result) {
callback(null, result)
});
},
], (err, result) => {
let loaders = result[0];
const resource = result[1];
//do something
})
async模块是一整套异步编程的解决方案。async官方文档
Module模块
- 运行loaders数组内的函数,支持同步,异步loaders,得到编译前源码。
- 源码交由Parser进行解析,分析出modules依赖和blocks切割文件依赖
- 提供替换函数,将源码替换,如require(‘./a’)替换为__webpack_require__(1)
一个编译好的module对象包含modules依赖ModuleDependency和blocks依赖RequireEnsureDependenciesBlock,loaders,源码_source,其数据结构如下:
{
chunks: [],
id: null,
parser:
Tapable {
_plugins:
{ 'evaluate Literal': [Array],
'evaluate ArrayExpression': [Array],
'evaluate CallExpression': [Array],
'call require': [Array],
'call require:commonjs:item': [Array],
'call require.ensure': [Array] },
options: {},
scope: { declarations: [] },
state: { current: [Circular], module: [Circular] },
_currentPluginApply: undefined },
fileDependencies:
[ '/Users/zhujian/Documents/workspace/webpack/simple-webpack/example/a.js' ],
dependencies:
[ ModuleDependency {
request: './module!d',
range: [Array],
class: [Function: ModuleDependency],
type: 'cms require' },
ModuleDependency {
request: './assets/test',
range: [Array],
class: [Function: ModuleDependency],
type: 'cms require' } ],
blocks:
[ RequireEnsureDependenciesBlock {
blocks: [],
dependencies: [Array],
requires: [Array],
chunkName: '',
beforeRange: [Array],
afterRange: [Array] } ],
loaders: [],
request: '/Users/zhujian/Documents/workspace/webpack/simple-webpack/example/a.js',
fileName: 'a.js',
requires: [ [ 0, 7 ], [ 23, 30 ] ],
context: '/Users/zhujian/Documents/workspace/webpack/simple-webpack/example',
built: true,
_source:
RawSource {
_result:
{ source: 'require(\'./module!d\');\nrequire(\'./assets/test\');\nrequire.ensure([\'./e\',\'./b\'], function () {\n console.log(1)\n console.log(1)\n console.log(1)\n console.log(1)\n require(\'./m\');\n require(\'./e\');\n});\n' },
_source: 'require(\'./module!d\');\nrequire(\'./assets/test\');\nrequire.ensure([\'./e\',\'./b\'], function () {\n console.log(1)\n console.log(1)\n console.log(1)\n console.log(1)\n require(\'./m\');\n require(\'./e\');\n});\n'
}
}
Compilation模块
- 通过entry和context,获取到入口module对象,并创建入口chunk。
- 通过module的modules依赖和blocks切割文件构建出含有chunk和modules包含关系的chunk对象。
- 给modules和chunks的排序并生成id,触发一系列optimize相关的事件(如CommonsChunkPlugin就是使用optimize-chunks事件进行开发),最终构建出有文件名和源码映射关系的assets对象
一个典型的含有切割文件的多入口entry的assets对象数据结构如下:
assets:
{ '0.bundle.js':
Chunk {
name: '',
parents: [Array],
modules: [Array],
id: 0,
source: [Object] },
'main.bundle.js':
Chunk {
name: 'main',
parents: [],
modules: [Array],
id: 1,
entry: true,
chunks: [Array],
blocks: true,
source: [Object] },
'multiple.bundle.js':
Chunk {
name: 'multiple',
parents: [],
modules: [Array],
id: 2,
entry: true,
chunks: [Array],
source: [Object] }
}
Compiler模块
- 解析CLI, webpack配置获取options对象,初始化resolver,parser对象。
- 实例化compilation对象,触发make 并行事件调用compilation对象的addEntry开启编译。
- 获取到assets对象,通过触发before-emit事件开启文件写入。通过JsonMainTemplate模版完成主入口bundle文件的写入,JsonpChunkTemplate模版完成chunk切割文件的写入。 使用async.forEach管理异步多文件写入的结果。
- 监听modules的变化,并重新编译。
考虑到多入口entry的可能,make调用的是并行异步事件
this.applyPluginsParallel('make', compilation, err => {
//do something
compilation.seal(err=>{})
//do something
}
代码实现
本人的简易版webpack实现simple-webpack
总结
相信大家都有设计过业务/开源代码,很多情况是越往后写,越难维护。一次次的定制化的需求,将原有的设计改的支离破碎。这个时候可以试试借鉴webpak的思想,充分思考并抽象出稳定的基础模块,划分生命周期,将模块之间的业务逻辑,特殊需求交由插件去解决。
完。