做为一个敲码5分钟,调试两小时的bug大叔,天天和console.log打的交道天然很多,人到中年,愈来愈懒,因而想把console.log('bug: ', bug)变成log.bug来让个人懒癌病发得更加完全。因而硬着头皮看了下webpack插件的写法,在此记录一下webpack系统的学习笔记。webpack
去吧!皮卡丘!!!git
// webpack.config.js
const webpack = require('webpack');
const WebpackPlugin = require('webpack-plugin');
const options = {
entry: 'index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'index.bundle.js'
},
module: {
rules: []
},
plugins: [
new WebpackPlugin()
]
// ...
};
webpack(options);
复制代码
// webpack.js
// 引入Compiler类
const Compiler = require("./Compiler");
// webpack入口函数
const webpack = (options, callback) => {
// 建立一个Compiler实例
compiler = new Compiler(options.context);
// 实例保存webpack的配置对象
compiler.options = options
// 依次调用webpack插件的apply方法,并传入compiler引用
for (const plugin of options.plugins) {
plugin.apply(compiler);
}
//执行实例的run方法
compiler.run(callback)
//返回实例
return compiler
}
module.exports = webpack
复制代码
compiler
实例。compiler
是什么?—— 明显发现compiler保存了完整的webpack的配置参数options。因此官方说:compiler 对象表明了完整的 webpack 环境配置。这个对象在启动 webpack 时被一次性创建,并配置好全部可操做的设置,包括 options,loader 和 plugin。可使用 compiler 来访问 webpack 的主环境。github
webpack-plugin
也在这里经过提供一个叫 apply
的方法给webpack调用,以完成初始化的工做,而且接收到刚建立的 compiler
的引用。// Compiler.js
const {
Tapable,
SyncHook,
SyncBailHook,
AsyncParallelHook,
AsyncSeriesHook
} = require("tapable");
const Compilation = require("./Compilation");
class Compiler extends Tapable {
hooks = [
hook1: new SyncHook(),
hook2: new AsyncSeriesHook(),
hook3: new AsyncParallelHook()
]
run () {
// ...
// 触发钩子回调执行
this.hooks[hook1].call()
this.hooks[hook2].callAsync(() => {
this.hooks[hook4].call()
})
// 进入编译流程
this.compile()
// ...
this.hooks[hook3].promise(() => {})
}
compile () {
// 建立一个Compilation实例
const compilation = new Compilation(this)
}
}
复制代码
研究下Compiler.js这个文件,它引入了一个 tapable
类(源码)的库,这个库提供了一些 Hook
类,内部实现了相似的 事件订阅/发布系统
。web
哪里用到 Hook
类?—— 在 Compiler
类(Compiler.js源码) 里拥有不少有意思的hook,这些hook表明了整个编译过程的各个关键事件节点,它们都是继承于 Hook
类 ,因此都支持 监听/订阅
,只要咱们的插件提早作好事件订阅,那么编译流程进行到该事件点时,就会执行咱们提供的 事件回调
,作咱们想作的事情了。express
如何进行订阅呢?——在上文中每一个webpack-plugin中都有一个apply方法。其实注册的代码就藏在里面,经过如下相似的代码实现。任何的webpack插件均可以订阅hook1,所以hook1维护了一个taps数组,保存着全部的callback。npm
compiler.hooks.hook1.tap(name, callback) // 注册/订阅api
compiler.hooks.hook1.call() // 触发/发布数组
准备工做作好了以后,当 run()
方法开始被调用时,编译就正式开始了。在该方法里,执行 call/callAsync/promise
这些事件时(他们由webpack内部包括一些官方使用的webpack插件进行触发的管理,无需开发者操心),相应的hook就会把本身的taps里的函数均执行一遍。大概的逻辑以下所示。 promise
其中,hooks的执行是按照编译的流程顺序来的,hooks之间有彼此依赖的关系。bash
compilation
实例也在这里建立了,它表明了一次资源版本构建。每当检测到文件变化,就会建立一个新的 compilation,从而生成一组新的编译资源。一个 compilation 对象表现了当前的模块资源、编译生成资源、变化的文件、以及被跟踪依赖的状态信息。并且compilation也和compiler相似,拥有不少hooks(Compilation.js源码),所以一样提供了不少事件节点给予咱们订阅使用。
class MyPlugin {
apply(compiler) {
// 设置回调来访问 compilation 对象:
compiler.hooks.compilation.tap('myPlugin', (compilation) => {
// 如今,设置回调来访问 compilation 中的任务点:
compilation.hooks.optimize.tap('myPlugin', () => {
console.log('Hello compilation!');
});
});
}
}
module.exports = MyPlugin;
复制代码
compiler
和 complation
的事件节点都在webpack-plugin中的 apply
方法里书写,具体的演示如上。固然你想拿到编译过程当中的什么资源,首先得要找出能提供该资源引用的对应的compiler事件节点进行订阅(上帝先创造了亚当,再有夏娃)。每一个compiler时间节点能提供什么参数,在hook的实例化时已经作了说明(以下),更多可查看源码this.hooks = {
// 拿到compilation和params
compilation: new SyncHook(["compilation", "params"]),
// 拿到stats
done: new AsyncSeriesHook(["stats"]),
// 拿到compiler
beforeRun: new AsyncSeriesHook(["compiler"])
}
复制代码
通过以上的初步探索,写webpack插件须要了解的几个知识点应该有了大概的掌握:
apply
方法供 webpack
调用进行初始化tap
注册方式钩入 compiler
和 compilation
的编译流程webpack
提供的 api
进行资源的个性化处理。写插件的套路已经知道了,如今还剩如何找出合适的钩子,修改资源这件事。在webpack系统里,钩子即流程,是编译构建工做的生命周期
。固然,想要了解全部 tapable
实例对象的钩子的具体做用,须要探索webpack全部的内部插件如何使用这些钩子,作了什么工做来进行总结,想一想就复杂,因此只能抽取重要流程作思路归纳,借用淘宝的一张经典图示。 整个编译流程大概分红三个阶段,如今从新整理一下:
准备阶段
,webpack的初始化apply
方法,让插件们作好事件注册。WebpackOptionsApply
接收组合了 命令行
和 webpack.config.js
的配置参数,负责webpack内部基础流程使用的插件和compiler上下文环境的初始化工做。(*Plugin
均为webpack内部使用的插件)// webpack.js
// 开发者自定义的插件初始化
if (options.plugins && Array.isArray(options.plugins)) {
for (const plugin of options.plugins) {
if (typeof plugin === "function") {
plugin.call(compiler, compiler)
} else {
plugin.apply(compiler)
}
}
}
// ...
// webpack内部使用的插件初始化
compiler.options = new WebpackOptionsApply().process(options, compiler)
// WebpackOptionsApply.js
class WebpackOptionsApply extends OptionsApply {
process (options, compiler) {
// ...
new WebAssemblyModulesPlugin({
mangleImports: options.optimization.mangleWasmImports
}).apply(compiler);
new EntryOptionPlugin().apply(compiler);
compiler.hooks.entryOption.call(options.context, options.entry);
new CompatibilityPlugin().apply(compiler);
new HarmonyModulesPlugin(options.module).apply(compiler);
new AMDPlugin(options.module, options.amd || {}).apply(compiler);
// ...
}
}
复制代码
run / watch
(一次打包/监听打包模式)触发 编译
阶段编译阶段
,生成module
,chunk
资源。
run
→compile
编译 → 建立compilation
对象。compilation
的建立是一次编译的起步,即将对全部模块加载(load)
、封存(seal)
、优化(optimiz)
、分块(chunk)
、哈希(hash)
和从新建立(restore)
。
module.exports = class Compiler extends Tapable {
run () {
// 声明编译结束回调
function onCompiled () {}
// 触发run钩子
this.hooks.run.callAsync(this, err => {
this.compile(onCompiled)
})
}
compile(callback) {
// ...
// 编译开始前,触发beforeCompile钩子
this.hooks.beforeCompile.callAsync(params, err => {
// 编译开始,触发compile钩子
this.hooks.compile.call(params);
// 建立compilation实例
const compilation = this.newCompilation(params);
// 触发make钩子
this.hooks.make.callAsync(compilation, err => {
// 模块解析完毕,执行compilation的finish方法
compilation.finish();
// 资源封存,执行seal方法
compilation.seal(err => {
// 编译结束,执行afterCompile钩子
this.hooks.afterCompile.callAsync(compilation, err => {
// ...
});
});
});
});
}
}
复制代码
加载模块
:make
钩子触发 → DllEntryPlugin
内部插件调用compilation.addEntry
→ compilation
维护了一些资源生成工厂方法 compilation.dependencyFactories
,负责把入口文件及其(循环)依赖转换成 module
( module
的解析过程会应用匹配的 loader
)。每一个 module
的解析过程提供 buildModule / succeedModule
等钩子, 全部 module
解析完成后触发 finishModules
钩子。封存
:seal
方法包含了 优化/分块/哈希
, 编译中止接收新模块,开始生成chunks。此阶段依赖了一些webpack内部插件对module进行优化,为本次构建生成的chunk加入hash等。createChunkAssets()会根据chunk类型使用不一样的模板进行渲染。此阶段执行完毕后就表明一次编译完成,触发 afterCompile
钩子
优化
:BannerPlugin
compilation.hooks.optimizeChunkAssets.tapAsync('MyPlugin', (chunks, callback) => {
chunks.forEach(chunk => {
chunk.files.forEach(file => {
compilation.assets[file] = new ConcatSource(
'\/**Sweet Banner**\/',
'\n',
compilation.assets[file]
);
});
});
callback();
});
复制代码
分块
:用来分割chunk的 SplitChunksPlugin
插件监听了optimizeChunksAdvanced
钩子哈希
:createHash
emit
,遍历 compilation.assets
生成全部文件。simple-log-webpack-plugin
一张效果图先上为敬。(对照图片)只需写
log.a
,经过本身的webpack插件自动补全字段标识a字段:
,加入文件路径
,轻松支持打印颜色
效果,相同文件的日志信息可折叠
,给你一个简洁方便的调试环境。
const ColorHash = require('color-hash')
const colorHash = new ColorHash()
const Dependency = require('webpack/lib/Dependency');
class LogDependency extends Dependency {
constructor(module) {
super();
this.module = module;
}
}
LogDependency.Template = class {
apply(dep, source) {
const before = `;console.group('${source._source._name}');`
const after = `;console.groupEnd();`
const _size = source.size()
source.insert(0, before)
source.replace(_size, _size, after)
}
};
module.exports = class LogPlugin {
constructor (opts) {
this.options = {
expression: /\blog\.(\w+)\b/ig,
...opts
}
this.plugin = { name: 'LogPlugin' }
}
doLog (module) {
if (!module._source) return
let _val = module._source.source(),
_name = module.resource;
const filedColor = colorHash.hex(module._buildHash)
// 判断是否须要加入
if (this.options.expression.test(_val)) {
module.addDependency(new LogDependency(module));
}
_val = _val.replace(
this.options.expression,
`console.log('%c$1字段:%c%o, %c%s', 'color: ${filedColor}', 'color: red', $1, 'color: pink', '${_name}')`
)
return _val
}
apply (compiler) {
compiler.hooks.compilation.tap(this.plugin, (compilation) => {
// 注册自定义依赖模板
compilation.dependencyTemplates.set(
LogDependency,
new LogDependency.Template()
);
// modlue解析完毕钩子
compilation.hooks.succeedModule.tap(this.plugin, module => {
// 修改模块的代码
module._source._value = this.doLog(module)
})
})
}
}
复制代码