Skip to content

Latest commit

 

History

History
773 lines (556 loc) · 36.5 KB

File metadata and controls

773 lines (556 loc) · 36.5 KB

六、文件系统

对于许多 JavaScript 开发人员来说,访问文件系统很难实现。理由一直是——正确的——让 Web 脚本访问文件系统存在太大的安全风险。然而,Node 通常不会从互联网的黑暗角落执行任意脚本。作为一种成熟的服务器端语言,Node 拥有与 PHP、Python 和 Java 等语言相同的权利和责任。因此,对于 JavaScript 开发人员来说,文件系统是一个不依赖于特定于供应商的实现或黑客的现实。本章展示了文件系统如何成为 Node 开发人员工具箱中的另一个工具。

相关路径

每个 Node 应用都包含许多变量,这些变量提供了关于 Node 在文件系统中的哪个位置工作的洞察力。这些变量中最简单的是__filename__dirname。第一个变量__filename,是当前执行文件的绝对路径。类似地,__dirname是包含当前执行文件的目录的绝对路径。清单 6-1 中的例子显示了__filename__dirname的用法。请注意,这两者都可以在不导入任何模块的情况下访问。当这个例子从目录/home/colin中执行时,结果输出显示在清单 6-2 中。

清单 6-1 。使用__filename__dirname变量

console.log("This file is " + __filename);
console.log("It's located in " + __dirname);

清单 6-2 。运行清单 6-1 中代码的输出

$ node file-paths.js
This file is /home/colin/file-paths.js
It's located in /home/colin

image 注意__filename__dirname的值取决于引用它们的文件。因此,即使在单个 Node 应用中,它们的值也可能不同——例如,当从应用中的两个不同模块引用__filename时,就可能发生这种情况。

当前工作目录

应用的当前工作目录是应用在创建相对路径时引用的文件系统目录。这方面的一个例子是pwd命令,它返回一个 shell 的当前工作目录。在 Node 应用中,当前工作目录可通过process对象的cwd()方法获得。使用cwd()方法的例子如清单 6-3 所示。结果输出如清单 6-4 所示。

清单 6-3 。使用process.cwd()方法

console.log("The current working directory is " + process.cwd());

清单 6-4 。运行清单 6-3 中代码的输出

$ node cwd-example.js
The current working directory is /home/colin

更改当前工作目录

在执行过程中,应用可以改变其当前的工作目录。在 shell 中,这是通过cd命令完成的。process对象提供了一个名为chdir()的方法,通过接受一个表示要更改的目录名的字符串参数来完成相同的任务。该方法同步执行,如果目录更改由于任何原因失败(比如,如果目标目录不存在),该方法将引发异常。

清单 6-5 中的显示了一个例子,它使用chdir()方法显示当前工作目录,然后试图切换到根目录/。如果出现错误,它会被捕获,然后打印到stderr。最后,显示更新的工作目录。

清单 6-5 。使用process.chdir()改变当前工作目录

console.log("The current working directory is " + process.cwd());

try {
  process.chdir("/");
} catch (exception) {
  console.error("chdir error:  " + exception.message);
}

console.log("The current working directory is now " + process.cwd());

清单 6-6 显示了成功执行清单 6-5 中的代码。接下来,尝试将chdir()中的路径更改为某个不存在的路径,并再次运行该示例。清单 6-7 显示了一个失败的例子,它试图将chdir()改为/foo。请注意当前工作目录在失败后是如何保持不变的。

清单 6-6 。清单 6-5 中流程的成功运行

$ node chdir-example.js
The current working directory is /home/colin
The current working directory is now /

清单 6-7 。清单 6-5 中的流程运行失败

$ node chdir-example.js
The current working directory is /home/colin
chdir error:  ENOENT, no such file or directory
The current working directory is now /home/colin

定位node可执行文件

node可执行文件的路径也可以通过process对象获得。具体来说,可执行路径在process.execPath属性中。清单 6-8 显示了一个显示node可执行路径的例子,相应的输出显示在清单 6-9 中。请注意,您自己的路径可能会因操作系统或 Node 安装路径的不同而不同。

清单 6-8 。显示process.execPath的值

