本文介绍Node.js中文件拷贝与目录遍历的实现方法。小文件可直接读写,大文件需使用Stream管道传输以避免内存溢出。目录遍历采用深度优先+先序遍历算法,分别演示了同步与异步两种实现方式,并指出递归性能问题及优化方向。
Node.js文件拷贝实现
文件复制操作 Node.js 内置了基础的文件处理接口,但未涵盖如文件复制这类高级功能,故以此程序作为入门示例。该程序需兼容源文件路径与目标文件路径两个参数,类似于 copy 命令的行为。
小型文件复制 借助 Node.js 内置的 fs 模块,可简单构建此程序,实现方式如下。
var fs = require('fs');
function copy(src, dst) {
fs.writeFileSync(dst, fs.readFileSync(src));
}
function main(argv) {
copy(argv[0], argv[1]);
}
main(process.argv.slice(2));
上述代码通过 fs.readFileSync 从源路径读取文件数据,并借助 fs.writeFileSync 将数据写入目标路径。
技术细节: process 为全局对象,利用 process.argv 可获取命令行参数。其中 argv[0] 恒定为 Node.js 执行文件的绝对路径,argv[1] 恒定为主模块的绝对路径,故首个用户提供的命令行参数起始于 argv[2]。
大型文件复制 前述程序处理小文件时表现良好,但因其策略为一次性将全部文件内容载入内存后写入磁盘,故不适用于大文件复制,否则会导致内存溢出。针对大文件,应采取边读边写的策略直至复制完成,因此需对前述程序进行如下改造。
var fs = require('fs');
function copy(src, dst) {
fs.createReadStream(src).pipe(fs.createWriteStream(dst));
}
function main(argv) {
copy(argv[0], argv[1]);
}
main(process.argv.slice(2));
上述代码利用 fs.createReadStream 构建源文件的只读数据流,同时利用 fs.createWriteStream 构建目标文件的只写数据流,并通过 pipe 方法将二者串联。串联后的机制可形象地理解为水流经管道从一桶转移至另一桶。
Node.js目录遍历算法
目录遍历
目录遍历是文件操作中的常见需求。例如,编写程序以查找并处理指定目录下的所有 JS 文件时,必须遍历整个目录结构。
递归策略 目录遍历通常采用递归算法,这有助于保持代码简洁。递归算法与数学归纳法原理相似,通过不断缩减问题规模来求解。以下示例展示了该思路。
function factorial(n) {
if (n === 1) {
return 1;
} else {
return n * factorial(n - 1);
}
}
上述函数用于计算 N 的阶乘(N!)。当 N 大于 1 时,问题转化为计算 N 乘以 N-1 的阶乘;当 N 等于 1 时,问题达到最小规模,无需进一步简化,直接返回 1。
注意事项: 尽管递归算法编写的代码简洁,但由于每次递归都会产生函数调用,在性能优先的场景下,应将递归算法转换为循环算法,以降低函数调用次数。
遍历策略 目录呈现树状结构,遍历时通常采用深度优先结合先序遍历的策略。深度优先指访问节点后优先遍历子节点而非邻居节点;先序遍历指首次到达某节点即视为该节点遍历完成,而非最后一次返回时才算。因此,使用该策略遍历下图所示树结构的顺序为 A > B > D > E > C > F。
A
/ \
B C
/ \ \
D E F
同步遍历 掌握必要算法后,可简单实现以下目录遍历函数。
function travel(dir, callback) {
fs.readdirSync(dir).forEach(function (file) {
var pathname = path.join(dir, file);
if (fs.statSync(pathname).isDirectory()) {
travel(pathname, callback);
} else {
callback(pathname);
}
});
}
可见,该函数以指定目录为遍历起点。遇到子目录时,优先继续遍历该子目录;遇到文件时,将文件的绝对路径传递给回调函数。回调函数获取路径后可执行各类判断与处理。假设存在如下目录结构:
- /home/user/
- foo/
x.js
- bar/
y.js
z.css
使用以下代码遍历该目录时,输出结果如下。
travel('/home/user', function (pathname) {
console.log(pathname);
});
/home/user/foo/x.js
/home/user/bar/y.js
/home/user/z.css
异步遍历 若读取目录或文件状态时采用异步 API,目录遍历函数的实现将更为复杂,但原理一致。travel 函数的异步版本如下。
function travel(dir, callback, finish) {
fs.readdir(dir, function (err, files) {
(function next(i) {
if (i < files.length) {
var pathname = path.join(dir, files[i]);
fs.stat(pathname, function (err, stats) {
if (stats.isDirectory()) {
travel(pathname, callback, function () {
next(i + 1);
});
} else {
callback(pathname, function () {
next(i + 1);
});
}
});
} else {
finish && finish();
}
}(0));
});
}
此处不深入探讨异步遍历函数的编写技巧,将在后续章节详细阐述。总之,可以看出异步编程具有一定的复杂性。