写一个为await自动加上catch的loader逐渐了解AST以及babel
为什么要写这个loader
我们在日常开发中经常用到async await去请求接口,解决异步。可async await语法的缺点就是若await后的Promise抛出错误不能捕获,整段代码区就会卡住。从而使下面的逻辑不能顺利执行。也许会有人说,卡住就是为了不进行后续的代码,以免造成更大的错误,可大多数情况下需要catch住错误并给出一个边界值使代码正常执行。 我以前经常常常会这么写:
const request = async (){
const { data = [] } = await getList() || {};
//...other
};
这样写看似有些**高端**,但其实风险系数很高,假设```getList()```请求发生了错误并且没有捕获到,那么后边的逻辑或表达式并不会生效,后续的代码并不能顺序执行。
这种情况的最优解就是```getList()```能后捕获到错误,虽然现在大多数axios都会catch,但是业务开发中应该不止请求才会用到Promise。那么另一种解法是?
const request = async (){
const { data = [] } = await getList().catch(err=>{ //...do you want to do }) || {};
//...other
};
这个loader解决的问题
自己写的loader就是解决日常开发中忘记写catch的情况。
先说一下自己写的loader的功能:
1. 可以自动为await后的promise加上catch
2. 可以决定是否需要在catch函数中打印error以及return出一个边界值,可以选择加上自己的代码
3. 若是await函数外层有被try catch包裹或者本身后边就已经有catch,则不会做任何处理
//一个普通的async函数
const fn = async () => {
const a = await pro()
}
//会被转化成
const fn = async () => {
const a = await pro().catch(err=>{})
}
//若是需要打印error以及return出一个边界值
const fn = async () => {
const { a } = await pro().catch(err=>{ console.log(err); return { } });
}
//or
const fn = async () => {
const [ a ] = await pro().catch(err=>{ console.log(err); return [ ] });
}
//若是需要自己额外的代码处理,自己的代码贼会在console前面,假设自己代码为 message.error(error)
const fn = async () => {
const [ a ] = await pro().catch(err=>{ message.error(err);console.log(err); return [ ] });
}
// 如果被try catch包裹,则不会进行任何处理,因为catch可以捕获到错误,擅自增加catch会扰乱原有的逻辑
const fn = async () => {
// 保持原样
try{
const [ a ] = await pro()
}catch(err){}
}
具体代码+讲解
接下来上代码
//add-catch-loader.js
const parser = require("@babel/parser");
const traverse = require("babel-traverse").default;
const t = require("babel-types");
const template = require("@babel/template");
const babel = require("@babel/core");
先来介绍一下各个babel包的作用
- @babel/parser:解析js代码生成ast,因为loader读取的js文件中的源码,而我们又不能直接操作源码进行修改,只能先转为ast进行操作。
- babel-traverse:遍历ast,因为ast是一颗树形结构,其中每个操作符、表达式等都是一个节点,是整颗树上的一个枝干,我们通过traverse去遍历整棵树来获取其中一个节点的信息来修改它。
- babel-types:我用来判断一个节点的类型。
- @babel/template:我用来将代码段转为ast节点。
- @babel/core:代码生成,ast操作完后得到了一颗新的ast,那么需要把ast在转为js代码输出到文件中。
通过上边的几个包就看出了babel处理js的三个过程:解析(parase)、转换(transform)、生成(generator)
loader就是一个纯函数,它能获取当前文件的源代码
//a.js
const num = 1;
console.log(num);
```
那么source就是"const num = 1;console.log(num);"而我们把它转化为ast又是什么样子呢?
我把它转化为了json结果,我只截取了部分(因为太长了),大家可以去[这个网站](https://astexplorer.net/)输入一段js代码看看转化成了什么样~
## AST的大概结构
```json
{
"type": "File",
"start": 0,
"end": 32,
"loc": {
"start": {
"line": 1,
"column": 0
},
"end": {
"line": 3,
"column": 16
}
},
"errors": [],
"program": {
"type": "Program",
"start": 0,
"end": 32,
"loc": {
"start": {
"line": 1,
"column": 0
},
"end": {
"line": 3,
"column": 16
...
},
"comments": []
}
解决问题的思路
在ast结构中,每一个有type属性的对象都是一个节点,里面包含了这个节点的全部信息,而我们既然要操做await后的promise,那么就只需要看await操作符上下的节点就可以了,先看一下await的节点长什么样子。
上图只是 const a = await po()这一段代码的ast,其中大部分还折叠起来了。但是我们只需要关系await后的代码ast,即po()。
AwaitExpression这个节点是await po()这段代码,CallExpression这个节点是po()这个节点。那么await po().catch(err=>{ })代码的节点又长什么样子呢?
如下图,AwaitExpression是await pro().catch(err=>{});整段代码的节点,MemberExpression是pro().catch;的节点,arguments是函数体的参数,而ArrowFunctionExpression代表的就是err={},所以我们只需要把po()替换成po().catch(err=>{})。
比较一下po()和po().catch的不同(由于catch函数中的回调函数是参数,属于和po().catch一个级别,所以不把它算在内)
po()
po().catch()
从上图中就可以看出来CallExpression节点换成了MemberExpression,那么开始上代码。
具体代码
source就是读取的文件中的源码内容。
parser.parse就是将源代码转为AST,如果源代码中使用export和import,那么sourceType必须是module,plugin必须使用dynamicImport,jsx是为了解析jsx语法,classProperties是为了解析class语法。
//add-catch-loader.js
const parser = require("@babel/parser");
const traverse = require("babel-traverse").default;
const t = require("babel-types");
const template = require("@babel/template");
const babel = require("@babel/core");
const { createCatchIdentifier, createArguments } = require("./utils"); //自己写的方法 function addCatchLoader(source){
let ast = parser.parse(source, {
sourceType: "module",
plugins: ["dynamicImport", "jsx","classProperties"],
});
}
获得到AST语法树我们就可以使用traverse进行遍历了,traverse第一个参数是要遍历的ast,第二个参数是暴露出来的节点API。
//add-catch-loader.js
const parser = require("@babel/parser");
const traverse = require("babel-traverse").default;
const t = require("babel-types");
const template = require("@babel/template");
const babel = require("@babel/core"); const createCatchIdentifier = () => {
const catchIdentifier = t.identifier("catch");
return catchIdentifier;
}; function addCatchLoader(source){
const self = this; //缓存当前this
let ast = parser.parse(source, {
sourceType: "module",
plugins: ["dynamicImport", "jsx"],
}); const awaitMap = []; traverse(ast,{
/*
我们既然是要替换await后的整颗节点,就要先获取AwaitExpression这个节点的信息。因为有些
人在用async await习惯用try catch进行包裹,而用了try catch就没必要再加catch了,所以
我们这里需要判断await的父级节点有没有try catch。若有就使用path.skip()停止接下来的循
环,没有将当前节点的argument缓存进一个数组中,为了接下来进行比较。
*/
AwaitExpression(path) {
const tryCatchPath = path.findParent((p) => {
return t.isTryStatement(p);
});
if (tryCatchPath) return path.skip();
/*
这里leftId就是 = 左边的值,因为可能需要在catch里return,所以需要判断它的类型
*/
const leftId = path.parent.id;
if (leftId) {
const type = leftId.type;
path.node.argument.returnType = type;
}
awaitMap.push(path.node.argument);
},
/*
CallExpression节点就是我们需要替换的节点,因为整颗ast中不止一个地方有
CallExpression类型的节点,所以我们需要比较缓存的数组中有没有它,如有就代表是我们
要替换的```po()```。在这里我们需要在进行一次判断,因为源代码中可能会有await后自动加
catch的情况,我们就不必处理了。
*/
CallExpression(path) {
if (!awaitMap.length) return null;
awaitMap.forEach((item, index) => {
if (item === path.node) {
const callee = path.node.callee;
const returnType = path.node.returnType; //这里取出等号左边的类型
if (t.isMemberExpression(callee)) return; //若是已经有了.catch则不需要处理
const MemberExpression = t.memberExpression(
item,
createCatchIdentifier()
);
const createArgumentsSelf = createArguments.bind(self); //绑定当前this
const ArrowFunctionExpression_1 = createArgumentsSelf(returnType);//创建catch的回调函数里的逻辑
const CallExpression = t.callExpression(MemberExpression, [
ArrowFunctionExpression_1,
]);
path.replaceWith(CallExpression);
awaitMap[index] = null;
}
});
},
})
我们看一下createArgumentsSelf的逻辑
const t = require("babel-types");
const template = require("@babel/template");
const loaderUtils = require("loader-utils");
const { typeMap } = require("./constant"); const createCatchIdentifier = () => {
const catchIdentifier = t.identifier("catch");
return catchIdentifier;
}; function createArguments(type) {
//上边我们缓存了this并把this传入到当前函数中,就是为了取出loader的参数
const { needReturn, consoleError, customizeCatchCode } =
loaderUtils.getOptions(this) || {}; let returnResult = needReturn && type && typeMap[type];
let code = "";
let returnStatement = null;
if (returnResult) {
code = `return ${returnResult}`;
}
if (code) {
returnStatement = template.statement(code)();
} /* 创建arguments:(err)=>{}
先创建ArrowFunctionExpression 参数(params,body为必须);params为err
param是参数列表,为一个数组,每一项为Identifier;body为BlockStatement;
*/
// 创建body
const consoleStatement =
consoleError && template.statement(`console.log(error)`)();
const customizeCatchCodeStatement =
typeof customizeCatchCode === "string" &&
template.statement(customizeCatchCode)();
const blockStatementMap = [
customizeCatchCodeStatement,
consoleStatement,
returnStatement,
].filter(Boolean);
const blockStatement = t.blockStatement(blockStatementMap);
// 创建ArrowFunctionExpression
const ArrowFunctionExpression_1 = t.arrowFunctionExpression(
[t.identifier("error")],
blockStatement
);
return ArrowFunctionExpression_1;
} module.exports = {
createCatchIdentifier,
createArguments,
};
确定了就是替换这个节点,那么我们需要创建一个MemberExpression节点,查看babel-type的问的文档
object和property是必须的,而在我们的ast中,object和property又分别代表什么呢?
po()就是object,catch就是property,这样我们的po().catch体就创建成功了。而po().catch是肯定不够的,我们需要一个完整的```po().catch(err=>{})``` 结构,而err=>{}作为参数是和MemberExpression节点平级的,createArgumentsSelf函数就是创建了err=>{},其中需要根据参数判断是否需要打印error,是否需要return边界值,以及是否有别的逻辑代码,原理和创建catch一样。最后创建好了使用path.replaceWith(要替换成的节点)就可以了。但是要注意将缓存节点的数组中将这个节点删掉,因为ast遍历中若是某个节点发生了改变,那么就会一直遍历,造成死循环!
因为我目前的处理的是await后跟的是一个函数的情况,即po()是一个函数,函数执行返回的是一个promise,那么还有await后直接跟promise的情况,比如这种
const pro = new Promise((resolve,reject)=>{ reject('我错了!') }) const fn = async () => {
const data = await pro;
}
这种情况也需要考虑进去,我代码上就不放了,pro是一个```Identifier```节点,思路和```CallExpression```完全一样。
最后我们处理完ast节点,需要把新节点在转回代码返回回去
//add-catch-loader.js
const parser = require("@babel/parser");
const traverse = require("babel-traverse").default;
const t = require("babel-types");
const template = require("@babel/template");
const babel = require("@babel/core"); const createCatchIdentifier = () => {
const catchIdentifier = t.identifier("catch");
return catchIdentifier;
}; function addCatchLoader(source){
const self = this; //缓存当前this
let ast = parser.parse(source, {
sourceType: "module",
plugins: ["dynamicImport", "jsx"],
}); const awaitMap = []; traverse(ast,{
/*
我们既然是要替换await后的整颗节点,就要先获取AwaitExpression这个节点的信息。因为有些
人在用async await习惯用try catch进行包裹,而用了try catch就没必要再加catch了,所以
我们这里需要判断await的父级节点有没有try catch。若有就使用path.skip()停止接下来的循
环,没有将当前节点的argument缓存进一个数组中,为了接下来进行比较。
*/
AwaitExpression(path) {
const tryCatchPath = path.findParent((p) => {
return t.isTryStatement(p);
});
if (tryCatchPath) return path.skip();
/*
这里leftId就是 = 左边的值,因为可能需要在catch里return,所以需要判断它的类型
*/
const leftId = path.parent.id;
if (leftId) {
const type = leftId.type;
path.node.argument.returnType = type;
}
awaitMap.push(path.node.argument);
},
/*
CallExpression节点就是我们需要替换的节点,因为整颗ast中不止一个地方有
CallExpression类型的节点,所以我们需要比较缓存的数组中有没有它,如有就代表是我们
要替换的```po()```。在这里我们需要在进行一次判断,因为源代码中可能会有await后自动加
catch的情况,我们就不必处理了。
*/
CallExpression(path) {
if (!awaitMap.length) return null;
awaitMap.forEach((item, index) => {
if (item === path.node) {
const callee = path.node.callee;
const returnType = path.node.returnType; //这里取出等号左边的类型
if (t.isMemberExpression(callee)) return; //若是已经有了.catch则不需要处理
const MemberExpression = t.memberExpression(
item,
createCatchIdentifier()
);
const createArgumentsSelf = createArguments.bind(self); //绑定当前this
const ArrowFunctionExpression_1 = createArgumentsSelf(returnType);//创建catch的回调函数里的逻辑
const CallExpression = t.callExpression(MemberExpression, [
ArrowFunctionExpression_1,
]);
path.replaceWith(CallExpression);
awaitMap[index] = null;
}
});
},
})
const { code } = babel.transformFromAstSync(ast, null, {
configFile: false, // 屏蔽 babel.config.js,否则会注入 polyfill 使得调试变得困难
});
return code;
有些人可能在替换节点时用继续深度遍历当前节点的方法,因为要替换的节点必定是AwaitExpression的子节点嘛,我为了使整体代码结构看起来更结构化,所以这里使用了缓存节点。
在项目中使用
[github地址](https://github.com/mayu888/await-add-catch-loader),```欢迎大家star or issues!```
npm i await-add-catch-loader --save-dev
// or
yarn add await-add-catch-loader --save-dev //webpack.config.js
module.exports = {
//...
module: {
rules: [
{
test: /\.(js|jsx)$/,
exclude: /node_modules/, //刨除哪个文件里的js文件
include: path.resolve(__dirname, "./src"),
use: [
{loader: "babel-loader"},
{
loader: 'await-add-catch-loader',
options: {
needReturn: true,
consoleError: true,
customizeCatchCode: "//please input you want to do",
},
},
],
},
],
},
}
项目中的源代码:
loader处理后的代码
写loader中的一些困难及想法
从功能上来说单纯为了给promise加上catch而写一个loader是完全没必要的,因为loader的核心作用是为了处理一个文件级别的模块,单纯实现一个小功能有些杀鸡用宰牛刀的感觉,我一开始的目的其实是写一个babel的插件,想在babel处理js的过程中就完成这个功能,但是babel插件有一个点就是在处理每一个ast节点时,会顺序的执行每一个插件,也就是每一个ast节点在babel插件中只进行一次处理,并不是在执行完一个插件后再去执行下一个插件,其目的是优化性能,毕竟dom树太复杂遍历一次的成本就会越高。这样带来的问题就是我的插件在处理到AwaitExpression节点前,别的插件已经把async await替换成了generator,这样我的插件就失效了。
//webpack.config.js
{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
include: path.resolve(__dirname, './src'),
use: [
{
loader: 'babel-loader?cacheDirectory',
options: {
presets: [
[
'@babel/preset-env', //调用es6-es5的模块],
'@babel/preset-react' //转化react语法的模块
],
plugins:
[
'@babel/plugin-transform-runtime',
[path.resolve(__dirname, 'babel-plugin', 'await-catch-babel-plugin')]//自己写的babel插件
]}
因为要使用'@babel/preset-env'将es6转es5,而使用这个预设必须要使用@babel/plugin-transform-runtime来处理async await,通过分析源码,@babel/plugin-transform-runtime在pre阶段对async函数generator化,pre阶段就是刚进入节点的阶段,是自己写的插件在后续的遍历中没有了AwaitExpression节点。这个问题搜了好久也未曾找到解决办法,特意去了stackOverflow提问,也没人回复,但是发现一个类似的问题,也没解决办法,所以放弃了babel插件的写法。
也曾想过使用webpack插件来完成此功能,但是也会偏离webpack插件的核心思想,所以就放弃了。
我的目的也是想更深次的学习一下webpack、babel在编译过程中做的事,掌握它们的原理,所以最后还是选择了loader的写法。
写一个为await自动加上catch的loader逐渐了解AST以及babel的更多相关文章
- 如何手动写一个Python脚本自动爬取Bilibili小视频
如何手动写一个Python脚本自动爬取Bilibili小视频 国庆结束之余,某个不务正业的码农不好好干活,在B站瞎逛着,毕竟国庆嘛,还让不让人休息了诶-- 我身边的很多小伙伴们在朋友圈里面晒着出去游玩 ...
- .NET 除了用 Task 之外,如何自己写一个可以 await 的对象?
.NET 中的 async / await 写异步代码用起来真的很爽,就像写同步一样.我们可以在各种各样的异步代码中看到 Task 返回值,这样大家便可以使用 await 等待这个方法.不过,有时需要 ...
- 写一个umi插件 自动生成代码 解放cv的双手
引言 最近在写一个中台项目,使用的react的umi框架. 各种增删改查.基本是列表页 新建页 详情页这种页面 为了避免不必要的简单重复(主要是想偷懒) 于是想去实现自己的一个代码生成器 探索 首先, ...
- Cordova webapp实战开发:(5)如何写一个Andorid下自动更新的插件?
在 <Cordova webapp实战开发:(4)Android环境搭建>中我们搭建好了开发环境,也给大家布置了调用插件的预习作业,做得如何了呢?今天我们来学一下如何自己从头建立一个And ...
- 写一个shell,自动执行jmeter测试脚本
贡献一个自己写的shell脚本,很简单,但又可以高效率的自动执行jmeter压测脚本. #!/bin/bash #author:zhangyl #version:V1 #该脚本放置于压测脚本的上一层目 ...
- 写一个TT模板自动生成spring.net下面的配置文件。
这个是目标. 然后想着就怎么开始 1.
- 通过用jQuery写一个页面,我学到了什么
概述 前几天面试,hr发来一个测试文件,让我做做看.我一看,其实就是根据PSD需求写一个页面,再加上一些互动效果即可. 刚好我之前学了切图,jquery等知识还没练手过,于是高兴的答应了. 最后花了3 ...
- 定义一组抽象的 Awaiter 的实现接口,你下次写自己的 await 可等待对象时将更加方便
我在几篇文章中都说到了在 .NET 中自己实现 Awaiter 情况.async / await 写异步代码用起来真的很爽,就像写同步一样.然而实现 Awaiter 没有现成的接口,它需要你按照编译器 ...
- Cordova webapp实战开发:(6)如何写一个iOS下获取APP版本号的插件?
上一篇我们学习了如何写一个Andorid下自动更新的插件,我想还有一部分看本系列blog的开发人员希望学习在iOS下如何做插件的吧,那么今天你就可以来看看这篇文字了. 本次练习你能学到的 学习如何获取 ...
随机推荐
- (二)廖师兄springboot微信点餐虚拟机说明文档
虚拟机 VirtualBox-5.1.22 系统 CentOS7.3账号 root密码 123456 软件:jdk 1.8.0_111nginx 1.11.7mysql 5.7.17redis 3. ...
- hadoop启动脚本
记录一下一个简单的hadoop启动脚本 就是启动zookeeper集群,hadoop的HDFS和YRAN的脚本 start-cluster.sh 关于关闭的脚本,只需要顺序换一下,然后将start改为 ...
- MediaCodec编码OpenGL速度和清晰度均衡
## 概述 在安卓平台为了实现h264视频编码,我们通常可以使用libx264, ffmpeg等第三方视频编码库,但是如果对编码的速度有一定的要求,要实现实时甚至超实时的高速视频编码,我们并没有太 ...
- 微信公众号获取openid(php实例)
微信公众号获取openid 公众号获取openid的方法跟小程序获取openid其实是一样的,只是code获取的方式不一样 小程序获取code: 用户授权登录时调用wx.login即可获取到code ...
- centos虚拟机配置网络
首先想用xshell链接虚拟机,发现没有联网, 首先打开Windows下的服务 将虚拟机的网络设置为net模式 centos的网卡默认是关闭的,需要手动打开,首先进入这个目录 用vim打开ifcfg- ...
- 【建议收藏】一份阿里大牛花了三天整理出来的XML学习笔记,写的非常详细
1. 什么是XML? XML 指可扩展标记语言(EXtensible Markup Language)XML 是一种标记语言,很类似 HTMLXML 的设计宗旨是传输数据,而非显示数据XML 标签没有 ...
- Boom 3D的广播有哪些,有啥特色
Boom 3D(Windows系统)不仅为用户提供了包括3D立体音效.古典音乐音效在内的多种音效增强功能,而且还为用户提供了广播功能.该广播功能不仅涵盖了国内广播节目,而且还涵盖了国际广播节目. 接下 ...
- 常用命令合集『Postgres、Redis、Docker等等』每周更新,建议收藏备用
Command CMD POSTGRES 进入数据库命令行 psql -U 用户名 -d 数据库名 psql -U example -d exampledb 导出数据库 pg_dump -U 用户名 ...
- 【VUE】7.Vuex基本使用
1. 安装Vuex npm install vuex --save 2. 导入Vuex包 import Vuex from 'vuex' Vue.use(Vuex) 3. 创建store对象 cons ...
- 【PUPPETEER】初探之获取元素文本值(三)
一.知识点 page.$eval(selector, pageFunction[, ...args]) page.$$eval(selector, pageFunction[, ...args]) i ...