微信小程序捕获async/await函数异常实践

时间:2019-09-03 11:31:00   收藏:0   阅读:168

背景

我们的小程序项目的构建是与web项目保持一致的,完全使用webpack的生态来构建,没有使用小程序自带的构建功能,那么就需要我们配置代码转换的babel插件如PromiseProxy等;另外,项目中涉及到异步的功能我们统一使用async/await来处理。我们知道,小程序的onError 生命周期只能捕获同步错误,而完全不采用小程序自带构建工具的情况下,开发模式下遇到的问题:

小程序异步代码中的异常onError无法捕获,开发者工具控制台也没有抛出异常信息

这样在开发过程中页面展示异常,但是无任何异常信息输出,只有代码单步调试时走到异常之处才能发现异常发生的地方,这对开发者很不友好。下面就来说说项目在完全用webpack构建情况下如何在小程序项目中捕获异步代码方面的实践。

几个需要知道的知识点

首先,在切入正文之前介绍几个知识点:

小程序捕获async/await异步代码异常实现

上面提到,try-catch可以捕获到async/await代码中的异常,利用这一点我们可以对async函数添加try-catch封装来捕获其中异常错误信息。但是手动的为每个async函数添加try-catch过于机械,并且对已有项目均需要添加。为此我们可以利用webpack loader来对代码进行转换,自动为async函数添加try-catch封装。例如:

async function test() {
 console.log('hello async')
}

转换为:

async function test(){
    try{
        console.log('hello async')
    }catch(err) {
        console.error('async test函数异常:', err)
    }
}

具体的转换规则如下:

我们写的源码其实就是字符串,对源码进行转换其实就是对字符串内容进行转换,可以想到两种方式来实现:

因为我们使用webpack来构建项目,所以利用webpack loader对字符串代码进行AST转换是自然而然的事。webpack loader的原理本文就不做过多介绍,类似文章有很多,不熟悉的可以自行google。

因为小程序项目都是使用Page(object)或者Component(object),因此我们将代码变换范围缩小为Page或者Component方法的对象参数中的async函数。

loader开发

webpack loader接收源码字符串,要经过三个步骤来完成代码转换,babel6/7分别有对应的npm包来负责处理,例如babel7中:

根据上面提到的,我们只对Page和Component方法中传入的对象参数中的async函数进行转换,所以我们对AST的ObjectMethod进行转换。

const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const generate = require('@babel/generator').default;
const t = require('@babel/types');

module.exports = function(source) {
    let ast = parser.parse(source, {sourceType: 'module'}); // 支持es6 module
    
    traverse(ast, {
      ObjectMethod(path) {
        ...
      }
    });
   return generate(ast).code
}

根据上面代码转换规则,只对整个函数体没有被try-catch包裹的aysnc函数进行转换,若有则不进行转换。

const vistor = {
    ObjectMethod(path) {
      const isAsyncFun = t.isObjectMethod(path.node, {async: true});
      if (isAsyncFun) {
        const currentBodyNode = path.get('body');
        if (t.isBlockStatement(currentBodyNode)) {
          const asyncFunFirstNode = currentBodyNode.node.body;

          if (asyncFunFirstNode.length === 0) {
            return;
          }
          if (asyncFunFirstNode.length !== 1 || !t.isTryStatement(asyncFunFirstNode[0])) {
            let catchCode = `console.error("async ${path.get('key').node.name}函数异常: ", err)`;
            let tryCatchAst = t.tryStatement(
              currentBodyNode.node,
              t.catchClause(
                t.identifier('err'),
                t.blockStatement(parser.parse(catchCode).program.body)
              )
            );
            currentBodyNode.replaceWithMultiple([tryCatchAst]);
          }
        }
      }
    }
  };

loader使用

一般loader使用是通过webpack来配置loader适用的匹配规则的,如js文件使用loader配置一样:

{
    test: /\.js$/,
    use: "babel-loader"
}

但是对于使用滴滴开源的MPX来搭建的小程序项目,其跟vue类似:模板、js、样式以及页面配置JSON内容写在一个后缀为.mpx文件中;其配套提供的@mpxjs/webpack-plugin包自带loader来处理该后缀文件,其作用与vue-loader类似,将模板、js、css和json内容转换以loader内联的方式来进行分别处理。

例如对index.mpx文件经过该loader输出内容如下图:
技术图片

这样就对不同的内容处理成选择对应的loader以内联方式来处理。而我们处理async函数的loader是要对mpx文件中的js内容进行转换,所以就不能直接像上面配置js文件使用babel-loader来处理一样;我们需要在babel-loader处理转换js内容之前添加自定义loader,即在处理js内容的内联loader字符串中加入自已的loader。

如何加呢?我们可以利用webpack的插件机制,在webpack解析模块时修改内联loader内容,正好webpack提供了normalModuleFactory钩子函数:

const path = require('path');
const asyncCatchLoader = path.resolve(__dirname, './mpx-async-catch-loader.js');
class AsyncTryCatchPlugin {
  constructor(options) {
    this.options = options;
  }

  apply(compiler) {
    compiler.hooks.normalModuleFactory.tap('AsyncTryCatchPlugin', normalModuleFactory => {
      normalModuleFactory.hooks.beforeResolve.tapAsync('AsyncTryCatchPlugin', (data, callback) => {
        let request = data.request;
        if (/!+babel-loader!/.test(request)) {
          let elements = request.replace(/^-?!+/, '').replace(/!!+/g, '!').split('!');
          let resourcePath = elements.pop();
          let resourceQuery = '?';
          const queryIdx = resourcePath.indexOf(resourceQuery);
          if (queryIdx >= 0) {
            resourcePath = resourcePath.substr(0, queryIdx);
          }
          if (!/node_modules/.test(data.context) && /\.mpx$/.test(resourcePath)) {
            data.request = data.request.replace(/(babel-loader!)/, `$1${asyncCatchLoader}!`);
          }
        }
        callback(null, data);
      });
    });
  }
}

module.exports = AsyncTryCatchPlugin;

这样添加该插件后,该loader就会对mpx文件的js内容添加对async函数的转换;目前该loader插件只用在开发环境,通过console.error方法在控制台打印出错异步方法的堆栈信息,及时发现开发过程遇到的问题,增强开发者的开发体验。

参考文献

评论(0
© 2014 mamicode.com 版权所有 京ICP备13008772号-2  联系我们:gaon5@hotmail.com
迷上了代码!