原文來自語雀專欄:www.yuque.com/egg/nodejs/…
作者:蘇千、天豬
簡單回顧
npminstall 是 cnpm 的核心邏輯庫之一,它通過 link 的方式來安裝 Node.js 依賴,可以極大的提升安裝速度。
回顧 npminstall 第一版的程式碼,預設支援 Node.js 4,那個時候 async/await
還沒成為 Node.js 的預設功能,各種三方庫還是 callback 介面。所以我們選擇基於 co/generator
模式來開發,避免 Callback Hell。
時光如梭,Node.js 已經發布了 12.x 版本,ES6 早已普及,async/await
早已經在 Node.js 8 就預設開啟,所以我們決定給 npminstall 進行一次大重構,徹底擁抱 async/await
,跟 co/generator
說再見。
再次感謝 TJ,讓我們提前好多年就享受著 async/await 般的編碼體驗。
generator 轉 async
這是最容易的替換,幾乎可以無腦全域性替換。
function*
=>async function
yield
=>await
老程式碼:
module.exports = function* (options) {
// ...
yield fn();
};
複製程式碼
新程式碼:
module.exports = async options => {
// ...
await fn();
};
複製程式碼
Promise.all()
值得關注的是併發執行的任務,在 co/generator
模式下只需要 yield tasks
即可實現,而 async/await
模式下需要明確地使用 Promise.all(tasks)
來宣告。
老程式碼:
const tasks = [];
for (const pkg of pkgs) {
tasks.push(installOne(pkg));
}
yield tasks;
複製程式碼
新程式碼:
const tasks = [];
for (const pkg of pkgs) {
tasks.push(installOne(pkg));
}
await Promise.all(tasks);
複製程式碼
常用的模組
co-parallel => p-map
最大的思維差別是 async function
馬上開始執行,而 generator function
是延遲執行。
老程式碼:
const parallel = require('co-parallel');
for (const childPkg of pkgs) {
childPkg.name = childPkg.name || '';
rootPkgsMap.set(childPkg.name, true);
options.progresses.installTasks++;
tasks.push(installOne(options.targetDir, childPkg, options));
}
yield parallel(tasks, 10);
複製程式碼
新程式碼:
在 mapper
被呼叫的時候才會真實執行。
const pMap = require('p-map');
const mapper = async childPkg => {
childPkg.name = childPkg.name || '';
rootPkgsMap.set(childPkg.name, true);
options.progresses.installTasks++;
await installOne(options.targetDir, childPkg, options);
};
await pMap(pkgs, mapper, 10);
複製程式碼
mz-modules
mz-modules 和 mz 是我們用的比較多的 2 個模組。
const { mkdirp, rimraf, sleep } = require('mz-modules');
const { fs } = require('mz');
async function run() {
// 非阻塞方式刪除目錄
await mkdirp('/path/to/dir');
// +1s
await sleep('1s');
// 非阻塞的 mkdir -p
await mkdirp('/path/to/dir');
// 讀取檔案,請把 `fs.readFileSync` 從你的頭腦裡面徹底遺忘。
const content = await fs.readFile('/path/to/file.md', 'utf-8');
}
複製程式碼
co-fs-extra => fs-extra
fs-extra 已經預設支援 async/await,不需要再使用 co 包裝一層。
老程式碼:
const fse = require('co-fs-extra');
yield fse.emptyDir(targetdir);
複製程式碼
新程式碼:
const fse = require('fs-extra');
await fse.emptyDir(targetdir);
複製程式碼
runscript
node-modules/runscript 用於執行一條指令。
const runScript = require('runscript');
async function run() {
const { stdout, stderr } = await runScript('node -v', { stdio: 'pipe' });
}
複製程式碼
yieldable => awaitable
- 我們之前在 Egg 1.x 升級 2.x 的時候,也總結了一份更詳細的 yiedable-to-awaitable 指南:
- 更多 Promise 的語法糖參見:promise-fun 這個倉庫。
總結
重構後整體程式碼量其實並不會變化太大,幾乎是等價的程式碼量。有一些需要特別回顧的注意點:
- async function 是會在被呼叫時立即執行,不像 generator function 是在 yield 的時候才被真正執行。
- 併發執行需要藉助
Promise.all()
。 - 需要掌握一些常用的輔助庫,如 p-map、mz、mz-modules 等。
- 大膽使用 try catch,它的效能很好。
- 可能你以後都不需要再使用 co 模組了。