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

背景

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

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

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

几个需要知道的知识点

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

  • 小程序onError只能捕获同步代码错误,不能捕获异步代码错误。

    具体原因是因为小程序在内部实现时会对逻辑层的js方法进行try-catch封装,对于其中的异步代码异常则不能捕获。

  • try-catch不能捕获异步异常,但是可以捕获async/await函数异常。

    如下面代码的异常try-catch可以捕获:

    function asyncFn() {
        try {
            await exectionFn()
        } catch(err) { // exectionFn函数发生的异常可以及时被catch住
            console.error(err)
        }
    }
  • 小程序项目代码中无法访问window对象,并不意味着其脱离web渲染。

    这一点对自定义的babel转换配置来说尤其需要注意,因为小程序无法访问window对象,那么该对象上的api就无法访问,例如Promise。这对根据window是否定义过指定api来判断是否对其转换的babel插件来说意味着,不管怎样都会对用到的es6新的api进行转换,即使浏览器已经内置了该api的实现。

    例如babel-runtime在转换Promise时就采用polyfill的实现机制,而不是内置实现机制,带来的问题是:

    Promise的polyfill实现,代码产生的异常在不用Promise.catch或者unhandledrejection事件进行捕获的情况下也不会向上抛异常(小程序开发者工具控制台无法得到错误信息),而内置的原生实现则会向上抛

    这也是为什么采用自定义babel代码转换配置时,控制台无法捕获到异步代码异常信息的原因。

    顺便说一下,有小程序经验的同学可能会问,用小程序自带的es6转es5代码转换构建时,异步代码中的异常是可以在小程序开发者工具控制台捕获到的啊;这是因为小程序自带的源码转换只对es6的语法进行转换,而没有对像Promise这样的api进行转换,所以其使用的是原生的Promise实现。

  • babel在转换async/await异步时会有两层try-catch封装

    babel是如何转换async/await的可以看看这篇文章 。下面简单看一下async/await的代码转换的两层try-catch封装。

    例如如下代码:

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

    转换后的代码如下图:

    其中,mark方法返回的函数,调用该函数原型上的方法会被加上try-catch,如下图:

    另外,wrap方法的参数函数callee$也会被try-catch包裹,如下

    function tryCatch(fn, obj, arg) { // fn为wrap方法的函数参数_callee$
        try {
          return { type: "normal", arg: fn.call(obj, arg) };
        } catch (err) {
          return { type: "throw", arg: err };
        }
      }

    这样,async/await异步方法发生异常时首先会被转换代码中的tryCatch捕获,最终转换代码会通过throw将异常抛出,而其会被上层的try-catch捕获到,其最终会通过调用Promise的reject方法来处理,代码如上图所示。

小程序捕获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)
    }
}

具体的转换规则如下:

  • 只对async函数进行转换,其他的函数不转换,若满足则看第二点
  • async函数整个函数体若有try-catch则不进行转换,否则进行转换。

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

  • 字符串配合正则

    这种方式需要利用字符串的相关API(如replace、substring等)并配合正则表达式来实现,是一种粗粒度的转换,并且对正则的要求比较高。

  • 抽象语法树(AST)

    这种方式将源码转换为JSON对象,可以更精细地对源码进行转换。例如下面代码

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

    经ast转换后生成的如下JSON内容以tree结构如下图:

    可以自己尝试在网站https://astexplorer.net在线查看代码转换结果。具体的ast可以参考babel手册对其的介绍。

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

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

loader开发

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

  • 代码解析,将代码解析为AST,由@babel/parser负责完成
  • AST转换,遍历并操作AST来改变源码,由@babel/traverse负责遍历AST,辅助@babel/types负责操作变换
  • 代码生成,根据变换后的AST生成代码,由@babel/generator负责完成

根据上面提到的,我们只对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方法在控制台打印出错异步方法的堆栈信息,及时发现开发过程遇到的问题,增强开发者的开发体验。

参考文献

原文地址:https://www.cnblogs.com/wonyun/p/11418099.html

时间: 2024-10-11 16:58:04

微信小程序捕获async/await函数异常实践的相关文章

微信小程序 如何定义全局函数?

微信小程序 定义全局数据.函数复用.模版等 微信小程序 定义全局数据.函数复用.模版等问题总结: 1.如何定义全局数据 在app.js的App({})中定义的数据或函数都是全局的,在页面中可以通过var app = getApp();? app.function/key的方式调用,不过我们没有必要再app.js中定义全局函数. 2.如何实现代码的复用 函数的复用: test.js test: function(){ } module.exports={ test:test } ? other.j

微信小程序如何创建云函数并安装wx-server-sdk依赖

