背景

我们的小程序项目的构建是与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对象,即使通过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函数异常实践的更多相关文章

  1. 微信小程序使用async await的一些技巧

    在小程序onLoad事件中使用getItems(this) 和this.getItems() getItems(this)对应的方法为 this.getItems()对应的方法为 在getItems( ...

  2. 微信小程序开发 -- 通过云函数下载任意文件

    微信小程序开发 -- 通过云函数下载任意文件 1.云开发介绍 ​ 微信小程序开发者众所周知,小程序开发拥有许多限制,当我还是一个菜鸟入门的时候,第一关就卡在了没有备案域名的HTTP请求上面,那时候云开 ...

  3. 微信小程序开发——使用回调函数出现异常:TypeError: Cannot read property 'setData' of undefined

    关键技术点: 作用域问题——回调函数中的作用域已经脱离了调用函数了,因此需要在回调函数外边把this赋给一个新的变量才可以了. 业务需求: 微信小程序开发,业务逻辑需要,需要把获取手机号码的业务逻辑作 ...

  4. 个微信小程序云开发云函数

    1. project.config.json写上云函数所在目录"cloudfunctionRoot": "cloudfunctions/",如图 2. app. ...

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

    微信小程序 定义全局数据.函数复用.模版等 微信小程序定义全局数据.函数复用.模版等问题总结: 1.如何定义全局数据 在app.js的App({})中定义的数据或函数都是全局的,在页面中可以通过var ...

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

    时间:2020/01/23 步骤 1.在微信开发者工具中云函数所在的文件夹的图标与其他文件夹是不同的,如下(第一个是云函数): 如果需要使一个普通文件变为云函数文件夹,需要在project.confi ...

  7. 微信小程序云开发-云函数-云函数获取参数并实现运算

    1.编写加法运算的云函数addData 2.在本地小程序页面调用云函数

  8. 微信小程序云开发-云函数-云函数实现数据的查询、修改和删除功能

    一.云函数获取商品信息 1.创建云函数getData,云函数功能:获取商品信息 2.在本地小程序页面调用云函数getData  二.云函数修改商品信息 1.创建云函数updateData,云函数功能: ...

  9. 微信小程序web-view的简单思考和实践

    微信小程序的组件web-view推出有一段时间了,这个组件的推出可以说是微信小程序开发的一个重要事件,让微信小程序不会只束缚在微信圈子里了,打开了一个口子,这个口子或许还比较小,但未来有无限可能. 简 ...

随机推荐

  1. IrisSkin2.dll 添加皮肤

    使用说明:把控件拖到你的form上,只需一行代码,即可实现整个form包括其所有控件的皮肤的更换,总共有几十套皮肤供使用,非常方便.省去你设计开发软件皮肤系统的时间和精力.全部源代码就一行: skin ...

  2. Shell基本语法---shell脚本的输入以及脚本拥有特效地输出

    shell脚本的输入 语法:read -参数 -p:给出提示符.默认不支持"\n"换行 -s:隐藏输入的内容 -t:给出等待的时间,超时会退出read,单位是秒 -n:限制读取字符 ...

  3. 【iOS】UITableViewDelegate 方法没有调用

    可能原因:没有调用 reloadData 方法. [self.tableView reloadData];

  4. 你可能不知道的Docker资源限制

    What is 资源限制? 默认情况下,容器是没有资源限制的,它会尽可能地使用宿主机能够分配给它的资源.Docker提供了一种控制分配多少量的内存.CPU或阻塞I/O给一个容器的方式,即通过在dock ...

  5. Transformations 方块转换 USACO 模拟 数组 数学 耐心

    1006: 1.2.2 Transformations 方块转换 时间限制: 1 Sec  内存限制: 128 MB提交: 10  解决: 7[提交] [状态] [讨论版] [命题人:外部导入] 题目 ...

  6. JavaScript ES6和ES5闭包的小demo

    版权声明:署名,允许他人基于本文进行创作,且必须基于与原先许可协议相同的许可协议分发本文 (Creative Commons) 可能有些小伙伴不知道ES6的写法,这儿先填写一个小例子 let conn ...

  7. 注解与AOP切面编程实现redis缓存与数据库查询的解耦

    一般缓存与数据库的配合使用是这样的. 1.查询缓存中是否有数据. 2.缓存中无数据,查询数据库. 3.把数据库数据插入到缓存中. 其实我们发现 1,3 都是固定的套路,只有2 是真正的业务代码.我们可 ...

  8. Hadoop 系列(三)—— 分布式计算框架 MapReduce

    一.MapReduce概述 Hadoop MapReduce 是一个分布式计算框架,用于编写批处理应用程序.编写好的程序可以提交到 Hadoop 集群上用于并行处理大规模的数据集. MapReduce ...

  9. java并发编程(十七)----(线程池)java线程池架构和原理

    前面我们简单介绍了线程池的使用,但是对于其如何运行我们还不清楚,Executors为我们提供了简单的线程工厂类,但是我们知道ThreadPoolExecutor是线程池的具体实现类.我们先从他开始分析 ...

  10. Unix-IO-同步,异步,阻塞,非阻塞-笔记篇

    概念更正 https://www.zhihu.com/question/19732473 错误的四个象限分类 https://www.ibm.com/developerworks/cn/linux/l ...