在Node.js开发中,处理异步操作一直是个绕不开的话题。从早期的回调函数到Promise,再到如今的Async/Await,异步编程模式不断演进。但当我们不得不与系统交互,特别是执行shell命令时,依然会陷入回调地狱的困扰。child_process.exec这个看似简单的API,却常常让代码变得难以维护。
Node.js的异步API大多采用"错误优先"的回调风格。这种模式在简单场景下尚可接受,但当逻辑变得复杂时,代码会迅速陷入所谓的"回调金字塔"——嵌套越来越深,可读性越来越差。
以获取Node版本为例,传统写法是这样的:
javascript复制import { exec } from 'node:child_process';
exec('node -v', (err, stdout, stderr) => {
if (err) {
console.error(`执行失败: ${err}`);
return;
}
console.log(`当前Node版本: ${stdout}`);
});
这种模式存在几个明显问题:
相比之下,Promise风格的代码更加线性:
javascript复制const version = await getNodeVersion();
console.log(`当前Node版本: ${version}`);
Node.js内置的util.promisify正是为解决这个问题而生。它能将遵循Node.js回调风格的函数转换为返回Promise的函数。
转换child_process.exec非常简单:
javascript复制import { exec } from 'node:child_process';
import { promisify } from 'node:util';
const execAsync = promisify(exec);
async function getNodeVersion() {
try {
const { stdout } = await execAsync('node -v');
return stdout.trim();
} catch (err) {
console.error('获取版本失败:', err);
throw err;
}
}
转换后的代码有几个明显优势:
async/await语法,代码更线性try/catch统一管理promisify的工作原理其实并不复杂。它本质上创建了一个包装函数,将回调风格的API转换为Promise风格。简化版的实现如下:
javascript复制function simplePromisify(original) {
return function(...args) {
return new Promise((resolve, reject) => {
original.call(this, ...args, (err, ...values) => {
if (err) {
return reject(err);
}
resolve(values.length > 1 ? values : values[0]);
});
});
};
}
实际Node.js的实现更复杂一些,主要处理了以下特殊情况:
this)exec返回stdout和stderr)当需要转换多个函数时,可以创建一个工具函数:
javascript复制import { readFile, writeFile } from 'node:fs';
import { promisify } from 'node:util';
const fsAsync = {
readFile: promisify(readFile),
writeFile: promisify(writeFile)
// 添加更多需要转换的函数...
};
async function processFile() {
const content = await fsAsync.readFile('input.txt', 'utf8');
await fsAsync.writeFile('output.txt', content.toUpperCase());
}
有些Node.js API会返回多个值,比如exec同时返回stdout和stderr。promisify会自动将这些值组合成一个对象:
javascript复制const { stdout, stderr } = await execAsync('ls -la');
对于不符合标准回调签名的函数,可以自定义promisify逻辑:
javascript复制import { promisify } from 'node:util';
// 假设有一个非标准回调函数
function customApi(param, success, failure) {
// ...
}
const promisified = promisify.custom = (fn) => {
return function(...args) {
return new Promise((resolve, reject) => {
fn.call(this, ...args, resolve, reject);
});
};
};
const customAsync = promisified(customApi);
Promise风格的错误处理更统一,但需要注意几点:
javascript复制async function runCommand() {
try {
const result = await execAsync('some-command');
// 处理结果
} catch (err) {
if (err.code === 'ENOENT') {
console.error('命令不存在');
} else {
console.error('执行出错:', err);
}
}
}
虽然promisify带来了代码清晰度,但也要注意:
javascript复制// 不好的做法:每次调用都promisify
function badPractice() {
return promisify(exec)('node -v');
}
// 好的做法:提前转换并复用
const execAsync = promisify(exec);
function goodPractice() {
return execAsync('node -v');
}
promisify转换的函数可以无缝与其他异步模式结合:
javascript复制// 与Promise.all结合
const [version, uptime] = await Promise.all([
execAsync('node -v'),
execAsync('uptime')
]);
// 在流处理中使用
import { pipeline } from 'node:stream';
const pipelineAsync = promisify(pipeline);
await pipelineAsync(
fs.createReadStream('input.txt'),
transformStream,
fs.createWriteStream('output.txt')
);
在实际项目中,合理使用util.promisify可以显著提升Node.js代码的可维护性。特别是在处理传统回调风格的API时,它提供了一种平滑过渡到现代异步编程模式的方式。