console.log(process.execPath);

清单 6-9 。清单 6-8 中的输出

$ node exec-path-example.js
/usr/local/bin/node

path模块

path模块是一个核心模块,它提供了许多使用文件路径的实用方法。虽然path模块使用文件路径,但是它的许多方法只执行简单的字符串转换,而不实际访问文件系统。清单 6-10 展示了path模块是如何包含在一个 Node 应用中的。

清单 6-10 。将path模块导入到 Node 应用中

var path = require("path");

跨平台差异

处理跨多个操作系统的路径可能有点痛苦。这主要是因为 Windows 使用反斜杠(\)来分隔文件路径的各个部分,而其他操作系统使用正斜杠(/)。Node 的 Windows 版本可以有效处理正斜杠,但大多数原生 Windows 应用不能。幸运的是,这个细节可以使用path.sep属性抽象出来。该属性保存当前操作系统的文件分隔符。这在 Windows 中是\\(记住,反斜杠必须被转义),但在其他地方是/清单 6-11 展示了如何将path.sep与数组join()方法结合使用,来创建特定于平台的文件路径。

清单 6-11 。使用path.sepjoin() 创建跨平台目录

var path = require("path");
var directories = ["foo", "bar", "baz"];
var directory = directories.join(path.sep);

console.log(directory);

image 注意 Windows 使用一个反斜杠作为它的路径分隔符。然而,反斜线必须在 JavaScript 字符串中转义。这就是为什么在 Windows 中path.sep返回\\

非 Windows 系统的结果输出如清单 6-12 中的所示。在本章的后面,我们将解释如何在目录上执行文件系统操作,但是现在我们只显示目录路径。

清单 6-12 。运行清单 6-11 中代码的输出

$ node sep-join-example.js
foo/bar/baz

Windows 和其他平台的另一个主要区别是在PATH环境变量中分隔目录的字符。Windows 使用分号(;),但其他所有系统都使用冒号(:)。path模块的delimiter属性用于将其抽象出来。清单 6-13 使用delimiter属性分割PATH环境变量并打印每个单独的目录。

清单 6-13 。拆分PATH环境变量的跨平台示例

var path = require("path");

process.env.PATH.split(path.delimiter).forEach(function(dir) {
  console.log(dir);
});

提取路径组件

path模块还提供了对几个关键路径组件的简单访问。具体来说,pathextname()basename()dirname()方法分别返回路径的文件扩展名、文件名和目录名。extname()方法 查找路径中的最后一个句点(.),并将其和所有后续字符作为扩展名返回。如果路径不包含句点,则返回空字符串。清单 6-14 显示了如何使用extname()

清单 6-14 。使用path.extname()方法

var path = require("path");
var fileName = "/foo/bar/baz.txt";
var extension = path.extname(fileName);

console.log(extension);
// extension is .txt

basename()方法 返回路径的最后一个非空部分。如果路径对应于一个文件,basename()返回完整的文件名,包括扩展名。清单 6-15 中显示了一个这样的例子。您还可以通过将extname()的结果作为第二个参数传递给basename()来检索不带扩展名的文件名。清单 6-16 显示了一个这样的例子。

清单 6-15 。使用path.basename()从路径中提取完整文件名

var path = require("path");
var fileName = "/foo/bar/baz.txt";
var file = path.basename(fileName);

console.log(file);
// file is baz.txt

清单 6-16 。使用path.basename()从路径中提取文件名减去扩展名

var path = require("path");
var fileName = "/foo/bar/baz.txt";
var extension = path.extname(fileName);
var file = path.basename(fileName, extension);

console.log(file);
// file is baz

dirname()方法 返回路径的目录部分。清单 6-17 展示了dirname()的用法。

清单 6-17 。使用path.dirname()从路径中提取目录名

var path = require("path");
var fileName = "/foo/bar/baz.txt";
var dirName = path.dirname(fileName);

console.log(dirName);
// dirName is /foo/bar

路径标准化

