在co/koa里該怎么處理異常

陸陸續(xù)續(xù)用了koaco也算差不多用了大半年了憔恳,大部分的場景都是在服務(wù)端使用koa來作為restful服務(wù)器用快鱼,使用場景比較局限败砂,這里總結(jié)一下。

異常處理其實(shí)是一個程序非常重要的部分宛蚓,我自己以前寫前端代碼的時候很不注意這個凄吏,用node來寫后臺并被焦作人很多次了之后才真正明白了它的重要性图柏。因?yàn)橛胣ode來寫后臺時,常常涉及到大量的網(wǎng)絡(luò)異步請求繁涂、用戶提交诸蚕、數(shù)據(jù)庫讀寫等操作盅抚。許多場景你即使做了充足的數(shù)據(jù)校驗(yàn)和多種情況考慮,但因?yàn)槭腔诰W(wǎng)絡(luò)和用戶的禁熏,必然存在你無法掌控的情況寄症,如果你沒有做好try catch,那么一個bug就可能讓你的node server倒下示姿。雖然借助于pm2等等工具,你可以讓他倒了再立馬爬起來,但是倒下的代價是巨大的,而且如果沒有try catch,沒有日志記錄的話,你甚至都不知道倒在哪了。所以錯誤處理對于提升程序的健壯性來說是必不可少的。",

<p class="tip">這篇文章需要koa和co以及generator的原理知識,因co和generator的處理流程有一點(diǎn)繞(捋順了其實(shí)巨好懂),如果你對co/koa/generaotor不太熟悉,最好先看看阮一峰老師es6入門的第十五章和第十七章。當(dāng)然這篇文章也會講到co的原理過程,也可能解釋你的一些困惑炕吸。</p>

co里處理異常

cokoa(1)的核心赫模,整個koa可以說就是http.listen + koa compress + co +context,co保證了你能在koa的中間件generator里進(jìn)行各種yield瀑罗,完成異步函數(shù)的串行化書寫斩祭,所以在koa里處理錯誤有相當(dāng)大的比重其實(shí)是在co里處理錯誤,我們先說說怎么在co里處理錯誤掠拳,這里幾乎涵蓋了90%的處理場景柏肪。

我以前的兩種寫法

首先說說我以前是怎么在使用co時處理錯誤的姐刁。

  • 第一種:
co(function* () {
  let abcd = yield promiseAbcd.catch(function(err){
    //錯誤處理寫在這
  })
})
  • 第二種:
co(function* () {
  let abcd 
  try{
    abcd = yield promiseAbcd
  }catch(err){
    //錯誤處理寫在這
  }
})

第二種寫法其實(shí)是明顯優(yōu)于第一種的,主要原因有三個:

  • 第一種的錯誤處理寫在回調(diào)函數(shù)里谬俄,這就意味著你沒辦法對函數(shù)外部的代碼進(jìn)行流程控制柏靶,你無法在錯誤了之后改變外部代碼的執(zhí)行流程,你沒法return溃论、break屎蜓、continue,這是最大的問題钥勋。而能在異步處理中進(jìn)行try/catch我覺得co/koa的一個非常大的優(yōu)勢(對TJ真是大寫的服)炬转,要知道你對一個異步函數(shù)try/catch是并不能捕捉到他的錯誤的辆苔,如下,這是很棘手的問題扼劈。
try{
  setTimeout(function(){
    throw new Error()
  },0)
}catch(e){
  //這里根本捕捉不到錯誤的哈
}
  • 第二點(diǎn)需要說的是你如果用這種方式寫的話驻啤,你需要很清楚你現(xiàn)在yield出去的是promiseAbcd.catch()方法生成的promise,而不是promiseAbcd荐吵,這有什么問題骑冗?我們知道.catch會生成一個新的promise,并且會以你在.catch綁定的回調(diào)函數(shù)中的返回值或者throw的error先煎,作為這個新的promise的成功原因或者失敗error贼涩。所以,當(dāng)promiseAbcd出錯榨婆,首先執(zhí)行你寫的這個.catch()的綁定回調(diào)磁携,然后如果你在綁定回調(diào)里面的返回值會被co接住,并扔回到generatorFunction,于是上面的這句代碼中:
