前端(vue)入門到精通課程:進(jìn)入學(xué)習(xí)
Apipost = Postman + Swagger + Mock + Jmeter 超好用的API調(diào)試工具:點(diǎn)擊使用
去阿里面試,三面的時(shí)候被問到了這個(gè)問題,當(dāng)時(shí)思路雖然正確,可惜表述的不夠清晰
后來花了一些時(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ò)誤
在開發(fā)過程中,為了保證系統(tǒng)健壯性,或者是為了捕獲異步的錯(cuò)誤,需要頻繁的在 async 函數(shù)中添加 try/catch,避免出現(xiàn)上述示例的情況
可是我很懶,不想一個(gè)個(gè)加,懶惰使我們進(jìn)步
?
下面,通過手寫一個(gè)babel 插件,來給所有的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ò)信息:
通過詳細(xì)的報(bào)錯(cuò)信息,幫助我們快速找到目標(biāo)文件和具體的報(bào)錯(cuò)方法,方便去定位問題
babel插件的實(shí)現(xiàn)思路
1)借助AST抽象語法樹,遍歷查找代碼中的await關(guān)鍵字
2)找到await節(jié)點(diǎn)后,從父路徑中查找聲明的async函數(shù),獲取該函數(shù)的body(函數(shù)中包含的代碼)
3)創(chuàng)建try/catch語句,將原來async的body放入其中
4)最后將async的body替換成創(chuàng)建的try/catch語句
babel的核心:AST
先聊聊 AST 這個(gè)帥小伙?,不然后面的開發(fā)流程走不下去
AST是代碼的樹形結(jié)構(gòu),生成 AST 分為兩個(gè)階段:詞法分析和 語法分析
詞法分析
詞法分析階段把字符串形式的代碼轉(zhuǎn)換為令牌(tokens) ,可以把tokens看作是一個(gè)扁平的語法片段數(shù)組,描述了代碼片段在整個(gè)代碼中的位置和記錄當(dāng)前值的一些信息
比如let a = 1
,對應(yīng)的AST是這樣的
語法分析
語法分析階段會(huì)把token轉(zhuǎn)換成 AST 的形式,這個(gè)階段會(huì)使用token中的信息把它們轉(zhuǎn)換成一個(gè) AST 的表述結(jié)構(gòu),使用type屬性記錄當(dāng)前的類型
例如 let 代表著一個(gè)變量聲明的關(guān)鍵字,所以它的 type 為 VariableDeclaration
,而 a = 1 會(huì)作為 let 的聲明描述,它的 type 為 VariableDeclarator
AST在線查看工具:AST explorer
再舉個(gè)?,加深對AST的理解
function demo(n) { return n * n; }
轉(zhuǎn)化成AST的結(jié)構(gòu)
{ "type": "Program", // 整段代碼的主體 "body": [ { "type": "FunctionDeclaration", // function 的類型叫函數(shù)聲明; "id": { // id 為函數(shù)聲明的 id "type": "Identifier", // 標(biāo)識(shí)符 類型 "name": "demo" // 標(biāo)識(shí)符 具有名字 }, "expression": false, "generator": false, "async": false, // 代表是否 是 async function "params": [ // 同級(jí) 函數(shù)的參數(shù) { "type": "Identifier",// 參數(shù)類型也是 Identifier "name": "n" } ], "body": { // 函數(shù)體內(nèi)容 整個(gè)格式呈現(xiàn)一種樹的格式 "type": "BlockStatement", // 整個(gè)函數(shù)體內(nèi)容 為一個(gè)塊狀代碼塊類型 "body": [ { "type": "ReturnStatement", // return 類型 "argument": { "type": "BinaryExpression",// BinaryExpression 二進(jìn)制表達(dá)式類型 "start": 30, "end": 35, "left": { // 分左 右 中 結(jié)構(gòu) "type": "Identifier", "name": "n" }, "operator": "*", // 屬于操作符 "right": { "type": "Identifier", "name": "n" } } } ] } } ], "sourceType": "module" }
常用的 AST 節(jié)點(diǎn)類型對照表
類型原名稱 | 中文名稱 | 描述 |
---|---|---|
Program | 程序主體 | 整段代碼的主體 |
VariableDeclaration | 變量聲明 | 聲明一個(gè)變量,例如 var let const |
FunctionDeclaration |
函數(shù)聲明 | 聲明一個(gè)函數(shù),例如 function |
ExpressionStatement | 表達(dá)式語句 | 通常是調(diào)用一個(gè)函數(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 | 標(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)用對象的成員,例如 console 對象的 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 |
對象中定義的方法 | 例如 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 語句 | 通常指 Switch 語句中的 Case |
await節(jié)點(diǎn)對應(yīng)的AST結(jié)構(gòu)
1)原始代碼
async function fn() { await f() }
對應(yīng)的AST結(jié)構(gòu)
2)增加try catch后的代碼
async function fn() { try { await f() } catch (e) { console.log(e) } }
對應(yīng)的AST結(jié)構(gòu)
通過AST結(jié)構(gòu)對比,插件的核心就是將原始函數(shù)的body放到try語句中
babel插件開發(fā)
我曾在之前的文章中聊過如何開發(fā)一個(gè)babel插件
這里簡單回顧一下
插件的基本格式示例
module.exports = function (babel) { let t = babel.type return { visitor: { // 設(shè)置需要范圍的節(jié)點(diǎn)類型 CallExression: (path, state) => { do soming …… } } } }
1)通過 babel
拿到 types
對象,操作 AST 節(jié)點(diǎn),比如創(chuàng)建、校驗(yàn)、轉(zhuǎn)變等
2)visitor
:定義了一個(gè)訪問者,可以設(shè)置需要訪問的節(jié)點(diǎn)類型,當(dāng)訪問到目標(biāo)節(jié)點(diǎn)后,做相應(yīng)的處理來實(shí)現(xiàn)插件的功能
尋找await節(jié)點(diǎn)
回到業(yè)務(wù)需求,現(xiàn)在需要找到await節(jié)點(diǎn),可以通過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ù)
通過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ù)為對象的方法
// 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ù)定義在對象中 const obj = { async fn() { await f() } }
需要對這幾種情況進(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可以用以字符串形式的代碼來構(gòu)建AST樹節(jié)點(diǎn),快速優(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é)點(diǎn) let tryNode = temp(tempArgumentObj);
async函數(shù)體替換成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); // 獲取父節(jié)點(diǎn)的函數(shù)體body let info = asyncPath.node.body; // 將函數(shù)體放到try語句的body中 tryNode.block.body.push(...info.body); // 將父節(jié)點(diǎn)的body替換成新創(chuàng)建的try語句 info.body = [tryNode]; } } } }
到這里,插件的基本結(jié)構(gòu)已經(jīng)成型,但還有點(diǎn)問題,如果函數(shù)已存在try/catch,該怎么處理判斷呢?
若函數(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]; } } } }
添加報(bào)錯(cuò)信息
獲取報(bào)錯(cuò)時(shí)的文件路徑 filePath
和方法名稱 funcName
,方便快速定位問題
獲取文件路徑
// 獲取編譯目標(biāo)文件的路徑,如:E:myappsrcApp.vue const filePath = this.filename || this.file.opts.filename || 'unknown';
獲取報(bào)錯(cuò)的方法名稱
// 定義方法名 let asyncName = ''; // 獲取async節(jié)點(diǎn)的type類型 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)來獲得同級(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ù)作為對象的方法,如vue項(xiàng)目中,在methods中定義的方法: methods: { async func() {} } case 'ObjectMethod': asyncName = asyncPath.node.key.name || ''; break; } // 若asyncName不存在,通過argument.callee獲取當(dāng)前執(zhí)行函數(shù)的name let funcName = asyncName || (node.argument.callee && node.argument.callee.name) || '';
添加用戶選項(xiàng)
用戶引入插件時(shí),可以設(shè)置exclude
、include
、 customLog
選項(xiàng)
exclude
: 設(shè)置需要排除的文件,不對該文件進(jìn)行處理
include
: 設(shè)置需要處理的文件,只對該文件進(jìn)行處理
customLog
: 用戶自定義的打印信息
最終代碼
入口文件index.js
// babel-template 用于將字符串形式的代碼來構(gòu)建AST樹節(jié)點(diǎn) const template = require('babel-template'); const { tryTemplate, catchConsole, mergeOptions, matchesFile } = require('./util'); module.exports = function (babel) { // 通過babel 拿到 types 對象,操作 AST 節(jié)點(diǎn),比如創(chuàng)建、校驗(yàn)、轉(zhuǎn)變等 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; } // 合并插件的選項(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á)式 || 對象的方法 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)來獲得同級(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ù)作為對象的方法,如vue項(xiàng)目中,在methods中定義的方法: methods: { async func() {} } case 'ObjectMethod': asyncName = asyncPath.node.key.name || ''; break; } // 若asyncName不存在,通過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 = { // 通過types.stringLiteral創(chuàng)建字符串字面量 CatchError: types.stringLiteral(catchConsole(filePath, funcName, options.customLog)) }; // 通過temp創(chuàng)建try語句 let tryNode = temp(tempArgumentObj); // 獲取async節(jié)點(diǎn)(父節(jié)點(diǎn))的函數(shù)體 let info = asyncPath.node.body; // 將父節(jié)點(diǎn)原來的函數(shù)體放到try語句中 tryNode.block.body.push(...info.body); // 將父節(jié)點(diǎn)的內(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 - 當(dāng)前執(zhí)行文件的路徑 * @param {string} funcName - 當(dāng)前執(zhí)行方法的名稱 * @param {string} customLog - 用戶自定義的打印信息 */ 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倉庫
babel插件的安裝使用
npm網(wǎng)站搜索babel-plugin-await-add-trycatch
有興趣的朋友可以下載玩一玩
babel-plugin-await-add-trycatch
總結(jié)
通過開發(fā)這個(gè)babel插件,了解很多 AST 方面的知識(shí),了解 babel 的原理。實(shí)際開發(fā)中,大家可以結(jié)合具體的業(yè)務(wù)需求開發(fā)自己的插件
【