阅读 1267

深入学习webpack plugin到手写一个plugin

什么是webpack插件

webpack 插件是整个 webpack 工具的骨架,而 webpack 本身也是利用这套插件机制构建出来的

const webpack = require('webpack')
const TestWebpackPlugin = require('test-webpack-plugin')
webpack({
	plugins: [new TestWebpackPlugin({/* opt 配置 */})]
})
复制代码

怎么样的一个东西可以称之为 webpack 插件呢?一个完整的 webpack 插件需要满足以下几点规则和特征

  • 是一个独立的js模块
  • 模块对外暴露一个js函数
  • 函数的原型上定义了一个apply方法,方法注入了compiler对象作为参数
  • apply函数中通过调用compiler对象挂载webpack事件钩子,钩子回调中能拿到当前编译compilation对象,如果是异步编译插件则拿到回调callback
  • 完成自定义编译流程,处理compiltion对象的内部数据
  • 如果是异步插件,则数据处理完后执行callback回调

所以一般我们的plugin长这样

// es5
var pluginName = 'test-webpack-plugin'
function TestWebpackPlugin (opt) {
	this.options = opt
}
TestWebpackPlugin.prototype.apply = function (compiler) {
	if (compiler.hooks) { // webpack4 +
    	compiler.hooks.emit.tapAsync(pluginName, function (compilation) {
        	// ...
        })
    } else {
    	compiler.plugin('emit', function (compilation, callback) {
        	// ...
            callback()
        })
    }
}
// es6
const pluginName = Symbol('test-webpack-plugin')
class TestWebpackPlugin {
	constructor (opt) {
    	this.options = opt
    }
    apply (compiler) {
    	const { hooks } = compiler
        if (hooks) {
        	hooks.emit.tapAsync(pluginName, (compilation) => {})
        } else {
        	compiler.plugin('emit', (compilation) => {})
        }
    }
}
复制代码

compiler和compilation对象

  • Compiler 对象包含了 Webpack 环境所有的的配置信息

  • Compilation 对象包含了当前的模块资源、编译生成资源、变化的文件等

  • Compiler 和 Compilation 的区别在于:Compiler 代表了整个 Webpack 从启动到关闭的生命周期,而 Compilation 只是代表了一次新的编译。

compiler

compiler 对象是 webpack 的编译器对象,compiler 对象会在启动 webpack 的时候被一次性的初始化,compiler 对象中包含了所有 webpack 可自定义操作的配置,例如 loader 的配置,plugin 的配置,entry 的配置等各种原始 webpack 配置等

compiler源码参考

compilation

compilation 实例继承于 compiler,compilation 对象代表了一次单一的版本 webpack 构建和生成编译资源的过程。当运行 webpack 开发环境中间件时,每当检测到一个文件变化,一次新的编译将被创建,从而生成一组新的编译资源以及新的 compilation 对象。一个 compilation 对象包含了 当前的模块资源、编译生成资源、变化的文件、以及 被跟踪依赖的状态信息。编译对象也提供了很多关键点回调供插件做自定义处理时选择使用。

compilation源码参考

webpack插件机制

Webpack 通过 Plugin 机制让其更加灵活,以适应各种应用场景。 在 Webpack 运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果,compiler.plugin(事件名称, 回调函数),compiler.hooks.someHook.tap(...),而这样的插件机制源于Tapable库

Tapable

webpack 的插件架构主要基于 Tapable 实现的,Tapable 是 webpack 项目组的一个内部库,主要是抽象了一套插件机制。

