编写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)
}

环境准备

安装webpackwebpack-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")],
  },
}

来吧,查看结果,很 nice! loader

结论

  • 优点:可通过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()],
}

运行yarn build 查看结果 plugin

plugin 通过处理 assets 显然不适合用来转换源码,如果有 plugin 更好的处理方式,记得发邮件联系我哦,我改进一下示例。

参考

Published under  on .

Last updated on .

pipihua

我是皮皮花,一个前后端通吃的前端攻城狮,如果感觉不错欢迎点击小心心♥(ˆ◡ˆԅ) star on GitHub!