let abcd = yield promiseAbcd.catch(function(err){})

變量abcd就會是function(err){}的返回值良风,如果你沒有注意到這個細(xì)節(jié)谊迄,沒有返回值,那么abcd就會得到默認(rèn)返回值undefined烟央,然后繼續(xù)往下執(zhí)行统诺,于是很有可能后續(xù)代碼就會報錯。

  • 此外第一種寫法的問題也依然還是出在回調(diào)函數(shù)上疑俭,既然是函數(shù)粮呢,作用域就是新的作用域,所以要想訪問回調(diào)函數(shù)外層的變量的和this的話钞艇,需要使用箭頭函數(shù)啄寡,所以這種寫法就和箭頭函數(shù)這種語法糖(其實(shí)箭頭函數(shù)不是語法糖)綁定起來了,也就帶來了隱患哩照。
  • 第四點(diǎn)問題在于代碼的可讀性上挺物,明顯寫在yield后方的.catch讓你不會明顯的注意到,而改為try catch的方式飘弧,在橫向長度上的減少帶來的是縱向方向上的清晰识藤,如果遇到 yield promise.then(function(){......}),本身就已經(jīng)很長了次伶,你再后面再加個catch痴昧,這這這....

所以,第一種方式是很優(yōu)雅的冠王,但是很多同學(xué)可能會懷有疑問赶撰,為什么yield出去的異步任務(wù)竟然還能在原來的generatorcatch住,那具體什么時候哪些情況可以catch住,這其實(shí)是對于co源碼和處理過程的不熟悉導(dǎo)致的扣囊。

而且如果是并行的多個任務(wù)呢乎折,比如你要并行的查多個sql,如果你希望在每個sql后面都綁定自己的錯誤處理侵歇,那么第一種方式的一個try catch肯定不夠骂澄,比如例子如下。

  let promiseArr = []
  promiseArr.push(sqlExecPromise('sql A'))
  promiseArr.push(sqlExecPromise('sql B'))
  try{
    let [resultA, resultB] = yield promiseArr
  }catch(e){
    //這里能捕捉到上面的錯誤嗎惕虑?
    //如果上面的兩個promise都報錯呢坟冲?
    //如果我想為各個promise綁定自己的錯誤處理,這種寫法也不能滿足需求吧溃蔫?
  }

所以我們需要首先來理清楚co里面的異辰√幔控制,結(jié)合co的源碼伟叛,弄明白co的處理過程私痹,才能知道我們到底可以怎么處理錯誤。

說一嘴 iterator和generator

首先說一點(diǎn)點(diǎn)基礎(chǔ)概念:generator函數(shù)统刮,執(zhí)行后的返回值是這個generator函數(shù)的iterator(遍歷器)紊遵,然后對這個iterator執(zhí)行.next()方法,可以讓generator函數(shù)往下執(zhí)行侥蒙,并且.next()的返回值是generator函數(shù)yield出來的值暗膜。而對這個iterator執(zhí)行.throw(err)方法,這將err“傳到”generator函數(shù)當(dāng)中:

function * generatorFunction (){
    try {
        yield 'hello'
        yield 'world'
    } catch (err) {
        console.log(err)
    }
}
// 執(zhí)行g(shù)enerator函數(shù),拿到它的iterator
let iterator = generatorFunction() 
// 返回一個對象,對象的value是你yield出來的值
iterator.next() // { value: \"hello\", done: false } 
iterator.next() // { value: \"world\", done: false }
iterator.throw(new Error('some thing wrong'))
// 此時這個err被\"傳入\" generator函數(shù),
// 并被其generator的try catch捕捉到
// log: Error: some thing wrong

結(jié)合源碼看看co的錯誤上報

要說的已經(jīng)在圖里面的(抱歉圖片左側(cè).catch
里面的代碼應(yīng)該寫console.log('outer error caught')鞭衩,寫錯了)学搜,我們在co里面其實(shí)遇到的錯誤有兩種情況:

  • 一種是yield的出去的這個promise,出了問題
    co(function*(){
      yield Promise.reject(new Error())
    })
    
  • 另外一種是generatorFunction的內(nèi)部流程代碼里出了問題:
    co(function*(){
      throw new Error()
    })
    

