前端(vue)入門(mén)到精通課程:進(jìn)入學(xué)習(xí)
Apipost = Postman + Swagger + Mock + Jmeter 超好用的API調(diào)試工具:點(diǎn)擊使用
去阿里面試,三面的時(shí)候被問(wèn)到了這個(gè)問(wèn)題,當(dāng)時(shí)思路雖然正確,可惜表述的不夠清晰
后來(lái)花了一些時(shí)間整理了下思路,那么如何實(shí)現(xiàn)給所有的async函數(shù)添加try/catch呢?
async如果不加 try/catch 會(huì)發(fā)生什么事?
// 示例 async function fn() { let value = await new Promise((resolve, reject) => { reject('failure'); }); console.log('do something...'); } fn()
導(dǎo)致瀏覽器報(bào)錯(cuò):一個(gè)未捕獲的錯(cuò)誤
在開(kāi)發(fā)過(guò)程中,為了保證系統(tǒng)健壯性,或者是為了捕獲異步的錯(cuò)誤,需要頻繁的在 async 函數(shù)中添加 try/catch,避免出現(xiàn)上述示例的情況
可是我很懶,不想一個(gè)個(gè)加,懶惰使我們進(jìn)步
?
下面,通過(guò)手寫(xiě)一個(gè)babel 插件,來(lái)給所有的async函數(shù)添加try/catch
babel插件的最終效果
原始代碼:
async function fn() { await new Promise((resolve, reject) => reject('報(bào)錯(cuò)')); await new Promise((resolve) => resolve(1)); console.log('do something...'); } fn();
使用插件轉(zhuǎn)化后的代碼:
async function fn() { try { await new Promise((resolve, reject) => reject('報(bào)錯(cuò)')); await new Promise(resolve => resolve(1)); console.log('do something...'); } catch (e) { console.log("nfilePath: E:\myapp\src\main.jsnfuncName: fnnError:", e); } } fn();
打印的報(bào)錯(cuò)信息:
通過(guò)詳細(xì)的報(bào)錯(cuò)信息,幫助我們快速找到目標(biāo)文件和具體的報(bào)錯(cuò)方法,方便去定位問(wèn)題
babel插件的實(shí)現(xiàn)思路
1)借助AST抽象語(yǔ)法樹(shù),遍歷查找代碼中的await關(guān)鍵字
2)找到await節(jié)點(diǎn)后,從父路徑中查找聲明的async函數(shù),獲取該函數(shù)的body(函數(shù)中包含的代碼)
3)創(chuàng)建try/catch語(yǔ)句,將原來(lái)async的body放入其中
4)最后將async的body替換成創(chuàng)建的try/catch語(yǔ)句
babel的核心:AST
先聊聊 AST 這個(gè)帥小伙?,不然后面的開(kāi)發(fā)流程走不下去
AST是代碼的樹(shù)形結(jié)構(gòu),生成 AST 分為兩個(gè)階段:詞法分析和 語(yǔ)法分析
詞法分析
詞法分析階段把字符串形式的代碼轉(zhuǎn)換為令牌(tokens) ,可以把tokens看作是一個(gè)扁平的語(yǔ)法片段數(shù)組,描述了代碼片段在整個(gè)代碼中的位置和記錄當(dāng)前值的一些信息
比如let a = 1
,對(duì)應(yīng)的AST是這樣的
語(yǔ)法分析
語(yǔ)法分析階段會(huì)把token轉(zhuǎn)換成 AST 的形式,這個(gè)階段會(huì)使用token中的信息把它們轉(zhuǎn)換成一個(gè) AST 的表述結(jié)構(gòu),使用type屬性記錄當(dāng)前的類(lèi)型
例如 let 代表著一個(gè)變量聲明的關(guān)鍵字,所以它的 type 為 VariableDeclaration
,而 a = 1 會(huì)作為 let 的聲明描述,它的 type 為 VariableDeclarator
AST在線查看工具:AST explorer
再舉個(gè)?,加深對(duì)AST的理解
function demo(n) { return n * n; }
轉(zhuǎn)化成AST的結(jié)構(gòu)
{ "type": "Program", // 整段代碼的主體 "body": [ { "type": "FunctionDeclaration", // function 的類(lèi)型叫函數(shù)聲明; "id": { // id 為函數(shù)聲明的 id "type": "Identifier", // 標(biāo)識(shí)符 類(lèi)型 "name": "demo" // 標(biāo)識(shí)符 具有名字 }, "expression": false, "generator": false, "async": false, // 代表是否 是 async function "params": [ // 同級(jí) 函數(shù)的參數(shù) { "type": "Identifier",// 參數(shù)類(lèi)型也是 Identifier "name": "n" } ], "body": { // 函數(shù)體內(nèi)容 整個(gè)格式呈現(xiàn)一種樹(shù)的格式 "type": "BlockStatement", // 整個(gè)函數(shù)體內(nèi)容 為一個(gè)塊狀代碼塊類(lèi)型 "body": [ { "type": "ReturnStatement", // return 類(lèi)型 "argument": { "type": "BinaryExpression",// BinaryExpression 二進(jìn)制表達(dá)式類(lèi)型 "start": 30, "end": 35, "left": { // 分左 右 中 結(jié)構(gòu) "type": "Identifier", "name": "n" }, "operator": "*", // 屬于操作符 "right": { "type": "Identifier", "name": "n" } } } ] } } ], "sourceType": "module" }
常用的 AST 節(jié)點(diǎn)類(lèi)型對(duì)照表
類(lèi)型原名稱(chēng) | 中文名稱(chēng) | 描述 |
---|---|---|
Program | 程序主體 | 整段代碼的主體 |
VariableDeclaration | 變量聲明 | 聲明一個(gè)變量,例如 var let const |
FunctionDeclaration |
函數(shù)聲明 | 聲明一個(gè)函數(shù),例如 function |
ExpressionStatement | 表達(dá)式語(yǔ)句 | 通常是調(diào)用一個(gè)函數(shù),例如 console.log() |
BlockStatement | 塊語(yǔ)句 | 包裹在 {} 塊內(nèi)的代碼,例如 if (condition){var a = 1;} |
BreakStatement | 中斷語(yǔ)句 | 通常指 break |
ContinueStatement | 持續(xù)語(yǔ)句 | 通常指 continue |
ReturnStatement | 返回語(yǔ)句 | 通常指 return |
SwitchStatement | Switch 語(yǔ)句 | 通常指 Switch Case 語(yǔ)句中的 Switch |
IfStatement | If 控制流語(yǔ)句 | 控制流語(yǔ)句,通常指 if(condition){}else{} |
Identifier | 標(biāo)識(shí)符 | 標(biāo)識(shí),例如聲明變量時(shí) var identi = 5 中的 identi |
CallExpression | 調(diào)用表達(dá)式 | 通常指調(diào)用一個(gè)函數(shù),例如 console.log() |
BinaryExpression | 二進(jìn)制表達(dá)式 | 通常指運(yùn)算,例如 1+2 |
MemberExpression | 成員表達(dá)式 | 通常指調(diào)用對(duì)象的成員,例如 console 對(duì)象的 log 成員 |
ArrayExpression | 數(shù)組表達(dá)式 | 通常指一個(gè)數(shù)組,例如 [1, 3, 5] |
FunctionExpression |
函數(shù)表達(dá)式 | 例如const func = function () {} |
ArrowFunctionExpression |
箭頭函數(shù)表達(dá)式 | 例如const func = ()=> {} |
AwaitExpression |
await表達(dá)式 | 例如let val = await f() |
ObjectMethod |
對(duì)象中定義的方法 | 例如 let obj = { fn () {} } |
NewExpression | New 表達(dá)式 | 通常指使用 New 關(guān)鍵詞 |
AssignmentExpression | 賦值表達(dá)式 | 通常指將函數(shù)的返回值賦值給變量 |
UpdateExpression | 更新表達(dá)式 | 通常指更新成員值,例如 i++ |
Literal | 字面量 | 字面量 |
BooleanLiteral | 布爾型字面量 | 布爾值,例如 true false |
NumericLiteral | 數(shù)字型字面量 | 數(shù)字,例如 100 |
StringLiteral | 字符型字面量 | 字符串,例如 vansenb |
SwitchCase | Case 語(yǔ)句 | 通常指 Switch 語(yǔ)句中的 Case |
await節(jié)點(diǎn)對(duì)應(yīng)的AST結(jié)構(gòu)
1)原始代碼
async function fn() { await f() }
對(duì)應(yīng)的AST結(jié)構(gòu)
2)增加try catch后的代碼
async function fn() { try { await f() } catch (e) { console.log(e) } }
對(duì)應(yīng)的AST結(jié)構(gòu)
通過(guò)AST結(jié)構(gòu)對(duì)比,插件的核心就是將原始函數(shù)的body放到try語(yǔ)句中
babel插件開(kāi)發(fā)
我曾在之前的文章中聊過(guò)如何開(kāi)發(fā)一個(gè)babel插件
這里簡(jiǎn)單回顧一下
插件的基本格式示例
module.exports = function (babel) { let t = babel.type return { visitor: { // 設(shè)置需要范圍的節(jié)點(diǎn)類(lèi)型 CallExression: (path, state) => { do soming …… } } } }
1)通過(guò) babel
拿到 types
對(duì)象,操作 AST 節(jié)點(diǎn),比如創(chuàng)建、校驗(yàn)、轉(zhuǎn)變等
2)visitor
:定義了一個(gè)訪問(wèn)者,可以設(shè)置需要訪問(wèn)的節(jié)點(diǎn)類(lèi)型,當(dāng)訪問(wèn)到目標(biāo)節(jié)點(diǎn)后,做相應(yīng)的處理來(lái)實(shí)現(xiàn)插件的功能
尋找await節(jié)點(diǎn)
回到業(yè)務(wù)需求,現(xiàn)在需要找到await節(jié)點(diǎn),可以通過(guò)AwaitExpression
表達(dá)式獲取
module.exports = function (babel) { let t = babel.type return { visitor: { // 設(shè)置AwaitExpression AwaitExpression(path) { // 獲取當(dāng)前的await節(jié)點(diǎn) let node = path.node; } } } }
向上查找 async 函數(shù)
通過(guò)findParent
方法,在父節(jié)點(diǎn)中搜尋 async 節(jié)點(diǎn)
// async節(jié)點(diǎn)的屬性為true const asyncPath = path.findParent(p => p.node.async)
async 節(jié)點(diǎn)的AST結(jié)構(gòu)
這里要注意,async 函數(shù)分為4種情況:函數(shù)聲明 、箭頭函數(shù) 、函數(shù)表達(dá)式 、函數(shù)為對(duì)象的方法
// 1️⃣:函數(shù)聲明 async function fn() { await f() } // 2️⃣:函數(shù)表達(dá)式 const fn = async function () { await f() }; // 3️⃣:箭頭函數(shù) const fn = async () => { await f() }; // 4️⃣:async函數(shù)定義在對(duì)象中 const obj = { async fn() { await f() } }
需要對(duì)這幾種情況進(jìn)行分別判斷
module.exports = function (babel) { let t = babel.type return { visitor: { // 設(shè)置AwaitExpression AwaitExpression(path) { // 獲取當(dāng)前的await節(jié)點(diǎn) let node = path.node; // 查找async函數(shù)的節(jié)點(diǎn) const asyncPath = path.findParent((p) => p.node.async && (p.isFunctionDeclaration() || p.isArrowFunctionExpression() || p.isFunctionExpression() || p.isObjectMethod())); } } } }
利用babel-template生成try/catch節(jié)點(diǎn)
babel-template可以用以字符串形式的代碼來(lái)構(gòu)建AST樹(shù)節(jié)點(diǎn),快速優(yōu)雅開(kāi)發(fā)插件
// 引入babel-template const template = require('babel-template'); // 定義try/catch語(yǔ)句模板 let tryTemplate = ` try { } catch (e) { console.log(CatchError:e) }`; // 創(chuàng)建模板 const temp = template(tryTemplate); // 給模版增加key,添加console.log打印信息 let tempArgumentObj = { // 通過(guò)types.stringLiteral創(chuàng)建字符串字面量 CatchError: types.stringLiteral('Error') }; // 通過(guò)temp創(chuàng)建try語(yǔ)句的AST節(jié)點(diǎn) let tryNode = temp(tempArgumentObj);
async函數(shù)體替換成try語(yǔ)句
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é)點(diǎn)的函數(shù)體body let info = asyncPath.node.body; // 將函數(shù)體放到try語(yǔ)句的body中 tryNode.block.body.push(...info.body); // 將父節(jié)點(diǎn)的body替換成新創(chuàng)建的try語(yǔ)句 info.body = [tryNode]; } } } }
到這里,插件的基本結(jié)構(gòu)已經(jīng)成型,但還有點(diǎn)問(wèn)題,如果函數(shù)已存在try/catch,該怎么處理判斷呢?
若函數(shù)已存在try/catch,則不處理
// 示例代碼,不再添加try/catch async function fn() { try { await f() } catch (e) { console.log(e) } }
通過(guò)isTryStatement
判斷是否已存在try語(yǔ)句
module.exports = function (babel) { let t = babel.type return { visitor: { AwaitExpression(path) { // 判斷父路徑中是否已存在try語(yǔ)句,若存在直接返回 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]; } } } }
添加報(bào)錯(cuò)信息
獲取報(bào)錯(cuò)時(shí)的文件路徑 filePath
和方法名稱(chēng) funcName
,方便快速定位問(wèn)題
獲取文件路徑
// 獲取編譯目標(biāo)文件的路徑,如:E:myappsrcApp.vue const filePath = this.filename || this.file.opts.filename || 'unknown';
獲取報(bào)錯(cuò)的方法名稱(chēng)
// 定義方法名 let asyncName = ''; // 獲取async節(jié)點(diǎn)的type類(lèi)型 let type = asyncPath.node.type; switch (type) { // 1️⃣函數(shù)表達(dá)式 // 情況1:普通函數(shù),如const func = async function () {} // 情況2:箭頭函數(shù),如const func = async () => {} case 'FunctionExpression': case 'ArrowFunctionExpression': // 使用path.getSibling(index)來(lái)獲得同級(jí)的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ù)作為對(duì)象的方法,如vue項(xiàng)目中,在methods中定義的方法: methods: { async func() {} } case 'ObjectMethod': asyncName = asyncPath.node.key.name || ''; break; } // 若asyncName不存在,通過(guò)argument.callee獲取當(dāng)前執(zhí)行函數(shù)的name let funcName = asyncName || (node.argument.callee && node.argument.callee.name) || '';
添加用戶(hù)選項(xiàng)
用戶(hù)引入插件時(shí),可以設(shè)置exclude
、include
、 customLog
選項(xiàng)
exclude
: 設(shè)置需要排除的文件,不對(duì)該文件進(jìn)行處理
include
: 設(shè)置需要處理的文件,只對(duì)該文件進(jìn)行處理
customLog
: 用戶(hù)自定義的打印信息
最終代碼
入口文件index.js
// babel-template 用于將字符串形式的代碼來(lái)構(gòu)建AST樹(shù)節(jié)點(diǎn) const template = require('babel-template'); const { tryTemplate, catchConsole, mergeOptions, matchesFile } = require('./util'); module.exports = function (babel) { // 通過(guò)babel 拿到 types 對(duì)象,操作 AST 節(jié)點(diǎn),比如創(chuàng)建、校驗(yàn)、轉(zhuǎn)變等 let types = babel.types; // visitor:插件核心對(duì)象,定義了插件的工作流程,屬于訪問(wèn)者模式 const visitor = { AwaitExpression(path) { // 通過(guò)this.opts 獲取用戶(hù)的配置 if (this.opts && !typeof this.opts === 'object') { return console.error('[babel-plugin-await-add-trycatch]: options need to be an object.'); } // 判斷父路徑中是否已存在try語(yǔ)句,若存在直接返回 if (path.findParent((p) => p.isTryStatement())) { return false; } // 合并插件的選項(xiàng) const options = mergeOptions(this.opts); // 獲取編譯目標(biāo)文件的路徑,如:E:myappsrcApp.vue const filePath = this.filename || this.file.opts.filename || 'unknown'; // 在排除列表的文件不編譯 if (matchesFile(options.exclude, filePath)) { return; } // 如果設(shè)置了include,只編譯include中的文件 if (options.include.length && !matchesFile(options.include, filePath)) { return; } // 獲取當(dāng)前的await節(jié)點(diǎn) let node = path.node; // 在父路徑節(jié)點(diǎn)中查找聲明 async 函數(shù)的節(jié)點(diǎn) // async 函數(shù)分為4種情況:函數(shù)聲明 || 箭頭函數(shù) || 函數(shù)表達(dá)式 || 對(duì)象的方法 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ù)表達(dá)式 // 情況1:普通函數(shù),如const func = async function () {} // 情況2:箭頭函數(shù),如const func = async () => {} case 'FunctionExpression': case 'ArrowFunctionExpression': // 使用path.getSibling(index)來(lái)獲得同級(jí)的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ù)作為對(duì)象的方法,如vue項(xiàng)目中,在methods中定義的方法: methods: { async func() {} } case 'ObjectMethod': asyncName = asyncPath.node.key.name || ''; break; } // 若asyncName不存在,通過(guò)argument.callee獲取當(dāng)前執(zhí)行函數(shù)的name let funcName = asyncName || (node.argument.callee && node.argument.callee.name) || ''; const temp = template(tryTemplate); // 給模版增加key,添加console.log打印信息 let tempArgumentObj = { // 通過(guò)types.stringLiteral創(chuàng)建字符串字面量 CatchError: types.stringLiteral(catchConsole(filePath, funcName, options.customLog)) }; // 通過(guò)temp創(chuàng)建try語(yǔ)句 let tryNode = temp(tempArgumentObj); // 獲取async節(jié)點(diǎn)(父節(jié)點(diǎn))的函數(shù)體 let info = asyncPath.node.body; // 將父節(jié)點(diǎn)原來(lái)的函數(shù)體放到try語(yǔ)句中 tryNode.block.body.push(...info.body); // 將父節(jié)點(diǎn)的內(nèi)容替換成新創(chuàng)建的try語(yǔ)句 info.body = [tryNode]; } }; return { name: 'babel-plugin-await-add-trycatch', visitor }; };
util.js
const merge = require('deepmerge'); // 定義try語(yǔ)句模板 let tryTemplate = ` try { } catch (e) { console.log(CatchError,e) }`; /* * catch要打印的信息 * @param {string} filePath - 當(dāng)前執(zhí)行文件的路徑 * @param {string} funcName - 當(dāng)前執(zhí)行方法的名稱(chēng) * @param {string} customLog - 用戶(hù)自定義的打印信息 */ let catchConsole = (filePath, funcName, customLog) => ` filePath: ${filePath} funcName: ${funcName} ${customLog}:`; // 默認(rèn)配置 const defaultOptions = { customLog: 'Error', exclude: ['node_modules'], include: [] }; // 判斷執(zhí)行的file文件 是否在 exclude/include 選項(xiàng)內(nèi) function matchesFile(list, filename) { return list.find((name) => name && filename.includes(name)); } // 合并選項(xiàng) function mergeOptions(options) { let { exclude, include } = options; if (exclude) options.exclude = toArray(exclude); if (include) options.include = toArray(include); // 使用merge進(jìn)行合并 return merge.all([defaultOptions, options]); } function toArray(value) { return Array.isArray(value) ? value : [value]; } module.exports = { tryTemplate, catchConsole, defaultOptions, mergeOptions, matchesFile, toArray };
github倉(cāng)庫(kù)
babel插件的安裝使用
npm網(wǎng)站搜索babel-plugin-await-add-trycatch
有興趣的朋友可以下載玩一玩
babel-plugin-await-add-trycatch
總結(jié)
通過(guò)開(kāi)發(fā)這個(gè)babel插件,了解很多 AST 方面的知識(shí),了解 babel 的原理。實(shí)際開(kāi)發(fā)中,大家可以結(jié)合具體的業(yè)務(wù)需求開(kāi)發(fā)自己的插件
【