1+1=10

扬长避短 vs 取长补短

Node.js 学习笔记(三)

接前面

  • JavaScript基础学习:ECMAScript 基础知识
  • Node.js学习笔记(一):Node.js是什么,如何安装,如何简单使用,工作目录与脚本所在目录获取
  • Node.js学习笔记(二):命令行参数、环境变量、标准输入、标准输出、标准出错、信号处理、程序返回值,打包
  • Node.js学习笔记(三):fs模块API,三种风格——同步、异步回调、异步promise,三种层级——底层、高层、流

继续了解Node.js。看看其内置模块 fs

  • 三种风格:同步API、异步回调API、异步promise API。

  • 三种抽象层级:底层API、高层API、流 API。

fs 的API 风格

fs模块提供了三套不同风格的 API:

  • 同步API
  • 异步回调API
  • 异步promise API
操作 同步 API 异步回调 API Promise API
创建文件 fs.openSync(file, 'w') fs.open(file, 'w', callback) fsPromises.open(file, 'w')
写入内容 fs.writeSync(fd, data) fs.write(fd, data, callback) fd.write(data)
关闭文件 fs.closeSync(fd) fs.close(fd, callback) fd.close()
打开文件 fs.openSync(file, 'r') fs.open(file, 'r', callback) fsPromises.open(file, 'r')
读取内容 fs.readSync(fd, buffer, ...) fs.read(fd, buffer, ..., callback) fd.read(buffer, ...)
删除文件 fs.unlinkSync(file) fs.unlink(file, callback) fsPromises.unlink(file)
文件是否存在 fs.existsSync(file) fs.access(file, callback) fsPromises.access(file)

接下来,用一个例子,分别看看,如何执行:创建、写入、关闭、打开、读取、删除文件等操作。

  • 使用同步API 【例子1】
  • 使用异步回调API
    • 直接用(回调嵌套)【例子2】
    • 封装成 promise 使用 then()【例子3】
    • 封装成 promise 使用 await【例子4】
  • 使用promises API
    • 使用 then()【例子5】
    • 使用 await【例子6】

例子1- 同步 API

简单直观,但是会阻塞进程。适用于简单脚本和小型程序。

import fs from 'node:fs';
import { Buffer } from 'node:buffer';

const filePath = 'dbzhang800_sync.txt';

// 创建文件并写入内容
try {
  const f = fs.openSync(filePath, 'w');
  fs.writeSync(f, 'Hello 1+1=10 from Node.js Sync!');
  fs.closeSync(f);
  console.log('File created and written successfully.');
} catch (error) {
  console.error('Error:', error);
}

// 打开文件并读取内容
try {
  const f = fs.openSync(filePath, 'r');
  // 申请一个缓冲区
  const buffer = Buffer.alloc(1024);
  const bytesRead = fs.readSync(f, buffer, 0, buffer.length, 0);
  const fileContent = buffer.toString('utf8', 0, bytesRead);
  fs.closeSync(f);
  console.log('File Content:', fileContent);
} catch (error) {
  console.error('Error:', error);
}

// 删除文件
try {
  fs.unlinkSync(filePath);
  console.log('File deleted successfully.');
} catch (error) {
  console.error('Error:', error);
}

例子2- 异步回调 API

非阻塞,适用于处理大量并发请求,不会造成性能问题。但是回调地狱(Callback Hell)可能会使代码难以维护。

import fs from 'node:fs';
import { Buffer } from 'node:buffer';

const filePath = 'dbzhang800_async.txt';

// 创建文件并写入内容
fs.open(filePath, 'w', (error, fd) => {
    if (error) {
        console.error('Error:', error);
        return;
    }

    const content = 'Hello 1+1=10 from Node.js Async!';
    fs.write(fd, content, (error) => {
        if (error) {
            console.error('Error:', error);
            return;
        }

        fs.close(fd, (error) => {
            if (error) {
                console.error('Error:', error);
                return;
            }

            console.log('File created and written successfully.');

            // 打开文件并读取内容
            fs.open(filePath, 'r', (error, fd) => {
                if (error) {
                    console.error('Error:', error);
                    return;
                }

                const buffer = Buffer.alloc(1024);
                fs.read(fd, buffer, 0, buffer.length, 0, (error, bytesRead) => {
                    if (error) {
                        console.error('Error:', error);
                        return;
                    }

                    const fileContent = buffer.toString('utf8', 0, bytesRead);
                    fs.close(fd, (error) => {
                        if (error) {
                            console.error('Error:', error);
                            return;
                        }

                        console.log('File Content:', fileContent);

                        // 删除文件
                        fs.unlink(filePath, (error) => {
                            if (error) {
                                console.error('Error:', error);
                                return;
                            }

                            console.log('File deleted successfully.');
                        });
                    });
                });
            });
        });
    });
});

