咱们在平常开发中常常用到async await去请求接口,解决异步。可async await语法的缺点就是若await后的Promise抛出错误不能捕获,整段代码区就会卡住。从而使下面的逻辑不能顺利执行。也许会有人说,卡住就是为了避免进行后续的代码,以避免形成更大的错误,可大多数状况下须要catch住错误并给出一个边界值使代码正常执行。 我之前常常经常会这么写:node
const request = async (){ const { data = [] } = await getList() || {}; //...other };
这样写看似有些**高端**,但其实风险系数很高,假设```getList()```请求发生了错误而且没有捕获到,那么后边的逻辑或表达式并不会生效,后续的代码并不能顺序执行。
这种状况的最优解就是```getList()```能后捕获到错误,虽然如今大多数axios都会catch,可是业务开发中应该不止请求才会用到Promise。那么另外一种解法是?react
const request = async (){ const { data = [] } = await getList().catch(err=>{ //...do you want to do }) || {}; //...other };
本身写的loader就是解决平常开发中忘记写catch的状况。
先说一下本身写的loader的功能:
1. 能够自动为await后的promise加上catch
2. 能够决定是否须要在catch函数中打印error以及return出一个边界值,能够选择加上本身的代码
3. 如果await函数外层有被try catch包裹或者自己后边就已经有catch,则不会作任何处理webpack
//一个普通的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){} }
接下来上代码ios
//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处理js的三个过程:解析(parase)、转换(transform)、生成(generator)git
loader就是一个纯函数,它能获取当前文件的源代码es6
//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的节点长什么样子。github
上图只是 const a = await po()这一段代码的ast,其中大部分还折叠起来了。可是咱们只须要关系await后的代码ast,即po()。web
AwaitExpression这个节点是await po()这段代码,CallExpression这个节点是po()这个节点。那么await po().catch(err=>{ })代码的节点又长什么样子呢?npm
以下图,AwaitExpression是await pro().catch(err=>{});整段代码的节点,MemberExpression是pro().catch;的节点,arguments是函数体的参数,而ArrowFunctionExpression表明的就是err={},因此咱们只须要把po()替换成po().catch(err=>{})。
比较一下po()和po().catch的不一样(因为catch函数中的回调函数是参数,属于和po().catch一个级别,因此不把它算在内)
po()json
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的写法。