浅谈Webpack的AMD插件开发和运行机制

参考:
  1. 细说 webpack 之流程篇
  2. webpack之plugin内部运行机制
  3. How webpack works
  4. github.com/webpack/tap…
  5. 玩转webpack(一)上篇:webpack的基本架构和构建流程

怎么写webpack插件

  在写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插件实际上是不难的,可是最难的怎么找到合适的事件钩子执行对应事件,也就是怎么去理解webpack运行机制,找到合适突破口。web

  理解webpack运行机制有几个概念是须要注意一下:segmentfault

  1. compiler-编译器对象数组

      compiler对象表明了完整的webpack环境配置。该对象在启动webpack时就被一次性建立,由webpack组合全部的配置项(包括原始配置,加载器和插件)构建生成。缓存

      当在webpack环境中应用一个插件时,插件会收到compiler的引用,经过使用compiler,插件就能够访问到整个webpack的环境(包括原始配置,加载器和插件)。架构

  2. compilation-构建过程app

      compilation对象在compiler的compile方法里建立,它表明了一次单一的版本构建以及构建生成资源的汇总:异步

    1. compilation对象负责组织整个打包过程,包含了每一个构建环节及输出环节所对应的方法
    2. 该对象内部存放着全部module、chunk、生成的assets以及用来生成最后打包文件的template的信息

    当运行webpack-dev-server时,每当检测到一个文件变化,就会建立一次新的编译,从而生成一组新的编译资源。

  3. 插件

      插件本质上是被实例化的带有apply原型方法的对象,其apply方法在安装插件时将被webpack编译器调用一次,apply入参提供了一个compiler(编译器对象)的引用,从而能够在插件内部访问到webpack的环境。

      具体使用方式,在webpack.config.js里require对应的插件,而后在module.exports对象的plugins数组里实例化对应插件便可。

Compiler运行模式

Compiler有两种运行模式,一种是run执行模式,另外一种是watch监测模式(热加载模式webpack-dev-server)。在执行模式下,compiler有三个主要的事件钩子:

  1. compile/编译

    开始编译,建立对应的compilation对象。

  2. make/构建

    根据配置的不一样Entry入口构建对应的chunk块,并输出assets资源。

  3. emit/提交

    将最终的assets资源写入硬盘,输出至指定的文件路径。

相比较,监测模式则分为两部分:

  1. run执行模式
  2. 监测依赖若是发生改动,从新回到第一步

Compiler事件钩子

在执行模式下,Compiler的事件钩子以下:

  1. entry-option:生成不一样的插件应用

    解析传给 webpack 的配置中的 entry 属性,而后生成不一样的插件应用到 Compiler 实例上。这些插件多是 SingleEntryPlugin, MultiEntryPlugin 或者 DynamicEntryPlugin。但不论是哪一个插件,内部都会监听 Compiler 实例对象的 make 任务点

  2. (before-)run:开始执行

    开始执行,启动构建

  3. (before/after-)compile:编译源码,建立对应的Compilation对象

    Compiler 实例将会开始建立 Compilation 对象,这个对象是后续构建流程中最核心最重要的对象,它包含了一次构建过程当中全部的数据。也就是说一次构建过程对应一个 Compilation 实例。

  4. make:构建

  5. (after-)emit:输出结果

  6. done:结束

在监测模式下则对应下面三个钩子

  1. watch-run
  2. invalid
  3. watch-close

同时,在Compiler里内嵌了compilation/编译对象、normal-module-factory/常规模块工厂、context-module-factory/上下文模块工厂,能够在事件钩子的回调函数里直接读取。

Compilation事件钩子

compiler的几个事件钩子贯穿了webpack的整个生命周期,可是具体到实际版本构建等操做,倒是由compilation来具体执行。compilation对象是指单一的版本构建以及构建生成资源的汇总,它是在compiler的compile/编译事件里被建立,重点负责后续的添加模块、构建模块,打包并输出资源的具体操做(对应了compiler的make构建、emit输出)。

addEntry开始构建模块

在compiler的make方法里开始构建模块,对应了compilation的addEntry方法开始添加模块该方法其实是调用了_addModuleChain/私有方法,根据模块的类型获取对应的模块工厂并建立模块、构建模块。

首先,这里用了工厂模式来建立module实例,compilation经过读取每一个module/模块的dependency/依赖信息,在依赖里读取对应的模块工厂,而后在用工厂的create事件建立出module实例。

建立出module/模块后,再添加至compilation对象里,而后由compilation来统筹模块的构建和module的依赖处理。

在处理module的依赖时,每个依赖模块仍是按照第一个步骤进行操做,迭代循环。直至模块及依赖彻底处理完。

addModule添加模块

咱们能够看看addModule事件里发生了什么事情,整个addModule里分为两块:读取依赖工厂,把模块添加至compilation。在后面一个步骤里webpack专门作了一系列的优化逻辑。

  在addModule里,compilation会经过identifier判断是否已经有当前module,若是有则跳过;

  • 若是没有的话,会在cache缓存里判断是否有这个模块,若是也没有那么直接添加至compilation里;
  • 若是有那么就须要判断缓存里的模块是否已过时,是否须要重构(经过timeStamps判断),若是须要重构那么就触发disconnect事件,若是不须要重构,那么触发unbuild事件;
  • 不论是否须要重构,只要缓存里有这个模块,都要添加至compilation里。

buildModule构建模块

通过上面的步骤,已经把全部的模块添加至compilation里,接下来是由compilation统筹进行构建

构建模块是整个webpack最耗时的操做,在这里分为几个步骤:

  1. 读取module/模块,调用对应Loader加载器进行处理,并输出对应源码

    1. 遇到依赖时,递归地处理依赖的module/模块
    2. 把处理完的module/模块依赖添加至当前模块
  2. 调用acorn解析加载器输出的源文件,并生成抽象语法树 AST

  3. 遍历AST树,构建该模块以及所依赖的模块

  4. 整合模块和全部对应的依赖,输出总体的module

seal打包输出模块

在构建完模块后,就会在回调函数里调用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运行机制

关于webpack运行机制的理解,建议你们有空再看看下面两个流程图,讲得很仔细,有助于理解。

编写AMD插件

假设咱们如今要写一个插件使得其可以生成 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
    ]
}
复制代码
相关文章
相关标签/搜索