摘要:普通的回調函數調用執行后續邏輯使用了以后的復雜邏輯獲取到正確的結果輸出兩個文件拼接后的內容雖說解決了的問題,不會出現一個函數前邊有二三十個空格的縮進。所以直接使用關鍵字替換原有的普通回調函數即可。
從今年過完年回來,三月份開始,就一直在做重構相關的事情。為什么要升級
就在今天剛剛上線了最新一次的重構代碼,希望高峰期安好,接近半年的Node.js代碼重構。
包含從callback+async.waterfall到generator+co,統統升級為了async,還順帶推動了TypeScript在我司的使用。
這些日子也踩了不少坑,也總結了一些小小的優化方案,進行精簡后將一些比較關鍵的點,拿出來分享給大家,希望有同樣在做重構的小伙伴們可以繞過這些。
首先還是要談談改代碼的理由,畢竟重構肯定是要有合理的理由的。
如果單純想看升級相關事項可以直接選擇跳過這部分。
從最原始的開始說起,期間確實遇到了幾個年代久遠的項目,Node 0.x,使用的普通callback,也有一些會應用上async.waterfall這樣在當年看起來很優秀的工具。
// 普通的回調函數調用 var fs = require("fs") fs.readFile("test1.txt", function (err, data1) { if (err) return console.error(err) fs.readFile("test2.txt", function (err, data2) { if (err) return console.error(err) // 執行后續邏輯 console.log(data1.toString() + data2.toString()) // ... }) }) // 使用了async以后的復雜邏輯 var async = require("fs") async.waterfall([ function (callback) { fs.readFile("test1.txt", function (err, data) { if (err) callback(err) callback(null, data.toString()) }) }, function (result, callback) { fs.readFile("test2.txt", function (err, data) { if (err) callback(err) callback(null, result + data.toString()) }) } ], function (err, result) { if (err) return console.error(err) // 獲取到正確的結果 console.log(result) // 輸出兩個文件拼接后的內容 })
雖說async.waterfall解決了callback hell的問題,不會出現一個函數前邊有二三十個空格的縮進。
但是這樣的流程控制在某些情況下會讓代碼變得很詭異,例如我很難在某個函數中選擇下一個應該執行的函數,而是只能按照順序執行,如果想要進行跳過,可能就要在中途的函數中進行額外處理:
async.waterfall([ function (callback) { if (XXX) { callback(null, null, null, true) } else { callback(null, data1, data2) } }, function (data1, data2, isPass, callback) { if (isPass) { callback(null, null, null, isPass) } else { callback(null, data1 + data2) } } ])
所以很可能你的代碼會變成這樣,里邊存在大量的不可讀的函數調用,那滿屏充斥的null占位符。
所以callback這種形式的,一定要進行修改, __這屬于難以維護的代碼__。
Generator實際上generator是依托于co以及類似的工具來實現的將其轉換為Promise,從編輯器中看,這樣的代碼可讀性已經沒有什么問題了,但是問題在于他始終是需要額外引入co來幫忙實現的,generator本身并不具備幫你執行異步代碼的功能。
不要再說什么async/await是generator的語法糖了
因為我司Node版本已經統一升級到了8.11.x,所以async/await語法已經可用。
這就像如果document.querySelectorAll、fetch已經可以滿足需求了,為什么還要引入jQuery呢。
所以,將generator函數改造為async/await函數也是勢在必行。
期間遇到的坑將callback的升級為async/await其實并沒有什么坑,反倒是在generator + co 那里遇到了一些問題:
數組執行的問題在co的代碼中,大家應該都見到過這樣的:
const results = yield list.map(function * (item) { return yield getData(item) })
在循環中發起一些異步請求,有些人會告訴你,從yield改為async/await僅僅替換關鍵字就好了。
那么恭喜你得到的results實際上是一個由Promise實例組成的數組。
const results = await list.map(async item => { return await getData(item) }) console.log(results) // [Promise, Promise, Promise, ...]
因為async并不會判斷你后邊的是不是一個數組(這個是在co中有額外的處理)而僅僅檢查表達式是否為一個Promise實例。
所以正確的做法是,添加一層Promise.all,或者說等新的語法await*,Node.js 10.x貌似還不支持。。
// 關于這段代碼的優化方案在下邊的建議中有提到 const results = await Promise.all(list.map(async item => { return await getData(item) })) console.log(results) // [1, 2, 3, ...]await / yield 執行順序的差異
這個一般來說遇到的概率不大,但是如果真的遇到了而栽了進去就欲哭無淚了。
首先這樣的代碼在執行上是沒有什么區別的:
yield 123 // 123 await 123 // 123
這樣的代碼也是沒有什么區別的:
yield Promise.resolve(123) // 123 await Promise.resolve(123) // 123
但是這樣的代碼,問題就來了:
yield true ? Promise.resolve(123) : Promise.resolve(233) // 123 await true ? Promise.resolve(123) : Promise.resolve(233) // Promise<123>
從字面上我們其實是想要得到yield那樣的效果,結果卻得到了一個Promise實例。
這個是因為yield、await兩個關鍵字執行順序不同所導致的。
在MDN的文檔中可以找到對應的說明:MDN | Operator precedence
可以看到yield的權重非常低,僅高于return,所以從字面上看,這個執行的結果很符合我們想要的。
而await關鍵字的權重要高很多,甚至高于最普通的四則運算,所以必然也是高于三元運算符的。
也就是說await版本的實際執行是這樣子的:
(await true) ? Promise.resolve(123) : Promise.resolve(233) // Promise<123>
那么我們想要獲取預期的結果,就需要添加()來告知解釋器我們想要的執行順序了:
await (true ? Promise.resolve(123) : Promise.resolve(233)) // 123一定不要漏寫 await 關鍵字
這個其實算不上升級時的坑,在使用co時也會遇到,但是這是一個很嚴重,而且很容易出現的問題。
如果有一個異步的操作用來返回一個布爾值,告訴我們他是否為管理員,我們可能會寫這樣的代碼:
async function isAdmin (id) { if (id === 123) return true return false } if (await isAdmin(1)) { // 管理員的操作 } else { // 普通用戶的操作 }
因為這種寫法接近同步代碼,所以遺漏關鍵字是很有可能出現的:
if (isAdmin(1)) { // 管理員的操作 } else { // 普通用戶的操作 }
因為async函數的調用會返回一個Promise實例,得益于我強大的弱類型腳本語言,Promise實例是一個Object,那么就不為空,也就是說會轉換為true,那么所有調用的情況都會進入if塊。
那么解決這樣的問題,有一個比較穩妥的方式,強制判斷類型,而不是簡單的使用if else,使用類似(a === 1)、(a === true)這樣的操作。_eslint、ts 之類的都很難解決這個問題_
一些建議 何時應該用 async ,何時應該直接用 Promise首先,async函數的執行返回值就是一個Promise,所以可以簡單地理解為async是一個基于Promise的包裝:
function fetchData () { return Promise().resolve(123) } // ==> async function fetchData () { return 123 }
所以可以認為說await后邊是一個Promise的實例。
而針對一些非Promise實例則沒有什么影響,直接返回數據。
在針對一些老舊的callback函數,當前版本的Node已經提供了官方的轉換工具util.promisify,用來將符合Error-first callback規則的異步操作轉換為Promise實例:
而一些沒有遵守這樣規則的,或者我們要自定義一些行為的,那么我們會嘗試手動實現這樣的封裝。
在這種情況下一般會采用直接使用Promise,因為這樣我們可以很方便的控制何時應該reject,何時應該resolve。
但是如果遇到了在回調執行的過程中需要發起其他異步請求,難道就因為這個Promise導致我們在內部也要使用.then來處理么?
function getList () { return new Promise((resolve, reject) => { oldMethod((err, data) => { fetch(data.url).then(res => res.json()).then(data => { resolve(data) }) }) }) } await getList()
但上邊的代碼也太丑了,所以關于上述問題,肯定是有更清晰的寫法的,不要限制自己的思維。
__async也是一個普通函數__,完全可以放在任何函數執行的地方。
所以關于上述的邏輯可以進行這樣的修改:
function getList () { return new Promise((resolve, reject) => { oldMethod(async (err, data) => { const res = await fetch(data.url) const data = await res.json() resolve(data) }) }) } await getList()
這完全是一個可行的方案,對于oldMethod來說,我按照約定調用了傳入的回調函數,而對于async匿名函數來說,也正確的執行了自己的邏輯,并在其內部觸發了外層的resolve,實現了完整的流程。
代碼變得清晰很多,邏輯沒有任何修改。
合理的減少 await 關鍵字await只能在async函數中使用,await后邊可以跟一個Promise實例,這個是大家都知道的。
但是同樣的,有些await其實并沒有存在的必要。
首先有一個我面試時候經常會問的題目:
Promise.resolve(Promise.resolve(123)).then(console.log) // ?
最終輸出的結果是什么。
這就要說到resolve的執行方式了,如果傳入的是一個Promise實例,亦或者是一個thenable對象(_簡單的理解為支持.then((resolve, reject) => {})調用的對象_),那么resolve實際返回的結果是內部執行的結果。
也就是說上述示例代碼直接輸出123,哪怕再多嵌套幾層都是一樣的結果。
通過上邊所說的,不知大家是否理解了 合理的減少 await 關鍵字 這句話的意思。
結合著前邊提到的在async函數中返回數據是一個類似Promise.resolve/Promise.reject的過程。
而await就是類似監聽then的動作。
所以像類似這樣的代碼完全可以避免:
const imgList = [] async function getImage (url) { const res = await fetch(url) return await res.blob() } await Promise.all(imgList.map(async url => await getImage(url))) // ==> async function getImage (url) { const res = fetch(url) return res.blob() } await Promise.all(imgList.map(url => getImage(url)))
上下兩種方案效果完全相同。
Express 與 koa 的升級首先,Express是通過調用response.send來完成請求返回數據的。
所以直接使用async關鍵字替換原有的普通回調函數即可。
而Koa也并不是說你必須要升級到2.x才能夠使用async函數。
在Koa1.x中推薦的是generator函數,也就意味著其內部是調用了co來幫忙做轉換的。
而看過co源碼的小伙伴一定知道,里邊同時存在對于Promise的處理。
也就是說傳入一個async函數完全是沒有問題的。
但是1.x的請求上下文使用的是this,而2.x則是使用的第一個參數context。
所以在升級中這里可能是唯一需要注意的地方,__在1.x不要使用箭頭函數來注冊中間件__。
// express express.get("/", async (req, res) => { res.send({ code: 200 }) }) // koa1.x router.get("/", async function (next) { this.body = { code: 200 } }) // koa2.x router.get("/", async (ctx, next) => { ctx.body = { code: 200 } })小結
重構項目是一件很有意思的事兒,但是對于一些注釋文檔都很缺失的項目來說,重構則是一件痛苦的事情,因為你需要從代碼中獲取邏輯,而作為動態腳本語言的JavaScript,其在大型項目中的可維護性并不是很高。
所以如果條件允許,還是建議選擇TypeScript之類的工具來幫助更好的進行開發。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/98033.html
摘要:前幾天在幫后端排查一個的問題的時候發現的一些小坑特此記錄的本質是出于安全原因,瀏覽器限制從腳本內發起的跨源請求。排查發現訪問失敗的都是需要用戶的登錄態的。 前幾天在幫后端排查一個cors的問題的時候發現的一些小坑特此記錄 ** cors的本質是出于安全原因,瀏覽器限制從腳本內發起的跨源HTTP請求。 例如,XMLHttpRequest和FetchAPI遵循同源策略。 這意味著使用這些A...
摘要:前幾天在幫后端排查一個的問題的時候發現的一些小坑特此記錄的本質是出于安全原因,瀏覽器限制從腳本內發起的跨源請求。排查發現訪問失敗的都是需要用戶的登錄態的。 前幾天在幫后端排查一個cors的問題的時候發現的一些小坑特此記錄 ** cors的本質是出于安全原因,瀏覽器限制從腳本內發起的跨源HTTP請求。 例如,XMLHttpRequest和FetchAPI遵循同源策略。 這意味著使用這些A...
摘要:介紹微信風格的,與客戶端體驗一致,這個自己去微信上看吧,略。微信調試一件套,網頁授權模擬集成代理遠程調試。這些在微信開發者中心有介紹,略。年微信開發經驗的人,終于又成為了零年開發經驗的人,重新走上了踩坑之路。 showImg(https://segmentfault.com/img/bVtEd1);活動地址:http://fequan.com/2016/ 注意:英文不好,小記也帶有自己...
摘要:前言最近將公司項目的從版本升到了版本,跟完全不兼容,是一次徹底的重寫。升級過程中踩了不少的坑,也有一些值得分享的點。沒有就會匹配所有路由最后不得不說升級很困難,坑也很多。 前言 最近將公司項目的 react-router 從 v3 版本升到了 v4 版本,react-router v4 跟 v3 完全不兼容,是一次徹底的重寫。這也給升級造成了極大的困難,與其說升級不如說是對 route...
閱讀 2805·2019-08-30 15:55
閱讀 2853·2019-08-30 15:53
閱讀 2289·2019-08-26 13:47
閱讀 2551·2019-08-26 13:43
閱讀 3153·2019-08-26 13:33
閱讀 2794·2019-08-26 11:53
閱讀 1789·2019-08-23 18:35
閱讀 795·2019-08-23 17:16