如果是第一種论衍,我們看上面那幅我制作了好半天的圖瑞佩,co給promise綁定了回調(diào),因此坯台,當(dāng)promise失敗時钉凌,會執(zhí)行藍(lán)色框里的那句代碼iterator.throw(err)那么這個時候,err就被返回到了generator中捂人,于是let a = Promise.reject(new Error('出錯啦'))就“變成”了throw new Error('出錯啦'),因此這就使得yield出去的promise發(fā)生錯誤時矢沿,也依然可以在內(nèi)部的try catch捕捉到滥搭。

那如果情況是第二種,那也就是順著圖片里面的左側(cè)代碼繼續(xù)執(zhí)行捣鲸,這個throw的err會“流落”到哪去呢瑟匆?,我們需要知道iterator.thow(err)不僅把錯誤"塞"回到了原來的generatorFunction里栽惶,還讓generatorFunction繼續(xù)執(zhí)行了愁溜,它和iterator.next(data)的功能都是一樣的疾嗅,我們可以把他們看做一個函數(shù),這個函數(shù)在內(nèi)部調(diào)用了generatorFunction繼續(xù)執(zhí)行的函數(shù)冕象,generatorFunction繼續(xù)執(zhí)行的函數(shù)出錯代承,這個函數(shù)必然就會像外拋出錯誤,所以iterator.thow(err)iterator.next(data)不僅會把err和data塞回generatorFunction渐扮,還會繼續(xù)generatorFunction的執(zhí)行论悴,并且在執(zhí)行報錯是捕捉到錯誤。

而在其實(shí)圖片里面墓律,我們可以看到coiterator.throw(err)的代碼外側(cè)是用try catch包裹起來的膀估,在catch里面,使用了reject(err)耻讽,在iterator.next(data)的外側(cè)其實(shí)也有相似處理察纯,其實(shí)在co里,所有的iterator.next(data)和throw外側(cè)都是用這個try catch包起來的针肥,這就保證了饼记,任何generatorFunction執(zhí)行時候的錯誤都能被catch捕捉到,并將這個錯誤reject(err)祖驱,也就是轉(zhuǎn)移到co最開始return的那個promise上握恳,因此如果是第二種情況也就是generatorFunction的內(nèi)部流程代碼里出了問題,那么這個error會報告到co函數(shù)返回的promise上捺僻。

于是co(function*(){}).catch()的這個.catch就能起到捕捉這個錯誤的作用乡洼。所以大家應(yīng)該明白為什么可以try catch 我們yield出去的promise上發(fā)生的錯誤,也知道其中的一整套原理匕坯,更明白如果是generatorFunction內(nèi)部錯誤束昵,那么我們應(yīng)該怎么去捕捉。

好了葛峻,現(xiàn)在基本算是回答了之前兩個問題中的一個锹雏。

那,并發(fā)的情況呢术奖?

大家先試試下面這段代碼:

co(function * () {
    let promiseArr = []
    promiseArr.push(Promise.reject(new Error('error 1')))
    promiseArr.push(Promise.reject(new Error('error 2')))
    try {
        yield promiseArr
    } catch (err) {
        console.log('inner caught')
        console.log(err.message)
    }
  //inner caught
  //error 1
})

首先我們要知道當(dāng)我們yield 出去一個填充了promise的array或者object的時候礁遵,co幫我們做了什么?我在上面那張圖里面介紹了一個小細(xì)節(jié)采记,當(dāng)co通過value下標(biāo)從iterator.next()返回的對象中取出你yield出來的東西時佣耐,這個東西可能的情況有很多,絕大部分的情況可能是promise或者是thunk唧龄,他調(diào)用了一個內(nèi)部的函數(shù)toPromise兼砖,這個函數(shù)會把你傳出來的東西轉(zhuǎn)換成promise,如果是一個數(shù)組,對數(shù)組執(zhí)行promise.all讽挟,那么會得到一個新的promise懒叛,然后在這個promise上綁定成功和失敗回調(diào),因此耽梅,在generatorFunction內(nèi)對yield進(jìn)行try catch薛窥,會捕捉到這個父promise上的異常。對于promise.all返回的這個父promise褐墅,如果所有的子promise都成功了拆檬,他才會成功,如果任意一個子promise失敗了妥凳,那么會導(dǎo)致他的失敗竟贯,而且最關(guān)鍵的是,如果一個子promise失敗了逝钥,那么這個子promise的失敗原因(error)會作為父promise的失敗原因(error)屑那,引起父promise的失敗回調(diào)執(zhí)行,而后續(xù)的子promise的失敗都不會在父promise上產(chǎn)生效果艘款,失敗回調(diào)都不會執(zhí)行(其實(shí)成功回調(diào)也不會執(zhí)行)持际,所以我們上面只能捕捉到一個error。

