webpack-plugin
实现简易的plugin
实现一个在文件头部插入打包时间注释的插件:
- 创建plugin/index.js文件:
class MyAddRenderDatePlugin {
  constructor(options) {
    this.options = options
  }
  apply (compiler) {
    const dateStr = `// rendertime: ${formatDate(this.options.format)}`;
    // 指定一个挂载到 compilation 的钩子,回调函数的参数为 compilation 。
    compiler.hooks.emit.tapAsync('MyAddRenderDatePlugin', (compilation, callback) => {
      // 现在可以通过 compilation 对象绑定各种钩子
      // 遍历每个文件进行处理
      compilation.assets && Object.keys(compilation.assets).forEach((filePath) => {
        // 判断是否为要处理的文件类型(只针对JS文件)
        if(/\.(js)$/.test(filePath)) {
          let content = compilation.assets[filePath].source();
          // 在文件最前面插入指定字符串
          content = `${dateStr}\n${content}`;
          // 替换原来的文件内容
          compilation.assets[filePath] = {
            source: () => content,
            size: () => Buffer.byteLength(content, 'utf8')
          };
        }
      });
      callback();
    });
  }
}
module.exports = MyAddRenderDatePlugin
- 在配置文件中引入:
const MyPlugin = require('./plugin')
module.exports = {
  plugins: [
    new MyPlugin({
      format: 'YYYYMMDD HH:mm:ss'
    })
  ],
}
从上面代码中可以看到导出的类:
- constructor构造方法在可以接收传入的参数
- apply 是必要的方法,接收compiler参数,Webpack会调用它来注册插件,可以在函数体中注册各种Webpack事件钩子来对编译过程进行干预 在apply中有两个对象:
- compiler: 表示整个Webpack编译过程的配置对象,可以访问Webpack配置文件中的选项、输入输出路径等信息。
- compilation: 表示Webpack每次编译生成的对象,包含了当前编译的状态和资源信息。
如何调试
Compiler支持可以监控文件系统的 监听(watching) 机制,并且在文件修改时重新编译。 当处于监听模式(watch mode)时, compiler 会触发诸如watchRun,watchClose和invalid等额外的事件。 通常在 开发环境 中使用, 也常常会在webpack-dev-server这些工具的底层调用, 由此开发人员无须每次都使用手动方式重新编译。 还可以通过 CLI 进入监听模式。
compiler 和 compilation区别
- compiler表示整个编译过程,在Webpack编译生命周期内,在构建过程中只有一个- Compiler实例存在
- compilation表示一次具体的编译过程,它包含了当前编译过程中产生的所有资源以及编译过程的状态信息。每当Webpack开始一次新的编译时,就会创建一个新的- Compilation实例
在构建过程中只有一个
Compiler实例存在,那什么情况下会有多个Compilation事例?难道一次过程会有不同的结果?
一般来说也是一次过程只有一次结果,什么情况下会有不同的结果?没错!就是编译的文件变了!
也就是说在watch模式下或者使用 webpack-dev-server 的时候执行webpack编译会有一对多的情况
compiler
Compiler模块是 webpack 的主要引擎,它通过 CLI 或者 Node API 传递的所有选项创建出一个 compilation 实例。 它扩展(extends)自Tapable类,用来注册和调用插件。 大多数面向用户的插件会首先在Compiler上注册。
Compiler对象代表了整个Webpack打包过程,它从webpack配置文件中读取配置选项,负责处理所有的资源文件,并最终生成打包结果。在构建过程中,会创建一个Compiler实例,在Webpack编译生命周期内,只有一个Compiler实例存在
开头实现的plugin源码中compiler.hooks.emit.tapAsync 使用的tapAsync钩子就是集成自tapable
所有的钩子及参数在 compiler 钩子 | webpack 中文文档 | webpack 中文文档 | webpack 中文网 都可以查到,列举几个常用的钩子:
entryOption
在解析Webpack配置文件的入口选项之前执行的操作。
使用该钩子来动态生成Webpack的入口配置,从而根据不同条件动态加载不同的模块
afterPlugins
在完成插件注册之后执行的操作。
使用该钩子来检查当前Webpack配置是否合法,或者对已注册的插件进行进一步的初始化
compile
在开始新一轮编译之前执行的操作
使用该钩子来输出一条日志信息,表示Webpack即将开始一个新的编译过程
emit
在Webpack生成最终资源之前执行的操作
使用该钩子来对输出的资源文件进行优化、压缩或加密等操作
done
在Webpack编译完成并输出资源文件之后执行的操作。
使用该钩子来输出一条消息,表示Webpack编译完成,并在此基础上执行一些额外的操作,如启动服务器、打开浏览器等
class MyPlugin {
  apply(compiler) {
    // 动态修改Webpack的入口配置,添加一个新的入口文件`./src/custom.js`
    compiler.hooks.entryOption.tap('MyPlugin', (context, entry) => {
      if (entry.app) {
        const appEntry = Array.isArray(entry.app) ? entry.app : [entry.app]
        entry.app = [...appEntry, './src/custom.js']
      }
    })
    // 检查当前Webpack配置是否合法
    compiler.hooks.afterPlugins.tap('MyPlugin', (compiler) => {
      if (!compiler.options.output.path) {
        throw new Error('Output path must be specified.')
      }
    })
    compiler.hooks.compile.tap('MyPlugin', () => {
      console.log('开始一次新的编译...')
    })
    
    // 对输出的JS文件进行优化处理,使用`optimize`函数来优化代码,并更新Webpack的资源列表
    compiler.hooks.emit.tap('MyPlugin', (compilation) => {
      for (const filename of Object.keys(compilation.assets)) {
        if (filename.endsWith('.js')) {
          const source = compilation.assets[filename].source()
          compilation.assets[filename] = {
            source: () => optimize(source),
            size: () => source.length
          }
        }
      }
    })
    // 编译完成
    compiler.hooks.done.tap('MyPlugin', (stats) => {
      console.log('Compilation completed successfully.')
      // 启动服务器或打开浏览器等操作
      startServer(stats)
      openBrowser(stats)
    })
  }
}
compilation
Compilation模块会被Compiler用来创建新的 compilation 对象(或新的 build 对象)。compilation实例能够访问所有的模块和它们的依赖(大部分是循环依赖)。 它会对应用程序的依赖图中所有模块, 进行字面上的编译(literal compilation)。 在编译阶段,模块会被加载(load)、封存(seal)、优化(optimize)、 分块(chunk)、哈希(hash)和重新创建(restore)。
和 compiler 用法相同,取决于不同的钩子类型, 所以也可以在某些钩子上访问 tapAsync 和 tapPromise。同样按照 compiler 钩子的相同方式来调用 tap:
compilation.hooks.someHook.tap(/* ... */);
compilation是指代表一次构建过程的实例对象。Compilation对象提供一些钩子(Hook),可以用于在Webpack的构建过程中干预、修改或扩展Compilation的功能。
所有的钩子及参数在 compilation 钩子 | webpack 中文文档 | webpack 中文文档 | webpack 中文网 中查找,列举一下常用的钩子:
optimize
在进行代码优化之前执行的操作
使用该钩子来检查当前编译过程中是否有错误或警告,并根据结果进行后续处理
optimizeModules
在对模块进行优化之前执行的操作
使用该钩子来检查当前模块的依赖关系,并在此基础上进行代码优化
seal
在完成资源处理之后执行的操作
使用该钩子来生成一些额外的资源文件,如源码映射文件、缓存清单等
afterSeal
在生成额外资源文件之后执行的操作
使用该钩子来输出一条日志信息,表示Webpack已经生成了所有的资源文件
additionalAssets
在生成最终资源之前执行的操作
使用该钩子来动态生成一些额外的资源文件,如JSON数据文件、HTML模板文件等
class MyPlugin {
  apply(compiler) {
    compiler.hooks.compilation.tap('MyPlugin', (compilation) => {
      // 检查当前编译过程中是否有错误或警告,并根据结果进行后续处理
      compilation.hooks.optimize.tap('MyPlugin', () => {
        const errors = compilation.errors
        if (errors && errors.length > 0) {
          console.error('Compilation failed:', errors)
          process.exit(1)
        }
      })
      // 检查当前模块的依赖关系,并在此基础上进行代码优化
      compilation.hooks.optimizeModules.tap('MyPlugin', (modules) => {
        for (const module of modules) {
          // ...
        }
      })
      // 生成一些额外的资源文件,如源码映射文件、缓存清单等
      compilation.hooks.seal.tap('MyPlugin', () => {
        compilation.assets['source-map.json'] = generateSourceMap()
      })
      // 输出一条日志信息,表示Webpack已经生成了所有的资源文件
      compilation.hooks.afterSeal.tap('MyPlugin', () => {
        console.log('Additional assets generated.')
      })
      // 动态生成一些额外的资源文件,如JSON数据文件、HTML模板文件等
      compilation.hooks.additionalAssets.tapAsync('MyPlugin', (callback) => {
        const data = { a: 1 }
        compilation.assets['data.json'] = {
          source: () => JSON.stringify(data),
          size: () => JSON.stringify(data).length
        }
        callback()
      })
    })
  }
}