• 【面试题】阿里面试官:如何给所有的async函数添加try/catch?


    给大家推荐一个实用面试题库

    1、前端面试题库 (面试必备)            推荐:★★★★★

    地址:前端面试题库

     

    前言

    三面的时候被问到了这个问题,当时思路虽然正确,可惜表述的不够清晰

    后来花了一些时间整理了下思路,那么如何实现给所有的async函数添加try/catch呢?

    async如果不加 try/catch 会发生什么事?

    1. // 示例
    2. async function fn() {
    3. let value = await new Promise((resolve, reject) => {
    4. reject('failure');
    5. });
    6. console.log('do something...');
    7. }
    8. fn()
    9. 复制代码

    导致浏览器报错:一个未捕获的错误

    在开发过程中,为了保证系统健壮性,或者是为了捕获异步的错误,需要频繁的在 async 函数中添加 try/catch,避免出现上述示例的情况

    可是我很懒,不想一个个加,懒惰使我们进步😂

    下面,通过手写一个babel 插件,来给所有的async函数添加try/catch

    babel插件的最终效果

    原始代码:

    1. async function fn() {
    2. await new Promise((resolve, reject) => reject('报错'));
    3. await new Promise((resolve) => resolve(1));
    4. console.log('do something...');
    5. }
    6. fn();
    7. 复制代码

    使用插件转化后的代码:

    1. async function fn() {
    2. try {
    3. await new Promise((resolve, reject) => reject('报错'));
    4. await new Promise(resolve => resolve(1));
    5. console.log('do something...');
    6. } catch (e) {
    7. console.log("\nfilePath: E:\\myapp\\src\\main.js\nfuncName: fn\nError:", e);
    8. }
    9. }
    10. fn();
    11. 复制代码

    打印的报错信息:

    通过详细的报错信息,帮助我们快速找到目标文件和具体的报错方法,方便去定位问题

    babel插件的实现思路

    1)借助AST抽象语法树,遍历查找代码中的await关键字

    2)找到await节点后,从父路径中查找声明的async函数,获取该函数的body(函数中包含的代码)

    3)创建try/catch语句,将原来async的body放入其中

    4)最后将async的body替换成创建的try/catch语句

    babel的核心:AST

    先聊聊 AST 这个帅小伙🤠,不然后面的开发流程走不下去

    AST是代码的树形结构,生成 AST 分为两个阶段:词法分析和 语法分析

    词法分析

    词法分析阶段把字符串形式的代码转换为令牌(tokens) ,可以把tokens看作是一个扁平的语法片段数组,描述了代码片段在整个代码中的位置和记录当前值的一些信息

    比如let a = 1,对应的AST是这样的

    语法分析

    语法分析阶段会把token转换成 AST 的形式,这个阶段会使用token中的信息把它们转换成一个 AST 的表述结构,使用type属性记录当前的类型

    例如 let 代表着一个变量声明的关键字,所以它的 type 为 VariableDeclaration,而 a = 1 会作为 let 的声明描述,它的 type 为 VariableDeclarator

    AST在线查看工具:AST explorer

    再举个🌰,加深对AST的理解

    1. function demo(n) {
    2. return n * n;
    3. }
    4. 复制代码

    转化成AST的结构

    1. {
    2. "type": "Program", // 整段代码的主体
    3. "body": [
    4. {
    5. "type": "FunctionDeclaration", // function 的类型叫函数声明;
    6. "id": { // id 为函数声明的 id
    7. "type": "Identifier", // 标识符 类型
    8. "name": "demo" // 标识符 具有名字
    9. },
    10. "expression": false,
    11. "generator": false,
    12. "async": false, // 代表是否 是 async function
    13. "params": [ // 同级 函数的参数
    14. {
    15. "type": "Identifier",// 参数类型也是 Identifier
    16. "name": "n"
    17. }
    18. ],
    19. "body": { // 函数体内容 整个格式呈现一种树的格式
    20. "type": "BlockStatement", // 整个函数体内容 为一个块状代码块类型
    21. "body": [
    22. {
    23. "type": "ReturnStatement", // return 类型
    24. "argument": {
    25. "type": "BinaryExpression",// BinaryExpression 二进制表达式类型
    26. "start": 30,
    27. "end": 35,
    28. "left": { // 分左 右 中 结构
    29. "type": "Identifier",
    30. "name": "n"
    31. },
    32. "operator": "*", // 属于操作符
    33. "right": {
    34. "type": "Identifier",
    35. "name": "n"
    36. }
    37. }
    38. }
    39. ]
    40. }
    41. }
    42. ],
    43. "sourceType": "module"
    44. }
    45. 复制代码

    常用的 AST 节点类型对照表

    类型原名称中文名称描述
    Program程序主体整段代码的主体
    VariableDeclaration变量声明声明一个变量,例如 var let const
    FunctionDeclaration函数声明声明一个函数,例如 function
    ExpressionStatement表达式语句通常是调用一个函数,例如 console.log()
    BlockStatement块语句包裹在 {} 块内的代码,例如 if (condition){var a = 1;}
    BreakStatement中断语句通常指 break
    ContinueStatement持续语句通常指 continue
    ReturnStatement返回语句通常指 return
    SwitchStatementSwitch 语句通常指 Switch Case 语句中的 Switch
    IfStatementIf 控制流语句控制流语句,通常指 if(condition){}else{}
    Identifier标识符标识,例如声明变量时 var identi = 5 中的 identi
    CallExpression调用表达式通常指调用一个函数,例如 console.log()
    BinaryExpression二进制表达式通常指运算,例如 1+2
    MemberExpression成员表达式通常指调用对象的成员,例如 console 对象的 log 成员
    ArrayExpression数组表达式通常指一个数组,例如 [1, 3, 5]
    FunctionExpression函数表达式例如const func = function () {}
    ArrowFunctionExpression箭头函数表达式例如const func = ()=> {}
    AwaitExpressionawait表达式例如let val = await f()
    ObjectMethod对象中定义的方法例如 let obj = { fn () {} }
    NewExpressionNew 表达式通常指使用 New 关键词
    AssignmentExpression赋值表达式通常指将函数的返回值赋值给变量
    UpdateExpression更新表达式通常指更新成员值,例如 i++
    Literal字面量字面量
    BooleanLiteral布尔型字面量布尔值,例如 true false
    NumericLiteral数字型字面量数字,例如 100
    StringLiteral字符型字面量字符串,例如 vansenb
    SwitchCaseCase 语句通常指 Switch 语句中的 Case

    await节点对应的AST结构

    1)原始代码

    1. async function fn() {
    2. await f()
    3. }
    4. 复制代码

    对应的AST结构

    2)增加try catch后的代码

    1. async function fn() {
    2. try {
    3. await f()
    4. } catch (e) {
    5. console.log(e)
    6. }
    7. }
    8. 复制代码

    对应的AST结构

    通过AST结构对比,插件的核心就是将原始函数的body放到try语句中

    babel插件开发

    我曾在《历时8个月!10w字前端知识体系+大厂面试笔记(工程化篇)🔥》中聊过如何开发一个babel插件

    这里简单回顾一下

    插件的基本格式示例

    1. module.exports = function (babel) {
    2. let t = babel.type
    3. return {
    4. visitor: {
    5. // 设置需要范围的节点类型
    6. CallExression: (path, state) => {
    7. do soming ……
    8. }
    9. }
    10. }
    11. }
    12. 复制代码

    1)通过 babel 拿到 types 对象,操作 AST 节点,比如创建、校验、转变等

    2)visitor:定义了一个访问者,可以设置需要访问的节点类型,当访问到目标节点后,做相应的处理来实现插件的功能

    寻找await节点

    回到业务需求,现在需要找到await节点,可以通过AwaitExpression表达式获取

    1. module.exports = function (babel) {
    2. let t = babel.type
    3. return {
    4. visitor: {
    5. // 设置AwaitExpression
    6. AwaitExpression(path) {
    7. // 获取当前的await节点
    8. let node = path.node;
    9. }
    10. }
    11. }
    12. }
    13. 复制代码

    向上查找 async 函数

    通过findParent方法,在父节点中搜寻 async 节点

    1. // async节点的属性为true
    2. const asyncPath = path.findParent(p => p.node.async)
    3. 复制代码

    async 节点的AST结构

    这里要注意,async 函数分为4种情况:函数声明 、箭头函数 、函数表达式 、函数为对象的方法

    1. // 1️⃣:函数声明
    2. async function fn() {
    3. await f()
    4. }
    5. // 2️⃣:函数表达式
    6. const fn = async function () {
    7. await f()
    8. };
    9. // 3️⃣:箭头函数
    10. const fn = async () => {
    11. await f()
    12. };
    13. // 4️⃣:async函数定义在对象中
    14. const obj = {
    15. async fn() {
    16. await f()
    17. }
    18. }
    19. 复制代码

    需要对这几种情况进行分别判断

    1. module.exports = function (babel) {
    2. let t = babel.type
    3. return {
    4. visitor: {
    5. // 设置AwaitExpression
    6. AwaitExpression(path) {
    7. // 获取当前的await节点
    8. let node = path.node;
    9. // 查找async函数的节点
    10. const asyncPath = path.findParent((p) => p.node.async && (p.isFunctionDeclaration() || p.isArrowFunctionExpression() || p.isFunctionExpression() || p.isObjectMethod()));
    11. }
    12. }
    13. }
    14. }
    15. 复制代码

    利用babel-template生成try/catch节点

    babel-template可以用以字符串形式的代码来构建AST树节点,快速优雅开发插件

    1. // 引入babel-template
    2. const template = require('babel-template');
    3. // 定义try/catch语句模板
    4. let tryTemplate = `
    5. try {
    6. } catch (e) {
    7. console.log(CatchError:e)
    8. }`;
    9. // 创建模板
    10. const temp = template(tryTemplate);
    11. // 给模版增加key,添加console.log打印信息
    12. let tempArgumentObj = {
    13. // 通过types.stringLiteral创建字符串字面量
    14. CatchError: types.stringLiteral('Error')
    15. };
    16. // 通过temp创建try语句的AST节点
    17. let tryNode = temp(tempArgumentObj);
    18. 复制代码

    async函数体替换成try语句

    1. module.exports = function (babel) {
    2. let t = babel.type
    3. return {
    4. visitor: {
    5. AwaitExpression(path) {
    6. let node = path.node;
    7. const asyncPath = path.findParent((p) => p.node.async && (p.isFunctionDeclaration() || p.isArrowFunctionExpression() || p.isFunctionExpression() || p.isObjectMethod()));
    8. let tryNode = temp(tempArgumentObj);
    9. // 获取父节点的函数体body
    10. let info = asyncPath.node.body;
    11. // 将函数体放到try语句的body中
    12. tryNode.block.body.push(...info.body);
    13. // 将父节点的body替换成新创建的try语句
    14. info.body = [tryNode];
    15. }
    16. }
    17. }
    18. }
    19. 复制代码

    到这里,插件的基本结构已经成型,但还有点问题,如果函数已存在try/catch,该怎么处理判断呢?

    若函数已存在try/catch,则不处理

    1. // 示例代码,不再添加try/catch
    2. async function fn() {
    3. try {
    4. await f()
    5. } catch (e) {
    6. console.log(e)
    7. }
    8. }
    9. 复制代码

    通过isTryStatement判读是否已存在try语句

    1. module.exports = function (babel) {
    2. let t = babel.type
    3. return {
    4. visitor: {
    5. AwaitExpression(path) {
    6. // 判断父路径中是否已存在try语句,若存在直接返回
    7. if (path.findParent((p) => p.isTryStatement())) {
    8. return false;
    9. }
    10. let node = path.node;
    11. const asyncPath = path.findParent((p) => p.node.async && (p.isFunctionDeclaration() || p.isArrowFunctionExpression() || p.isFunctionExpression() || p.isObjectMethod()));
    12. let tryNode = temp(tempArgumentObj);
    13. let info = asyncPath.node.body;
    14. tryNode.block.body.push(...info.body);
    15. info.body = [tryNode];
    16. }
    17. }
    18. }
    19. }
    20. 复制代码

    添加报错信息

    获取报错时的文件路径 filePath 和方法名称 funcName,方便快速定位问题

    获取文件路径

    1. // 获取编译目标文件的路径,如:E:\myapp\src\App.vue
    2. const filePath = this.filename || this.file.opts.filename || 'unknown';
    3. 复制代码

    获取报错的方法名称

    1. // 定义方法名
    2. let asyncName = '';
    3. // 获取async节点的type类型
    4. let type = asyncPath.node.type;
    5. switch (type) {
    6. // 1️⃣函数表达式
    7. // 情况1:普通函数,如const func = async function () {}
    8. // 情况2:箭头函数,如const func = async () => {}
    9. case 'FunctionExpression':
    10. case 'ArrowFunctionExpression':
    11. // 使用path.getSibling(index)来获得同级的id路径
    12. let identifier = asyncPath.getSibling('id');
    13. // 获取func方法名
    14. asyncName = identifier && identifier.node ? identifier.node.name : '';
    15. break;
    16. // 2️⃣函数声明,如async function fn2() {}
    17. case 'FunctionDeclaration':
    18. asyncName = (asyncPath.node.id && asyncPath.node.id.name) || '';
    19. break;
    20. // 3️⃣async函数作为对象的方法,如vue项目中,在methods中定义的方法: methods: { async func() {} }
    21. case 'ObjectMethod':
    22. asyncName = asyncPath.node.key.name || '';
    23. break;
    24. }
    25. // 若asyncName不存在,通过argument.callee获取当前执行函数的name
    26. let funcName = asyncName || (node.argument.callee && node.argument.callee.name) || '';
    27. 复制代码

    添加用户选项

    用户引入插件时,可以设置excludeinclude、 customLog选项

    exclude: 设置需要排除的文件,不对该文件进行处理

    include: 设置需要处理的文件,只对该文件进行处理

    customLog: 用户自定义的打印信息

    最终代码

    入口文件index.js

    1. // babel-template 用于将字符串形式的代码来构建AST树节点
    2. const template = require('babel-template');
    3. const { tryTemplate, catchConsole, mergeOptions, matchesFile } = require('./util');
    4. module.exports = function (babel) {
    5. // 通过babel 拿到 types 对象,操作 AST 节点,比如创建、校验、转变等
    6. let types = babel.types;
    7. // visitor:插件核心对象,定义了插件的工作流程,属于访问者模式
    8. const visitor = {
    9. AwaitExpression(path) {
    10. // 通过this.opts 获取用户的配置
    11. if (this.opts && !typeof this.opts === 'object') {
    12. return console.error('[babel-plugin-await-add-trycatch]: options need to be an object.');
    13. }
    14. // 判断父路径中是否已存在try语句,若存在直接返回
    15. if (path.findParent((p) => p.isTryStatement())) {
    16. return false;
    17. }
    18. // 合并插件的选项
    19. const options = mergeOptions(this.opts);
    20. // 获取编译目标文件的路径,如:E:\myapp\src\App.vue
    21. const filePath = this.filename || this.file.opts.filename || 'unknown';
    22. // 在排除列表的文件不编译
    23. if (matchesFile(options.exclude, filePath)) {
    24. return;
    25. }
    26. // 如果设置了include,只编译include中的文件
    27. if (options.include.length && !matchesFile(options.include, filePath)) {
    28. return;
    29. }
    30. // 获取当前的await节点
    31. let node = path.node;
    32. // 在父路径节点中查找声明 async 函数的节点
    33. // async 函数分为4种情况:函数声明 || 箭头函数 || 函数表达式 || 对象的方法
    34. const asyncPath = path.findParent((p) => p.node.async && (p.isFunctionDeclaration() || p.isArrowFunctionExpression() || p.isFunctionExpression() || p.isObjectMethod()));
    35. // 获取async的方法名
    36. let asyncName = '';
    37. let type = asyncPath.node.type;
    38. switch (type) {
    39. // 1️⃣函数表达式
    40. // 情况1:普通函数,如const func = async function () {}
    41. // 情况2:箭头函数,如const func = async () => {}
    42. case 'FunctionExpression':
    43. case 'ArrowFunctionExpression':
    44. // 使用path.getSibling(index)来获得同级的id路径
    45. let identifier = asyncPath.getSibling('id');
    46. // 获取func方法名
    47. asyncName = identifier && identifier.node ? identifier.node.name : '';
    48. break;
    49. // 2️⃣函数声明,如async function fn2() {}
    50. case 'FunctionDeclaration':
    51. asyncName = (asyncPath.node.id && asyncPath.node.id.name) || '';
    52. break;
    53. // 3️⃣async函数作为对象的方法,如vue项目中,在methods中定义的方法: methods: { async func() {} }
    54. case 'ObjectMethod':
    55. asyncName = asyncPath.node.key.name || '';
    56. break;
    57. }
    58. // 若asyncName不存在,通过argument.callee获取当前执行函数的name
    59. let funcName = asyncName || (node.argument.callee && node.argument.callee.name) || '';
    60. const temp = template(tryTemplate);
    61. // 给模版增加key,添加console.log打印信息
    62. let tempArgumentObj = {
    63. // 通过types.stringLiteral创建字符串字面量
    64. CatchError: types.stringLiteral(catchConsole(filePath, funcName, options.customLog))
    65. };
    66. // 通过temp创建try语句
    67. let tryNode = temp(tempArgumentObj);
    68. // 获取async节点(父节点)的函数体
    69. let info = asyncPath.node.body;
    70. // 将父节点原来的函数体放到try语句中
    71. tryNode.block.body.push(...info.body);
    72. // 将父节点的内容替换成新创建的try语句
    73. info.body = [tryNode];
    74. }
    75. };
    76. return {
    77. name: 'babel-plugin-await-add-trycatch',
    78. visitor
    79. };
    80. };
    81. 复制代码

    util.js

    1. const merge = require('deepmerge');
    2. // 定义try语句模板
    3. let tryTemplate = `
    4. try {
    5. } catch (e) {
    6. console.log(CatchError,e)
    7. }`;
    8. /*
    9. * catch要打印的信息
    10. * @param {string} filePath - 当前执行文件的路径
    11. * @param {string} funcName - 当前执行方法的名称
    12. * @param {string} customLog - 用户自定义的打印信息
    13. */
    14. let catchConsole = (filePath, funcName, customLog) => `
    15. filePath: ${filePath}
    16. funcName: ${funcName}
    17. ${customLog}:`;
    18. // 默认配置
    19. const defaultOptions = {
    20. customLog: 'Error',
    21. exclude: ['node_modules'],
    22. include: []
    23. };
    24. // 判断执行的file文件 是否在 options 选项 exclude/include 内
    25. function matchesFile(list, filename) {
    26. return list.find((name) => name && filename.includes(name));
    27. }
    28. // 合并选项
    29. function mergeOptions(options) {
    30. let { exclude, include } = options;
    31. if (exclude) options.exclude = toArray(exclude);
    32. if (include) options.include = toArray(include);
    33. // 使用merge进行合并
    34. return merge.all([defaultOptions, options]);
    35. }
    36. function toArray(value) {
    37. return Array.isArray(value) ? value : [value];
    38. }
    39. module.exports = {
    40. tryTemplate,
    41. catchConsole,
    42. defaultOptions,
    43. mergeOptions,
    44. matchesFile,
    45. toArray
    46. };
    47. 复制代码

    github仓库

    babel插件的安装使用

    npm网站搜索babel-plugin-await-add-trycatch

    有兴趣的朋友可以下载玩一玩

    babel-plugin-await-add-trycatch

    总结

    通过开发这个babel插件,了解很多 AST 方面的知识,了解 babel 的原理。实际开发中,大家可以结合具体的业务需求开发自己的插件

      给大家推荐一个实用面试题库

    1、前端面试题库 (面试必备)            推荐:★★★★★

    地址:前端面试题库

  • 相关阅读:
    thinkphp 生成邀请推广二维码,保存到服务器并接口返回给前端
    数据库直连提示 No suitable driver found for jdbc:postgresql
    Java从入门到架构师_Elasticsearch
    C#教学辅助系统网站as.net+sqlserver
    JavaWeb过滤器(Filter)详解,是时候该把过滤器彻底搞懂了(万字说明)
    kafka详解及集群环境搭建
    php以半小时为单位,输出指定的时间范围
    阿里云4核8G服务器优惠价格表,最低价格501.90元6个月、983.80元1年
    详解设计模式:享元模式
    CentOS7一键安装OpenStack
  • 原文地址:https://blog.csdn.net/weixin_42981560/article/details/127390580