cnpm 核心模块 npminstall 升级到 async 总结

2,915 阅读3分钟

原文来自语雀专栏: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…

它可以替代 Promise.all() 且提供并发数限制能力。

最大的思维差别是 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 rimraf('/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 模块了。