插一句哗咆,promise.all的這個機(jī)制很好理解蜘欲,我雖然不清楚其內(nèi)部具體實(shí)現(xiàn),但是其實(shí)類似一個普通的promise晌柬,當(dāng)你對它的reject或者resolve執(zhí)行過一次后姥份,不管你接下來再執(zhí)行多少次resolve或者reject,都不會導(dǎo)致這個promise上綁定的成功回調(diào)或失敗回調(diào)繼續(xù)執(zhí)行年碘。

上面說明的這種情況導(dǎo)致只有第一個出現(xiàn)錯誤的子promise的error會被iterator.throw(error)澈歉,從而被generatorFunction的try catch捕捉到,而后續(xù)的錯誤都不會被throw回去屿衅,也不會有任何的處理埃难。generatorFunction當(dāng)catch到第一個error就繼續(xù)往后執(zhí)行了,也不會停下來進(jìn)行等待涤久。導(dǎo)致的情況就是第一個子promise出bug以后涡尘,其他的子promise的就被遺忘在了隕落的賽博坦星球,他們不管成功或者失敗响迂,他們的data或者err我們都拿不到悟衩,而且很多時候我們甚至都無法終止他們(也就setTimeout和setInteval這種返回了句柄的可以終止),所以他們成了毫無意義的任務(wù)栓拜,一方面依然在執(zhí)行,我們沒法終止,另一方面執(zhí)行的結(jié)果和錯誤我們都根本拿不到幕与,成了占用著網(wǎng)絡(luò)資源和計算能力卻又沒任何作用的吸血蟲挑势,如果他們中存在閉包,而且這個任務(wù)又有可能一直卡住的話啦鸣,那么你可能要小心一點(diǎn)了潮饱,他可能會造成內(nèi)存泄露。

那我們應(yīng)該怎么去寫并發(fā)情況下的錯誤控制诫给,上面這種寫法的一個唯一的好處在于你可以在結(jié)果拿到之后第一時間得到錯誤信息香拉,如果你在此處就是希望all or nothing,而且你不關(guān)心出錯的原因是什么中狂,連日志記錄都不想要凫碌,就只希望出錯了不要繼續(xù)往下執(zhí)行,或者你的并發(fā)的代碼極少出錯胃榕,那么也許上面的寫法你會采用盛险。但是其中的風(fēng)險你應(yīng)該已經(jīng)明白了。

如果你沒有那么強(qiáng)烈的快速知道錯誤發(fā)生并立即停止往下執(zhí)行的需求(這種也許可以讓你的結(jié)果返回得更快那么一點(diǎn)點(diǎn)勋又,然并卵苦掘,子promise任務(wù)并沒有被中斷),那么我覺得最好的方式還是等所有的子任務(wù)的執(zhí)行完畢楔壤,不管他是成功或者失敽追取(因?yàn)樗凑荚趫?zhí)行),這是一種無法終止已經(jīng)開啟的異步任務(wù)和promise.all的回調(diào)只能執(zhí)行一次的回退方案蹲嚣,至少保險递瑰。而且這樣的話,至少端铛,你能有辦法記錄他出錯的原因泣矛,也能針對整個并行任務(wù)的完成情況,執(zhí)行后續(xù)的處理策略禾蚕。

既然子任務(wù)的error會導(dǎo)致父promise的執(zhí)行失敗您朽,那么就不能讓子promise的error直接拋出去,所以子promise yield出去前先綁好.catch是肯定需要的换淆,而且要處理好.catch綁定的錯誤回調(diào)里的返回值哗总,不然我們雖然接住了錯誤,但是co返回到generatorFunciton里面的卻是undefined倍试,我們不知道錯誤在哪了讯屈。

