微信小程序捕获async/await函数异常实践
背景
我们的小程序项目的构建是与web项目保持一致的,完全使用webpack的生态来构建,没有使用小程序自带的构建功能,那么就需要我们配置代码转换的babel插件如Promise、Proxy等;另外,项目中涉及到异步的功能我们统一使用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对象,即使通过
Function('return this')()来访问全局作用域也不起作用,因为小程序重写了Function,如下图源码;具体可以查看从微信小程序开发者工具源码看实现原理(一)- - 小程序架构设计这篇文章。

那么,就不能通过window访问该对象上的api,例如window.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方法在控制台打印出错异步方法的堆栈信息,及时发现开发过程遇到的问题,增强开发者的开发体验。
参考文献
微信小程序捕获async/await函数异常实践的更多相关文章
- 微信小程序使用async await的一些技巧
在小程序onLoad事件中使用getItems(this) 和this.getItems() getItems(this)对应的方法为 this.getItems()对应的方法为 在getItems( ...
- 微信小程序开发 -- 通过云函数下载任意文件
微信小程序开发 -- 通过云函数下载任意文件 1.云开发介绍 微信小程序开发者众所周知,小程序开发拥有许多限制,当我还是一个菜鸟入门的时候,第一关就卡在了没有备案域名的HTTP请求上面,那时候云开 ...
- 微信小程序开发——使用回调函数出现异常:TypeError: Cannot read property 'setData' of undefined
关键技术点: 作用域问题——回调函数中的作用域已经脱离了调用函数了,因此需要在回调函数外边把this赋给一个新的变量才可以了. 业务需求: 微信小程序开发,业务逻辑需要,需要把获取手机号码的业务逻辑作 ...
- 个微信小程序云开发云函数
1. project.config.json写上云函数所在目录"cloudfunctionRoot": "cloudfunctions/",如图 2. app. ...
- 微信小程序 如何定义全局函数?
微信小程序 定义全局数据.函数复用.模版等 微信小程序定义全局数据.函数复用.模版等问题总结: 1.如何定义全局数据 在app.js的App({})中定义的数据或函数都是全局的,在页面中可以通过var ...
- 微信小程序如何创建云函数并安装wx-server-sdk依赖
时间:2020/01/23 步骤 1.在微信开发者工具中云函数所在的文件夹的图标与其他文件夹是不同的,如下(第一个是云函数): 如果需要使一个普通文件变为云函数文件夹,需要在project.confi ...
- 微信小程序云开发-云函数-云函数获取参数并实现运算
1.编写加法运算的云函数addData 2.在本地小程序页面调用云函数
- 微信小程序云开发-云函数-云函数实现数据的查询、修改和删除功能
一.云函数获取商品信息 1.创建云函数getData,云函数功能:获取商品信息 2.在本地小程序页面调用云函数getData 二.云函数修改商品信息 1.创建云函数updateData,云函数功能: ...
- 微信小程序web-view的简单思考和实践
微信小程序的组件web-view推出有一段时间了,这个组件的推出可以说是微信小程序开发的一个重要事件,让微信小程序不会只束缚在微信圈子里了,打开了一个口子,这个口子或许还比较小,但未来有无限可能. 简 ...
随机推荐
- Webpack 下使用 web workers 及 基本原理 和 应用场景
_ 阅读目录 一:web workers的基本原理 二:web Workers 的基本用法 三:在webpack中配置 Web Workers 四:Web Worker的应用场景 回到顶部 一:web ...
- Hadoop自学系列集(一) ---- 使用VMware安装CentOS
1.概述 笔者的学习环境--在VMware虚拟机下安装四个CentOS系统(搭建Hadoop集群用),其中一个为Master,三个为Slave,Master作为Hadoop集群中的NameNode, ...
- Echarts图表插件(4.x版本)使用(二、带分类筛选的多个图表/实例化多个ECharts,以关系图/force为例)
导读 如果想在一个页面里实例化带分类筛选的多个Echarts该怎么做呢? 曾探讨了带分类选择的关系图显示为自定义图片的需求实现,传送门ECharts图表插件(4.x版本)使用(一.关系图force节点 ...
- 【Java例题】5.5 映射类的使用
5.映射类的使用.使用HashMap保存英文-中文对照单词词典.单词词典可以增加和删除词汇.输入一个英文单词,翻译成中文并显示.输入一个中文单词,翻译成英文并显示. package chapter6; ...
- DedeCMS V5.7 SP2前台文件上传漏洞(CVE-2018-20129)
DedeCMS V5.7 SP2前台文件上传漏洞(CVE-2018-20129) 一.漏洞描述 织梦内容管理系统(Dedecms)是一款PHP开源网站管理系统.Dedecms V5.7 SP2版本中的 ...
- Jedis的配置和优化
参数名:maxTotal 含义:资源池最大连接数 [默认值:8] 使用建议:需要考虑以下几点 1.业务希望的Redis并发量 2.客户端执行命令时间 3.Redis资源:例如应用个数(客户端)* ma ...
- Unity进阶之ET网络游戏开发框架 03-Hotfix层启动
版权申明: 本文原创首发于以下网站: 博客园『优梦创客』的空间:https://www.cnblogs.com/raymondking123 优梦创客的官方博客:https://91make.top ...
- Python 類別 class 的繼承 Inheritance
既然 Python 是面向物件 Object Oriented 語言,它就有類別 Class 與物件 Object 的概念. 甚麼是類別 class ? 簡單講: 類別好比蓋房子的施工藍圖 Blue ...
- warpAffine仿射变换
仿射变换,其实就是不同的坐标系的相互转换,用于图像的平移和旋转. 首先看一下官方的api描述. https://docs.opencv.org/2.4/modules/imgproc/doc/geom ...
- 拼写单词[哈希表]----leetcode周赛150_1001
题目描述: 给你一份『词汇表』(字符串数组) words 和一张『字母表』(字符串) chars. 假如你可以用 chars 中的『字母』(字符)拼写出 words 中的某个『单词』(字符串),那么我 ...