在写webpack的插件时,内部必需要实现一个apply方法,传入compiler参数,就能够经过调用plugin方法,在对应的事件钩子订阅事件,而后取出源码进行自定义开发后,return回去便可。webpack
这是由于webpack的内部机制是经过tapable来实现的,经过plugin方法订阅事件,咱们能够根据不一样事件钩子类型,执行对应的同步代码,或者触发对应的异步回调--串行、并行、瀑布流(上一个操做的输出是下一个操做的输入,有点相似pipeLine),在异步钩子的回调函数里,要返回callback参数。git
简单的同步插件示例以下:github
class MyPlugin {
apply(compiler) {
compiler.plugin(“compilation”, compilation => {
compilation.plugin(“optimize-modules”, modules => {
modules.forEach(…);
}
}
}
}
复制代码
咱们能够看到,遵循对应的同步、异步规则编写简单的webpack插件实际上是不难的,可是最难的怎么找到合适的事件钩子执行对应事件,也就是怎么去理解webpack运行机制,找到合适突破口。web
理解webpack运行机制有几个概念是须要注意一下:segmentfault
compiler-编译器对象数组
compiler对象表明了完整的webpack环境配置。该对象在启动webpack时就被一次性建立,由webpack组合全部的配置项(包括原始配置,加载器和插件)构建生成。缓存
当在webpack环境中应用一个插件时,插件会收到compiler的引用,经过使用compiler,插件就能够访问到整个webpack的环境(包括原始配置,加载器和插件)。架构
compilation-构建过程app
compilation对象在compiler的compile方法里建立,它表明了一次单一的版本构建以及构建生成资源的汇总:异步
当运行webpack-dev-server时,每当检测到一个文件变化,就会建立一次新的编译,从而生成一组新的编译资源。
插件
插件本质上是被实例化的带有apply原型方法的对象,其apply方法在安装插件时将被webpack编译器调用一次,apply入参提供了一个compiler(编译器对象)的引用,从而能够在插件内部访问到webpack的环境。
具体使用方式,在webpack.config.js里require对应的插件,而后在module.exports对象的plugins数组里实例化对应插件便可。
Compiler有两种运行模式,一种是run执行模式,另外一种是watch监测模式(热加载模式webpack-dev-server)。在执行模式下,compiler有三个主要的事件钩子:
compile/编译
开始编译,建立对应的compilation对象。
make/构建
根据配置的不一样Entry入口构建对应的chunk块,并输出assets资源。
emit/提交
将最终的assets资源写入硬盘,输出至指定的文件路径。
相比较,监测模式则分为两部分:
在执行模式下,Compiler的事件钩子以下:
entry-option:生成不一样的插件应用
解析传给 webpack 的配置中的 entry 属性,而后生成不一样的插件应用到 Compiler 实例上。这些插件多是 SingleEntryPlugin, MultiEntryPlugin 或者 DynamicEntryPlugin。但不论是哪一个插件,内部都会监听 Compiler 实例对象的 make 任务点
(before-)run:开始执行
开始执行,启动构建
(before/after-)compile:编译源码,建立对应的Compilation对象
Compiler 实例将会开始建立 Compilation 对象,这个对象是后续构建流程中最核心最重要的对象,它包含了一次构建过程当中全部的数据。也就是说一次构建过程对应一个 Compilation 实例。
make:构建
(after-)emit:输出结果
done:结束
在监测模式下则对应下面三个钩子
同时,在Compiler里内嵌了compilation/编译对象、normal-module-factory/常规模块工厂、context-module-factory/上下文模块工厂,能够在事件钩子的回调函数里直接读取。
compiler的几个事件钩子贯穿了webpack的整个生命周期,可是具体到实际版本构建等操做,倒是由compilation来具体执行。compilation对象是指单一的版本构建以及构建生成资源的汇总,它是在compiler的compile/编译事件里被建立,重点负责后续的添加模块、构建模块,打包并输出资源的具体操做(对应了compiler的make构建、emit输出)。
在compiler的make方法里开始构建模块,对应了compilation的addEntry方法开始添加模块该方法其实是调用了_addModuleChain/私有方法,根据模块的类型获取对应的模块工厂并建立模块、构建模块。
首先,这里用了工厂模式来建立module实例,compilation经过读取每一个module/模块的dependency/依赖信息,在依赖里读取对应的模块工厂,而后在用工厂的create事件建立出module实例。
建立出module/模块后,再添加至compilation对象里,而后由compilation来统筹模块的构建和module的依赖处理。
在处理module的依赖时,每个依赖模块仍是按照第一个步骤进行操做,迭代循环。直至模块及依赖彻底处理完。
咱们能够看看addModule事件里发生了什么事情,整个addModule里分为两块:读取依赖工厂,把模块添加至compilation。在后面一个步骤里webpack专门作了一系列的优化逻辑。
在addModule里,compilation会经过identifier判断是否已经有当前module,若是有则跳过;
通过上面的步骤,已经把全部的模块添加至compilation里,接下来是由compilation统筹进行构建
构建模块是整个webpack最耗时的操做,在这里分为几个步骤:
读取module/模块,调用对应Loader加载器进行处理,并输出对应源码
调用acorn解析加载器输出的源文件,并生成抽象语法树 AST
遍历AST树,构建该模块以及所依赖的模块
整合模块和全部对应的依赖,输出总体的module
在构建完模块后,就会在回调函数里调用compilation的seal方法。Compilation的seal事件里,会根据webpack里配置的每一个Entry入口开始打包,每一个Entry对应地打包出一个chunk,逐次对每一个module和chunk进行整理,生成编译后的源码chunk,合并、拆分、生成hash,最终输出Assets资源。
针对每个Entry入口构建chunk的过程有点相似上面的buildModule,具体细节能够查看流程图。在这里咱们须要重点关注createChunkAssets生成最终的资源并输出至指定文件路径。
在整个createChunkAssets过程里,有个有意思的地方须要注意,就是mainTemplate、chunkTemplate和moduleTemplate。template是用来处理上面输出的chunk,打包成最终的Assets资源。可是mainTemplate是用来处理入口模块,chunkTemplate是处理非入口模块,即引用的依赖模块。
经过这两个template的render处理输出source源码,都会汇总到moduleTemplate进行render处理输出module。接着module调用source抽象方法输出assets,最终由compiler统一调用emitAssets,输出至指定文件路径。
咱们须要注意一下mainTemplate的render(render-with-entry)方法,整个webpack经过该方法找到整个入口开始构建,引用依赖。若是咱们想自定义AMD插件,把整个webpack编译结果包裹起来,那么咱们能够经过mainTemplate的render-with-entry方法,进行自定义开发。
关于webpack运行机制的理解,建议你们有空再看看下面两个流程图,讲得很仔细,有助于理解。
假设咱们如今要写一个插件使得其可以生成 AMD 模块,那么咱们要怎么操做?咱们可能会有下面这些疑惑。
具体开发的思路,是经过在webpack打包输出最终资源时,从入口模块构建的节点入手,订阅compilation.mainTemplate的render-with-entry事件,在里边用AMD声明包裹住源码,而后返回便可。这样子后续展开依赖模块时也会统一被AMD声明所包裹。
具体插件源码以下:
/** plugin.js **/
const ConcatSource = require('webpack-sources').ConcatSource,
path = require('path')
class DefPlugin {
constructor(name) {
}
apply(compiler) {
compiler.plugin('compilation', (compilation)=>{
compilation.mainTemplate.plugin('render-with-entry', function(source, chunk, hash){
return new ConcatSource(`global.define(['require','module','exports'],function(require, module, exports) {
${source.source()}
})`);
})
})
}
}
module.exports = DefPlugin
复制代码
实际调用时,只须要在webpack.config.js的plugins数组里实例化该插件便可。
/** webpack.config.js **/
const path = require('path')
const Plugin = require('./plugin')
module.exports = {
context: path.join(__dirname, 'src'),
entry: './index.js',
output: {
libraryTarget: 'commonjs2',
path: path.join(__dirname, 'dist'),
filename: './bundle.js'
},
plugins: [
new Plugin
]
}
复制代码