原文来自语雀专栏: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
它可以替代 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-map、mz、mz-modules 等。
- 大胆使用 try catch,它的性能很好。
- 可能你以后都不需要再使用 co 模块了。