舉個例子,我們可以寫一個包裝函數(shù)县习,然后為我們的每一個promise綁定好成功回調(diào)和錯誤回調(diào)涮母,并借助閉包谆趾,讓他們成功或失敗后修改一個對象里面的對應(yīng)屬性,我們根據(jù)這個對象去判斷是否執(zhí)行成功叛本。

function protect (promiseArr) {
    //這個數(shù)組用于存儲最后的結(jié)果
    let resultArr = []
    //為每個promise綁定成功和失敗回調(diào)
    promiseArr.forEach((promise,index) => {
        //在resultArr的對應(yīng)位置先插入一個對象
        resultArr[index] = {
            success: true,
        }
        promiseArr[index] = promise.then((data) => {
            //如果成功,那么把結(jié)果寫回結(jié)果數(shù)組中的對象
            resultArr[index].data = data
        },(err) => {
            //失敗就寫入失敗原因
            resultArr[index].error = err
            resultArr[index].success = false
        })
    })
    // 這一步綁定.then必可不能少
    return Promise.all(promiseArr).then(() => {
        return resultArr
    })
}
function generateRandomPromise() {
    //這個函數(shù)隨機(jī)產(chǎn)生一個promise,這個promise在100毫秒內(nèi)可能成功or失敗
    let randomNum = Math.random() * 100
    return new Promise (function(resolve, reject) {
        setTimeout( function() {
            if(randomNum > 50){
                resolve('data!')
            }else{
                reject(new Error('error'))
            }
        },randomNum)
    })
}

co(function * () {
    let promiseArr = []
    for (var i = 0; i < 10; i++) {
        promiseArr.push(generateRandomPromise())
    }
    //下面這一句不用包裹try catch
    let missionResults = yield protect(promiseArr)
    console.log(missionResults)
})

上面的代碼中沪蓬,我們最后拿到的這個missionResults是一個數(shù)組,里面的每一個元素包含了我們的成功和失敗信息来候,這種方式因?yàn)槊總€子promise都不會失敗跷叉,所以也就不用在yield protect(promiseArr)外層包裹try catch,你要做的就是在拿到這個missionResult之后對里面的元素判斷成功或失敗营搅,接著完成相關(guān)的處理云挟,寫日志、向用戶返回500转质、嘗試重新執(zhí)行錯誤的任務(wù)之類的园欣,這些都比你一行直接的console.log('internal error')要好得多吧。

如果你的某一個子任務(wù)在卡住時會很長都不會reject峭拘,你覺得你可能難以接受所有的promise都必須成功或失敗時才執(zhí)行完這個父promise俊庇,那么你大可以為這個子promise包裹一層Promise.race(),讓他和一個setTimeout并行執(zhí)行鸡挠,時間太長就當(dāng)做錯誤了去處理辉饱,讓setTimeout優(yōu)先返回(但是這依然是無法解決任務(wù)繼續(xù)在后臺執(zhí)行的問題的)〖鹫梗總之這些都是你的細(xì)節(jié)處理彭沼。但是關(guān)于并發(fā)情況下的我們該怎么寫,各種寫法有什么隱患都說完了备埃。我覺得雖然沒有找到完美的方法解決并行時候的錯誤處理姓惑,但是我覺得其實(shí)我們自己已經(jīng)能夠根據(jù)自己的使用場景找到了應(yīng)該還算是不錯的處理方法。其實(shí)如果你的并發(fā)非常容易出錯按脚,出錯情況非常多于毙,錯誤可能會長期卡住沒返回之類的,我覺得你可能需要思考和優(yōu)化的是你的并發(fā)任務(wù)本身辅搬。(另外唯沮,我正在寫關(guān)于co使用過程中的小技巧的文章,里面會包含并發(fā)數(shù)控制的內(nèi)容堪遂,寫好后會替換這個括號)介蛉。

koa里處理異常

