『Webpack进阶系列』—— 手写一个babel plugin

lxf2023-03-14 16:46:01

背景

前端项目中经常都会用到async await处理异步操作,而且每次都要写try catch去捕获异常,不然抛出异常的时候会导致代码堵塞无法继续往下执行

解决方案

方案一

编写一个babel plugin给所有的async await加上try catch

项目结构

新建一个新的工程用来编写babel plugin的逻辑,下面是主要的项目结构

// lib/index.js

// babel-template 用于将字符串形式的代码来构建AST树节点
const template = require('babel-template');

const { tryTemplate, catchConsole, mergeOptions, matchesFile } = require('./util.js');

module.exports = function (babel) {
  // 通过babel 拿到 types 对象,操作 AST 节点,比如创建、校验、转变等
  let types = babel.types;

  // visitor:插件核心对象,定义了插件的工作流程,属于访问者模式
  const visitor = {
    AwaitExpression(path) {
      // 通过this.opts 获取用户的配置
      if (this.opts && !typeof this.opts === 'object') {
        return console.error('[babel-plugin-await-add-trycatch]: options need to be an object.');
      }

      // 判断父路径中是否已存在try语句,若存在直接返回
      if (path.findParent((p) => p.isTryStatement())) {
        return false;
      }

      // 合并插件的选项
      const options = mergeOptions(this.opts);

      // 获取编译目标文件的路径,如:E:\myapp\src\App.vue
      const filePath = this.filename || this.file.opts.filename || 'unknown';

      console.log(filePath, 'filePath');

      // 在排除列表的文件不编译
      if (matchesFile(options.exclude, filePath)) {
        return;
      }

      // 如果设置了include,只编译include中的文件
      if (options.include.length && !matchesFile(options.include, filePath)) {
        return;
      }

      // 获取当前的await节点
      let node = path.node;

      // 在父路径节点中查找声明 async 函数的节点
      // async 函数分为4种情况:函数声明 || 箭头函数 || 函数表达式 || 对象的方法
      const asyncPath = path.findParent((p) => p.node.async && (p.isFunctionDeclaration() || p.isArrowFunctionExpression() || p.isFunctionExpression() || p.isObjectMethod()));

      // 获取async的方法名
      let asyncName = '';

      let type = asyncPath.node.type;

      switch (type) {
        // 1️⃣函数表达式
        // 情况1:普通函数,如const func = async function () {}
        // 情况2:箭头函数,如const func = async () => {}
        case 'FunctionExpression':
        case 'ArrowFunctionExpression':
          // 使用path.getSibling(index)来获得同级的id路径
          let identifier = asyncPath.getSibling('id');
          // 获取func方法名
          asyncName = identifier && identifier.node ? identifier.node.name : '';
          break;

        // 2️⃣函数声明,如async function fn2() {}
        case 'FunctionDeclaration':
          asyncName = (asyncPath.node.id && asyncPath.node.id.name) || '';
          break;

        // 3️⃣async函数作为对象的方法,如vue项目中,在methods中定义的方法: methods: { async func() {} }
        case 'ObjectMethod':
          asyncName = asyncPath.node.key.name || '';
          break;
      }

      // 若asyncName不存在,通过argument.callee获取当前执行函数的name
      let funcName = asyncName || (node.argument.callee && node.argument.callee.name) || '';

      const temp = template(tryTemplate);

      // 给模版增加key,添加console.log打印信息
      let tempArgumentObj = {
        // 通过types.stringLiteral创建字符串字面量
        CatchError: types.stringLiteral(catchConsole(filePath, funcName, options.customLog))
      };

      // 通过temp创建try语句
      let tryNode = temp(tempArgumentObj);

      // 获取async节点(父节点)的函数体
      let info = asyncPath.node.body;

      // 将父节点原来的函数体放到try语句中
      tryNode.block.body.push(...info.body);

      // 将父节点的内容替换成新创建的try语句
      info.body = [tryNode];123465
      
    }
  };
  return {
    name: 'babel-plugin-await-add-try-catch',
    visitor
  };
};
// lib/util.js

