這篇“怎么給所有的async函數(shù)添加try/catch”文章的知識點大部分人都不太理解,所以小編給大家總結了以下內(nèi)容,內(nèi)容詳細,步驟清晰,具有一定的借鑒價值,希望大家閱讀完這篇文章能有所收獲,下面我們一起來看看這篇“怎么給所有的async函數(shù)添加try/catch”文章吧。
目前成都創(chuàng)新互聯(lián)已為近1000家的企業(yè)提供了網(wǎng)站建設、域名、網(wǎng)站空間、網(wǎng)站托管、企業(yè)網(wǎng)站設計、藤縣網(wǎng)站維護等服務,公司將堅持客戶導向、應用為本的策略,正道將秉承"和諧、參與、激情"的文化,與客戶和合作伙伴齊心協(xié)力一起成長,共同發(fā)展。
// 示例
async function fn() {
let value = await new Promise((resolve, reject) => {
reject('failure');
});
console.log('do something...');
}
fn()
導致瀏覽器報錯:一個未捕獲的錯誤
在開發(fā)過程中,為了保證系統(tǒng)健壯性,或者是為了捕獲異步的錯誤,需要頻繁的在 async 函數(shù)中添加 try/catch,避免出現(xiàn)上述示例的情況
可是我很懶,不想一個個加,懶惰使我們進步
?
下面,通過手寫一個babel 插件,來給所有的async函數(shù)添加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節(jié)點后,從父路徑中查找聲明的async函數(shù),獲取該函數(shù)的body(函數(shù)中包含的代碼)
3)創(chuàng)建try/catch語句,將原來async的body放入其中
4)最后將async的body替換成創(chuàng)建的try/catch語句
先聊聊 AST 這個帥小伙?,不然后面的開發(fā)流程走不下去
AST是代碼的樹形結構,生成 AST 分為兩個階段:詞法分析和 語法分析
詞法分析
詞法分析階段把字符串形式的代碼轉換為令牌(tokens),可以把tokens看作是一個扁平的語法片段數(shù)組,描述了代碼片段在整個代碼中的位置和記錄當前值的一些信息
比如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 的類型叫函數(shù)聲明;
"id": { // id 為函數(shù)聲明的 id
"type": "Identifier", // 標識符 類型
"name": "demo" // 標識符 具有名字
},
"expression": false,
"generator": false,
"async": false, // 代表是否 是 async function
"params": [ // 同級 函數(shù)的參數(shù)
{
"type": "Identifier",// 參數(shù)類型也是 Identifier
"name": "n"
}
],
"body": { // 函數(shù)體內(nèi)容 整個格式呈現(xiàn)一種樹的格式
"type": "BlockStatement", // 整個函數(shù)體內(nèi)容 為一個塊狀代碼塊類型
"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 | 函數(shù)聲明 | 聲明一個函數(shù),例如 function |
ExpressionStatement | 表達式語句 | 通常是調(diào)用一個函數(shù),例如 console.log() |
BlockStatement | 塊語句 | 包裹在 {} 塊內(nèi)的代碼,例如 if (condition){var a = 1;} |
BreakStatement | 中斷語句 | 通常指 break |
ContinueStatement | 持續(xù)語句 | 通常指 continue |
ReturnStatement | 返回語句 | 通常指 return |
SwitchStatement | Switch 語句 | 通常指 Switch Case 語句中的 Switch |
IfStatement | If 控制流語句 | 控制流語句,通常指 if(condition){}else{} |
Identifier | 標識符 | 標識,例如聲明變量時 var identi = 5 中的 identi |
CallExpression | 調(diào)用表達式 | 通常指調(diào)用一個函數(shù),例如 console.log() |
BinaryExpression | 二進制表達式 | 通常指運算,例如 1+2 |
MemberExpression | 成員表達式 | 通常指調(diào)用對象的成員,例如 console 對象的 log 成員 |
ArrayExpression | 數(shù)組表達式 | 通常指一個數(shù)組,例如 [1, 3, 5] |
FunctionExpression | 函數(shù)表達式 | 例如const func = function () {} |
ArrowFunctionExpression | 箭頭函數(shù)表達式 | 例如const func = ()=> {} |
AwaitExpression | await表達式 | 例如let val = await f() |
ObjectMethod | 對象中定義的方法 | 例如 let obj = { fn () {} } |
NewExpression | New 表達式 | 通常指使用 New 關鍵詞 |
AssignmentExpression | 賦值表達式 | 通常指將函數(shù)的返回值賦值給變量 |
UpdateExpression | 更新表達式 | 通常指更新成員值,例如 i++ |
Literal | 字面量 | 字面量 |
BooleanLiteral | 布爾型字面量 | 布爾值,例如 true false |
NumericLiteral | 數(shù)字型字面量 | 數(shù)字,例如 100 |
StringLiteral | 字符型字面量 | 字符串,例如 vansenb |
SwitchCase | Case 語句 | 通常指 Switch 語句中的 Case |
1)原始代碼
async function fn() {
await f()
}
對應的AST結構
2)增加try catch后的代碼
對應的AST結構async function fn() {
try {
await f()
} catch (e) {
console.log(e)
}
}
通過AST結構對比,插件的核心就是將原始函數(shù)的body放到try語句中
插件的基本格式示例
module.exports = function (babel) {
let t = babel.type
return {
visitor: {
// 設置需要范圍的節(jié)點類型
CallExression: (path, state) => {
do soming ……
}
}
}
}
1)通過 babel
拿到 types
對象,操作 AST 節(jié)點,比如創(chuàng)建、校驗、轉變等
2)visitor
:定義了一個訪問者,可以設置需要訪問的節(jié)點類型,當訪問到目標節(jié)點后,做相應的處理來實現(xiàn)插件的功能
回到業(yè)務需求,現(xiàn)在需要找到await節(jié)點,可以通過AwaitExpression
表達式獲取
module.exports = function (babel) {
let t = babel.type
return {
visitor: {
// 設置AwaitExpression
AwaitExpression(path) {
// 獲取當前的await節(jié)點
let node = path.node;
}
}
}
}
通過findParent
方法,在父節(jié)點中搜尋 async 節(jié)點
// async節(jié)點的屬性為true
const asyncPath = path.findParent(p => p.node.async)
async 節(jié)點的AST結構
這里要注意,async 函數(shù)分為4種情況:函數(shù)聲明 、箭頭函數(shù) 、函數(shù)表達式 、函數(shù)為對象的方法
// 1??:函數(shù)聲明
async function fn() {
await f()
}
// 2??:函數(shù)表達式
const fn = async function () {
await f()
};
// 3??:箭頭函數(shù)
const fn = async () => {
await f()
};
// 4??:async函數(shù)定義在對象中
const obj = {
async fn() {
await f()
}
}
需要對這幾種情況進行分別判斷
module.exports = function (babel) {
let t = babel.type
return {
visitor: {
// 設置AwaitExpression
AwaitExpression(path) {
// 獲取當前的await節(jié)點
let node = path.node;
// 查找async函數(shù)的節(jié)點
const asyncPath = path.findParent((p) => p.node.async && (p.isFunctionDeclaration() || p.isArrowFunctionExpression() || p.isFunctionExpression() || p.isObjectMethod()));
}
}
}
}
babel-template可以用以字符串形式的代碼來構建AST樹節(jié)點,快速優(yōu)雅開發(fā)插件
// 引入babel-template
const template = require('babel-template');
// 定義try/catch語句模板
let tryTemplate = `
try {
} catch (e) {
console.log(CatchError:e)
}`;
// 創(chuàng)建模板
const temp = template(tryTemplate);
// 給模版增加key,添加console.log打印信息
let tempArgumentObj = {
// 通過types.stringLiteral創(chuàng)建字符串字面量
CatchError: types.stringLiteral('Error')
};
// 通過temp創(chuàng)建try語句的AST節(jié)點
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);
// 獲取父節(jié)點的函數(shù)體body
let info = asyncPath.node.body;
// 將函數(shù)體放到try語句的body中
tryNode.block.body.push(...info.body);
// 將父節(jié)點的body替換成新創(chuàng)建的try語句
info.body = [tryNode];
}
}
}
}
到這里,插件的基本結構已經(jīng)成型,但還有點問題,如果函數(shù)已存在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節(jié)點的type類型
let type = asyncPath.node.type;
switch (type) {
// 1??函數(shù)表達式
// 情況1:普通函數(shù),如const func = async function () {}
// 情況2:箭頭函數(shù),如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??函數(shù)聲明,如async function fn2() {}
case 'FunctionDeclaration':
asyncName = (asyncPath.node.id && asyncPath.node.id.name) || '';
break;
// 3??async函數(shù)作為對象的方法,如vue項目中,在methods中定義的方法: methods: { async func() {} }
case 'ObjectMethod':
asyncName = asyncPath.node.key.name || '';
break;
}
// 若asyncName不存在,通過argument.callee獲取當前執(zhí)行函數(shù)的name
let funcName = asyncName || (node.argument.callee && node.argument.callee.name) || '';
用戶引入插件時,可以設置exclude
、include
、 customLog
選項
exclude
: 設置需要排除的文件,不對該文件進行處理
include
: 設置需要處理的文件,只對該文件進行處理
customLog
: 用戶自定義的打印信息
入口文件index.js
// babel-template 用于將字符串形式的代碼來構建AST樹節(jié)點
const template = require('babel-template');
const { tryTemplate, catchConsole, mergeOptions, matchesFile } = require('./util');
module.exports = function (babel) {
// 通過babel 拿到 types 對象,操作 AST 節(jié)點,比如創(chuàng)建、校驗、轉變等
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節(jié)點
let node = path.node;
// 在父路徑節(jié)點中查找聲明 async 函數(shù)的節(jié)點
// async 函數(shù)分為4種情況:函數(shù)聲明 || 箭頭函數(shù) || 函數(shù)表達式 || 對象的方法
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??函數(shù)表達式
// 情況1:普通函數(shù),如const func = async function () {}
// 情況2:箭頭函數(shù),如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??函數(shù)聲明,如async function fn2() {}
case 'FunctionDeclaration':
asyncName = (asyncPath.node.id && asyncPath.node.id.name) || '';
break;
// 3??async函數(shù)作為對象的方法,如vue項目中,在methods中定義的方法: methods: { async func() {} }
case 'ObjectMethod':
asyncName = asyncPath.node.key.name || '';
break;
}
// 若asyncName不存在,通過argument.callee獲取當前執(zhí)行函數(shù)的name
let funcName = asyncName || (node.argument.callee && node.argument.callee.name) || '';
const temp = template(tryTemplate);
// 給模版增加key,添加console.log打印信息
let tempArgumentObj = {
// 通過types.stringLiteral創(chuàng)建字符串字面量
CatchError: types.stringLiteral(catchConsole(filePath, funcName, options.customLog))
};
// 通過temp創(chuàng)建try語句
let tryNode = temp(tempArgumentObj);
// 獲取async節(jié)點(父節(jié)點)的函數(shù)體
let info = asyncPath.node.body;
// 將父節(jié)點原來的函數(shù)體放到try語句中
tryNode.block.body.push(...info.body);
// 將父節(jié)點的內(nèi)容替換成新創(chuàng)建的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 - 當前執(zhí)行文件的路徑
* @param {string} funcName - 當前執(zhí)行方法的名稱
* @param {string} customLog - 用戶自定義的打印信息
*/
let catchConsole = (filePath, funcName, customLog) => `
filePath: ${filePath}
funcName: ${funcName}
${customLog}:`;
// 默認配置
const defaultOptions = {
customLog: 'Error',
exclude: ['node_modules'],
include: []
};
// 判斷執(zhí)行的file文件 是否在 exclude/include 選項內(nèi)
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網(wǎng)站搜索babel-plugin-await-add-trycatch
以上就是關于“怎么給所有的async函數(shù)添加try/catch”這篇文章的內(nèi)容,相信大家都有了一定的了解,希望小編分享的內(nèi)容對大家有幫助,若想了解更多相關的知識內(nèi)容,請關注創(chuàng)新互聯(lián)行業(yè)資訊頻道。