时间:2020/01/23 步骤 1.在微信开发者工具中云函数所在的文件夹的图标与其他文件夹是不同的,如下(第一个是云函数): 如果需要使一个普通文件变为云函数文件夹,需要在project.config.json(配置文件)中添加一行: "cloudfunctionRoot": "cloudfunctions/", 2.要使用云函数必须先安装node.js,安装后在cmd中输入node -v和npm -v打印版本号就可以知道是否安装成功. 3.需要为云函数安装wx-

带你入门微信小程序

认识微信小程序 2016年1月9日,TX启动小程序研发,于2017年1月9日正式发布.不同于微信订阅号或公众号,微信小程序被赋予了应用程序的能力,他是一种无需安装即可使用的应用,它实现了应用“触手可及”的梦想,用户“扫一扫”或者“搜一搜”即可打开应用:体现了“用完就走”的理念,用户不再需要关心是否安装太多应用问题.应用将无处不在,随时随地可用,无需卸载. 微信小程序相关技术 微信小程序自定义了一套语言,称为WXML微信标记语言,它的使用方法类似于HTML语言.另外,微信小程序还定义了自己的样式语

微信小程序开发之不能使用eval函数的问题

一 eval函数问题 JavaScript中的eval函数是颇受开发者争议的问题之一,问题主要在于其可能导致的不安全性.有关此方面问题,在此不再赘述,读者可能很容易地浏览到许多介绍性文章. 但是,eval函数的优点也是很明显的.例如,使用JS编写一个计算器程序,在遇到"2+1-3*5"这样的字符串时,使用eval就可以很容易地计算出,类似如: var s="2+1-3*5"; console.log(eval(s)); 二 微信小程序练手遇到问题 (1)微信小程序环

如何在微信小程序定义全局变量、全局函数、如何实现 函数复用 模块化开发等问题详解

1.如何定义全局数据 在app.js的App({})中定义的数据或函数都是全局的,在页面中可以通过var app = getApp();  app.function/key的方式调用,不过我们没有必要再app.js中定义全局函数. 定义 全局函数 如下图: 2.如何实现代码的复用 实现函数的复用前我们先复习一个知识点:require 用来加载代码,而 exports 和 module.exports 则用来导出代码.module.exports 对象是由模块系统创建的.在我们自己写模块的时候,需

如何使用微信小程序云函数发送短信验证码

其实微信小程序前端和云端都是可以调用短信平台接口发送短信的,使用云端云函数的好处是无需配置域名,也没有个数限制. 本文使用的是榛子云短信平台(http://smsow.zhenzikj.com) ,SDK下载: http://smsow.zhenzikj.com/doc/sdk.html 1. 安装下载后的SDK在cloudfunctions文件夹下会包含3个云函数文件夹,如下: 由于目前IDE没有云函数导入功能,您需要手工创建同名的云函数,然后将云函数下的文件手工拷进去 注:下载的SDK是一个

微信小程序 res =>的意义及userInfoReadyCallback函数的作用

刚开始接触微信小程序,想写个迷你计算器的小程序,感觉开发挺方便的,当准备使用用户信息时,打算看一下它是怎么获取用户信息的,为之后获取用户openid准备.获取用户信息主要在app.js 和 index.js中 代码中的 res =>可以理解为function(res),其中res即为返回的数据结果对象,不知道又是哪新出的语法规则(后来补充,这个是ES6的箭头函数,https://blog.csdn.net/zjw_python/article/details/80880208) 原文:https

微信小程序:防止多次点击跳转(函数节流)

场景 在使用小程序的时候会出现这样一种情况:当网络条件差或卡顿的情况下,使用者会认为点击无效而进行多次点击,最后出现多次跳转页面的情况,就像下图(快速点击了两次): 解决办法 然后从 轻松理解JS函数节流和函数防抖 中找到了解决办法,就是函数节流(throttle):函数在一段时间内多次触发只会执行第一次,在这段时间结束前,不管触发多少次也不会执行函数. /utils/util.js: function throttle(fn, gapTime) { if (gapTime == null ||

如何优雅地处理Async/Await的异常?

译者按: 使用.catch()来捕获所有的异常 原文: Async Await Error Handling in JavaScript 译者: Fundebug 本文采用意译,版权归原作者所有 async/await 中的异常处理很让人混乱.尽管有很多种方式来应对async 函数的异常,但是连经验丰富的开发者有时候也会搞错. 假设你有一个叫做run()的异步函数.在本文中,我会描述 3 种方式来处理run()的异常情形: try/catch, Go 语言风格, 函数调用的时候使用 catch(