Webpack能够将其理解是一种基于事件流的编程范例,一个插件合集。css
而将这些插件控制在webapck事件流上的运行的就是webpack本身写的基础类Tapable
。html
Tapable暴露出挂载plugin
的方法,使咱们能 将plugin控制在webapack事件流上运行(以下图)。后面咱们将看到核心的对象 Compiler
、Compilation
等都是继承于Tabable
类。(以下图所示)node
tapable库暴露了不少Hook(钩子)类,为插件提供挂载的钩子。webpack
const {
SyncHook,
SyncBailHook,
SyncWaterfallHook,
SyncLoopHook,
AsyncParallelHook,
AsyncParallelBailHook,
AsyncSeriesHook,
AsyncSeriesBailHook,
AsyncSeriesWaterfallHook
} = require("tapable");
复制代码复制代码
const hook1 = new SyncHook(["arg1", "arg2", "arg3"]);
复制代码复制代码
tabpack提供了同步
&异步
绑定钩子的方法,而且他们都有绑定事件
和执行事件
对应的方法。git
Async* | Sync* |
---|---|
绑定:tapAsync/tapPromise/tap | 绑定:tap |
执行:callAsync/promise | 执行:call |
const hook1 = new SyncHook(["arg1", "arg2", "arg3"]);
//绑定事件到webapck事件流
hook1.tap('hook1', (arg1, arg2, arg3) => console.log(arg1, arg2, arg3)) //1,2,3
//执行绑定的事件
hook1.call(1,2,3)
复制代码复制代码
同步钩子
accelerate、break(accelerate接受一个参数)、异步钩子
calculateRoutes绑定和执行方法
tapPromise
能够返回一个promise
对象。//引入tapable
const {
SyncHook,
AsyncParallelHook
} = require('tapable');
//建立类
class Car {
constructor() {
this.hooks = {
accelerate: new SyncHook(["newSpeed"]),
break: new SyncHook(),
calculateRoutes: new AsyncParallelHook(["source", "target", "routesList"])
};
}
}
const myCar = new Car();
//绑定同步钩子
myCar.hooks.break.tap("WarningLampPlugin", () => console.log('WarningLampPlugin'));
//绑定同步钩子 并传参
myCar.hooks.accelerate.tap("LoggerPlugin", newSpeed => console.log(`Accelerating to ${newSpeed}`));
//绑定一个异步Promise钩子
myCar.hooks.calculateRoutes.tapPromise("calculateRoutes tapPromise", (source, target, routesList, callback) => {
// return a promise
return new Promise((resolve,reject)=>{
setTimeout(()=>{
console.log(`tapPromise to ${source}${target}${routesList}`)
resolve();
},1000)
})
});
//执行同步钩子
myCar.hooks.break.call();
myCar.hooks.accelerate.call('hello');
console.time('cost');
//执行异步钩子
myCar.hooks.calculateRoutes.promise('i', 'love', 'tapable').then(() => {
console.timeEnd('cost');
}, err => {
console.error(err);
console.timeEnd('cost');
})
复制代码复制代码
运行结果github
WarningLampPlugin
Accelerating to hello
tapPromise to ilovetapable
cost: 1003.898ms
复制代码复制代码
calculateRoutes也可使用tapAsync
绑定钩子,注意:此时用callback
结束异步回调。web
myCar.hooks.calculateRoutes.tapAsync("calculateRoutes tapAsync", (source, target, routesList, callback) => {
// return a promise
setTimeout(() => {
console.log(`tapAsync to ${source}${target}${routesList}`)
callback();
}, 2000)
});
myCar.hooks.calculateRoutes.callAsync('i', 'like', 'tapable', err => {
console.timeEnd('cost');
if(err) console.log(err)
})
复制代码复制代码
运行结果shell
WarningLampPlugin
Accelerating to hello
tapAsync to iliketapable
cost: 2007.850ms
复制代码复制代码
到这里可能已经学会使用tapable了,可是它如何与webapck/webpack插件关联呢?express
咱们将刚才的代码稍做改动,拆成两个文件:Compiler.js、Myplugin.js编程
Compiler.js
Compiler
const {
SyncHook,
AsyncParallelHook
} = require('tapable');
class Compiler {
constructor(options) {
this.hooks = {
accelerate: new SyncHook(["newSpeed"]),
break: new SyncHook(),
calculateRoutes: new AsyncParallelHook(["source", "target", "routesList"])
};
let plugins = options.plugins;
if (plugins && plugins.length > 0) {
plugins.forEach(plugin => plugin.apply(this));
}
}
run(){
console.time('cost');
this.accelerate('hello')
this.break()
this.calculateRoutes('i', 'like', 'tapable')
}
accelerate(param){
this.hooks.accelerate.call(param);
}
break(){
this.hooks.break.call();
}
calculateRoutes(){
const args = Array.from(arguments)
this.hooks.calculateRoutes.callAsync(...args, err => {
console.timeEnd('cost');
if (err) console.log(err)
});
}
}
module.exports = Compiler
复制代码复制代码
MyPlugin.js
webpack 插件是一个具备
apply
方法的 JavaScript 对象。apply 属性会被 webpack compiler 调用
,而且 compiler 对象可在整个编译生命周期访问。
向 plugins 属性传入 new 实例
。const Compiler = require('./Compiler')
class MyPlugin{
constructor() {
}
apply(conpiler){//接受 compiler参数
conpiler.hooks.break.tap("WarningLampPlugin", () => console.log('WarningLampPlugin'));
conpiler.hooks.accelerate.tap("LoggerPlugin", newSpeed => console.log(`Accelerating to ${newSpeed}`));
conpiler.hooks.calculateRoutes.tapAsync("calculateRoutes tapAsync", (source, target, routesList, callback) => {
setTimeout(() => {
console.log(`tapAsync to ${source}${target}${routesList}`)
callback();
}, 2000)
});
}
}
//这里相似于webpack.config.js的plugins配置
//向 plugins 属性传入 new 实例
const myPlugin = new MyPlugin();
const options = {
plugins: [myPlugin]
}
let compiler = new Compiler(options)
compiler.run()
复制代码复制代码
运行结果
Accelerating to hello
WarningLampPlugin
tapAsync to iliketapable
cost: 2015.866ms
复制代码复制代码
改造后运行正常,仿照Compiler和webpack插件的思路慢慢得理顺插件的逻辑成功。
type | function |
---|---|
Hook | 全部钩子的后缀 |
Waterfall | 同步方法,可是它会传值给下一个函数 |
Bail | 熔断:当函数有任何返回值,就会在当前执行函数中止 |
Loop | 监听函数返回true表示继续循环,返回undefine表示结束循环 |
Sync | 同步方法 |
AsyncSeries | 异步串行钩子 |
AsyncParallel | 异步并行执行钩子 |
咱们能够根据本身的开发需求,选择适合的同步/异步钩子。
经过上面的阅读,咱们知道了如何在webapck事件流上挂载钩子。
假设如今要自定义一个插件更改最后产出资源的内容,咱们应该把事件添加在哪一个钩子上呢?哪个步骤能拿到webpack编译的资源从而去修改?
因此接下来的任务是:了解webpack的流程。
贴一张淘宝团队分享的经典webpack流程图,再慢慢分析~
从配置文件package.json 和 Shell 语句中读取与合并参数,得出最终的参数;
每次在命令行输入 webpack 后,操做系统都会去调用
./node_modules/.bin/webpack
这个 shell 脚本。这个脚本会去调用./node_modules/webpack/bin/webpack.js
并追加输入的参数,如 -p , -w 。
yargs.parse(process.argv.slice(2), (err, argv, output) => {})
复制代码复制代码
(1)构建compiler对象
let compiler = new Webpack(options)
复制代码复制代码
(2)注册NOdeEnvironmentPlugin插件
new NodeEnvironmentPlugin().apply(compiler);
复制代码复制代码
(3)挂在options中的基础插件,调用WebpackOptionsApply
库初始化基础插件。
if (options.plugins && Array.isArray(options.plugins)) {
for (const plugin of options.plugins) {
if (typeof plugin === "function") {
plugin.apply(compiler);
} else {
plugin.apply(compiler);
}
}
}
compiler.hooks.environment.call();
compiler.hooks.afterEnvironment.call();
compiler.options = new WebpackOptionsApply().process(options, compiler);
复制代码复制代码
if (firstOptions.watch || options.watch) {
const watchOptions = firstOptions.watchOptions || firstOptions.watch || options.watch || {};
if (watchOptions.stdin) {
process.stdin.on("end", function(_) {
process.exit(); // eslint-disable-line
});
process.stdin.resume();
}
compiler.watch(watchOptions, compilerCallback);
if (outputOptions.infoVerbosity !== "none") console.log("\nwebpack is watching the files…\n");
} else compiler.run(compilerCallback);
复制代码复制代码
这里分为两种状况:
1)Watching:监听文件变化
2)run:执行编译
(1)在run的过程当中,已经触发了一些钩子:beforeRun->run->beforeCompile->compile->make->seal
(编写插件的时候,就能够将自定义的方挂在对应钩子上,按照编译的顺序被执行)
(2)构建了关键的 Compilation
对象
在run()方法中,执行了this.compile()
this.compile()中建立了compilation
this.hooks.beforeRun.callAsync(this, err => {
...
this.hooks.run.callAsync(this, err => {
...
this.readRecords(err => {
...
this.compile(onCompiled);
});
});
});
...
compile(callback) {
const params = this.newCompilationParams();
this.hooks.beforeCompile.callAsync(params, err => {
...
this.hooks.compile.call(params);
const compilation = this.newCompilation(params);
this.hooks.make.callAsync(compilation, err => {
...
compilation.finish();
compilation.seal(err => {
...
this.hooks.afterCompile.callAsync(compilation, err
...
return callback(null, compilation);
});
});
});
});
}
复制代码复制代码
const compilation = this.newCompilation(params);
复制代码复制代码
Compilation
负责整个编译过程,包含了每一个构建环节所对应的方法。对象内部保留了对compiler的引用。
当 Webpack 以开发模式运行时,每当检测到文件变化,一次新的 Compilation 将被建立。
划重点:Compilation很重要!编译生产资源变换文件都靠它。
compile中触发make
事件并调用addEntry
webpack的make钩子中, tapAsync注册了一个DllEntryPlugin
, 就是将入口模块经过调用compilation。
这一注册在Compiler.compile()方法中被执行。
addEntry方法将全部的入口模块添加到编译构建队列中,开启编译流程。
DllEntryPlugin.js
compiler.hooks.make.tapAsync("DllEntryPlugin", (compilation, callback) => {
compilation.addEntry(
this.context,
new DllEntryDependency(
this.entries.map((e, idx) => {
const dep = new SingleEntryDependency(e);
dep.loc = {
name: this.name,
index: idx
};
return dep;
}),
this.name
),
this.name,
callback
);
});
复制代码复制代码
流程走到这里让我以为很奇怪:刚刚还在Compiler.js中执行compile,怎么一会儿就到了DllEntryPlugin.js?
这就要说道以前WebpackOptionsApply.process()初始化插件的时候
,执行了compiler.hooks.entryOption.call(options.context, options.entry)
;
WebpackOptionsApply.js
class WebpackOptionsApply extends OptionsApply {
process(options, compiler) {
...
compiler.hooks.entryOption.call(options.context, options.entry);
}
}
复制代码复制代码
DllPlugin.js
compiler.hooks.entryOption.tap("DllPlugin", (context, entry) => {
const itemToPlugin = (item, name) => {
if (Array.isArray(item)) {
return new DllEntryPlugin(context, item, name);
}
throw new Error("DllPlugin: supply an Array as entry");
};
if (typeof entry === "object" && !Array.isArray(entry)) {
Object.keys(entry).forEach(name => {
itemToPlugin(entry[name], name).apply(compiler);
});
} else {
itemToPlugin(entry, "main").apply(compiler);
}
return true;
});
复制代码复制代码
其实addEntry方法,存在不少入口,SingleEntryPlugin也注册了compiler.hooks.make.tapAsync钩子。这里主要再强调一下WebpackOptionsApply.process()
流程(233)。
入口有不少,有兴趣能够调试一下前后顺序~
compilation.addEntry
中执行 _addModuleChain()
这个方法主要作了两件事情。一是根据模块的类型获取对应的模块工厂并建立模块,二是构建模块。
经过 *ModuleFactory.create方法建立模块,(有NormalModule , MultiModule , ContextModule , DelegatedModule 等)对模块使用的loader进行加载。调用 acorn 解析经 loader 处理后的源文件生成抽象语法树 AST。遍历 AST,构建该模块所依赖的模块
addEntry(context, entry, name, callback) {
const slot = {
name: name,
request: entry.request,
module: null
};
this._preparedEntrypoints.push(slot);
this._addModuleChain(
context,
entry,
module => {
this.entries.push(module);
},
(err, module) => {
if (err) {
return callback(err);
}
if (module) {
slot.module = module;
} else {
const idx = this._preparedEntrypoints.indexOf(slot);
this._preparedEntrypoints.splice(idx, 1);
}
return callback(null, module);
}
);
}
复制代码复制代码
webpack 会监听 seal事件调用各插件对构建后的结果进行封装,要逐次对每一个 module 和 chunk 进行整理,生成编译后的源码,合并,拆分,生成 hash 。 同时这是咱们在开发时进行代码优化和功能添加的关键环节。
template.getRenderMainfest.render()
复制代码复制代码
经过模板(MainTemplate、ChunkTemplate)把chunk生产_webpack_requie()的格式。
把Assets输出到output的path中。
webpack是一个插件合集,由 tapable 控制各插件在 webpack 事件流上运行。主要依赖的是compilation的编译模块和封装。
webpack 的入口文件其实就实例了Compiler并调用了run方法开启了编译,webpack的主要编译都按照下面的钩子调用顺序执行。
一个 Compilation 对象包含了当前的模块资源、编译生成资源、变化的文件等。
Compilation 对象也提供了不少事件回调供插件作扩展。
Compilation中比较重要的部分是assets 若是咱们要借助webpack帮你生成文件,就要在assets上添加对应的文件信息。
compilation.getStats()能获得生产文件以及chunkhash的一些信息。等等
此次尝试写一个简单的插件,帮助咱们去除webpack打包生成的bundle.js中多余的注释
参照webpack官方教程Writing a Plugin
一个webpack plugin由一下几个步骤组成:
compiler
对象的apply
方法。apply
函数中经过compiler插入指定的事件钩子,在钩子回调中拿到compilation对象在以前说Tapable的时候,写了一个MyPlugin类函数,它已经知足了webpack plugin结构的前两点(一个JavaScript类函数,在函数原型 (prototype)中定义一个注入compiler
)
如今咱们要让Myplugin知足后三点。首先,使用compiler指定的事件钩子。
class MyPlugin{
constructor() {
}
apply(conpiler){
conpiler.hooks.break.tap("WarningLampPlugin", () => console.log('WarningLampPlugin'));
conpiler.hooks.accelerate.tap("LoggerPlugin", newSpeed => console.log(`Accelerating to ${newSpeed}`));
conpiler.hooks.calculateRoutes.tapAsync("calculateRoutes tapAsync", (source, target, routesList, callback) => {
setTimeout(() => {
console.log(`tapAsync to ${source}${target}${routesList}`)
callback();
}, 2000)
});
}
}
复制代码复制代码
对象 | 钩子 |
---|---|
Compiler | run,compile,compilation,make,emit,done |
Compilation | buildModule,normalModuleLoader,succeedModule,finishModules,seal,optimize,after-seal |
Module Factory | beforeResolver,afterResolver,module,parser |
Module | |
Parser | program,statement,call,expression |
Template | hash,bootstrap,localVars,render |
class MyPlugin {
constructor(options) {
this.options = options
this.externalModules = {}
}
apply(compiler) {
var reg = /("([^\\\"]*(\\.)?)*")|('([^\\\']*(\\.)?)*')|(\/{2,}.*?(\r|\n))|(\/\*(\n|.)*?\*\/)|(\/\*\*\*\*\*\*\/)/g compiler.hooks.emit.tap('CodeBeautify', (compilation)=> { Object.keys(compilation.assets).forEach((data)=> { let content = compilation.assets[data].source() // 欲处理的文本 content = content.replace(reg, function (word) { // 去除注释后的文本 return /^\/{2,}/.test(word) || /^\/\*!/.test(word) || /^\/\*{3,}\//.test(word) ? "" : word; }); compilation.assets[data] = { source(){ return content }, size(){ return content.length } } }) }) } } module.exports = MyPlugin 复制代码复制代码
第一步,使用compiler的emit钩子
emit事件是将编译好的代码发射到指定的stream中触发,在这个钩子执行的时候,咱们能从回调函数返回的compilation对象上拿到编译好的stream。
compiler.hooks.emit.tap('xxx',(compilation)=>{})
复制代码复制代码
第二步,访问compilation对象,咱们用绑定提供了编译 compilation 引用的emit钩子函数,每一次编译都会拿到新的 compilation 对象。这些 compilation 对象提供了一些钩子函数,来钩入到构建流程的不少步骤中。
compilation中会返回不少内部对象,不彻底截图以下所示:
其中,咱们须要的是compilation.assets
assetsCompilation {
assets:
{ 'js/index/main.js':
CachedSource {
_source: [Object],
_cachedSource: undefined,
_cachedSize: undefined,
_cachedMaps: {} } },
errors: [],
warnings: [],
children: [],
dependencyFactories:
ArrayMap {
keys:
[ [Object],
[Function: MultiEntryDependency],
[Function: SingleEntryDependency],
[Function: LoaderDependency],
[Object],
[Function: ContextElementDependency],
values:
[ NullFactory {},
[Object],
NullFactory {} ] },
dependencyTemplates:
ArrayMap {
keys:
[ [Object],
[Object],
[Object] ],
values:
[ ConstDependencyTemplate {},
RequireIncludeDependencyTemplate {},
NullDependencyTemplate {},
RequireEnsureDependencyTemplate {},
ModuleDependencyTemplateAsRequireId {},
AMDRequireDependencyTemplate {},
ModuleDependencyTemplateAsRequireId {},
AMDRequireArrayDependencyTemplate {},
ContextDependencyTemplateAsRequireCall {},
AMDRequireDependencyTemplate {},
LocalModuleDependencyTemplate {},
ModuleDependencyTemplateAsId {},
ContextDependencyTemplateAsRequireCall {},
ModuleDependencyTemplateAsId {},
ContextDependencyTemplateAsId {},
RequireResolveHeaderDependencyTemplate {},
RequireHeaderDependencyTemplate {} ] },
fileTimestamps: {},
contextTimestamps: {},
name: undefined,
_currentPluginApply: undefined,
fullHash: 'f4030c2aeb811dd6c345ea11a92f4f57',
hash: 'f4030c2aeb811dd6c345',
fileDependencies: [ '/Users/mac/web/src/js/index/main.js' ],
contextDependencies: [],
missingDependencies: [] }
复制代码复制代码
优化全部 chunk 资源(asset)。资源(asset)会以key-value的形式被存储在
compilation.assets
。
第三步,遍历assets。
1)assets数组对象中的key是资源名,在Myplugin插件中,遍历Object.key()咱们拿到了
main.css
bundle.js
index.html
复制代码复制代码
2)调用Object.source() 方法,获得资源的内容
compilation.assets[data].source()
复制代码复制代码
3)用正则,去除注释
Object.keys(compilation.assets).forEach((data)=> {
let content = compilation.assets[data].source()
content = content.replace(reg, function (word) {
return /^\/{2,}/.test(word) || /^\/\*!/.test(word) || /^\/\*{3,}\//.test(word) ? "" : word;
})
});
复制代码复制代码
第四步,更新compilation.assets[data]对象
compilation.assets[data] = {
source(){
return content
},
size(){
return content.length
}
}
复制代码复制代码
第五步 在webpack中引用插件
webpack.config.js
const path = require('path')
const MyPlugin = require('./plugins/MyPlugin')
module.exports = {
entry:'./src/index.js',
output:{
path:path.resolve('dist'),
filename:'bundle.js'
},
plugins:[
...
new MyPlugin()
]
}
复制代码复制代码
参考资料