前言
大厂面试题分享 面试题库
前端面试题库 (面试必备) 推荐:★★★★★
地址:前端面试题库
三面的时候被问到了这个问题,当时思路虽然正确,可惜表述的不够清晰
后来花了一些时间整理了下思路,那么如何实现给所有的async函数添加try/catch呢?
async如果不加 try/catch 会发生什么事?
// 示例asyncfunctionfn() { let value = awaitnewPromise((resolve, reject) => { reject('failure'); }); console.log('do something...'); } fn() 复制代码
导致浏览器报错:一个未捕获的错误
在开发过程中,为了保证系统健壮性,或者是为了捕获异步的错误,需要频繁的在 async 函数中添加 try/catch,避免出现上述示例的情况
可是我很懒,不想一个个加,懒惰使我们进步😂
下面,通过手写一个babel 插件,来给所有的async函数添加try/catch
babel插件的最终效果
原始代码:
asyncfunctionfn() { awaitnewPromise((resolve, reject) =>reject('报错')); awaitnewPromise((resolve) =>resolve(1)); console.log('do something...'); } fn(); 复制代码
使用插件转化后的代码:
asyncfunctionfn() { try { awaitnewPromise((resolve, reject) =>reject('报错')); awaitnewPromise(resolve =>resolve(1)); console.log('do something...'); } catch (e) { console.log("\nfilePath: E:\\myapp\\src\\main.js\nfuncName: fn\nError:", e); } } fn(); 复制代码
打印的报错信息:
通过详细的报错信息,帮助我们快速找到目标文件和具体的报错方法,方便去定位问题
babel插件的实现思路
1)借助AST抽象语法树,遍历查找代码中的await关键字
2)找到await节点后,从父路径中查找声明的async函数,获取该函数的body(函数中包含的代码)
3)创建try/catch语句,将原来async的body放入其中
4)最后将async的body替换成创建的try/catch语句
babel的核心:AST
先聊聊 AST 这个帅小伙🤠,不然后面的开发流程走不下去
词法分析
词法分析阶段把字符串形式的代码转换为令牌(tokens) ,可以把tokens看作是一个扁平的语法片段数组,描述了代码片段在整个代码中的位置和记录当前值的一些信息
比如let a = 1,对应的AST是这样的
语法分析
语法分析阶段会把token转换成 AST 的形式,这个阶段会使用token中的信息把它们转换成一个 AST 的表述结构,使用type属性记录当前的类型
例如 let 代表着一个变量声明的关键字,所以它的 type 为 VariableDeclaration,而 a = 1 会作为 let 的声明描述,它的 type 为 VariableDeclarator
再举个🌰,加深对AST的理解
functiondemo(n) { return n * n; } 复制代码
转化成AST的结构
{"type":"Program",// 整段代码的主体"body":[{"type":"FunctionDeclaration",// function 的类型叫函数声明;"id":{// id 为函数声明的 id"type":"Identifier",// 标识符 类型"name":"demo"// 标识符 具有名字 },"expression":false,"generator":false,"async":false,// 代表是否 是 async function"params":[// 同级 函数的参数 {"type":"Identifier",// 参数类型也是 Identifier"name":"n"}],"body":{// 函数体内容 整个格式呈现一种树的格式"type":"BlockStatement",// 整个函数体内容 为一个块状代码块类型"body":[{"type":"ReturnStatement",// return 类型"argument":{"type":"BinaryExpression",// BinaryExpression 二进制表达式类型"start":30,"end":35,"left":{// 分左 右 中 结构"type":"Identifier","name":"n"},"operator":"*",// 属于操作符"right":{"type":"Identifier","name":"n"}}}]}}],"sourceType":"module"}复制代码
常用的 AST 节点类型对照表
await节点对应的AST结构
1)原始代码
async function fn() { await f() } 复制代码
对应的AST结构
2)增加try catch后的代码
asyncfunctionfn() { try { awaitf() } catch (e) { console.log(e) } } 复制代码
对应的AST结构
通过AST结构对比,插件的核心就是将原始函数的body放到try语句中
babel插件开发
插件的基本格式示例
module.exports = function (babel) { let t = babel.typereturn { visitor: { // 设置需要范围的节点类型CallExression: (path, state) => { do soming …… } } } } 复制代码
1)通过 babel 拿到 types 对象,操作 AST 节点,比如创建、校验、转变等
2)visitor:定义了一个访问者,可以设置需要访问的节点类型,当访问到目标节点后,做相应的处理来实现插件的功能
寻找await节点
回到业务需求,现在需要找到await节点,可以通过AwaitExpression表达式获取
module.exports = function (babel) { let t = babel.typereturn { visitor: { // 设置AwaitExpressionAwaitExpression(path) { // 获取当前的await节点let node = path.node; } } } } 复制代码
向上查找 async 函数
通过findParent方法,在父节点中搜寻 async 节点
// async节点的属性为trueconst asyncPath = path.findParent(p => p.node.async) 复制代码
async 节点的AST结构
这里要注意,async 函数分为4种情况:函数声明 、箭头函数 、函数表达式 、函数为对象的方法
// 1⃣:函数声明asyncfunctionfn() { awaitf() } // 2⃣:函数表达式const fn = asyncfunction () { awaitf() }; // 3⃣:箭头函数constfn = async () => { awaitf() }; // 4⃣:async函数定义在对象中const obj = { asyncfn() { awaitf() } } 复制代码
需要对这几种情况进行分别判断
module.exports = function (babel) { let t = babel.typereturn { visitor: { // 设置AwaitExpressionAwaitExpression(path) { // 获取当前的await节点let node = path.node; // 查找async函数的节点const asyncPath = path.findParent((p) => p.node.async && (p.isFunctionDeclaration() || p.isArrowFunctionExpression() || p.isFunctionExpression() || p.isObjectMethod())); } } } } 复制代码
利用babel-template生成try/catch节点
babel-template可以用以字符串形式的代码来构建AST树节点,快速优雅开发插件
// 引入babel-template const template = require('babel-template'); // 定义try/catch语句模板 let tryTemplate = ` try { } catch (e) { console.log(CatchError:e) }`; // 创建模板 const temp = template(tryTemplate); // 给模版增加key,添加console.log打印信息 let tempArgumentObj = { // 通过types.stringLiteral创建字符串字面量 CatchError: types.stringLiteral('Error') }; // 通过temp创建try语句的AST节点 let tryNode = temp(tempArgumentObj);复制代码
async函数体替换成try语句
module.exports = function (babel) { let t = babel.type return { visitor: { AwaitExpression(path) { let node = path.node; const asyncPath = path.findParent((p) => p.node.async && (p.isFunctionDeclaration() || p.isArrowFunctionExpression() || p.isFunctionExpression() || p.isObjectMethod())); let tryNode = temp(tempArgumentObj); // 获取父节点的函数体body let info = asyncPath.node.body; // 将函数体放到try语句的body中 tryNode.block.body.push(...info.body); // 将父节点的body替换成新创建的try语句 info.body = [tryNode]; } } } } 复制代码
到这里,插件的基本结构已经成型,但还有点问题,如果函数已存在try/catch,该怎么处理判断呢?
若函数已存在try/catch,则不处理
// 示例代码,不再添加try/catchasyncfunctionfn() { try { awaitf() } catch (e) { console.log(e) } } 复制代码
通过isTryStatement判断是否已存在try语句
module.exports = function (babel) { let t = babel.type return { visitor: { AwaitExpression(path) { // 判断父路径中是否已存在try语句,若存在直接返回 if (path.findParent((p) => p.isTryStatement())) { return false; } let node = path.node; const asyncPath = path.findParent((p) => p.node.async && (p.isFunctionDeclaration() || p.isArrowFunctionExpression() || p.isFunctionExpression() || p.isObjectMethod())); let tryNode = temp(tempArgumentObj); let info = asyncPath.node.body; tryNode.block.body.push(...info.body);info.body = [tryNode]; } } } } 复制代码
添加报错信息
获取报错时的文件路径 filePath 和方法名称 funcName,方便快速定位问题
获取文件路径
// 获取编译目标文件的路径,如:E:\myapp\src\App.vueconst filePath = this.filename || this.file.opts.filename || 'unknown'; 复制代码
获取报错的方法名称
// 定义方法名 let asyncName = ''; // 获取async节点的type类型 let type = asyncPath.node.type; switch (type) { // 1⃣函数表达式 // 情况1:普通函数,如const func = async function () {} // 情况2:箭头函数,如const func = async () => {} case 'FunctionExpression': case 'ArrowFunctionExpression': // 使用path.getSibling(index)来获得同级的id路径 let identifier = asyncPath.getSibling('id'); // 获取func方法名 asyncName = identifier && identifier.node ? identifier.node.name : ''; break; // 2⃣函数声明,如async function fn2() {} case 'FunctionDeclaration': asyncName = (asyncPath.node.id && asyncPath.node.id.name) || ''; break; // 3⃣async函数作为对象的方法,如vue项目中,在methods中定义的方法: methods: { async func() {} } case 'ObjectMethod': asyncName = asyncPath.node.key.name || ''; break; } // 若asyncName不存在,通过argument.callee获取当前执行函数的name let funcName = asyncName || (node.argument.callee && node.argument.callee.name) || '';复制代码
添加用户选项
用户引入插件时,可以设置exclude、include、 customLog选项
exclude: 设置需要排除的文件,不对该文件进行处理
include: 设置需要处理的文件,只对该文件进行处理
customLog: 用户自定义的打印信息
最终代码
入口文件index.js
// babel-template 用于将字符串形式的代码来构建AST树节点const template = require('babel-template'); const { tryTemplate, catchConsole, mergeOptions, matchesFile } = require('./util'); module.exports = function (babel) { // 通过babel 拿到 types 对象,操作 AST 节点,比如创建、校验、转变等let types = babel.types; // visitor:插件核心对象,定义了插件的工作流程,属于访问者模式const visitor = { AwaitExpression(path) { // 通过this.opts 获取用户的配置if (this.opts && !typeofthis.opts === 'object') { returnconsole.error('[babel-plugin-await-add-trycatch]: options need to be an object.'); } // 判断父路径中是否已存在try语句,若存在直接返回if (path.findParent((p) => p.isTryStatement())) { returnfalse; } // 合并插件的选项const options = mergeOptions(this.opts); // 获取编译目标文件的路径,如:E:\myapp\src\App.vueconst filePath = this.filename || this.file.opts.filename || 'unknown'; // 在排除列表的文件不编译if (matchesFile(options.exclude, filePath)) { return; } // 如果设置了include,只编译include中的文件if (options.include.length && !matchesFile(options.include, filePath)) { return; } // 获取当前的await节点let node = path.node; // 在父路径节点中查找声明 async 函数的节点// async 函数分为4种情况:函数声明 || 箭头函数 || 函数表达式 || 对象的方法const asyncPath = path.findParent((p) => p.node.async && (p.isFunctionDeclaration() || p.isArrowFunctionExpression() || p.isFunctionExpression() || p.isObjectMethod())); // 获取async的方法名let asyncName = ''; lettype = asyncPath.node.type; switch (type) { // 1⃣函数表达式// 情况1:普通函数,如const func = async function () {}// 情况2:箭头函数,如const func = async () => {}case'FunctionExpression': case'ArrowFunctionExpression': // 使用path.getSibling(index)来获得同级的id路径let identifier = asyncPath.getSibling('id'); // 获取func方法名 asyncName = identifier && identifier.node ? identifier.node.name : ''; break; // 2⃣函数声明,如async function fn2() {}case'FunctionDeclaration': asyncName = (asyncPath.node.id && asyncPath.node.id.name) || ''; break; // 3⃣async函数作为对象的方法,如vue项目中,在methods中定义的方法: methods: { async func() {} }case'ObjectMethod': asyncName = asyncPath.node.key.name || ''; break; } // 若asyncName不存在,通过argument.callee获取当前执行函数的namelet funcName = asyncName || (node.argument.callee && node.argument.callee.name) || ''; const temp = template(tryTemplate); // 给模版增加key,添加console.log打印信息let tempArgumentObj = { // 通过types.stringLiteral创建字符串字面量CatchError: types.stringLiteral(catchConsole(filePath, funcName, options.customLog)) }; // 通过temp创建try语句let tryNode = temp(tempArgumentObj); // 获取async节点(父节点)的函数体let info = asyncPath.node.body; // 将父节点原来的函数体放到try语句中 tryNode.block.body.push(...info.body); // 将父节点的内容替换成新创建的try语句 info.body = [tryNode]; } }; return { name: 'babel-plugin-await-add-trycatch', visitor }; }; 复制代码
util.js
const merge = require('deepmerge'); // 定义try语句模板let tryTemplate = ` try { } catch (e) { console.log(CatchError,e) }`; /* * catch要打印的信息 * @param {string} filePath - 当前执行文件的路径 * @param {string} funcName - 当前执行方法的名称 * @param {string} customLog - 用户自定义的打印信息 */letcatchConsole = (filePath, funcName, customLog) => ` filePath: ${filePath} funcName: ${funcName}${customLog}:`; // 默认配置const defaultOptions = { customLog: 'Error', exclude: ['node_modules'], include: [] }; // 判断执行的file文件 是否在 exclude/include 选项内functionmatchesFile(list, filename) { return list.find((name) => name && filename.includes(name)); } // 合并选项functionmergeOptions(options) { let { exclude, include } = options; if (exclude) options.exclude = toArray(exclude); if (include) options.include = toArray(include); // 使用merge进行合并return merge.all([defaultOptions, options]); } functiontoArray(value) { returnArray.isArray(value) ? value : [value]; } module.exports = { tryTemplate, catchConsole, defaultOptions, mergeOptions, matchesFile, toArray };
babel插件的安装使用
npm网站搜索babel-plugin-await-add-trycatch
总结
通过开发这个babel插件,了解很多 AST 方面的知识,了解 babel 的原理。实际开发中,大家可以结合具体的业务需求开发自己的插件
大厂面试题分享 面试题库
前端面试题库 (面试必备) 推荐:★★★★★
地址:前端面试题库