webpack插件开发及修改源码的几种方式

11575次浏览

前言

webpack是我们平时工作中必不可少的工具,难免有时候需要对webpack编译的代码进行个性化处理操作,那么需要通过webpack插件或者修改其代码的方式来完成,本篇文章着重介绍webpack插件开发及修改源码的一些方式。很久之前我也写过webpack相关文章,例如:webpack前端技术小结,更多webpack知识,欢迎关注haorooms前端博客

插件使用

我们工作中,肯定用过webpack插件,用法如下:

// webpack.config.js
var HelloWorldPlugin = require('hello-world');
 module.exports = {  
// ... config settings here ...  
plugins: [new HelloWorldPlugin({ options: true })]
};

插屏开发

webpack 插件开发主要基于 Tapable 的插件机制提供丰富的自定义 API 和生命周期事件,可以控制 webpack 编译的每个流程,

Compiler 包含 webpack 环境的所有配置信息

Compilation 包含整个编译过程中所有环节对应的方法


class CustomPlugin {
  constructor(options) {
      ...
  }
  apply(compiler) {
    compiler.hooks.compilation.tap('CustomPlugin', (compilation) => {
      compilation.hooks.optimizeChunkAssets.tap('CustomPlugin', (chunks) => {
        console.log(chunks)
      });
    });
  }
};

Compiler 代表着 webpack 从启动到关闭的整个生命周期,而 Compilation 只代表来一次编译,而修改源码的时机正好需要在编译的过程中修改。 上述例子中通过 optimizeChunkAssets 的钩子可以拿到所有的 chunks 信息,针对具体的 chunks 可以修改对应的源码,例如在增加头尾的源码:

// 处理源码拼接库
const ConcatSource = require('webpack-sources').ConcatSource;
class CustomPlugin {
  constructor(options) {
      ...
  }
  apply(compiler) {
    compiler.hooks.compilation.tap('CustomPlugin', (compilation) => {
      compilation.hooks.optimizeChunkAssets.tap('CustomPlugin', (chunks) => {
        chunks.forEach((chunk) => {
          chunk.files.forEach((fileName) => {
            // 判断具体要修改的文件,假设简单通过 chunk 的文件名称判断入口
            if (filename.indexOf('index') > -1) {
              // 在源码头尾各增加内容
              compilation.assets[fileName] = new ConcatSource(
                `console.log('code before')`,
                compilation.assets[fileName],
                `console.log('code after')`,
              );
            }
          });
        });
      });
    });
  }
};

插件提供了丰富的生命周期,在修改源码过程中也要特别要注意插件的生命周期带来的影响,比如上述在 optimizeChunkAssets 阶段,这个阶段拿到的 chunk 资源已经完成各种 Loader 的处理,这个时候如果新增源码内容是 ES6,将不会再被转化。

webpack hooks 介绍

webpack官网地址:https://webpack.js.org/api/compiler-hooks/#hooks

介绍几个常用的hooks

    // tap 同步
    compiler.hooks.emit.tap("tap", (compilation) => {
      console.log("***** tap *****")
    })
    // tapAsync 参数cb未调用之前进程会暂停
    compiler.hooks.emit.tapAsync("tapAsync", (compilation,cb) => {
      start(0);
      function start(index){
          console.log(index);
          if(index<=3){
              setTimeout(() => {
                  start(++index);
              }, 1000);
          }else{
              cb()
          }
      }
    })
    // tapPromise 通过promise的方式调用
    compiler.hooks.emit.tapPromise("tapPromise", (compilation)=>{
        return new Promise((resolve,reject)=>{
            console.log("start tap-promise");
            setTimeout(()=>{
                resolve()
            },2000)
        })
    })

   compiler.hooks.afterCompile.tapAsync({
      name: 'haoroomstest',
    }, (compilation, callback) => {

      callback()
    })

插件开发demo

实现一个防止代码被调试的插件。 思路,生产环境代码中注入debuger

const { ConcatSource } = require('webpack-sources')