例子3- 异步回调 API,封装成 promise,then()

要避免多层嵌套造成的回调地狱额,尝试使用EMCAScript的Promise进行改写

import { Buffer } from 'node:buffer';
import fs from 'node:fs';

const filePath = 'dbzhang800_async.txt';

function openFileAsync(path, flags) {
  return new Promise((resolve, reject) => {
    fs.open(path, flags, (error, fd) => {
      if (error) {
        reject(error);
      } else {
        resolve(fd);
      }
    });
  });
}

function writeFileAsync(fd, content) {
  return new Promise((resolve, reject) => {
    fs.write(fd, content, (error) => {
      if (error) {
        reject(error);
      } else {
        resolve();
      }
    });
  });
}

function closeFileAsync(fd) {
  return new Promise((resolve, reject) => {
    fs.close(fd, (error) => {
      if (error) {
        reject(error);
      } else {
        resolve();
      }
    });
  });
}

function openAndReadFileAsync(path, flags, buffer, offset, length, position) {
  return new Promise((resolve, reject) => {
    fs.open(path, flags, (error, fd) => {
      if (error) {
        reject(error);
      } else {
        fs.read(fd, buffer, offset, length, position, (error, bytesRead) => {
          if (error) {
            reject(error);
          } else {
            resolve({ fd, bytesRead, buffer });
          }
        });
      }
    });
  });
}

function unlinkFileAsync(path) {
  return new Promise((resolve, reject) => {
    fs.unlink(path, (error) => {
      if (error) {
        reject(error);
      } else {
        resolve();
      }
    });
  });
}

openFileAsync(filePath, 'w')
  .then((fd) => {
    const content = 'Hello 1+1=10 from Node.js Async!';
    return writeFileAsync(fd, content)
      .then(() => closeFileAsync(fd))
      .then(() => {
        console.log('File created and written successfully.');

        return openAndReadFileAsync(filePath, 'r', Buffer.alloc(1024), 0, 1024, 0);
      });
  })
  .then(({ fd, bytesRead, buffer }) => {
    const fileContent = buffer.toString('utf8', 0, bytesRead);

    return closeFileAsync(fd)
      .then(() => fileContent);
  })
  .then((fileContent) => {
    console.log('File Content:', fileContent);

    // 删除文件
    return unlinkFileAsync(filePath);
  })
  .then(() => {
    console.log('File deleted successfully.');
  })
  .catch((error) => {
    console.error('Error:', error);
  });

例子4- 异步回调 API,封装成 promise,await

再使用await 取代then写法,改写如下:

import { Buffer } from 'node:buffer';
import fs from 'node:fs';

const filePath = 'dbzhang800_async.txt';

function openFileAsync(path, flags) {
    return new Promise((resolve, reject) => {
        fs.open(path, flags, (error, fd) => {
            if (error) {
                reject(error);
            } else {
                resolve(fd);
            }
        });
    });
}

function writeFileAsync(fd, content) {
    return new Promise((resolve, reject) => {
        fs.write(fd, content, (error) => {
            if (error) {
                reject(error);
            } else {
                resolve();
            }
        });
    });
}

function closeFileAsync(fd) {
    return new Promise((resolve, reject) => {
        fs.close(fd, (error) => {
            if (error) {
                reject(error);
            } else {
                resolve();
            }
        });
    });
}

function openAndReadFileAsync(path, flags, buffer, offset, length, position) {
    return new Promise((resolve, reject) => {
        fs.open(path, flags, (error, fd) => {
            if (error) {
                reject(error);
            } else {
                fs.read(fd, buffer, offset, length, position, (error, bytesRead) => {
                    if (error) {
                        reject(error);
                    } else {
                        resolve({ fd, bytesRead, buffer });
                    }
                });
            }
        });
    });
}

function unlinkFileAsync(path) {
    return new Promise((resolve, reject) => {
        fs.unlink(path, (error) => {
            if (error) {
                reject(error);
            } else {
                resolve();
            }
        });
    });
}

try {
    const fd = await openFileAsync(filePath, 'w');
    const content = 'Hello 1+1=10 from Node.js Async!';
    await writeFileAsync(fd, content);
    await closeFileAsync(fd);

    console.log('File created and written successfully.');

    const { fd: readFd, bytesRead, buffer } = await openAndReadFileAsync(
        filePath,
        'r',
        Buffer.alloc(1024),
        0,
        1024,
        0
    );

    const fileContent = buffer.toString('utf8', 0, bytesRead);
    await closeFileAsync(readFd);
    console.log('File Content:', fileContent);

    // 删除文件
    await unlinkFileAsync(filePath);
    console.log('File deleted successfully.');
} catch (error) {
    console.error('Error:', error);
}