如果混合了"."".."部分,路径会变得过于复杂和混乱。如果用户将路径作为命令行参数传入,很可能会发生这种情况。例如,用户发出cd命令来改变目录,通常会提供相对路径。反过来,path模块提供了一个normalize()方法来简化这些路径。在清单 6-18 的例子中,一个相当复杂的路径被规范化了。在跟随几个父目录和当前目录引用之后,结果路径就是/baz

清单 6-18 。使用path.normalize()实现路径标准化

var path = require("path");
var dirName = "/foo/bar/.././bar/../../baz";
var normalized = path.normalize(dirName);

console.log(normalized);
// normalized is /baz

path模块还有一个join()方法。对任意数量的字符串进行操作,join()获取这些字符串并创建一个单一的规范化路径。在清单 6-19 的例子中,展示了如何使用join()来规范化来自清单 6-18 的路径,输入路径被分成几个字符串。注意,如果传入一个字符串,join()的工作方式与normalize()完全一样。

清单 6-19 。使用path.join()实现路径标准化

var path = require("path");
var normalized = path.join("/foo/bar", ".././bar", "../..", "/baz");

console.log(normalized);
// normalized is /baz

解析目录之间的相对路径

path.relative()方法可用于确定从一个目录到另一个目录的相对路径,它采用两个字符串作为参数。第一个参数表示计算的起点,而第二个参数对应于终点。在清单 6-20 的例子中,显示了relative()的用法,计算了从/foo/bar/baz/biff的相对路径。基于这个目录结构,在遍历/baz/biff之前,相对路径向上移动两级到根目录。

清单 6-20 。使用path.relative()确定相对路径

var path = require("path");
var from = "/foo/bar";
var to = "/baz/biff";
var relative = path.relative(from, to);

console.log(relative);
// relative is ../../baz/biff

fs模块

Node 应用通过fs模块执行文件 I/O,这个核心模块的方法提供了标准文件系统操作的包装器。清单 6-21 展示了文件系统模块是如何导入到一个 Node 应用中的。你可能还记得第三章中的这个模块,其中实现了一个文件阅读器程序。

清单 6-21 。将模块fs导入到 Node 应用中

var fs = require("fs");

关于fs模块特别值得注意的一点是它的同步方法的扩散。更具体地说,几乎所有的文件系统方法都有异步和同步版本。同步的可以通过使用Sync后缀来识别。每个方法的异步版本都将回调函数作为其最终参数。在 Node 的早期版本中,许多异步fs方法允许您省略回调函数。但是根据官方文档,从 Node 0.12 开始,省略回调函数会导致异常。

如您所见,异步方法是 Node 编程模型的核心。使用异步编程使 Node 看起来高度并行,而实际上它是单线程的。即使是一个同步方法的粗心使用也有可能使整个应用停止(如果你需要复习,请参见第 3 章)。那么为什么将近一半的文件系统方法是同步的呢?

碰巧的是,许多应用访问文件系统来获取配置数据。这通常在启动时的配置过程中完成。在这种情况下,同步读取配置文件通常要简单得多,无需担心性能的最大化。此外,Node 可用于创建简单的实用程序,类似于 shell 脚本。这些脚本可能会逃脱同步行为。一般来说,可以同时调用多次的代码应该是异步的。虽然作为开发人员,您可以随意使用同步方法,但是使用时要非常小心。

确定文件是否存在

exists()existsSync()方法用于确定给定路径是否存在。这两种方法都将路径字符串作为参数。如果使用同步版本,则返回一个表示路径存在的布尔值。如果使用异步版本,相同的布尔值将作为参数传递给回调函数。

清单 6-22 使用existsSync()exists()检查根目录是否存在。当调用exists()回调函数时,比较两种方法的结果。当然,这两种方法应该返回相同的值。假设等价,路径被打印出来,后面跟着表示它存在的布尔值。

清单 6-22 。使用exists()existsSync() 检查文件是否存在

var fs = require("fs");
var path = "/";
var existsSync = fs.existsSync(path);

fs.exists(path, function(exists) {
  if (exists !== existsSync) {
    console.error("Something is wrong!");
  } else {
    console.log(path + " exists:  " + exists);
  }
});

正在检索文件统计信息

fs模块提供了一组用于读取文件统计数据的函数。这些功能是stat()lstat()fstat()。当然,这些方法也有同步的对等物— statSync()lstatSync()fstatSync()。这些方法最基本的形式是stat(),它将路径字符串和回调函数作为参数。回调函数也是用两个参数调用的。第一个表示发生的任何错误。第二个是包含实际文件统计信息的fs.Stats对象。在探索fs.Stats对象之前,让我们看一个使用stat()方法的例子。在清单 6-23 中,stat()用于收集我们假设存在的文件foo.js的信息。如果出现异常(比如文件不存在),错误信息会打印到stderr。否则,打印Stats对象。

清单 6-23 。正在使用的fs.stat()方法

var fs = require("fs");
var path = "foo.js";

fs.stat(path, function(error, stats) {
  if (error) {
    console.error("stat error:  " + error.message);
  } else {
    console.log(stats);
  }
});

清单 6-24 显示了一次成功运行的输出样本。表 6-1 包含了列表中显示的各种fs.Stats对象属性的解释。请注意,您的输出可能会有所不同,尤其是在使用 Windows 的情况下。事实上,在 Windows 中,有些属性根本不会出现。

清单 6-24 。清单 6-23 中代码的输出示例

$ node stat-example.js
{ dev: 16777218,
  mode: 33188,
  nlink: 1,
  uid: 501,
  gid: 20,
  rdev: 0,
  blksize: 4096,
  ino: 2935040,
  size: 75,
  blocks: 8,
  atime: Sun Apr 28 2013 12:55:17 GMT-0400 (EDT),
  mtime: Sun Apr 28 2013 12:55:17 GMT-0400 (EDT),
  ctime: Sun Apr 28 2013 12:55:17 GMT-0400 (EDT) }

表 6-1 。各种 fs 的解释。统计对象属性

|

财产

|

描述

| | --- | --- | | dev | 包含文件的设备的 ID。 | | mode | 文件的保护。 | | nlink | 指向文件的硬链接的数量。 | | uid | 文件所有者的用户 ID。 | | gid | 文件所有者的组 ID。 | | rdev | 如果文件是特殊文件,则为设备 ID。 | | blksize | 文件系统 I/O 的块大小。 | | ino | 文件的索引 Node 号。inode 是存储文件信息的文件系统数据结构。 | | size | 文件的总大小,以字节为单位。 | | blocks | 为文件分配的块数。 | | atime | 代表文件上次访问时间的对象。 | | mtime | Date代表文件最后修改时间的对象。 | | ctime | Date表示文件的信息 Node 最后一次被更改的对象。 |

fs.Stats对象也有几个帮助识别文件类型的方法(见表 6-2 )。这些方法是同步的,它们没有参数,并且返回一个布尔值。例如,isFile()方法为普通文件返回true,但是isDirectory()为目录返回true

表 6-2 。各种 fs 的解释。统计方法

|

方法

|

描述

| | --- | --- | | isFile() | 指示文件是否是正常文件。 | | isDirectory() | 指示文件是否是目录。 | | isBlockDevice() | 指示文件是否是块设备文件。这包括硬盘、光盘和闪存驱动器等设备。 | | isCharacterDevice() | 指示文件是否是字符设备文件。这包括像键盘这样的设备。 | | isSymbolicLink() | 指示文件是否是符号链接。这仅在使用lstat()lstatSync()时有效。 | | isFIFO() | 指示文件是否是 FIFO 特殊文件。 | | isSocket() | 指示文件是否是套接字。 |

其他stats()变化

lstat()fstat()的变化几乎与stat()相同。与lstat()的唯一区别是,如果路径参数是一个符号链接,那么fs.Stats对象对应的是链接本身,而不是它所引用的文件。对于fstat(),唯一的区别是第一个参数是文件描述符而不是字符串。文件描述符用于与打开的文件进行通信(稍后会有更详细的描述)。当然,statSync()lstatSync()fstatSync()的行为就像它们的异步对应物一样。因为同步方法没有回调函数,所以直接返回fs.Stats对象。

打开文件

使用open()openSync()方法打开文件。这两个方法的第一个参数是一个字符串,表示要打开的文件名。第二个是一个flags字符串,表示文件应该如何打开(读、写等)。).表 6-3 总结了 Node 让你打开文件的各种方式。

表 6-3 。open()和 openSync()可用的各种标志的分类

|

旗帜

|

描述

| | --- | --- | | r | 打开阅读。如果文件不存在,则会发生异常。 | | r+ | 为阅读和写作而打开。如果文件不存在,则会发生异常。 | | rs | 以同步模式打开进行读取。这指示操作系统绕过系统缓存。这主要用于打开 NFS 挂载上的文件。这并没有使而不是成为同步方法。 | | rs+ | 以同步模式打开进行读写。 | | w | 打开写。如果文件不存在,则创建该文件。如果文件已经存在,它将被截断。 | | wx | 类似于w标志,但是文件是以独占模式打开的。独占模式确保文件是新创建的。 | | w+ | 为阅读和写作而打开。如果文件不存在,则创建该文件。如果文件已经存在,它将被截断。 | | wx+ | 类似于w+标志,但是文件是以独占模式打开的。 | | a | 打开以追加。如果文件不存在,则创建该文件。 | | ax | 类似于a标志,但是文件是以独占模式打开的。 | | a+ | 打开以供阅读和追加。如果文件不存在,则创建该文件。 | | ax+ | 类似于a+标志,但是文件是以独占模式打开的。 |

第三个参数是可选的,给open()openSync()指定了modemode默认为"0666"。异步open()方法将回调函数作为第四个参数。作为一个参数,回调函数接受一个错误和打开文件的文件描述符。文件描述符是一种用于与打开的文件交互的结构。文件描述符,无论是传递给回调函数还是由openSync()返回,都可以传递给其他函数来执行诸如读写之类的文件操作。选择清单 6-25 中的例子,使用open()打开文件/dev/null,是因为对它的任何写入都会被简单地丢弃。请注意,该文件在 Windows 中不存在。但是,您可以更改第二行的path的值,以指向一个不同的文件。建议使用当前不存在的文件路径,因为现有文件的内容将被覆盖,如本例所示。

清单 6-25 。使用open()打开/dev/null

var fs = require("fs");
var path = "/dev/null";

fs.open(path, "w+", function(error, fd) {
  if (error) {
    console.error("open error:  " + error.message);
  } else {
    console.log("Successfully opened " + path);
  }
});

从文件中读取数据

read()readSync()方法用于从打开的文件中读取数据。这些方法有许多参数,所以使用一个例子可能会使研究它们变得更容易(见清单 6-26 )。该示例从应用目录中的文件foo.txt读取数据(为了简单起见,省略了错误处理代码),从调用stat()开始。它必须这样做,因为稍后将需要该文件的大小。接下来,使用open()打开文件。获取文件描述符需要这一步。文件打开后,初始化一个数据缓冲区,这个缓冲区足够容纳整个文件。

清单 6-26 。使用read()从文件中读取

var fs = require("fs");
var path = __dirname + "/foo.txt";

fs.stat(path, function(error, stats) {
  fs.open(path, "r", function(error, fd) {
    var buffer = new Buffer(stats.size);

    fs.read(fd, buffer, 0, buffer.length, null, function(error, bytesRead, buffer) {
      var data = buffer.toString("utf8");

      console.log(data);
    });
  });
});

接下来是对read() 的实际调用。第一个参数是由open()提供的文件描述符。第二个是用来保存从文件中读取的数据的缓冲区。第三个是缓冲区内放置数据的偏移量(在本例中,偏移量为零,对应于缓冲区的开始)。第四个参数是要读取的字节数(在本例中,读取了文件的全部内容)。第五个是一个整数,指定文件中开始读取的位置。如果该值为null,则从当前文件位置开始读取,该位置被设置为文件最初打开时的开头,并在每次读取时更新。

如果这是对readSync()、的调用,它将返回从文件中成功读取的字节数。异步read()函数将一个回调函数作为它的最终参数,这个回调函数又将一个错误对象、读取的字节数和缓冲区作为参数。在回调函数中,原始数据缓冲区被转换为 UTF-8 字符串,然后打印到控制台。

image 注意这个例子在对read()的一次调用中读取整个文件。如果文件非常大,内存消耗可能是个问题。在这种情况下,您的应用应该初始化一个较小的缓冲区,并使用循环以较小的块读取文件。

readFile()readFileSync()方法

readFile()readFileSync()方法 提供了一种更简洁的从文件中读取数据的方法。以文件名作为参数,它们自动读取文件的全部内容,不需要文件描述符、缓冲区或其他麻烦。清单 6-27 显示了使用readFile()重写的来自清单 6-26 的代码。注意,readFile()的第二个参数指定数据应该作为 UTF-8 字符串返回。如果省略该参数或null,则返回原始缓冲区。

清单 6-27 。使用readFile()读取整个文件

var fs = require("fs");
var path = __dirname + "/foo.txt";

fs.readFile(path, "utf8", function(error, data) {
  if (error) {
    console.error("read error:  " + error.message);
  } else {
    console.log(data);
  }
});

将数据写入文件

将数据写入文件类似于读取数据。用于写入文件的方法有write()writeSync()。在清单 6-28 的例子中,使用write()方法 ,打开一个名为foo.txt的文件进行写操作。还创建了一个缓冲区来保存要写入文件的数据。接下来,write()用于将数据实际写入文件。write()的第一个参数是由open()提供的文件描述符。第二个是包含要写入的数据的缓冲区。第三和第四个参数对应于开始写入的缓冲区偏移量和要写入的字节数。第五个是一个整数,表示文件中开始写入的位置。如果该参数为null,则数据被写入当前文件位置,writeFileSync()返回成功写入文件的字节数。另一方面,write()接受一个带有三个参数的回调函数:异常对象、写入的字节数和缓冲区对象。

清单 6-28 。使用write()将数据写入文件

var fs = require("fs");
var path = __dirname + "/foo.txt";
var data = "Lorem ipsum dolor sit amet";

fs.open(path, "w", function(error, fd) {
  var buffer = new Buffer(data);

  fs.write(fd, buffer, 0, buffer.length, null, function(error, written, buffer) {
    if (error) {
      console.error("write error:  " + error.message);
    } else {
      console.log("Successfully wrote " + written + " bytes.");
    }
  });
});

writeFile()writeFileSync()方法

方法writeFile()writeFileSync()write()writeSync()提供快捷方式。清单 6-29 中的例子显示了writeFile()的用法,它将文件路径和要写入的数据作为它的前两个参数。通过可选的第三个参数,您可以指定编码(默认为 UTF-8)和其他选项。对writeFile() 的回调函数将一个错误对象作为其唯一的参数。

清单 6-29 。使用writeFile()写入文件

var fs = require("fs");
var path = __dirname + "/foo.txt";
var data = "Lorem ipsum dolor sit amet";

fs.writeFile(path, data, function(error) {
  if (error) {
    console.error("write error:  " + error.message);
  } else {
    console.log("Successfully wrote " + path);
  }
});

另外两种方法,appendFile()appendFileSync(),用于在不覆盖现有数据的情况下向现有文件追加数据。如果该文件尚不存在,则创建该文件。这些方法的用法和writeFile()writeFileSync()一模一样。

关闭文件

作为一个通用的编程经验,总是关闭你打开的任何东西。在 Node 应用中,使用close()closeSync()方法关闭文件。两者都将文件描述符作为参数。在异步版本中,回调函数应该作为第二个参数。回调函数的唯一参数用于指示可能的错误。在清单 6-30 的例子中,使用open()打开一个文件,然后使用close()立即关闭。

清单 6-30 。用open()close()打开然后关闭文件

var fs = require("fs");
var path = "/dev/null";

fs.open(path, "w+", function(error, fd) {
  if (error) {
    console.error("open error:  " + error.message);
  } else {
    fs.close(fd, function(error) {
      if (error) {
        console.error("close error:  " + error.message);
      }
    });
  }
});

image 注意没有必要关闭使用readFile()writeFile()等方法打开的文件。这些方法在内部处理一切。此外,它们没有提供文件描述符来传递给close()

重命名文件

要重命名文件,使用rename()renameSync()方法。这些方法的第一个参数是要重命名的文件的当前名称。正如您可能猜到的,第二个是文件的新名称。rename() 的回调函数只有一个参数,代表一个可能的异常。清单 6-31 中的例子将一个名为foo.txt的文件重命名为bar.txt

清单 6-31 。使用rename() 重命名文件

var fs = require("fs");
var oldPath = __dirname + "/foo.txt";
var newPath = __dirname + "/bar.txt";

fs.rename(oldPath, newPath, function(error) {
  if (error) {
    console.error("rename error:  " + error.message);
  } else {
    console.log("Successfully renamed the file!");
  }
});

删除文件

使用unlink()unlinkSync()方法删除文件,这两种方法将文件路径作为参数。异步版本也接受回调函数作为参数。回调函数只接受一个表示可能异常的参数。在清单 6-32 的示例中,展示了unlink()方法的使用,应用试图删除位于同一目录中的一个名为foo.txt的文件。

清单 6-32 。使用fs.unlink()方法删除文件

var fs = require("fs");
var path = __dirname + "/foo.txt";

fs.unlink(path, function(error) {
  if (error) {
    console.error("unlink error:  " + error.message);
  }
});

创建目录

使用mkdir()mkdirSync()方法创建新目录。mkdir()的第一个参数是要创建的目录路径。由于mkdir()只创建最后一级目录,mkdir()不能用于在一次调用中构建整个目录层次结构。这个方法还带有一个可选的第二个参数,它指定了目录的权限,默认为"0777"。异步版本还采用回调函数,该函数的唯一参数是一个可能的异常。清单 6-33 提供了一个使用mkdir()在应用的目录中创建目录树foo/bar的例子。

清单 6-33 。使用mkdir()创建几个目录

var fs = require("fs");
var path = __dirname + "/foo";

fs.mkdir(path, function(error) {
  if (error) {
    console.error("mkdir error:  " + error.message);
  } else {
    path += "/bar";
    fs.mkdir(path, function(error) {
      if (error) {
        console.error("mkdir error:  " + error.message);
      } else {
        console.log("Successfully built " + path);
      }
    });
  }
});

读取目录的内容

readdir()readdirSync()方法用于获取给定目录的内容。要读取的目录路径作为参数传入。readdirSync()方法返回包含目录中的文件和子目录的字符串数组,而readdir()将错误和相同的文件数组传递给回调函数。清单 6-34 显示了使用readdir()来读取进程当前工作目录的内容。注意readdir()readdirSync()提供的数组不包含目录"."".."

清单 6-34 。使用readdir() 读取目录的内容

var fs = require("fs");
var path = process.cwd();

fs.readdir(path, function(error, files) {
  files.forEach(function(file) {
    console.log(file);
  });
});

删除目录

您也可以使用rmdir()rmdirSync()方法删除目录。要移除的目录路径作为第一个参数传递给每个方法。rmdir()的第二个参数是一个回调函数,它将一个潜在的异常作为唯一的参数。清单 6-35 中的例子使用了rmdir()

清单 6-35 。使用rmdir() 删除目录

var fs = require("fs");
var path = __dirname + "/foo";

fs.rmdir(path, function(error) {
  if (error) {
    console.error("rmdir error:  " + error.message);
  }
});

如果试图删除非空目录,将会出现错误。删除这样一个目录需要更多的工作。清单 6-36 中的代码展示了一种实现在非空目录下工作的rmdir()函数的方法。在删除一个非空目录之前,我们首先要清空它。为此,删除目录中的所有文件,并递归删除所有子目录。

清单 6-36 。实现递归rmdir()功能

var fs = require("fs");
var path = __dirname + "/foo";

function rmdir(path) {
  if (fs.existsSync(path)) {
    fs.readdirSync(path).forEach(function(file) {
      var f = path + "/" + file;
      var stats = fs.statSync(f);

      if (stats.isDirectory()) {
        rmdir(f);
      } else {
        fs.unlinkSync(f);
      }
    });

    fs.rmdirSync(path);
  }
}

// now call the recursive rmdir() function
rmdir(path);

清单 6-36 中所有的函数调用都是同步的,这极大地简化了代码,使算法更容易理解。然而,同步函数不是 Node 方式。清单 6-37 展示了使用异步调用实现的相同功能。关于这个例子,首先要注意的是已经包含了async模块。因此,我们可以专注于实际的算法,因为async负责驯服异步函数调用。

清单 6-37 。递归的异步实现rmdir()

var async = require("async");
var fs = require("fs");
var path = __dirname + "/foo";

function rmdir(path, callback) {
  // first check if the path exists
  fs.exists(path, function(exists) {
    if (!exists) {
      return callback(new Error(path + " does not exist"));
    }

    fs.readdir(path, function(error, files) {
      if (error) {
        return callback(error);
      }

      // loop over the files returned by readdir()
      async.each(files, function(file, cb) {
        var f = path + "/" + file;

        fs.stat(f, function(error, stats) {
          if (error) {
            return cb(error);
          }

          if (stats.isDirectory()) {
            // recursively call rmdir() on the directory
            rmdir(f, cb);
          } else {
            // delete the file
            fs.unlink(f, cb);
          }
        });
      }, function(error) {
        if (error) {
          return callback(error);
        }

        // the directory is now empty, so delete it
        fs.rmdir(path, callback);
      });
    });
  });
}

// now call the recursive rmdir() function
rmdir(path, function(error) {
  if (error) {
    console.error("rmdir error:  " + error.message);
  } else {
    console.log("Successfully removed " + path);
  }
});

观看文件

fs模块让您的应用监视特定文件的修改。这是使用watch()方法完成的。watch()的第一个参数是要查看的文件的路径。可选的第二个参数是一个对象。如果存在的话,这个对象应该包含一个名为persistent的布尔属性。如果persistenttrue(默认),只要至少有一个文件被查看,应用就会继续运行。watch()的第三个参数是一个可选的回调函数,每次修改目标文件时都会触发这个函数。

如果存在,回调函数接受两个参数。第一个,观察事件的类型,将是changerename。回调函数的第二个参数是被监视文件的名称。

清单 6-38 的例子中,显示了watch()方法 的运行,一个名为foo.txt的文件被持久地监视。也就是说,除非程序被终止或被监视的文件被删除,否则应用不会终止。每当foo.txt被修改时,回调函数就会触发并处理一个事件。如果文件被删除,将触发并处理一个rename事件,然后程序退出。

清单 6-38 。使用watch()方法观看文件

var fs = require("fs");
var path = __dirname + "/foo.txt";

fs.watch(path, {
  persistent: true
}, function(event, filename) {
  if (event === "rename") {
    console.log("The file was renamed/deleted.");
  } else if (event === "change") {
    console.log("The file was changed.");
  }
});

watch()方法也返回一个类型为fs.FSWatcher的对象。如果省略可选的回调函数,FSWatcher可以用来处理事件(通过第 4 章中介绍的熟悉的事件处理语法)。清单 6-39 显示了一个使用FSWatcher来处理文件监视事件的例子。另外,请注意close()方法,它用于指示FSWatcher停止查看有问题的文件。因此,此示例只处理一个文件更改事件。

清单 6-39 。使用可选的watch()语法查看文件

var fs = require("fs");
var path = __dirname + "/foo.txt";
var watcher;

watcher = fs.watch(path);
watcher.on("change", function(event, filename) {
  if (event === "rename") {
    console.log("The file was renamed/deleted.");
  } else if (event === "change") {
    console.log("The file was changed.");
  }

  watcher.close();
});

image 注意 Node 的官方文档将watch()列为不稳定,因为它依赖于底层的文件系统,并且没有跨平台实现 100%的一致性。例如,watch()回调函数的filename参数并非在所有系统中都可用。

摘要

本章介绍了 Node 的文件系统 API。在任何合法的应用中,有效地使用文件系统是一个关键因素。如果不能访问文件系统,应用就无法完成读取配置文件、创建输出文件和写入错误日志等任务。Node 中的许多文件系统任务都是使用fs模块来处理的,因此本章涵盖了fs提供的最重要的方法。但是,本章还没有介绍许多其他方法,这些方法允许您完成诸如更改文件所有权和权限之类的任务。读者可以参考完整的文档(http://nodejs.org/api/fs.html)以获得所有可能方法的列表。