koa里,我們通過app.use(function*(){})溶褪,綁定了許多的中間件generatorFunction币旧,app.use就是負(fù)責(zé)把你傳入的generatorFunction放到一個中間件數(shù)組middleware里,接下來用koa-compose對這個中間件數(shù)組處理了一下猿妈,使得koa對中間件的執(zhí)行流程成為了我們大家所熟知的洋蔥結(jié)構(gòu):

file

koa-compose的源碼如下吹菱,巨短巍虫,就是先拿到后一個generatorFunction的iterator,然后把這個iterator作為當(dāng)前generatorFunction的next毁葱,這樣你在當(dāng)前中間件里執(zhí)行yield next垫言,就可以執(zhí)行后續(xù)的中間件流程,后續(xù)的中間件流程執(zhí)行完之后又回到當(dāng)前的中間件倾剿。而這一整套串好的中間件最終通過co包裹起來。

function compose(middleware){
  return function *(next){
    if (!next) next = noop();

    var i = middleware.length;

    while (i--) {
      next = middleware[i].call(this, next);
    }

    return yield *next;
  }
}
function *noop(){}

所以大概會像這樣:

function out() {
function mid(){
  function in() {
    //....
  }
}
}

也正是如此蚌成,內(nèi)層的中間件中發(fā)生的錯誤前痘,是能被外層的中間件給捕獲到的,也就是你先app.use()的中間件能捕捉到后app.use()的中間件的錯誤担忧。

所以:

var app = new Koa()
app.use(function* (next){
  try{
    yield next
  }catch(err){
  //打印錯誤
  //日志記錄
  }
})
app.use(function* (next){
  //業(yè)務(wù)邏輯
})

同時芹缔,我們說了,這一整套串好的中間件最終通過co包裹起來(其實(shí)是co.wrap)瓶盛,因此co會返回一個promise(我在前一節(jié)說過哈)最欠,因此如果這一整套串好的中間件在執(zhí)行過程中出了什么錯沒有被catch住,那么最終會導(dǎo)致co返回的這個promise的reject惩猫,而koa在這個promise上通過.catch綁定了一個默認(rèn)的錯誤回調(diào)芝硬,錯誤回調(diào)就是設(shè)置http status為500,然后把錯誤信息this.res.end(msg)發(fā)送出去轧房。因此出錯時拌阴,瀏覽器會收到一個說明錯誤的500報文。

同時奶镶,這個錯誤回調(diào)里執(zhí)行了this.app.emit('error', err, this)迟赃,koa的app是繼承自event模塊的EventEmitter,所以可以在app上觸發(fā)error事件厂镇,而你也可以在app上面監(jiān)聽錯誤回調(diào)纤壁,完成最差情況下的錯誤處理。

