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