嘿跷坝,不要給 async 函數(shù)寫那么多 try/catch 了

轉(zhuǎn): https://blog.csdn.net/lgno2/article/details/109446712

前言

在開(kāi)發(fā)中,你是否會(huì)為了系統(tǒng)健壯性弟头,亦或者是為了捕獲異步的錯(cuò)誤纲爸,而頻繁的在 async 函數(shù)中寫 try/catch 的邏輯褥琐?

async function func() {    
try {       
 let res = await asyncFunc() 
} catch (e) { 
     //......   
}}

曾經(jīng)我在《一個(gè)合格的中級(jí)前端工程師必須要掌握的 28 個(gè) JavaScript 技巧》:https://juejin.im/post/6844903856489365518 中提到過(guò)一個(gè)優(yōu)雅處理 async/await 的方法

image.png

這樣我們就可以使用一個(gè)輔助函數(shù)包裹這個(gè) async 函數(shù)實(shí)現(xiàn)錯(cuò)誤捕獲

async function func() {   
 let [err, res] = await errorCaptured(asyncFunc)    
 if (err) {     
   //... 錯(cuò)誤捕獲  
 }   
 //...
}

但是這么做有一個(gè)缺陷就是每次使用的時(shí)候朵纷,都要引入 errorCaptured 這個(gè)輔助函數(shù)炭臭,有沒(méi)有“懶”的方法呢?

答案肯定是有的袍辞,我在那篇博客后提出了一個(gè)新的思路徽缚,可以通過(guò)一個(gè) webpack loader 來(lái)自動(dòng)注入 try/catch 代碼,最后的結(jié)果希望是這樣的

// development 
async function func() {  
 let res = await asyncFunc()    
//...其他邏輯
} // release
async function func() {    
try {       
 let res = await asyncFunc()    
} catch (e) {     
 //......    
}    
//...其他邏輯
}

是不是很棒革屠?在開(kāi)發(fā)環(huán)境中不需要任何多余的代碼,讓 webpack 自動(dòng)給生產(chǎn)環(huán)境的代碼注入錯(cuò)誤捕獲的邏輯排宰,接下來(lái)我們來(lái)逐步實(shí)現(xiàn)這個(gè) loader

loader 原理

在實(shí)現(xiàn)這個(gè) webpack loader 之前似芝,先簡(jiǎn)要介紹一下 loader 的原理,我們?cè)?webpack 中定義的一個(gè)個(gè) loader板甘,本質(zhì)上只是一個(gè)函數(shù)党瓮,在定義 loader 同時(shí)還會(huì)定義一個(gè) test 屬性,webpack 會(huì)遍歷所有的模塊名盐类,當(dāng)匹配 test 屬性定義的正則時(shí)寞奸,會(huì)將這個(gè)模塊作為 source 參數(shù)傳入 loader 中執(zhí)行

{    test: /\.vue$/,    use: "vue-loader",}

當(dāng)匹配到 .vue 結(jié)尾的文件名時(shí),會(huì)將文件作為 source 參數(shù)傳給 vue-loader在跳,use 屬性后面可以是一個(gè)字符串也可以是一個(gè)路徑枪萄,當(dāng)是字符串時(shí)默認(rèn)會(huì)視為 nodejs 模塊去 node_modules 中找

而這些文件本質(zhì)上其實(shí)就是字符串(圖片,視頻就是 Buffer 對(duì)象)猫妙,以 vue-loader 為例瓷翻,當(dāng) loader 接受到文件時(shí),通過(guò)字符串匹配將其分為 3 份割坠,模版字符串會(huì) vue-loader 編譯為 render 函數(shù)齐帚,script 部分會(huì)交給 babel-loader,style 部分會(huì)交給 css-loader彼哼,同時(shí) loader 遵守單一原則对妄,即一個(gè) loader 只做一件事,這樣可以靈活組合多個(gè) loader敢朱,互不干擾

實(shí)現(xiàn)思路

