cnpm 核心模組 npminstall 升級到 async 總結

天豬發表於2019-05-09

原文來自語雀專欄:www.yuque.com/egg/nodejs/…

作者:蘇千、天豬

簡單回顧

npminstallcnpm 的核心邏輯庫之一,它通過 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

github.com/sindresorhu…

最大的思維差別是 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-mapmzmz-modules 等。
  • 大膽使用 try catch,它的效能很好。
  • 可能你以後都不需要再使用 co 模組了。

相關文章