前端(vue)入門到精通課程:進入學習
Apipost = Postman + Swagger + Mock + Jmeter 超好用的API偵錯工具:
去阿里面試,三面的時候被問到了這個問題,當時思路雖然正確,可惜表述的不夠清晰
後來花了一些時間整理了下思路,那麼如何實現給所有的async函數新增try/catch呢?
// 範例
async function fn() {
let value = await new Promise((resolve, reject) => {
reject('failure');
});
console.log('do something...');
}
fn()
登入後複製
導致瀏覽器報錯:一個未捕獲的錯誤
在開發過程中,為了保證系統健壯性,或者是為了捕獲非同步的錯誤,需要頻繁的在 async 函數中新增 try/catch,避免出現上述範例的情況
可是我很懶,不想一個個加,懶惰使我們進步
?
下面,通過手寫一個babel 外掛,來給所有的async函數新增try/catch
原始程式碼:
async function fn() {
await new Promise((resolve, reject) => reject('報錯'));
await new Promise((resolve) => resolve(1));
console.log('do something...');
}
fn();
登入後複製
使用外掛轉化後的程式碼:
async function fn() {
try {
await new Promise((resolve, reject) => reject('報錯'));
await new Promise(resolve => resolve(1));
console.log('do something...');
} catch (e) {
console.log("\nfilePath: E:\\myapp\\src\\main.js\nfuncName: fn\nError:", e);
}
}
fn();
登入後複製
列印的報錯資訊:
通過詳細的報錯資訊,幫助我們快速找到目標檔案和具體的報錯方法,方便去定位問題
1)藉助AST抽象語法樹,遍歷查詢程式碼中的await關鍵字
2)找到await節點後,從父路徑中查詢宣告的async函數,獲取該函數的body(函數中包含的程式碼)
3)建立try/catch語句,將原來async的body放入其中
4)最後將async的body替換成建立的try/catch語句
先聊聊 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的理解
function demo(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"
}
登入後複製
型別原名稱 | 中文名稱 | 描述 |
---|---|---|
Program | 程式主體 | 整段程式碼的主體 |
VariableDeclaration | 變數宣告 | 宣告一個變數,例如 var let const |
FunctionDeclaration | 函數宣告 | 宣告一個函數,例如 function |
ExpressionStatement | 表示式語句 | 通常是呼叫一個函數,例如 console.log() |
BlockStatement | 塊語句 | 包裹在 {} 塊內的程式碼,例如 if (condition){var a = 1;} |
BreakStatement | 中斷語句 | 通常指 break |
ContinueStatement | 持續語句 | 通常指 continue |
ReturnStatement | 返回語句 | 通常指 return |
SwitchStatement | Switch 語句 | 通常指 Switch Case 語句中的 Switch |
IfStatement | If 控制流語句 | 控制流語句,通常指 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 = ()=> {} |
AwaitExpression | await表示式 | 例如let val = await f() |
ObjectMethod | 物件中定義的方法 | 例如 let obj = { fn () {} } |
NewExpression | New 表示式 | 通常指使用 New 關鍵詞 |
AssignmentExpression | 賦值表示式 | 通常指將函數的返回值賦值給變數 |
UpdateExpression | 更新表示式 | 通常指更新成員值,例如 i++ |
Literal | 字面量 | 字面量 |
BooleanLiteral | 布林型字面量 | 布林值,例如 true false |
NumericLiteral | 數位型字面量 | 數位,例如 100 |
StringLiteral | 字元型字面量 | 字串,例如 vansenb |
SwitchCase | Case 語句 | 通常指 Switch 語句中的 Case |
1)原始程式碼
async function fn() {
await f()
}
登入後複製
對應的AST結構
2)增加try catch後的程式碼
async function fn() {
try {
await f()
} catch (e) {
console.log(e)
}
}
登入後複製
對應的AST結構
通過AST結構對比,外掛的核心就是將原始函數的body放到try語句中
我曾在之前的文章中聊過如何開發一個babel外掛
這裡簡單回顧一下
module.exports = function (babel) {
let t = babel.type
return {
visitor: {
// 設定需要範圍的節點型別
CallExression: (path, state) => {
do soming ……
}
}
}
}
登入後複製
1)通過 babel
拿到 types
物件,操作 AST 節點,比如建立、校驗、轉變等
2)visitor
:定義了一個存取者,可以設定需要存取的節點型別,當存取到目標節點後,做相應的處理來實現外掛的功能
回到業務需求,現在需要找到await節點,可以通過AwaitExpression
表示式獲取
module.exports = function (babel) {
let t = babel.type
return {
visitor: {
// 設定AwaitExpression
AwaitExpression(path) {
// 獲取當前的await節點
let node = path.node;
}
}
}
}
登入後複製
通過findParent
方法,在父節點中搜尋 async 節點
// async節點的屬性為true
const asyncPath = path.findParent(p => p.node.async)
登入後複製
async 節點的AST結構
這裡要注意,async 函數分為4種情況:函數宣告 、箭頭函數 、函數表示式 、函數為物件的方法
// 1️⃣:函數宣告
async function fn() {
await f()
}
// 2️⃣:函數表示式
const fn = async function () {
await f()
};
// 3️⃣:箭頭函數
const fn = async () => {
await f()
};
// 4️⃣:async函數定義在物件中
const obj = {
async fn() {
await f()
}
}
登入後複製
需要對這幾種情況進行分別判斷
module.exports = function (babel) {
let t = babel.type
return {
visitor: {
// 設定AwaitExpression
AwaitExpression(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可以用以字串形式的程式碼來構建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);
登入後複製
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
async function fn() {
try {
await f()
} 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.vue
const 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 && !typeof this.opts === 'object') {
return console.error('[babel-plugin-await-add-trycatch]: options need to be an object.');
}
// 判斷父路徑中是否已存在try語句,若存在直接返回
if (path.findParent((p) => p.isTryStatement())) {
return false;
}
// 合併外掛的選項
const options = mergeOptions(this.opts);
// 獲取編譯目標檔案的路徑,如:E:\myapp\src\App.vue
const 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 = '';
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) || '';
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 - 使用者自定義的列印資訊
*/
let catchConsole = (filePath, funcName, customLog) => `
filePath: ${filePath}
funcName: ${funcName}
${customLog}:`;
// 預設設定
const defaultOptions = {
customLog: 'Error',
exclude: ['node_modules'],
include: []
};
// 判斷執行的file檔案 是否在 exclude/include 選項內
function matchesFile(list, filename) {
return list.find((name) => name && filename.includes(name));
}
// 合併選項
function mergeOptions(options) {
let { exclude, include } = options;
if (exclude) options.exclude = toArray(exclude);
if (include) options.include = toArray(include);
// 使用merge進行合併
return merge.all([defaultOptions, options]);
}
function toArray(value) {
return Array.isArray(value) ? value : [value];
}
module.exports = {
tryTemplate,
catchConsole,
defaultOptions,
mergeOptions,
matchesFile,
toArray
};
登入後複製
npm網站搜尋babel-plugin-await-add-trycatch
有興趣的朋友可以下載玩一玩
babel-plugin-await-add-trycatch
通過開發這個babel外掛,瞭解很多 AST 方面的知識,瞭解 babel 的原理。實際開發中,大家可以結合具體的業務需求開發自己的外掛
【相關推薦:、】
以上就是面試題:如何給所有的async函數新增try/catch?的詳細內容,更多請關注TW511.COM其它相關文章!