返回

Node.js 全栈学习笔记(持续更新)

记录我学习 Node.js 基础模块时的笔记,先从 Buffer 和 fs 开始,边学边补充,不把它当成已经完成的教程。

这是一篇还在更新中的 Node.js 学习笔记。现在先记录我已经遇到的基础模块和代码例子,后面会继续补充网络、服务端和全栈开发相关内容。笔记里的理解会随着实践调整,适合和代码一起对照着看。

Buffer(缓冲器)

概念

  • 一个类似于 Array 的对象,即表示一段固定长度的内存空间,用于处理二进制数据

特点

  • Buffer 大小固定且无法调整
  • 性能较好,直接对计算机内存进行操作
  • 每个元素的大小为 1 字节(byte),即 8 个 bit

创建方式

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// 1. alloc
let buf = Buffer.alloc(10)

// 2. allocUnsafe,不会对原内存清空,可能包含旧数据
// 不需要归零操作,因此速度快于 alloc
let buf_2 = Buffer.allocUnsafe(10)

// 3. from
let buf_3 = Buffer.from('hello')
let buf_4 = Buffer.from([166, 233, 145, 764, 103, 221])
console.log(buf_4) // <Buffer a6 e9 91 fc 67 dd>

// Buffer.from('hello') 创建时,会使用 UTF-8 编码算法,把字符串转为一串字节。
// console.log 打印 Buffer 时,只是把 Buffer 内部每个字节的值,用 16 进制格式展示出来。
// Buffer 底层存储的是字节,本质上是二进制数据。

Buffer 的元素转换

1
2
3
4
5
6
7
// Buffer 与字符串的转换
let buf_4 = Buffer.from([105, 108, 111, 118, 101, 121, 111, 117])
console.log(buf_4.toString()) // iloveyou,通过 UTF-8,也就是 toString('utf-8')

// 字节转二进制
let buf = Buffer.from('hello')
console.log(buf[0].toString(2)) // 01101000

注意

  • 溢出

因为 8 位二进制最多表示 255。

1
2
let buf = Buffer.from('hello')
buf[0] = 361 // 舍弃高位数字 0001 0110 1001,最后只保留低 8 位
  • 中文

一个中文字符在 UTF-8 中通常占 3 个字节。

fs 模块

概念与使用

fs 模块可以实现与硬盘的交互,例如:

  • 文件的创建、删除、重命名、移动
  • 文件内容的写入和读取
  • 文件夹的相关操作

文件写入

换行可以使用 \r\n

  1. writeFile:异步写入。文件操作交给系统处理,主线程可以继续执行。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// 1. 导入 fs 模块
const fs = require('fs')

// 2. 写入文件
fs.writeFile('./座右铭.txt', '三人行,则必有我师焉', err => {
    // err:写入失败时是错误对象,写入成功时为 null
    if (err) {
        console.log('写入失败')
    } else {
        console.log('写入成功')
    }
})
  1. writeFileSync:同步写入。主线程会等待文件写入完成后再继续。
1
fs.writeFileSync('./data.txt', 'text')
  1. 第三个参数 { flag: 'a' }
  • 可以实现追加写入。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 1. 导入 fs 模块
const fs = require('fs')

// 2. 以追加模式写入文件
fs.writeFile('./座右铭.txt', '三人行,则必有我师焉', { flag: 'a' }, err => {
    if (err) {
        console.log('写入失败')
    } else {
        console.log('写入成功')
    }
})
  1. appendFile:异步追加。
  2. appendFileSync:同步追加。
1
fs.appendFileSync('./座右铭.txt', '\r\n温故而知新,可以为师矣')
  1. 流式写入

程序打开一个文件需要消耗资源。流式写入可以减少反复打开和关闭文件的次数,适合大文件或频繁写入的场景;writeFile 更适合写入频率较低、内容较小的场景。

比如上传大文件时,可以把文件分成多个分片并发上传,也可以让单个 HTTP 请求持续推送数据,再由后端用流写入磁盘。前者支持失败分片重传,但要处理并发;后者实现更直接,但网络中断后通常需要重新上传本次请求。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
const fs = require('fs')

// 创建写入流对象
const ws = fs.createWriteStream('./观书有感.txt')

ws.write('半亩方塘一鉴开 1\r\n')
ws.write('半亩方塘一鉴开 2\r\n')
ws.write('半亩方塘一鉴开 3\r\n')
ws.write('半亩方塘一鉴开 4')

// 关闭通道
ws.close()
  1. 文件写入场景
  • 下载文件
  • 安装软件
  • 保存程序日志,例如 Git 日志
  • 录制视频

需要把数据持久化保存时,就应该想到文件写入。

文件读取

  1. 异步和同步读取
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
const fs = require('fs')