const merge = require('deepmerge');

// 定义try语句模板
let tryTemplate = `
try {
} catch (e) {
console.log(CatchError,e)
}`;

/*
 * catch要打印的信息
 * @param {string} filePath - 当前执行文件的路径
 * @param {string} funcName - 当前执行方法的名称
 * @param {string} customLog - 用户自定义的打印信息
 */
let catchConsole = (filePath, funcName, customLog) => `
filePath: ${filePath}
funcName: ${funcName}
${customLog}:`;

// 默认配置
const defaultOptions = {
  customLog: 'Error',
  exclude: ['node_modules'],
  include: []
};

// 判断执行的file文件 是否在 options 选项 exclude/include 内
function matchesFile(list, filename) {
  return list.find((name) => name && filename.includes(name));
}

function mergeOptions(options) {
  let { exclude, include } = options;
  if (exclude) options.exclude = toArray(exclude);
  if (include) options.include = toArray(include);

  // 合并选项
  return merge.all([defaultOptions, options]);
}

function toArray(value) {
  return Array.isArray(value) ? value : [value];
}

module.exports = {
  tryTemplate,
  catchConsole,
  defaultOptions,
  mergeOptions,
  matchesFile,
  toArray
};
// package.json

{
  "name": "babel-plugin-async-await-add-try-catch",
  "version": "1.0.0",
  "description": "a babel plugin which can add try catch block statement in async await",
  "main": "lib/index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/forturegrant/babel-plugin-await-add-try-catch.git"
  },
  "keywords": [
    "babel",
    "plugin"
  ],
  "author": "forturegrant",
  "license": "MIT",
  "bugs": {
    "url": "https://github.com/forturegrant/babel-plugin-await-add-try-catch/issues"
  },
  "homepage": "https://github.com/forturegrant/babel-plugin-await-add-try-catch#readme",
  "dependencies": {
    "babel-template": "^6.26.0",
    "deepmerge": "^4.2.2"
  }
}
使用方法

还没发布这个plugin上npm之前,可以先用npm link进行本地调试
在.babelrc或者babel.config.js中加入我们的babel-plugin

// babel.config.js

module.exports = {
  presets: [
  ],
  plugins: [
    [
      require("babel-plugin-async-await-add-try-catch"),
      {
        exclude: ["build"], // 默认值 ['node_modules']
        include: ["src/client/index.tsx"], // 默认值 []
        customLog: "My customLog", // 默认值 'Error'
      },
    ],
  ],
};

在代码中加入async await语法

// src/client/index.tsx
async function fn() {
  await new Promise((resolve, reject) => reject("报错"));
  await new Promise((resolve) => resolve(1));
  console.log("do something...");
}

fn();

控制台可以看到报错的文件位置通过我们的babel-plugin-await-add-try-catch打印了出来

『Webpack进阶系列』—— 手写一个babel plugin

这证明我们已经在所有async await中加上了try catch

插件发布

最后我们npm publish发布一下这个插件,就可以通过npm i babel-plugin-async-await-add-try-catch --save-dev去使用了

方案二

全局监听事件


const test = async() => {
    await new Promise((resolve, reject) => {
        reject('报错');
        // reject(new Error('报错'));
        throw new Error('报错');
    })
    console.log('下面执行的内容')
}

test();

window.addEventListener('unhandledrejection', function (event) {
    event.preventDefault();  // 可以阻止报错
    console.log(event, 'event');
})

这样虽然也能捕获到错误,但是如果不是reject一个new Error是无法拿到错误的具体报错位置的堆栈信息的

好处与弊端

这些方案的好处在于可以不用去所有的async await加上try catch去处理,但似乎弊端也很明显,就是无法阻止报错带来的堵塞,如果想要在捕获到错误之后继续执行,还是得使用try...catch...finally去对业务进行具体的处理,或者还有一些比较好用的库,比如async-to-js等库,相比try catch会更加优雅一点

其他

仓库链接:github.com/forturegran…