webpack实现自定义plugin

337 阅读2分钟

webpack简介

基本概念

  • Entry:入口,Webpack 执行构建的第一步将从 Entry 开始,可抽象成输入。

  • Module:模块,在 Webpack 里一切皆模块,一个模块对应着一个文件。Webpack 会从配置的 Entry 开始递归找出所有依赖的模块。

  • Chunk:代码块,一个 Chunk 由多个模块组合而成,用于代码合并与分割。

  • Loaderloader 让 webpack 能够去处理那些非 JavaScript 文件(webpack 自身只理解 JavaScript)。loader 可以将所有类型的文件转换为 webpack 能够处理的有效模块,然后你就可以利用 webpack 的打包能力,对它们进行处理。本质上,webpack loader 将所有类型的文件,转换为应用程序的依赖图(和最终的 bundle)可以直接引用的模块。

  • Plugin:插件的范围包括,从打包优化和压缩,一直到重新定义环境中的变量。插件接口功能极其强大,可以用来处理各种各样的任务,扩展插件,在 Webpack 构建流程中的特定时机会广播出对应的事件,插件可以监听这些事件的发生,在特定时机做对应的事情

vue2.0以后使用vue.config.js,对应plugins放到configureWebpack中

module.exports = { 
    pages: pages,
    configureWebpack: { 
        // 环境模式: 
        // 开发环境,打包时代码不压缩,默认开启调试模式
        mode: 'development', 
        // 生产环境,打包发布到线上的,默认开启代码压缩,代码混淆,未开启代码调试 
        // mode: 'production' 
        // 打包时的入口文件路径,即从哪个路径进行打包,webpack以js文件为入口文件 
        entry: './src/main.js', 
        // 打包时的出口路径,即打包好后的文件路径 
        output: { 
            // 输出的目录,与webpack.config.js对比,如不生成在当前目录,输出目录需给全路径 
            // path: path.join(__dirname, 'dist'), 
            // __dirname为node对象,即当前目录 
            path: path.join(__dirname, 'dist'), 
            // js文件打包好后的文件名及位置,[name]指向entry中的key值,此时是main 
            filename: 'js/[name].bundle.js' 
        },
        module: {
            rules: [
              { test: /.txt$/, use: 'raw-loader' }
            ]
        },
        plugins: [new CustomPlugin({pages})]
   }

工作流程

Webpack 的运行流程是一个串行的过程,从启动到结束会依次执行以下流程:

image.png

  1. 初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数;

  2. 开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译;

  3. 确定入口:根据配置中的 entry 找出所有的入口文件;

  4. 编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理;

  5. 完成模块编译:在经过第4步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系;

  6. 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会;

  7. 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统

image.png

comiler vs compilation 对象

Compiler 负责监听文件和启动编译 它可以读取到 webpack 的 config 信息,整个 Webpack 从启动到关闭的生命周期,一般只有一个 Compiler 实例,整个生命周期里暴露了很多方法,常见的 run,make,compile,finish,seal,emit 等,我们写的插件就是作用在这些暴露方法的 hook 上

Compilation 负责构建编译。 每一次编译(文件只要发生变化,)就会生成一个 Compilation 实例,Compilation 可以读取到当前的模块资源,编译生成资源,变化的文件,以及依赖跟踪等状态信息。同时也提供很多事件回调给插件进行拓展

什么是plugin?

plugin是插件的意思,通常用于对现有的架构进行扩展。webpack中的插件,就是对webpack现有功能的各种扩展,比如打包优化,文件压缩等等。

loader和plugin的区别:

loader主要用于转换某些类型的模块,是一个加载器;
plugin是插件,对webpack本身进行扩展,是一个扩展器;

plugin的使用步骤:

通过npm 安装需要使用的plugins(有些内置的不需要再安装);
在webpack.config.js中的plugins中配置插件;

那如何自定义plugin

webpack 插件是一个具有 apply 属性的 JavaScript 对象。apply 属性会被 webpack compiler 调用,并且 compiler 对象可在整个编译生命周期访问

实现一个html转换ejs的插件

  1. 第一步创建CustomPlugin,利用Compiler Hooks中钩子函数emit
  • emit : 生成资源到 output 目录之前。

  • entryOption : 在 webpack 选项中的 entry 配置项 处理过之后,执行插件。

  • afterPlugins : 设置完初始插件之后,执行插件。

  • compilation : 编译创建之后,生成文件之前,执行插件。

  • done : 编译完成。

compiler.hooks 下指定事件钩子函数,便会触发钩子时,执行回调函数。

Webpack 继承自Tapable 类,Tapable提供三种触发钩子的方法,可以使用这些方法,注入自定义的构建步骤,这些步骤将在整个编译过程中不同时机触发:能否使用同步异步触发,取决于钩子函数是否支持同步和异步

  • tap :以同步方式触发钩子;

  • tapAsync :以异步方式触发钩子;

  • tapPromise :以异步方式触发钩子,返回 Promise;

  1. compiler.hooks.emit.tap利用tap触发钩子,传递插件名称,传递的方法中利用compilation获取资源

  2. compilation.assets是存放当前所有即将输出的资源,获取到对应的页面html

  3. 通过拿到的资源名称,和内容compilation.assets[filename].source(),写入到ejs文件中,在这个过程也可以实现其他的预置公共内容

  4. vue.config.js 中对应配置使用plugins: [new CustomPlugin({pages})]

  5. build后会对应生成ejs文件

const pluginName = 'custom-plugin';

class CustomPlugin {
  constructor (options) {
    this.options = Object.assign({}, options) 
  }
  apply (compiler) {
      compiler.hooks.emit.tap(pluginName, compilation => {
        let templateNames = []
        for (let key in this.options.pages) {
          // 获取模版名称
          let filename = this.options.pages[key].filename || `${key}.html`
          if (compilation.assets[filename]) {
            templateNames.push(filename)
            // 获取对应文件内容
            let htmlStr = compilation.assets[filename].source()
            let htmlName = filename.split('.')[0]
            // 写入新文件
            await this.writeEjsFile(compilation, htmlName, htmlStr)
          }
        }
        if (templateNames.length === 0) {
          console.log(`${pluginName}----文件模版名不正确,${pluginName}停止`)
        }
        // 输出部署路径log
        this.printLog(compilation)   
      })
  }
  // 输出ejs文件
  async writeEjsFile (compilation, htmlName, htmlStr) {
    let url = `./${htmlName}/${htmlName}.${env}.ejs`
    await this.writeFile(compilation, url, htmlStr)
  }
  
  // 写文件
  writeFile (compilation, url, data) {
    compilation.assets[url] = {
      source: function() {
        return data;
      },
      size: function() {
        return data.length;
      }
    }
  }

参考资料:
github.com/6fedcom/fe-…
blog.csdn.net/weixin_3628…