// 异步读取
fs.readFile('./观书有感.txt', (err, data) => {
    if (err) {
        console.log('读取失败')
        return
    }
    console.log(data) // 输出为 Buffer,因此要转成字符串
    console.log(data.toString())
})

// 同步读取
let dataSync = fs.readFileSync('./观书有感.txt')
console.log(dataSync.toString())
  1. 读取文件应用场景
  • 电脑开机
  • 程序运行
  • 编辑器打开文件
  • 查看图片
  • 播放视频、音乐
  • git log 查看日志
  • 上传文件(读取文件后再传到服务端)
  • 查看聊天记录
  1. 流式读取
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
const fs = require('fs')

// 创建读取流对象
const rs = fs.createReadStream('../资料/笑看风云.mp4')

// data 事件中的 chunk 表示一块数据
rs.on('data', chunk => {
    console.log(chunk)
    console.log(chunk.length) // 66536 字节,约 64 KB
})

rs.on('end', () => {
    console.log('读取完成')
})

文件复制案例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
const fs = require('fs')
const process = require('process')

// 方式 1:readFile,一次性读入内存
let data = fs.readFileSync('../资料/笑看风云.mp4')
fs.writeFileSync('../资料/笑看风云复制.mp4', data)

// 检查内存使用量,rss 字段单位是字节
console.log(process.memoryUsage()) // 110710784 字节,约 105 MB

// 方式 2:流式操作
const ws = fs.createWriteStream('../资料/笑看风云流式复制.mp4')
const rs = fs.createReadStream('../资料/笑看风云.mp4')

// rs.pipe(ws) 会自动处理背压,比手动监听 data 再 write 更稳妥
rs.pipe(ws)

方式 1 会把整个文件读进内存。如果 MP4 资源有 78 MB,内存占用可能明显增加;方式 2 通常只保留正在处理的数据块,内存压力小很多。这里的“只占 64 KB”是理想情况,实际还会受到读取速度和写入速度差异的影响。

文件的重命名和移动

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
const fs = require('fs')

// 重命名
fs.rename('./座右铭.txt', './论语.txt', err => {
    if (err) {
        console.log('重命名失败')
        return
    }
    console.log('重命名成功')
})

// 移动文件:把目标路径写成另一个文件夹即可
fs.rename('./座右铭.txt', '../资料/移动文件.txt', err => {
    if (err) {
        console.log('移动失败')
        return
    }
    console.log('移动成功')
})

文件删除

同步函数也有对应版本,例如 unlinkSync;写法更直接,但需要自己处理异常。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
const fs = require('fs')

// 调用 unlink 方法,删除文件
fs.unlink('./观书有感.txt', err => {
    if (err) {
        console.log('删除失败')
        return
    }
    console.log('删除成功')
})

// Node.js 14.4 及以上可以使用 rm
fs.rm('../资料/移动文件.txt', err => {
    if (err) {
        console.log('删除失败')
        return
    }
    console.log('删除成功')
})

文件夹操作

参数 1 是路径,参数 2 是可选的 options,最后传入回调函数。

  1. 创建文件夹
1
2
3
4
5
6
7
8
9
const fs = require('fs')

fs.mkdir('./html', err => {
    if (err) {
        console.log('创建失败')
        return
    }
    console.log('创建成功')
})
  1. 递归创建
1
2
3
4
5
6
7
fs.mkdir('./a/b/c', { recursive: true }, err => {
    if (err) {
        console.log('创建失败')
        return
    }
    console.log('创建成功')
})
  1. 读取文件夹
1
2
3
4
5
6
7
fs.readdir('../资料', (err, data) => {
    if (err) {
        console.log('读取失败')
        return
    }
    console.log(data)
})
  1. 删除文件夹
1
2
3
4
5
6
7
fs.rmdir('./html', err => {
    if (err) {
        console.log('删除失败')
        return
    }
    console.log('删除成功')
})
  1. 递归删除

旧版本可以用 rmdir 配合 { recursive: true },现在更推荐使用 rm

1
2
3
4
5
6
7
fs.rm('./a', { recursive: true }, err => {
    if (err) {
        console.log('删除失败')
        return
    }
    console.log('删除成功')
})

目前的学习提醒

  • 同步文件操作会阻塞当前线程,服务端请求里要谨慎使用。
  • allocUnsafe 速度更快,但使用前要确认数据会被完整覆盖,不能把旧内存内容直接返回给用户。
  • 处理大文件时优先考虑 stream 和背压,不要默认把整个文件一次性读进内存。
  • 这些例子是学习记录,不是可直接用于生产环境的完整错误处理方案。
评论区 正在连接 GitHub Discussions…
使用 Hugo 构建
主题 StackJimmy 设计