编写webpack loader处理各种各样的源代码
背景
之前开发了 babel plugin 向 catch 块中添加 logger
来捕获错误来防止错误漏报,然后同事就问我开发 babel 插件的时候遇到了这么多问题为什么不使用 webpack loader 来实现这个功能呢?没有开发过 webpack loader 的我也不知道怎么回答,因为我确实没调研过 webpack loader 和 babel plugin 的区别。再因为 umi 对 webpack 的高度封装,在使用了 babel plugin 的时候 拿到的是处理后的 AST,确实该调研下 webpack loader 的方式。
需求
try {
} catch (e) {}
跟 babel plugin 开发需求一样,将以上代码转化成下面的代码
try {
} catch (e) {
console.log(e)
}
环境准备
安装webpack
和webpack-cli
命令行工具
yarn add webpack webpack-cli -D
在package.json
中添加 scripts
字段,每次构建只需要运行 yarn build 即可
"scripts": {
"build": "webpack --mode=production"
}
webpack loader
此示例要求你有webpack loader
编写的知识,可以前往writing-a-loader学习,创建文件目录如下
❯ tree -I 'node_modules'
.
├── dist
│ └── index.html
├── loaders
│ ├── catch-loader.js
├── package-lock.json
├── package.json
├── src
│ ├── data.tsx
│ └── index.js
├── webpack.config.js
└── yarn.lock
3 directories, 11 files
index.html
<!DOCTYPE html>
<html lang="zh-cn">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Webpack Loader 示例</title>
</head>
<body>
<h3>Webpack Loader 示例</h3>
<p id="message"></p>
<script src="./bundle.js"></script>
</body>
</html>
data.tsx
try {
} catch (e) {}
catch-ast-loader.js
/**
* @param {string|Buffer} content 源文件的内容
* @param {object} [map] 可以被 https://github.com/mozilla/source-map 使用的 SourceMap 数据
* @param {any} [meta] meta 数据,可以是任何内容
*/
function catchStringLoader(content = "", map, meta) {
const newContent = content.replace("catch{", "catch(e){console.log(e)")
return `module.exports = {code:'${newContent}'}`
}
module.exports = catchStringLoader
/*
直接通过字符串替换的方式存在以下弊端,
1. 不好判断catch是否有参数,如果有,把错误参数传递给logger也是不太好处理的
2. 万一有函数命名为catch则会出现bug
3. 不好判断当前catch块中是否已存在logger
...
所以,最好的方式还是通过AST处理源代码,插入错误日志记录对象logger
*/
const { parse } = require("@babel/parser")
const generate = require("@babel/generator").default
const traverse = require("@babel/traverse").default
const template = require("@babel/template").default
function catchASTLoader(content = "", map, meta) {
// 转化成ast
const sourceAst = parse(content)
// 遍历AST处理节点
traverse(sourceAst, {
CatchClause(path) {
// 获取catch块函数的参数
const error = path.node.param || "error"
// 生成logger的ast节点,console.log(e)
const ast = template.ast`console.log(${error})`
// 插入ast树
path.get("body").unshiftContainer("body", ast)
},
})
// 转化成字符串代码
const { code } = generate(sourceAst)
const result = code.replace(/\s/g, "")
return `module.exports = "${result}"`
}
module.exports = catchASTLoader
index.js
import data from "./data.tsx"
const msgElement = document.querySelector("#message")
msgElement.innerText = data
最重要的 webpack.config.js
const path = require("path")
module.exports = {
entry: "./src/index.js",
output: {
filename: "bundle.js",
path: path.resolve(__dirname, "dist"),
},
module: {
rules: [
{
test: /\.tsx$/i,
use: ["catch-ast-loader"],
},
],
},
resolveLoader: {
modules: [path.resolve(__dirname, "loaders")],
},
}
结论
- 优点:可通过
webpack-chain
修改 loader 的执行顺序,这样就不会像 babel plugin 那样只能拿到之前 plugin 处理后的结果并且不能进行顺序的调整了。按理也可以通过webpack-chain
来修改babel-loader
来改变它的 plugin 的顺序,没有实践过。 - 缺点:需要自己处理字符串和 AST,不像 babel plugin 那样封装好的直接拿到 AST 进行处理
等等,我还有想法
能不能用 webpack plugin 来处理源代码呢?,研究了一下,可以对构建输出的
assets
进行转化,这样就不是对每个 module 进行转换了,不符合需求。
在刚才建立的文件目录下新建 plugins 文件夹并创建 catch-plugin.js 文件
.
├── plugins
│ └── catch-plugin.js
catch-plugin.js
class CatchPlugin {
constructor(options) {
this.options = options
}
apply(compiler) {
compiler.hooks.emit.tap("CodeBeautify", compilation => {
Object.keys(compilation.assets).forEach(data => {
// 欲处理的文本
let content = compilation.assets[data].source()
// 源代码都被转化成字符串了,所以不能通过ast进行处理
content = content.replace("console.log", "console.error")
compilation.assets[data] = {
source() {
return content
},
size() {
return content.length
},
}
})
})
}
}
module.exports = CatchPlugin
修改 webpack.config.js
const path = require("path")
const catchPlugin = require("./plugins/catch-plugin")
module.exports = {
entry: "./src/index.js",
output: {
filename: "bundle.js",
path: path.resolve(__dirname, "dist"),
},
module: {
rules: [
{
test: /\.tsx$/i,
use: ["catch-ast-loader"],
},
],
},
resolveLoader: {
modules: [path.resolve(__dirname, "loaders")],
},
plugins: [new catchPlugin()],
}
plugin 通过处理 assets 显然不适合用来转换源码,如果有 plugin 更好的处理方式,记得发邮件联系我哦,我改进一下示例。