现场教学,优雅地处理基于 Vue CLI 项目中的 async await 异常

2,684 阅读4分钟

前言

了解过在实际项目中处理 async await 异常方式同学应该知道,常见的捕获方式:

  • 使用 errorCaptured 来调用 async 函数。
  • 使用 pluginloader 在打包的时候统一包裹 try catch

相比较这两者,显然后者更好,因为它甚至连 errorCaptured 都不需要写。但是,实际中,使用 loader 并不是这么简单,例如当你的项目是基于「Vue CLI」的时候,此时你想改一些配置,那可能够你喝一壶的...

所以,今天我们就来聊聊如何优雅地处理基于「Vue CLI」项目中的 async await 异常~

准备一个 Loader

其实,对于「loader」而言,这里可以是「babel-plugin」,也可以是「webpack-loader」。前者是基于 visitor,后者是基于 @babel/traverse 的能力。但是,这两者的本质是一样的,都是针对 AwaitExpression AST 节点来做 TryStatement 「AST」节点的添加。

这里我们就用很多同学熟悉的桃翁写的一个「webpack-loader」async-catch-loader

  1. 安装 async-catch-loader 依赖:
npm i async-catch-loader -D

2.优化一下 async-catch-loader 源码:

// 部分代码
...
traverse(ast, {
  AwaitExpression(path) {
    // 递归向上找异步函数的 node 节点
    while (path && path.node) {
      let parentPath = path.parentPath;
      if (
        // 找到 async Function
        t.isBlockStatement(path.node) &&
        isAsyncFuncNode(parentPath.node)
      ) {
        let tryCatchAst = t.tryStatement(
          path.node,
          t.catchClause(
            t.identifier(options.identifier),
            t.blockStatement(catchNode)
          ),
          finallyNode && t.blockStatement(finallyNode)
        );
        path.replaceWithMultiple([tryCatchAst]);
        return;
      } else if (
        // 已经包含 try 语句则直接退出
        t.isBlockStatement(path.node) &&
        t.isTryStatement(parentPath.node)
      ) {
        return;
      }
      path = parentPath;
    }
  }
});
...

可以看到,原先它是在 traverseAwaitExpression 中通过循环 pathpath = path.parent 的方式向上查找 BlockStatement,即块级 AST 节点,然后替换该 AST 节点为一个 TryStatement AST 节点。

但是,其实 babelpath 对象上提供了很多便捷的方法可以便于节点的查找,这里我们使用 findParent() 来代替 while 循环向上查找过程:

traverse(ast, {
    AwaitExpression(path) {
      // 已经包含 try 语句则直接退出
      if (
        path.findParent(path => t.isTryStatement(path.node))
      ) {
        return;
      }

      const blockParent = path.findParent(path => t.isBlockStatement(path.node))
      const tryCatchAst = t.tryStatement(
        blockParent.node,
        t.catchClause(
          t.identifier(options.identifier),
          t.blockStatement(catchNode)
        ),
        finallyNode && t.blockStatement(finallyNode)
      )
      blockParent.replaceWithMultiple([tryCatchAst])
    }
  });

在处理好 async-catch-loader 后,我们就需要使用它,即在「Webpack」对应规则中配置它。

魔改 Vue CLI 源码

魔改一词,听起来是不是有点激动。但是,实不相瞒,我们只是在「Vue CLI」的源码中加了几行代码。

当然,这也是在理解「Vue CLI」的运行流程的前提下,才能这么精准的加代码!

由于,我们需要借助 @babel/traverse 的能力,所以必须要在 babel-loader 对代码进行转化之前使用它。

而,传统的基于「Vue CLI」使用 JavaScript 开发的同学倒是不用担忧这个问题,直接使用官网提供的 API 在 js 规则下添加 async-catch-loader 就行:

const jsRule = config.module.rule('js')

jsRule.use('async-catch-loader')
      .loader('async-catch-loader')
      .tap(() => {
        return {
          catchCode: `console.error(e)`
        }
      })

但是,对于基于「Vue CLI」使用 TypeScript 开发的同学,可能就有点伤脑筋了...因为,此时在 babel-loader 之前加载的是 ts-loader,如果此时你也像上面一样直接添加 async-catch-loader,那迎接你的就是一堆报错,告诉你一些 feature 暂不支持。

并且,很不幸地是「Vue CLI」并不支持在某一个地方插入一个 loader,你只能自上而下地添加 loader。那么,此时可供我们选择的只有重写整个 ts 规则对应的 loader,但是实际上你只是为了加一个 loader,其余 loader 只是还原原先写法。明显,这个选择很...

所谓,山穷水尽疑无路,柳暗花明又一村。「Vue CLI」中是由 @vue/cli-plugin-typescript/index.js 来负责处理 .ts 文件,进行 .ts.js 的转化过程。所以,如果仅仅是本地环境下,我们可以在这个文件中添加 async-catch-loader,让它在 ts-loader 之后加载,babel-loader 之前加载:

// node_modules/@vue/cli-plugin-typescript/index.js 64 行
addLoader({
  name: "async-catch-loader",
  loader: require.resolve("async-catch-loader"),
  options: {
    catchCode: `console.error(e)`
  }
})

这样,最终 .ts 规则的 loader 加载顺序就是 ts-laoderasync-catch-loader 再到 babel-loader,也就避免了重写 .ts 规则时的重复劳动,便捷地实现了 async-catch-loader 的引入。

写在最后

虽然,通过往 @vue/cli-plugin-typescript/index.js 手动加入代码可以更改 loader 加载顺序,然后实现优雅地处理 async await 异常。但是,这种方式存在的缺陷是很明显的,由于我们是在安装的 npm 包中修改的代码,那么在团队合作方面显然是不友好的,因为每个人都会存在第一次或重新安装依赖的过程,此时如果他们也需要打包,就需要重复上述操作。

所以,这之间的取舍,各位同学自己看哈,或者其他方式实现。

参考

async-catch-loader

babel 插件开发手册

往期文章回顾

深度解读 Vue3 源码 | 组件创建过程 | 掘金技术征文-双节特别篇

深度解读 Vue3 源码 | 内置组件 teleport 是什么“来头”?

深度解读 Vue3 源码 | compile 和 runtime 结合的 patch 过程

❤️爱心三连击

写作不易,如果你觉得有收获的话,可以爱心三连击!!!

前端问路人 —— 五柳(微信公众号: Code center)