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

10,660次阅读
没有评论

共计 4509 个字符,预计需要花费 12 分钟才能阅读完成。

前言

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 {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  {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 的主要内容。

    正文完
     0
    Yojack
    版权声明:本篇文章由 Yojack 于1970-01-01发表,共计4509字。
    转载说明:
    1 本网站名称:优杰开发笔记
    2 本站永久网址:https://yojack.cn
    3 本网站的文章部分内容可能来源于网络,仅供大家学习与参考,如有侵权,请联系站长进行删除处理。
    4 本站一切资源不代表本站立场,并不代表本站赞同其观点和对其真实性负责。
    5 本站所有内容均可转载及分享, 但请注明出处
    6 我们始终尊重原创作者的版权,所有文章在发布时,均尽可能注明出处与作者。
    7 站长邮箱:laylwenl@gmail.com
    评论(没有评论)