koa下的錯誤處理方式還是比較齊全的捺信,另有koa-onerror等npm包可供使用酌媒,使用了koa-onerror之后,你可以在中間件里直接this.throw(500,\"錯誤原因\")残黑,它會自動根據(jù)request的header的accept字段馍佑,返回客戶端能accept的返回類型(比如客戶端要application/json,那么返回的body就是{error:\"錯誤原因\"})。npm上有較多類似的包梨水,不再贅述拭荤。

此外就是萬一還是出現(xiàn)了你沒有捕捉到的錯誤,在node里如果有未捕捉的錯誤時疫诽,會在process上觸發(fā)事件uncaughtException舅世,所以我們可以在process上監(jiān)聽此事件旦委,但是并不應(yīng)該是單純的把error log記錄好就完了,很明顯如果這個錯誤也是卡住著的雏亚,內(nèi)存是不會回收的缨硝,那么很有可能會發(fā)生內(nèi)存泄露的錯誤,對于koa這種需要長期跑著的程序罢低,這是相當(dāng)大的風(fēng)險的查辩。所以最好的方法是把這個server關(guān)掉。

你聽到這可能就有點(diǎn)崩潰了网持,“什么宜岛?我想方設(shè)法不讓服務(wù)器掛掉,結(jié)果你主動給我關(guān)了功舀?”所以為了讓你的服務(wù)還能繼續(xù)運(yùn)行萍倡,比較好的方法就是用node的cluster模塊起一個master,然后master里面fork出幾個child_process辟汰,每個child_process就是你對應(yīng)的koa server列敲,當(dāng)child_process遇到錯誤時,那么應(yīng)該記錄日志帖汞,然后把koa給停了戴而,并通過disconnect()告訴master“我關(guān)閉了,你再fork一個新的”涨冀,再接著等連接都關(guān)閉了填硕,把自己給 process.exit()了。但是如果你想實(shí)現(xiàn)更加優(yōu)雅的退出鹿鳖,想實(shí)現(xiàn)當(dāng)前的連接都關(guān)閉之后再關(guān)閉服務(wù)器扁眯,那應(yīng)該怎么做?

我們是
二手轉(zhuǎn)轉(zhuǎn)前端(大轉(zhuǎn)轉(zhuǎn)FE)
知乎專欄:https://zhuanlan.zhihu.com/zhuanzhuan
官方微信公共號:zhuanzhuanfe
微信公眾二維碼:

微信公眾二維碼

關(guān)注我們翅帜,我們會定期分享一些團(tuán)隊對前端的想法與沉淀

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末姻檀,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子涝滴,更是在濱河造成了極大的恐慌绣版,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件歼疮,死亡現(xiàn)場離奇詭異杂抽,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)韩脏,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進(jìn)店門缩麸,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人赡矢,你說我怎么就攤上這事杭朱≡淖校” “怎么了?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵弧械,是天一觀的道長八酒。 經(jīng)常有香客問我,道長刃唐,這世上最難降的妖魔是什么羞迷? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮画饥,結(jié)果婚禮上闭树,老公的妹妹穿的比我還像新娘。我一直安慰自己荒澡,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布与殃。 她就那樣靜靜地躺著单山,像睡著了一般。 火紅的嫁衣襯著肌膚如雪幅疼。 梳的紋絲不亂的頭發(fā)上米奸,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天,我揣著相機(jī)與錄音爽篷,去河邊找鬼悴晰。 笑死,一個胖子當(dāng)著我的面吹牛逐工,可吹牛的內(nèi)容都是我干的铡溪。 我是一名探鬼主播,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼泪喊,長吁一口氣:“原來是場噩夢啊……” “哼棕硫!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起袒啼,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤哈扮,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后蚓再,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體滑肉,經(jīng)...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年摘仅,在試婚紗的時候發(fā)現(xiàn)自己被綠了靶庙。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,690評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡实檀,死狀恐怖惶洲,靈堂內(nèi)的尸體忽然破棺而出按声,到底是詐尸還是另有隱情,我是刑警寧澤恬吕,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布签则,位于F島的核電站,受9級特大地震影響铐料,放射性物質(zhì)發(fā)生泄漏渐裂。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一钠惩、第九天 我趴在偏房一處隱蔽的房頂上張望柒凉。 院中可真熱鬧,春花似錦篓跛、人聲如沸膝捞。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽蔬咬。三九已至,卻和暖如春沐寺,著一層夾襖步出監(jiān)牢的瞬間林艘,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工混坞, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留狐援,地道東北人。 一個月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓究孕,卻偏偏與公主長得像啥酱,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子蚊俺,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,577評論 2 353

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

  • Co源碼以及與Koa的深入理解 tj大神的co泳猬,將本來應(yīng)該是一個數(shù)據(jù)類型的generator變成了一種處理異步的解...
    潘逸飛閱讀 2,332評論 1 8
  • 導(dǎo)語:用過node的同學(xué)相信都知道koa批钠。截止目前為止,koa目前官方版本最新是2.7.0得封。本文意在深入分析koa...
    宮若石閱讀 755評論 0 5
  • 弄懂js異步 講異步之前埋心,我們必須掌握一個基礎(chǔ)知識-event-loop。 我們知道JavaScript的一大特點(diǎn)...
    DCbryant閱讀 2,710評論 0 5
  • Koa 學(xué)習(xí) 歷史 Express Express是第一代最流行的web框架忙上,它對Node.js的http進(jìn)行了封...
    Junting閱讀 2,819評論 0 0
  • 一. Callback (回調(diào)函數(shù)) 1.定義:把函數(shù)當(dāng)作變量傳到另一個函數(shù)里项秉,傳進(jìn)去之后執(zhí)行甚至返回等待之后的...
    hutn閱讀 1,525評論 0 2