因?yàn)?loader 可以讀取匹配到的文件剪菱,經(jīng)過(guò)處理變成期望的輸出結(jié)果,所以我們可以自己實(shí)現(xiàn)一個(gè) loader拴签,接受 js 文件琅豆,當(dāng)遇到 await 關(guān)鍵字時(shí),給代碼包裹一層 try/catch

那么如何能夠準(zhǔn)確給 await 及后面的表達(dá)式包裹 try/catch 呢篓吁?這里需要用到抽象語(yǔ)法樹(shù)(AST)相關(guān)的知識(shí)

AST

抽象語(yǔ)法樹(shù)是源代碼[1]語(yǔ)法[2]結(jié)構(gòu)的一種抽象表示茫因。它以樹(shù)狀[3]的形式表現(xiàn)編程語(yǔ)言[4]的語(yǔ)法結(jié)構(gòu),樹(shù)上的每個(gè)節(jié)點(diǎn)都表示源代碼中的一種結(jié)構(gòu)

通過(guò) AST 可以實(shí)現(xiàn)很多非常有用的功能杖剪,例如將 ES6 以后的代碼轉(zhuǎn)為 ES5冻押,eslint 的檢查驰贷,代碼美化,甚至 js 引擎都是依賴 AST 實(shí)現(xiàn)的洛巢,同時(shí)因?yàn)榇a本質(zhì)只是單純的字符串括袒,所以并不僅限于 js 之間的轉(zhuǎn)換,scss稿茉,less 等 css 預(yù)處理器也是通過(guò) AST 轉(zhuǎn)為瀏覽器認(rèn)識(shí)的 css 代碼锹锰,我們來(lái)舉個(gè)例子

let a = 1let b = a + 5

將其轉(zhuǎn)換為抽象語(yǔ)法樹(shù)后是這樣的

[圖片上傳失敗...(image-894e6a-1658826986754)]

將字符串轉(zhuǎn)為 AST 樹(shù)需要經(jīng)過(guò)詞法分析和語(yǔ)法分析兩步

詞法分析將一個(gè)個(gè)代碼片段轉(zhuǎn)為 token (詞法單元),去除空格注釋漓库,例如第一行會(huì)將 let恃慧,a,=渺蒿,1 這 4 個(gè)轉(zhuǎn)為 token痢士,token 是一個(gè)對(duì)象,描述了代碼片段在整個(gè)代碼中的位置和記錄當(dāng)前值的一些信息

[圖片上傳失敗...(image-cc0d91-1658826986754)]

語(yǔ)法分析會(huì)將 token 結(jié)合當(dāng)前語(yǔ)言(JS)的語(yǔ)法轉(zhuǎn)換成 Node(節(jié)點(diǎn))茂装,同時(shí) Node 包含一個(gè) type 屬性記錄當(dāng)前的類型怠蹂,例如 let 在 JS 中代表著一個(gè)變量聲明的關(guān)鍵字,所以它的 type 為 VariableDeclaration少态,而 a = 1 會(huì)作為 let 的聲明描述城侧,它的 type 為 VariableDeclarator,而聲明描述是依賴于變量聲明的彼妻,所以是一種上下的層級(jí)關(guān)系

另外可以發(fā)現(xiàn)并不是一個(gè) token 對(duì)應(yīng)一個(gè) Node赞庶,等號(hào)左右必須都有值才能組成一個(gè)聲明語(yǔ)句,否則會(huì)作出警告澳骤,這就是 eslint 的基本原理歧强。最后所有的 Node 組合在一起就形成了 AST 語(yǔ)法樹(shù)

推薦一個(gè)很實(shí)用的 AST 查看工具,AST explorer为肮,更直觀的查看代碼是如何轉(zhuǎn)為抽象語(yǔ)法樹(shù)

回到代碼的實(shí)現(xiàn)摊册,我們只需要通過(guò) AST 樹(shù)找到 await 表達(dá)式,將 await 外面包裹一層 try/catch 的 Node 節(jié)點(diǎn)即可

async function func() {   await asyncFunc()}

對(duì)應(yīng) AST 樹(shù):

image.png
async function func() {    
try {        
    await asyncFunc()    
} catch (e) {       
 console.log(e)    
}}