const {
	SyncHook, // 同步串行钩子,在触发事件之后,会按照事件注册的先后顺序执行所有的事件处理函数
	SyncBailHook, // 同步保险钩子,如果事件处理函数执行时有一个返回值不为空。则跳过剩下未执行的事件处理函数
	SyncWaterfallHook, // 同步瀑布流钩子,上一个事件处理函数的返回值作为参数传递给下一个事件处理函数,依次类推
	SyncLoopHook, // 同步循环钩子,事件处理函数返回true表示继续循环,如果返回undefined的话,表示结束循环
	AsyncParallelHook, // 异步并行钩子
	AsyncParallelBailHook, // 异步并行保险钩子
	AsyncSeriesHook, // 异步串行钩子
	AsyncSeriesBailHook, // 异步串行保险钩子
	AsyncSeriesWaterfallHook // 异步串行瀑布流钩子
 } = require("tapable");
复制代码

Tapable源码参考

compiler 事件钩子

事件钩子触发时机参数类型
entryOption在 entry 配置项处理过之后,执行插件SyncBailHook 同步保险
run开始读取 records 之前,钩入(hook into) compilercompilerAsyncSeriesHook 异步串行
compile一个新的编译(compilation)创建之后,钩入(hook into) compilercompilationSyncHook 同步
compilation编译(compilation)创建之后,执行插件compilationSyncHook 同步
make从 entry 开始递归分析依赖,准备对每个模块进行 buildcompilationAsyncParallelHook 异步并行
after-compile编译 build 过程结束compilationAsyncSeriesHook 异步串行
emit生成资源到 output 目录之前compilationAsyncSeriesHook 异步串行
after-emit生成资源到 output 目录之后compilationAsyncSeriesHook 异步串行
done编译(compilation)完成statsSyncHook 同步

上图

干货,项目中上传完sourceMap后删除sourceMap的插件

const _del = require('del')
const symbols = require('log-symbols')
const chalk = require('chalk')
const pluginName = {
  name: 'clean-source-map-webpack-plugin'
}
class CleanSourceMapWebpackPlugin {
  constructor (options) {
    const defaultOptions = {
      sourceMapPath: [], // sourceMap的文件位置
      dangerouslyAllowCleanPatternsOutsideProject: false // 是否允许跨文件夹删除
    }
    this.options = Object.assign(defaultOptions, options)
    this.outputPath = ''
  }
  handleDone () {
    const { sourceMapPath, dangerouslyAllowCleanPatternsOutsideProject } = this.options
    const { outputPath } = this
    try {
    // 调用_del库来删除sourceMap文件
      _del.sync(sourceMapPath.map(item => outputPath + '/' +item), { force: dangerouslyAllowCleanPatternsOutsideProject })
      console.log(symbols.success, chalk.green('clean-source-map-webpack-plugin: complated'))
    } catch (error) {
      const needsForce = /Cannot delete files\/folders outside the current working directory\./.test(error.message)
      if (needsForce) {
        const message = 'clean-source-map-webpack-plugin: Cannot delete files/folders outside the current working directory. Can be overridden with the `dangerouslyAllowCleanPatternsOutsideProject` option.'
        throw new Error(message)
      }
      throw error
    }
  }
  apply (compiler) {
  	// 如果没有获取到输出地址
    if (!compiler.options.output || !compiler.options.output.path) {
      console.warn(symbols.warning, chalk.red('clean-source-map-webpack-plugin: options.output.path not defined. Plugin disabled...'))
      return
    }
    const { sourceMapPath } = this.options
    // 如果没有获取到sourceMap的相对地址
    if (sourceMapPath.length === 0) {
      console.warn(symbols.warning, chalk.red('clean-source-map-webpack-plugin: please input sourceMapPath. Plugin disabled...'))
      return
    }
    const { hooks } = compiler
    this.outputPath = compiler.options.output.path
    // 如果是webpack4+
    if (hooks) {
    // 在done的事件钩子中注册删除事件
      hooks.done.tap(pluginName, stats => {
        this.handleDone()
      })
    } else {
      compiler.plugin('done', stats => {
        this.handleDone()
      })
    }
  }
}
module.exports = CleanSourceMapWebpackPlugin

复制代码

看清楚真正的 Webpack 插件

webpack4核心模块tapable源码解析

深入浅出 Webpack

文章分类
前端
文章标签