例子5- 异步promise API,使用then

模块导入,官方手册给出写法:

import fsPromises from 'node:fs/promises';

可能这个更容易理解:

import { promises as fsPromises } from 'node:fs';

使用 Promise 对象,重写前面的例子。相比回调,可以更容易处理异步操作的结果和错误。

import fsPromises from 'node:fs/promises';

const filePath = 'dbzhang800_async.txt';

// 创建文件并写入内容
fsPromises.open(filePath, 'w')
  .then((fd) => {
    const content = 'Hello 1+1=10 from Node.js Async!';
    return fd.write(content)
      .then(() => fd.close())
      .then(() => {
        console.log('File created and written successfully.');

        // 打开文件并读取内容
        return fsPromises.open(filePath, 'r');
      });
  })
  .then((fd) => {
    const buffer = Buffer.alloc(1024);
    return fd.read(buffer, 0, buffer.length, 0)
      .then((result) => ({ fd, bytesRead: result.bytesRead, buffer }));
  })
  .then(({ fd, bytesRead, buffer }) => {
    const fileContent = buffer.toString('utf8', 0, bytesRead);
    return fd.close().then(() => fileContent);
  })
  .then((fileContent) => {
    console.log('File Content:', fileContent);

    // 删除文件
    return fsPromises.unlink(filePath);
  })
  .then(() => {
    console.log('File deleted successfully.');
  })
  .catch((error) => {
    console.error('Error:', error);
  });

例子6- 异步promise API,使用await

通过 async/await 语法可以进行更清晰的异步代码编写:

import { promises as fsPromises } from 'node:fs';

const filePath = 'dbzhang800_async.txt';

// 创建文件并写入内容
try {
    const fd = await fsPromises.open(filePath, 'w');
    const data = 'Hello 1+1=10 from Node.js ASync!';
    await fd.write(data);
    await fd.close();
    console.log('File created and written successfully.');
} catch (error) {
    console.error('Error:', error);
}

// 打开文件并读取内容
try {
    const fd = await fsPromises.open(filePath, 'r');
    const buffer = Buffer.alloc(1024);
    const { bytesRead } = await fd.read(buffer, 0, buffer.length, 0);
    const fileContent = buffer.toString('utf8', 0, bytesRead);
    await fd.close();
    console.log('File Content:', fileContent);
} catch (error) {
    console.error('Error:', error);
}

// 删除文件
try {
    await fsPromises.unlink(filePath);
    console.log('File deleted successfully.');
} catch (error) {
    console.error('Error:', error);
}

API抽象层级

前面例子中只使用文件操作的底层API,它还有更高层级的封装

  • 底层:open、close、read、wirte、...
  • 高层:readFile、writeFile、...
  • 流:createReadStream、createWriteStream、...

读文件

例子1-底层API

前面例子全都是用的底层API,所以都需要管理Buffer。

import fs from 'node:fs/promises';

const fd = await fs.open('mytest.mjs', 'r');
const buffer = Buffer.alloc(1024);
const ret = await fd.read(buffer, 0, buffer.length, 0);
await fd.close();
console.log('Read data:', buffer.toString('utf-8', 0, ret.bytesRead));

例子2-高层API

使用同步API和高层API:readFileAync

import fs from 'node:fs';

const buffer = fs.readFileSync('mytest.mjs');
console.log('Read data:', buffer.toString('utf-8'));

使用回调API和高层API:readFile

import fs from 'node:fs';

fs.readFile('mytest.mjs', 'utf-8', (err, data) => {
    if (err) {
        console.error('Error reading file:', err);
        return;
    }

    console.log('Read data:', data);
});

使用promises和高层API:readFile

import fs from 'node:fs/promises';

const buffer = await fs.readFile('mytest.mjs');
console.log('Read data:', buffer.toString('utf-8'));

例子3-流API

使用回调API:

import fs from 'node:fs';

const readStream = fs.createReadStream('mytest.mjs', 'utf-8');
readStream.on('data', (chunk) => {
    console.log('Read data:', chunk);
});
readStream.on('end', () => {
    console.log('Finished reading file');
});

使用promises API:

import fsPromises from 'fs/promises';

const fd = await fsPromises.open('mytest.mjs', 'r');

const stream = fd.createReadStream();
stream.on('data', (chunk) => {
    console.log(chunk.toString());
});

参考

  • https://nodejs.org/api/fs.html

Comments