對(duì)應(yīng) AST 樹(shù):

[圖片上傳失敗...(image-728776-1658826986754)]

loader 開(kāi)發(fā)

有了具體的思路颊艳,接下來(lái)我們開(kāi)始編寫 loader茅特,當(dāng)我們的 loader 接收到 source 文件時(shí),通過(guò) @babel/parser
這個(gè)包可以將文件轉(zhuǎn)換為 AST 抽象語(yǔ)法樹(shù)棋枕,那么如何找到對(duì)應(yīng)的 await 表達(dá)式呢白修?

這就需要另外一個(gè) babel 的包 @babel/traverse,通過(guò) @babel/traverse 可以傳入一個(gè) AST 樹(shù)和一些鉤子函數(shù)重斑,隨后深度遍歷傳入的 AST 樹(shù)兵睛,當(dāng)遍歷的節(jié)點(diǎn)和鉤子函數(shù)的名字相同時(shí),會(huì)執(zhí)行對(duì)應(yīng)的回調(diào)

const parser = require("@babel/parser")
const traverse = require("@babel/traverse").default module.exports = function (source) {    
    let ast = parser.parse(source)     traverse(ast, {        
    AwaitExpression(path) {            
      //...         
     }   
 })  
  //...}

通過(guò) @babel/traverse 我們能夠輕松的找到 await 表達(dá)式對(duì)應(yīng)的 Node 節(jié)點(diǎn),接下來(lái)就是創(chuàng)建一個(gè)類型為 TryStatement 的 Node 節(jié)點(diǎn)祖很,最后 await 放入其中笛丙。這里還需要依賴另外一個(gè)包 @babel/types,可以理解為 babel 版的 loadsh 庫(kù)假颇,它提供了很多和 AST 的 Node 節(jié)點(diǎn)相關(guān)的輔助函數(shù)胚鸯,我們需要用到其中的 tryStatement 方法,即創(chuàng)建一個(gè) TryStatement 的 Node 節(jié)點(diǎn)

const parser = require("@babel/parser")
const traverse = require("@babel/traverse").default
const t = require("@babel/types") module.exports = function (source) {    
   let ast = parser.parse(source)     traverse(ast, {        
AwaitExpression(path) {            
let tryCatchAst = t.tryStatement(                //...            )            //...        }    })}

tryStatement 接受 3 個(gè)參數(shù)笨鸡,第一個(gè)是 try 子句姜钳,第二個(gè)是 catch 子句,第三個(gè)是 finally 子句形耗,一個(gè)完整的 try/catch 語(yǔ)句對(duì)應(yīng)的 Node 節(jié)點(diǎn)看起來(lái)像這樣

const parser = require("@babel/parser")const traverse = require("@babel/traverse").defaultconst t = require("@babel/types") const parser = require("@babel/parser")const traverse = require("@babel/traverse").defaultconst t = require("@babel/types") module.exports = function (source) {    let ast = parser.parse(source)     traverse(ast, {        AwaitExpression(path) {            let tryCatchAst = t.tryStatement(                // try 子句(必需項(xiàng))                t.blockStatement([                    t.expressionStatement(path.node)                ]),                // catch 子句                t.catchClause(                    //...                )            )            path.replaceWithMultiple([                tryCatchAst            ])        }    })      //...}

使用 blockStatement 哥桥,expressionStatement 方法創(chuàng)建一個(gè)塊級(jí)作用域和承載 await 表達(dá)式的 Node 節(jié)點(diǎn),@babel/traverse 會(huì)給每個(gè)鉤子函數(shù)傳入一個(gè) path 參數(shù)趟脂,包含了當(dāng)前遍歷的一些信息,例如當(dāng)前節(jié)點(diǎn)例衍,上個(gè)遍歷的 path 對(duì)象和對(duì)應(yīng)的節(jié)點(diǎn)昔期,最重要的是里面有一些可以操作 Node 節(jié)點(diǎn)的方法,我們需要使用到 replaceWithMultiple 這個(gè)方法來(lái)將當(dāng)前的 Node 節(jié)點(diǎn)替換為 try/catch 語(yǔ)句的 Node 節(jié)點(diǎn)

另外我們要考慮到 await 表達(dá)式可能是是作為一個(gè)聲明語(yǔ)句

 let res = await asyncFunc()

也有可能是一個(gè)賦值語(yǔ)句

 res = await asyncFunc()

還有可能只是一個(gè)單純的表達(dá)式

 await asyncFunc()

這 3 種情況對(duì)應(yīng)的 AST 也是不一樣的佛玄,所以我們需要對(duì)其分別處理硼一,@bable/types 提供了豐富的判斷函數(shù),在 AwaitExpression 鉤子函數(shù)中梦抢,我們只需要判斷上級(jí)節(jié)點(diǎn)是哪種類型的 Node 節(jié)點(diǎn)即可般贼,另外也可以通過(guò) AST explorer 來(lái)查看最終需要生成的 AST 樹(shù)的結(jié)構(gòu)

const parser = require("@babel/parser")const traverse = require("@babel/traverse").defaultconst t = require("@babel/types") module.exports = function (source) {    let ast = parser.parse(source)     traverse(ast, {        AwaitExpression(path) {            if (t.isVariableDeclarator(path.parent)) { // 變量聲明                let variableDeclarationPath = path.parentPath.parentPath                let tryCatchAst = t.tryStatement(                    t.blockStatement([                        variableDeclarationPath.node // Ast                    ]),                    t.catchClause(                        //...                    )                )                variableDeclarationPath.replaceWithMultiple([                    tryCatchAst                ])            } else if (t.isAssignmentExpression(path.parent)) { // 賦值表達(dá)式                let expressionStatementPath = path.parentPath.parentPath                let tryCatchAst = t.tryStatement(                    t.blockStatement([                        expressionStatementPath.node                    ]),                    t.catchClause(                        //...                    )                )                expressionStatementPath.replaceWithMultiple([                    tryCatchAst                ])            } else { // await 表達(dá)式                let tryCatchAst = t.tryStatement(                    t.blockStatement([                        t.expressionStatement(path.node)                    ]),                    t.catchClause(                        //...                    )                )                path.replaceWithMultiple([                    tryCatchAst                ])            }        }    })      //...}

在拿到替換后的 AST 樹(shù)后,使用 @babel/core 包中的 transformFromAstSync 方法將 AST 樹(shù)重新轉(zhuǎn)為對(duì)應(yīng)的代碼字符串返回即可

const parser = require("@babel/parser")const traverse = require("@babel/traverse").defaultconst t = require("@babel/types")const core = require("@babel/core") module.exports = function (source) {    let ast = parser.parse(source)     traverse(ast, {        AwaitExpression(path) {              // 同上        }    })    return core.transformFromAstSync(ast).code}

在這基礎(chǔ)上還暴露了一些 loader 配置項(xiàng)以提高易用性奥吩,例如如果 await 語(yǔ)句已經(jīng)被 try/catch 包裹則不會(huì)再次注入哼蛆,其原理也是基于 AST,利用 path 參數(shù)的 findParent 方法向上遍歷所有父節(jié)點(diǎn)霞赫,判斷是否被 try/catch 的 Node 包裹

traverse(ast, {    AwaitExpression(path) {        if (path.findParent((path) => t.isTryStatement(path.node))) return        // 處理邏輯    }})

另外 catch 子句中的代碼片段也支持自定義腮介,這樣使得所有錯(cuò)誤都使用統(tǒng)一邏輯處理,原理是將用戶配置的代碼片段轉(zhuǎn)為 AST端衰,在 TryStatement 節(jié)點(diǎn)被創(chuàng)建的時(shí)候作為參數(shù)傳入其 catch 節(jié)點(diǎn)

進(jìn)一步改進(jìn)

經(jīng)過(guò)評(píng)論區(qū)的交流叠洗,我將默認(rèn)給每個(gè) await 語(yǔ)句添加一個(gè) try/catch,修改為給整個(gè) async 函數(shù)包裹 try/catch旅东,原理是先找到 await 語(yǔ)句灭抑,然后遞歸向上遍歷

當(dāng)找到 async 函數(shù)時(shí),創(chuàng)建一個(gè) try/catch 的 Node 節(jié)點(diǎn)抵代,并將原來(lái) async 函數(shù)中的代碼作為 Node 節(jié)點(diǎn)的子節(jié)點(diǎn)腾节,并替換 async 函數(shù)的函數(shù)體

當(dāng)遇到 try/catch,說(shuō)明已經(jīng)被 try/catch 包裹,取消注入禀倔,直接退出遍歷榄融,這樣當(dāng)用戶有自定義的錯(cuò)誤捕獲代碼就不會(huì)執(zhí)行 loader 默認(rèn)的捕獲邏輯了

image.png

對(duì)應(yīng) AST 樹(shù):

image.png
image.png

對(duì)應(yīng) AST 樹(shù):

image.png

這只是最基本的 async 函數(shù)聲明的 node 節(jié)點(diǎn),另外還有函數(shù)表達(dá)式救湖,箭頭函數(shù)愧杯,作為對(duì)象的方法等這些表現(xiàn)形式,當(dāng)滿足其中一種情況就注入 try/catch 代碼塊

// 函數(shù)表達(dá)式const func = async function () {    await asyncFunc()} // 箭頭函數(shù)const func2 = async () => {    await asyncFunc()} // 方法const vueComponent = {    methods: {        async func() {            await asyncFunc()        }    }}

總結(jié)

本文意在拋磚引玉鞋既,在日常開(kāi)發(fā)過(guò)程中力九,可以結(jié)合自己的業(yè)務(wù)線,開(kāi)發(fā)更加適合自己的 loader邑闺,例如技術(shù)棧是 jQuery 的老項(xiàng)目跌前,可以匹配 $.ajax 的 Node 節(jié)點(diǎn),統(tǒng)一注入錯(cuò)誤處理邏輯陡舅,甚至可以自定義一些 ECMA 沒(méi)有的新語(yǔ)法

抱歉抵乓,懂編譯原理,真的是可以為所欲為

image.png

通過(guò)開(kāi)發(fā)這個(gè) loader 不僅可以學(xué)習(xí)到 webpack loader 是如何運(yùn)行的靶衍,同時(shí)了解很多 AST 方面的知識(shí)灾炭,了解 babel 的原理,更多的方法可以查看

babel 的官方文檔 :https://www.babeljs.cn/

或者

babel 手書(shū):https://github.com/jamiebuilds/babel-handbook

關(guān)于這個(gè) loader 我已經(jīng)發(fā)布到 npm 上颅眶,有興趣的朋友可以直接調(diào)用 npm install async-catch-loader \-D 安裝和研究蜈出,使用方法和一般 loader 一樣,記得放在 babel-loader 后面涛酗,以便優(yōu)先執(zhí)行铡原,將注入后的結(jié)果繼續(xù)交給 babel 轉(zhuǎn)義

{    test: /\.js$/,    use: [        "babel-loader?cacheDirectory=true",        'async-catch-loader'    ]}

更多細(xì)節(jié)和源代碼可以查看 github,同時(shí)本文對(duì)您有收獲的話商叹,希望能點(diǎn)個(gè) star燕刻,非常感謝~

async-catch-loader : https://github.com/yeyan1996/async-catch-loader

參考資料

[1]

源代碼: https://zh.wikipedia.org/wiki/%E6%BA%90%E4%BB%A3%E7%A0%81

[2]

語(yǔ)法學(xué): https://zh.wikipedia.org/wiki/%E8%AF%AD%E6%B3%95%E5%AD%A6

[3]

樹(shù) (圖論): https://zh.wikipedia.org/wiki/樹(shù)_(圖論)

[4]

編程語(yǔ)言: https://zh.wikipedia.org/wiki/%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80

最后

看完點(diǎn)個(gè)贊,分享一下吧剖笙,讓更多的朋友能夠看到酌儒。如果你喜歡前端開(kāi)發(fā)博客的分享,就給公號(hào)標(biāo)個(gè)星吧枯途,這樣就不會(huì)錯(cuò)過(guò)我的文章了忌怎。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市酪夷,隨后出現(xiàn)的幾起案子榴啸,更是在濱河造成了極大的恐慌,老刑警劉巖晚岭,帶你破解...
    沈念sama閱讀 221,888評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件鸥印,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)库说,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,677評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門狂鞋,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人潜的,你說(shuō)我怎么就攤上這事骚揍。” “怎么了啰挪?”我有些...
    開(kāi)封第一講書(shū)人閱讀 168,386評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵信不,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我亡呵,道長(zhǎng)抽活,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,726評(píng)論 1 297
  • 正文 為了忘掉前任锰什,我火速辦了婚禮下硕,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘汁胆。我一直安慰自己梭姓,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,729評(píng)論 6 397
  • 文/花漫 我一把揭開(kāi)白布沦泌。 她就那樣靜靜地躺著糊昙,像睡著了一般辛掠。 火紅的嫁衣襯著肌膚如雪谢谦。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 52,337評(píng)論 1 310
  • 那天萝衩,我揣著相機(jī)與錄音回挽,去河邊找鬼。 笑死猩谊,一個(gè)胖子當(dāng)著我的面吹牛千劈,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播牌捷,決...
    沈念sama閱讀 40,902評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼墙牌,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了暗甥?” 一聲冷哼從身側(cè)響起喜滨,我...
    開(kāi)封第一講書(shū)人閱讀 39,807評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎撤防,沒(méi)想到半個(gè)月后虽风,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,349評(píng)論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,439評(píng)論 3 340
  • 正文 我和宋清朗相戀三年辜膝,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了无牵。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,567評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡厂抖,死狀恐怖茎毁,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情验游,我是刑警寧澤充岛,帶...
    沈念sama閱讀 36,242評(píng)論 5 350
  • 正文 年R本政府宣布,位于F島的核電站耕蝉,受9級(jí)特大地震影響崔梗,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜垒在,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,933評(píng)論 3 334
  • 文/蒙蒙 一蒜魄、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧场躯,春花似錦谈为、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,420評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至签舞,卻和暖如春秕脓,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背儒搭。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,531評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工吠架, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人搂鲫。 一個(gè)月前我還...
    沈念sama閱讀 48,995評(píng)論 3 377
  • 正文 我出身青樓傍药,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親魂仍。 傳聞我的和親對(duì)象是個(gè)殘疾皇子拐辽,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,585評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容

  • 前言 在開(kāi)發(fā)中俱诸,你是否會(huì)為了系統(tǒng)健壯性,亦或者是為了捕獲異步的錯(cuò)誤仑氛,而頻繁的在 async 函數(shù)中寫 try/ca...
    心_c2a2閱讀 3,498評(píng)論 1 19
  • 引言 當(dāng)下乙埃,正面臨著近幾年來(lái)的最嚴(yán)重的互聯(lián)網(wǎng)寒冬闸英,聽(tīng)得最多的一句話便是:相見(jiàn)于江湖~??〗橥啵縮減 HC甫何、裁員不絕于耳,...
    王大白_閱讀 907評(píng)論 0 0
  • 0 HTML5相關(guān) websocket WebSocket 使用ws或wss協(xié)議遇伞,Websocket是一個(gè)持久化的...
    可愛(ài)多小姐閱讀 884評(píng)論 0 0
  • 這也是個(gè)人去年面試準(zhǔn)備的一些問(wèn)題辙喂,有點(diǎn)效果吧,過(guò)了一個(gè)多小時(shí)的大廠一面鸠珠,分享給大家巍耗。 問(wèn):如何理解html標(biāo)簽語(yǔ)義...
    飛躍瘋?cè)嗽篲a閱讀 868評(píng)論 0 2
  • 一、CSS相關(guān)問(wèn)題 1渐排、 行內(nèi)元素炬太,塊級(jí)元素,空元素 行內(nèi)元素有:a b span select strong(強(qiáng)...
    余音繞梁_0809閱讀 548評(píng)論 0 0