这是一篇还在更新中的 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。
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('写入成功')
}
})
|
writeFileSync:同步写入。主线程会等待文件写入完成后再继续。
1
| fs.writeFileSync('./data.txt', 'text')
|
- 第三个参数
{ 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('写入成功')
}
})
|
appendFile:异步追加。appendFileSync:同步追加。
1
| fs.appendFileSync('./座右铭.txt', '\r\n温故而知新,可以为师矣')
|
- 流式写入
程序打开一个文件需要消耗资源。流式写入可以减少反复打开和关闭文件的次数,适合大文件或频繁写入的场景;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()
|
- 文件写入场景
- 下载文件
- 安装软件
- 保存程序日志,例如 Git 日志
- 录制视频
需要把数据持久化保存时,就应该想到文件写入。
文件读取
- 异步和同步读取
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())
|
- 读取文件应用场景
- 电脑开机
- 程序运行
- 编辑器打开文件
- 查看图片
- 播放视频、音乐
- 用
git log 查看日志 - 上传文件(读取文件后再传到服务端)
- 查看聊天记录
- 流式读取
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
2
3
4
5
6
7
8
9
| const fs = require('fs')
fs.mkdir('./html', err => {
if (err) {
console.log('创建失败')
return
}
console.log('创建成功')
})
|
- 递归创建
1
2
3
4
5
6
7
| fs.mkdir('./a/b/c', { recursive: true }, err => {
if (err) {
console.log('创建失败')
return
}
console.log('创建成功')
})
|
- 读取文件夹
1
2
3
4
5
6
7
| fs.readdir('../资料', (err, data) => {
if (err) {
console.log('读取失败')
return
}
console.log(data)
})
|
- 删除文件夹
1
2
3
4
5
6
7
| fs.rmdir('./html', err => {
if (err) {
console.log('删除失败')
return
}
console.log('删除成功')
})
|
- 递归删除
旧版本可以用 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 和背压,不要默认把整个文件一次性读进内存。
- 这些例子是学习记录,不是可直接用于生产环境的完整错误处理方案。