class HaoroomssAddDebugger {
  /**
   * @param options.min 最小间隔秒数
   * @param options.max 最大间隔秒数
   */
  constructor (options = { min: 1, max: 20 }) {
    this.min = options.min && options.min > 0 ? options.min : 1
    this.max = options.max && options.max <= 600 ? options.max : 600
  }

  apply (compiler) {
    compiler.hooks.afterCompile.tapAsync({
      name: 'HaoroomssAddDebugger',
    }, (compilation, callback) => {
      let assetNames = Object.keys(compilation.assets)
      for (const name of assetNames) {
        if (name.endsWith('.js')) { // 跳过非js文件
          let seconds = Math.ceil(Math.random() * (this.max - this.min)) +
            this.min
           let appendContent = `(() => {
        function block() {
            if (
                window.outerHeight - window.innerHeight > 200 ||
                window.outerWidth - window.innerWidth > 200
            ) {
                document.body.innerHTML =
                    "检测到非法调试,请关闭后刷新重试!";
            }
            setInterval(() => {
                (function () {
                    return false;
                }
                    ["constructor"]("debugger")
                    ["call"]());
            }, ${seconds});
        }
        try {
            block();
        } catch (err) {}
    })();`
          compilation.updateAsset(
            name,
            old => new ConcatSource(old, '\n', appendContent),
          )
        }
      }
      callback()
    })
  }
}

module.exports = HaoroomssAddDebugger

修改webpack源码

一、webpack Loader 注入代码

在 babel 7.4.0 之后,添加 polyfills 的方式推荐在入口 import core-js/stable 和 regenerator-runtime/runtime 并配合 @babel/preset-env 的 useBuiltIns 属性自动导入对应的包,我们希望工程上能自动完成入口注入的事情,而不是每次手动的增删。基于指定文件的内容处理,可以交给 webpack Loader 来实现。

webpack Loader 作为 webpack 核心能力之一,其基本工作就是将一个文件以字符串的形式读入,再对其进行语法分析和转换后再交给下一环节处理。

既然是以字符串形式读入,那修改源码就变得非常简单,结合 webpack 在指定 Loader 时的 test 匹配,可以快速定位到指定类型的文件

module: {
  rules: [
    {
      // 具体匹配文件的规则,匹配 src/index.js 文件
      test: /src\/index\.js/,
      use: [
        {
          loader: './customLoader.js'
        }
      ]
    }
  ]
}

在 customLoader 中注入代码:

module.exports = function(source) {
    return `
import "core-js/stable";
import "regenerator-runtime/runtime";
${content}
  `;
}

二、结合 AST 修改源码

上述 webpack Plugin 和 Loader 修改源码的例子,大部分都是通过简单的字符串拼接的方式,如果想具体修改源码的逻辑呢,那就需要结合 AST 进行源码修改。

而针对指定文件内容的处理,可以借鉴源码处理的三板斧:

将 Plugin / Loader 中获取到的源码转换成 AST

对 AST 修改后在转化为源码

将新生成源码返回或写入到对应资源文件中

以 Babel AST 相关操作为例:

const traverse = require('@babel/traverse').default;
const generate = require('@babel/generator').default;
const parser = require('@babel/parser');
const t = require('@babel/types');
...
// 转化 AST
const ast = parser.parse(source, {
    // 根据源码内容添加 sourceType 和 plugins
    sourceType: 'module',
    plugins: ['jsx', 'typescript']
});
traverse(ast, {
    Program(path) {
        // 在对应 AST 节点上进行操作和修改
    }
});
// 生成源码
const newSource = generate(ast, {});
...

也可以在webpack插件中,通过compilation修改。

const asset = compilation.assets[fileName];
let input = asset.source();
// 拿到源码后进行修改,一般基于 AST 修改
// 修改完成后重新写回
compilation.assets[fileName] = new ConcatSource(input);

关于AST相关文章,后面写。

三、修改 Entry入口的方式

这种方式主要是添加为项目添加 polyfills 的时候

例如如下:

module.exports = {
  entry: ['@babel/polyfill', './src/index.js'],
  ...
}

小结

以上就是今天的webpack的主要内容。

相关文章: