NodeJs 文件操作

HTTP模块

HTTP模块的作用是创建一个服务器,HTTP是Node的内置模块,不用在单独下载

使用requier指令来导入http模块,将实例化的HTTP赋值给http

const http = require("http");

使用http模块的createServer() 方法创建服务器,使用listen 方法绑定端口号,listen方法接收两个个参数,第一个参数要绑定的端口号,第二个参数服务启动成功后执行的回调函数createServer()方法有两个参数,request, response分别表示接收数据和响应数据,基本结构如以下代码

const http = require("http");

const server = http.createServer((request, response) => {
  response.end("hello");
});

server.listen(3000, () => {
  console.log("服务器启动成功 请访问 http://localhost:3000/");
});

response.end("hello")向页面响应数据,访问http://localhost:3000/ 就可以看到响应的数据

image-20200925200231905

如果响应的数据为中文,则会出现乱码

image-20200925202709457

出现乱码的原因是头部编码,在响应前设置编码头信息就可以解决

response.setHeader("content-type", "text/html;charset=utf-8");

FS模块

FS模块的作用是用来读写文件,对文件进行交互的模块

通过required来引入模块

const fs = require("fs");

FS模块中有两种才操作形式:同步和异步

  • 同步:会阻塞程序的进行,等文件全部读取完毕才会向下执行代码
  • 异步:不会阻塞程序的进行,在读取文件的时候,可以进行其他的操作

读取文件

同步操作

fs.readFileSync方法是同步来读取文件,首先创建一个txt文件,随便写点内容

image-20200925201404951

然后通过fs.readFileSync来读取文件,并响应到页面中

const http = require("http");
const fs = require("fs");

const server = http.createServer((request, response) => {
  let res = fs.readFileSync("./1.txt");
  response.end(res);
});

server.listen(3000, () => {
  console.log("服务器启动成功 请访问 http://localhost:3000/");
});

访问http://localhost:3000/ 就可以看到文件中的内容

image-20200925201655775

异步操作

fs.readFile()方法是来异步读取文件,异步和同步读取方法的区别就是是否带有Sync关键字,凡是加了Sync的方法同步操作,否则就是异步操作

const http = require("http");
const fs = require("fs");

const server = http.createServer((request, response) => {
  let res = fs.readFile("./1.txt");
  response.end(res);
});

server.listen(3000, () => {
  console.log("服务器启动成功 请访问 http://localhost:3000/");
});

访问http://localhost:3000/可以正常看到读取文件中的内容

image-20200925201655775

写入文件

fs.writeFileSync()方法:接收四个参数 fs.writeFile(file, data[, options], callback)

  • file - 文件名或文件描述符
  • data - 要写入文件的数据
  • options - 该参数是一个对象,用来表明写入的行为
    • r+ : 读取文件
    • w+ : 打开文件用于读写,会覆盖目标文件的原内容,如果文件不存在则创建文件
    • a :追加写入,如果文件不存在则创建文件。
  • callback - 回调函数,回调函数只包含错误信息参数(err),在写入失败时返回
const http = require("http");
const fs = require("fs");

const server = http.createServer((request, response) => {
  response.setHeader("content-type", "text/html;charset=utf-8");
  fs.writeFileSync("2.txt", "Hello world", { flag: "w" }, (err) => {
    if (err) {
      console.log(err);
    }
  });
  response.end("你好");
});

server.listen(3000, () => {
  console.log("服务器启动成功 请访问 http://localhost:3000/");
});

向2.txt文件中写入内容hello world,如果2.txt没有,那么系统会自动帮我们创建

打开2.txt就可以看到我们刚才写入的内容

image-20200925213612961

如果我们更改一下写入的内容,重新运行代码,会发现之前写入的内容消失了,而显示的是新写入的内容

fs.writeFileSync("2.txt", "木易", { flag: "w" }, (err) => {
    if (err) {
      console.log(err);
    }
  });

image-20200925214449339

这是因为我们使用的写入行为是w,会覆盖文件中的内容,将写入行为改为a就可以追加写入了

fs.writeFileSync("2.txt", "博客", { flag: "a" }, (err) => {
    if (err) {
      console.log(err);
    }
  });

image-20200925214621462

修改文件名

使用 fs.rename()fs.renameSync() 可以重命名文件。 第一个参数要被修改的文件名,第二个参数是新的文件名

现在将2.txt重命名为3.txt,代码如下

fs.rename("2.txt", "3.txt", (err) => {
  if (err) {
    console.log(err);
  }
});

删除文件

fs.unlink()方法可以删除指定的文件,该方法接收两个参数,第一个参数表示要删除的文件名,第二个参数为报错时运行的回调函数

现在将3.txt进行删除,代码如下

fs.unlink("3.txt", (err) => {
  if (err) {
    return console.log(err);
  }
  console.log("删除成功");
});

流方式读取文件

fs.readFileSync方法和fs.writeFileSync()方法适合拷贝一些小文件,但是如果想一次性把所有文件读取到内存再一次性写入到文件中这两种方法就不适合,对于大文件,我们只能读一点写一点,流方式非常适合大文件的读取

const http = require("http");
const fs = require("fs");

const server = http.createServer((request, response) => {
  response.setHeader("content-type", "text/html;charset=utf-8");
  fs.createReadStream("./1.txt").pipe(fs.createWriteStream("./3.txt"));
  response.end("hello");
});

server.listen(3000, () => {
  console.log("服务器启动成功 请访问 http://localhost:3000/");
});

以上代码使用fs.createReadStream()方法创建第一个源文件只读数据流,使用fs.createWriteStream()方法创建了一个目标文件的只写数据流,使用pipe方法把两个数据流连接起来,整个读写过程如下图

image-20200927110116025

PATH模块

PATH模块的主要功能是用来处理路径的工具,可以简化路径的相关操作

  • path.normalize

    将传入的路径转换为标准路径,除了可以解析路径中的...外,还可以去除多余的斜杠

    let cache = {};
    
    function store(key, value) {
        cache[path.normalize(key)] = value;
    }
    
    store('foo/bar', 1);
    store('foo//baz//../bar', 2);
    console.log(cache);  //  { "foo/bar": 2 }
  • path.extname

    可以获取文件的后缀名,当我们需要根据不同文件的后缀名做不同操作时,可以使用该方法

    path.extname('server.js'); // => ".js"

Buffer

  • Buffer是用来处理二进制数据的缓存区
  • 除了读取文件获取Buffer以外,还可以直接构造
  • Buffer将 JS 的数据处理能力从字符串扩展到了任意二进制数据
let bin = new Buffer([ 0x68, 0x65, 0x6c, 0x6c, 0x6f ]);

Buffer与字符串类似,可以用length属性获得字节长度,也可以使用[]方式去读指定位置的字节

bin[0]; //  0x68;

Buffer与字符串可以相互转化,可以将指定的二进制数据转换为字符串

let str = bin.toString('utf-8'); // => "hello"

Stream (数据流)

当内存中无法一次性存储需要处理的数据,可以改为一边读取一边处理的方法,这就需要用到数据流,NodeJs中可以使用 Stream来对数据流进行操作

所有的Stream对象都是EventEmitter 的实例。常用的事件有:

  • data:当有数据可读取时触发
  • end:没有更多的数据可读取时触发
  • error:在接收和写入过程中发生错误时触发
  • finish:所有数据已被写入到底层系统时触发
let rs = fs.createReadStream(pathname);

rs.on('data', function (chunk) {
    doSomething(chunk);
});

rs.on('end', function () {
    cleanUp();
});

上述代码中data事件会一直触发,不管doSomething函数是否能处理过来,createReadStream()中有两个方法可以解决此问题

  • rs.pause() 暂停读取,会暂停data事件的触发,将流动模式转变非流动模式
  • rs.resume()恢复data事件,继续读取,变为流动模式
let rs = fs.createReadStream(src);

rs.on('data', function (chunk) {
    rs.pause();
    doSomething(chunk, function () {
        rs.resume();
    });
});

rs.on('end', function () {
    cleanUp();
});

上述代码给doSomething函数添加了一个回调函数,在处理数据前暂停数据读取,在处理后继续读取数据

遍历目录是操作文件时的一个常见需求。比如写一个程序,需要找到并处理指定目录下的所有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));
    });
}