前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
  • 基础篇

    • 环境搭建
    • node部署
    • 基础应用
  • 内置模块

    • 1.0 本地路径处理 path
    • 2.0 文件系统操作 fs
    • 3.1 基础调试 console
    • 3.2 本地调试远程服务器上的Node代码
    • 4.1 网络服务 http
    • 4.2 网络服务 http res
    • 4.3 网络服务 http req
    • 4.4 网络服务 http server
    • 4.5 网络服务 http client
    • 4.6 网络服务 https
    • 4.7 网络TCP net
    • 4.8 网络UDP dgram
    • 4.9 域名解析 dns
    • 5.0 网络地址解析 url
    • 5.1 URL查询字符串 querystring
    • 6.1 流操作 stream
    • 6.2 逐行读取 readline
    • 7.1 进程相关 process
    • 7.2 子进程 child
    • 8.1 二进制数据 buffer
    • 8.2 二进制解码 string_decoder
    • 9.1 事件机制 events
    • 9.2 实用工具模块 util
    • 9.3 数据加密 crypto
    • 9.4 MD5入门介绍及crypto模块的应用
    • 9.4 资源压缩 zlib
    • 9.5 集群 cluster
    • 9.6 v8
  • 进阶篇

    • 5分钟入门非对称加密用法
    • cluster
    • cookie_parser深入
    • crypto模块之理论篇
    • express+cookie_parser签名机制深入剖析
    • express+session实现简易身份认证
    • https
    • log4js入门实例
    • node8_napi
    • 使用 async 控制并发
    • 使用 eventproxy 控制并发
    • 使用 superagent 与 cheerio 完成简单爬虫
    • 基于express+muter的文件上传
    • 将图片转成datauri嵌入到html
    • 常用中间件 body_parser 实现解析
    • 日志模块morgan
    • 服务端字符编解码&乱码处理 charset_enc_dec
    • 测试用例mocha,should,istanbul
    • 调试日志打印debug模块
  • Express

    • 1.1 Express概览篇
    • 1.2 Express
  • Koa2

    • 1.0 koa2概览篇
    • 1.1 快速开始
    • 1.2 async await使用
    • 1.3 koa2简析结构
    • 1.4 koa中间件开发与使用
    • 10.1 单元测试
    • 11.1 开发debug
    • 12.1 快速启动
    • 12.2 框架设计
    • 12.3 分层操作
    • 12.4 数据库设计
    • 12.5 路由设计
    • 12.6 webpack4环境搭建
    • 12.7 使用react
    • 12.8 登录注册功能实现
    • 12.9 session登录态判断处理
    • 13.1 import export使用
    • 2.1 原生koa2实现路由
    • 2.2 koa router中间
    • 3.1 GET请求数据获取
    • 3.2 POST请求数据获取
    • 3.3 koa bodyparser中间件
    • 4.1 原生koa2实现静态资源服务器
    • 4.2 koa static中间件
    • 5.1 koa2使用cookie
    • 5.2 koa2实现session
    • 6.1 koa2加载模板引擎
    • 6.2 ejs模板引擎
    • 7.1 busboy模块
    • 7.2 上传文件简单实现
    • 7.3 异步上传图片实现
    • 8.1 mysql模块
    • 8.2 async await封装使用mysql
    • 8.3 项目建表初始化
    • 9.1 原生koa2实现JSONP
    • 9.2 koa jsonp中间件
  • 其他

    • 操作数据库
    • Session 与 Token
    • Cookie、Session、Token、JWT
    • Socket

# 文件系统操作 fs

30 秒速记

  • 三套写法:回调 fs.readFile、同步 fs.readFileSync、Promise 版 fs/promises;新代码优先 Promise + await
  • 同步 API 会卡住事件循环,只放在启动阶段或一次性脚本里
  • 别先 access 再操作:检查和使用之间文件可能变了(TOCTOU),直接操作再按 err.code 处理
  • 大文件用流,readFile 会把整个文件塞进内存
  • 重要文件写临时文件再 rename,避免写到一半进程挂掉留下半个文件

fs 是 Node.js 操作文件的模块,同一个功能一般有回调、同步、Promise 三种写法,新代码我默认用 fs/promises 加 await。 同步写法最直观,但它执行期间整个进程什么都干不了,在 HTTP 服务里读一个大文件,所有请求都得等着,所以只放在启动读配置、一次性脚本这种地方。另外两条工程经验:别先 fs.access 判断再去读,直接读并处理 ENOENT;写配置这类重要文件先写到临时文件再 rename 过去,这样进程中途崩了也不会留下半截文件。

理解「文件系统操作 fs」时,要先明确输入、运行阶段和可观察输出。这里的交互实验会分别执行正常路径与失败路径,并把 Node.js 的关键状态变化和同屏代码对应起来;不能只记命令名称。

工程中还要验证退出码、资源清理和异常输入。属于 filesystem 场景的配置应保持最小可复现,并在开发环境与 CI 中使用相同命令复跑,避免把平台差异或隐式默认值带到线上。

fs 模块的接口很多,但大部分都是同一个操作的三种写法,先把这三种分清楚:

const fs = require('fs');
const fsp = require('fs/promises');

// 1. 同步:直观,但执行期间整个进程卡住
const a = fs.readFileSync('./a.txt', 'utf8');

// 2. 回调:错误优先,第一个参数是 err
fs.readFile('./a.txt', 'utf8', (err, data) => {
  if (err) return console.error(err.code); // 比如 ENOENT
  console.log(data);
});

// 3. Promise:新代码首选,配合 async/await
async function main() {
  try {
    const data = await fsp.readFile('./a.txt', 'utf8');
    console.log(data);
  } catch (err) {
    if (err.code === 'ENOENT') console.log('文件不存在');
    else throw err;
  }
}
main();
@前端进阶之旅: 代码已经复制到剪贴板

选型上可以简单记:服务运行期间一律用异步(Promise 优先),同步写法只用在启动阶段和命令行脚本里。

几个常见错误码要认识:ENOENT 文件或目录不存在,EACCES / EPERM 没权限,EISDIR 把目录当文件读了,EEXIST 文件已存在,EMFILE 打开的文件太多。按错误码分支处理,比提前用 access 检查更可靠,因为检查完到真正操作之间,文件状态随时可能被别的进程改掉。

写重要文件时用「临时文件 + rename」:

async function writeAtomic(file, content) {
  const tmp = file + '.' + process.pid + '.tmp';
  await fsp.writeFile(tmp, content);
  await fsp.rename(tmp, file); // 同一文件系统内是原子替换
}
@前端进阶之旅: 代码已经复制到剪贴板

面试官追问

追问 1服务里每个请求都 readFileSync 读一次模板,QPS 一高延迟就上去了,为什么?
参考回答

同步读取期间事件循环被卡住,其他请求只能排队。启动时读一次缓存到内存,或者改成 await fs.readFile。

追问 2先 fs.existsSync(p) 判断,再 readFile,有什么问题?
参考回答

两步之间文件可能被删或被换,判断结果就不准了,还多了一次系统调用。直接读,catch 里判断 err.code === 'ENOENT'。

追问 3写 config.json 时进程被 kill,重启后读出来是半个 JSON,怎么避免?
参考回答

先写到同目录的 config.json.tmp,写完再 fs.rename 覆盖过去。同一个文件系统内的 rename 是原子的,读到的要么是旧文件要么是新文件。

追问 4回调版、Promise 版、同步版性能有差别吗?
参考回答

回调和 Promise 版底层都走 libuv 线程池,差别很小;同步版单次可能更快,但会阻塞整个进程。选型看场景,不看这点速度差。

追问 5一次性 Promise.all 打开几万个文件,报 EMFILE,怎么办?
参考回答

文件描述符超过系统上限了。用 p-limit 之类控制并发,比如同时只开 100 个。

# 文件读取

30 秒速记

  • 小文件一次读完:await fs.readFile(p, 'utf8'),不传编码拿到的是 Buffer
  • 大文件用流:fs.createReadStream 每次读一块(默认 64KB),内存占用稳定
  • readFile 有上限(约 2GB),超了直接报 ERR_FS_FILE_TOO_LARGE
  • 按行处理用 readline + 流;流要处理 error 事件,用 pipeline 自动收尾
  • 同步读取只放启动阶段或脚本

读文件看大小选方式:小文件 readFile 一次读进内存,大文件用 createReadStream 一块一块读。 readFile 写起来最简单,但几百 MB 的日志一次读进来,内存直接飙上去,并发几个请求进程就可能被 OOM 干掉。流每次只读一块,读完处理完再读下一块,内存基本是平的。按行处理大文件我一般用 readline 配流,用 for await 逐行读;要把文件转发给响应或者压缩,就用 stream.pipeline,它出错时会帮你把两头的流都关掉。

理解「文件读取」时,要先明确输入、运行阶段和可观察输出。这里的交互实验会分别执行正常路径与失败路径,并把 Node.js 的关键状态变化和同屏代码对应起来;不能只记命令名称。

工程中还要验证退出码、资源清理和异常输入。属于 filesystem 场景的配置应保持最小可复现,并在开发环境与 CI 中使用相同命令复跑,避免把平台差异或隐式默认值带到线上。

读文件主要就两种方式,区别在于内存里同一时刻放多少数据:

const fs = require('fs');
const fsp = require('fs/promises');
const readline = require('readline');
const { pipeline } = require('stream/promises');
const zlib = require('zlib');

// 1. 小文件:一次读完
const text = await fsp.readFile('./config.json', 'utf8');

// 2. 大文件按行处理:内存只占一块缓冲区
const rl = readline.createInterface({
  input: fs.createReadStream('./access.log'),
  crlfDelay: Infinity,
});
let count = 0;
for await (const line of rl) {
  if (line.includes(' 500 ')) count++;
}
console.log('500 错误行数', count);

// 3. 大文件转存/压缩:pipeline 负责背压和出错时关闭两端
await pipeline(
  fs.createReadStream('./access.log'),
  zlib.createGzip(),
  fs.createWriteStream('./access.log.gz'),
);
@前端进阶之旅: 代码已经复制到剪贴板

(上面的顶层 await 需要在 ESM 或 async 函数里运行。)

一个直观的类比:readFile 是把整桶水一次倒进杯子,杯子不够大就溢出;流是接一根水管,接一点用一点。fs.createReadStream 默认每块 64KB,可以用 highWaterMark 调整。

还有两个细节。第一,流一定要处理错误,单独用 .pipe() 时某一端出错另一端不会自动关闭,文件描述符会泄漏,所以优先用 pipeline。第二,readFile 不适合读持续增长的文件(比如正在写的日志),它只读调用那一刻的内容,要跟踪新内容得用流配合 fs.watch 或者专门的 tail 库。

面试官追问

追问 1fs.readFile(p) 打印出来是 <Buffer 68 65 ...>,怎么拿字符串?
参考回答

传编码:readFile(p, 'utf8'),或者拿到 Buffer 后 data.toString('utf8')。

追问 2下载接口用 readFile 读 1GB 的文件再 res.end,内存爆了,怎么改?
参考回答

改成

基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
  • 基础篇

    • 环境搭建
    • node部署
    • 基础应用
  • 内置模块

    • 1.0 本地路径处理 path
    • 2.0 文件系统操作 fs
      • 文件读取
        • 普通读取
        • 通过文件流读取
      • 文件写入
      • 文件是否存在
      • 创建目录
      • 删除文件
      • 创建目录
      • 遍历目录
      • 文件重命名
      • 监听文件修改
      • 修改所有者
      • 修改权限
      • 获取文件状态
      • 访问/权限检测
      • 文件打开/关闭
      • 文件读取(底层)
      • 追加文件内容
      • 文件内容截取
      • 修改文件属性(时间)
      • 创建文件链接
      • 创建临时目录
      • 找出软连接指向的真实路径
      • 真实路径
      • 删除目录
      • 不常用
      • 待确认
    • 3.1 基础调试 console
    • 3.2 本地调试远程服务器上的Node代码
    • 4.1 网络服务 http
    • 4.2 网络服务 http res
    • 4.3 网络服务 http req
    • 4.4 网络服务 http server
    • 4.5 网络服务 http client
    • 4.6 网络服务 https
    • 4.7 网络TCP net
    • 4.8 网络UDP dgram
    • 4.9 域名解析 dns
    • 5.0 网络地址解析 url
    • 5.1 URL查询字符串 querystring
    • 6.1 流操作 stream
    • 6.2 逐行读取 readline
    • 7.1 进程相关 process
    • 7.2 子进程 child
    • 8.1 二进制数据 buffer
    • 8.2 二进制解码 string_decoder
    • 9.1 事件机制 events
    • 9.2 实用工具模块 util
    • 9.3 数据加密 crypto
    • 9.4 MD5入门介绍及crypto模块的应用
    • 9.4 资源压缩 zlib
    • 9.5 集群 cluster
    • 9.6 v8
  • 进阶篇

    • 5分钟入门非对称加密用法
    • cluster
    • cookie_parser深入
    • crypto模块之理论篇
    • express+cookie_parser签名机制深入剖析
    • express+session实现简易身份认证
    • https
    • log4js入门实例
    • node8_napi
    • 使用 async 控制并发
    • 使用 eventproxy 控制并发
    • 使用 superagent 与 cheerio 完成简单爬虫
    • 基于express+muter的文件上传
    • 将图片转成datauri嵌入到html
    • 常用中间件 body_parser 实现解析
    • 日志模块morgan
    • 服务端字符编解码&乱码处理 charset_enc_dec
    • 测试用例mocha,should,istanbul
    • 调试日志打印debug模块
  • Express

    • 1.1 Express概览篇
    • 1.2 Express
  • Koa2

    • 1.0 koa2概览篇
    • 1.1 快速开始
    • 1.2 async await使用
    • 1.3 koa2简析结构
    • 1.4 koa中间件开发与使用
    • 10.1 单元测试
    • 11.1 开发debug
    • 12.1 快速启动
    • 12.2 框架设计
    • 12.3 分层操作
    • 12.4 数据库设计
    • 12.5 路由设计
    • 12.6 webpack4环境搭建
    • 12.7 使用react
    • 12.8 登录注册功能实现
    • 12.9 session登录态判断处理
    • 13.1 import export使用
    • 2.1 原生koa2实现路由
    • 2.2 koa router中间
    • 3.1 GET请求数据获取
    • 3.2 POST请求数据获取
    • 3.3 koa bodyparser中间件
    • 4.1 原生koa2实现静态资源服务器
    • 4.2 koa static中间件
    • 5.1 koa2使用cookie
    • 5.2 koa2实现session
    • 6.1 koa2加载模板引擎
    • 6.2 ejs模板引擎
    • 7.1 busboy模块
    • 7.2 上传文件简单实现
    • 7.3 异步上传图片实现
    • 8.1 mysql模块
    • 8.2 async await封装使用mysql
    • 8.3 项目建表初始化
    • 9.1 原生koa2实现JSONP
    • 9.2 koa jsonp中间件
  • 其他

    • 操作数据库
    • Session 与 Token
    • Cookie、Session、Token、JWT
    • Socket
完整面试题地址:
作者:程序员poetry
扫码关注作者公众号:「前端进阶之旅」 每天分享技术干货
前端进阶之旅公众号二维码

文件系统操作 fs|Nodejs专题系列

# 文件系统操作 fs

30 秒速记

  • 三套写法:回调 fs.readFile、同步 fs.readFileSync、Promise 版 fs/promises;新代码优先 Promise + await
  • 同步 API 会卡住事件循环,只放在启动阶段或一次性脚本里
  • 别先 access 再操作:检查和使用之间文件可能变了(TOCTOU),直接操作再按 err.code 处理
  • 大文件用流,readFile 会把整个文件塞进内存
  • 重要文件写临时文件再 rename,避免写到一半进程挂掉留下半个文件

fs 是 Node.js 操作文件的模块,同一个功能一般有回调、同步、Promise 三种写法,新代码我默认用 fs/promises 加 await。 同步写法最直观,但它执行期间整个进程什么都干不了,在 HTTP 服务里读一个大文件,所有请求都得等着,所以只放在启动读配置、一次性脚本这种地方。另外两条工程经验:别先 fs.access 判断再去读,直接读并处理 ENOENT;写配置这类重要文件先写到临时文件再 rename 过去,这样进程中途崩了也不会留下半截文件。

理解「文件系统操作 fs」时,要先明确输入、运行阶段和可观察输出。这里的交互实验会分别执行正常路径与失败路径,并把 Node.js 的关键状态变化和同屏代码对应起来;不能只记命令名称。

工程中还要验证退出码、资源清理和异常输入。属于 filesystem 场景的配置应保持最小可复现,并在开发环境与 CI 中使用相同命令复跑,避免把平台差异或隐式默认值带到线上。

fs 模块的接口很多,但大部分都是同一个操作的三种写法,先把这三种分清楚:

const fs = require('fs');
const fsp = require('fs/promises');

// 1. 同步:直观,但执行期间整个进程卡住
const a = fs.readFileSync('./a.txt', 'utf8');

// 2. 回调:错误优先,第一个参数是 err
fs.readFile('./a.txt', 'utf8', (err, data) => {
  if (err) return console.error(err.code); // 比如 ENOENT
  console.log(data);
});

// 3. Promise:新代码首选,配合 async/await
async function main() {
  try {
    const data = await fsp.readFile('./a.txt', 'utf8');
    console.log(data);
  } catch (err) {
    if (err.code === 'ENOENT') console.log('文件不存在');
    else throw err;
  }
}
main();
@前端进阶之旅: 代码已经复制到剪贴板

选型上可以简单记:服务运行期间一律用异步(Promise 优先),同步写法只用在启动阶段和命令行脚本里。

几个常见错误码要认识:ENOENT 文件或目录不存在,EACCES / EPERM 没权限,EISDIR 把目录当文件读了,EEXIST 文件已存在,EMFILE 打开的文件太多。按错误码分支处理,比提前用 access 检查更可靠,因为检查完到真正操作之间,文件状态随时可能被别的进程改掉。

写重要文件时用「临时文件 + rename」:

async function writeAtomic(file, content) {
  const tmp = file + '.' + process.pid + '.tmp';
  await fsp.writeFile(tmp, content);
  await fsp.rename(tmp, file); // 同一文件系统内是原子替换
}
@前端进阶之旅: 代码已经复制到剪贴板

面试官追问

追问 1服务里每个请求都 readFileSync 读一次模板,QPS 一高延迟就上去了,为什么?
参考回答

同步读取期间事件循环被卡住,其他请求只能排队。启动时读一次缓存到内存,或者改成 await fs.readFile。

追问 2先 fs.existsSync(p) 判断,再 readFile,有什么问题?
参考回答

两步之间文件可能被删或被换,判断结果就不准了,还多了一次系统调用。直接读,catch 里判断 err.code === 'ENOENT'。

追问 3写 config.json 时进程被 kill,重启后读出来是半个 JSON,怎么避免?
参考回答

先写到同目录的 config.json.tmp,写完再 fs.rename 覆盖过去。同一个文件系统内的 rename 是原子的,读到的要么是旧文件要么是新文件。

追问 4回调版、Promise 版、同步版性能有差别吗?
参考回答

回调和 Promise 版底层都走 libuv 线程池,差别很小;同步版单次可能更快,但会阻塞整个进程。选型看场景,不看这点速度差。

追问 5一次性 Promise.all 打开几万个文件,报 EMFILE,怎么办?
参考回答

文件描述符超过系统上限了。用 p-limit 之类控制并发,比如同时只开 100 个。

# 文件读取

30 秒速记

  • 小文件一次读完:await fs.readFile(p, 'utf8'),不传编码拿到的是 Buffer
  • 大文件用流:fs.createReadStream 每次读一块(默认 64KB),内存占用稳定
  • readFile 有上限(约 2GB),超了直接报 ERR_FS_FILE_TOO_LARGE
  • 按行处理用 readline + 流;流要处理 error 事件,用 pipeline 自动收尾
  • 同步读取只放启动阶段或脚本

读文件看大小选方式:小文件 readFile 一次读进内存,大文件用 createReadStream 一块一块读。 readFile 写起来最简单,但几百 MB 的日志一次读进来,内存直接飙上去,并发几个请求进程就可能被 OOM 干掉。流每次只读一块,读完处理完再读下一块,内存基本是平的。按行处理大文件我一般用 readline 配流,用 for await 逐行读;要把文件转发给响应或者压缩,就用 stream.pipeline,它出错时会帮你把两头的流都关掉。

理解「文件读取」时,要先明确输入、运行阶段和可观察输出。这里的交互实验会分别执行正常路径与失败路径,并把 Node.js 的关键状态变化和同屏代码对应起来;不能只记命令名称。

工程中还要验证退出码、资源清理和异常输入。属于 filesystem 场景的配置应保持最小可复现,并在开发环境与 CI 中使用相同命令复跑,避免把平台差异或隐式默认值带到线上。

读文件主要就两种方式,区别在于内存里同一时刻放多少数据:

const fs = require('fs');
const fsp = require('fs/promises');
const readline = require('readline');
const { pipeline } = require('stream/promises');
const zlib = require('zlib');

// 1. 小文件:一次读完
const text = await fsp.readFile('./config.json', 'utf8');

// 2. 大文件按行处理:内存只占一块缓冲区
const rl = readline.createInterface({
  input: fs.createReadStream('./access.log'),
  crlfDelay: Infinity,
});
let count = 0;
for await (const line of rl) {
  if (line.includes(' 500 ')) count++;
}
console.log('500 错误行数', count);

// 3. 大文件转存/压缩:pipeline 负责背压和出错时关闭两端
await pipeline(
  fs.createReadStream('./access.log'),
  zlib.createGzip(),
  fs.createWriteStream('./access.log.gz'),
);
@前端进阶之旅: 代码已经复制到剪贴板

(上面的顶层 await 需要在 ESM 或 async 函数里运行。)

一个直观的类比:readFile 是把整桶水一次倒进杯子,杯子不够大就溢出;流是接一根水管,接一点用一点。fs.createReadStream 默认每块 64KB,可以用 highWaterMark 调整。

还有两个细节。第一,流一定要处理错误,单独用 .pipe() 时某一端出错另一端不会自动关闭,文件描述符会泄漏,所以优先用 pipeline。第二,readFile 不适合读持续增长的文件(比如正在写的日志),它只读调用那一刻的内容,要跟踪新内容得用流配合 fs.watch 或者专门的 tail 库。

面试官追问

追问 1fs.readFile(p) 打印出来是 <Buffer 68 65 ...>,怎么拿字符串?
参考回答

传编码:readFile(p, 'utf8'),或者拿到 Buffer 后 data.toString('utf8')。

追问 2下载接口用 readFile 读 1GB 的文件再 res.end,内存爆了,怎么改?
参考回答

改成 await pipeline(fs.createReadStream(p), res),pipeline 从 stream/promises 引入。内存只占一个缓冲区大小,客户端慢的时候还会自动背压。

追问 3按行处理 10GB 的日志,怎么写?
参考回答

const rl = readline.createInterface({ input: fs.createReadStream(p), crlfDelay: Infinity }),然后 for await (const line of rl)。crlfDelay 是为了把 \r\n 当成一个换行。

追问 4流式读中文文件,偶尔有个字变成乱码,为什么?
参考回答

一个汉字在 UTF-8 里占 3 个字节,可能正好被切在两个 chunk 之间,各自 toString 就坏了。创建流时传 encoding: 'utf8',内部会用 StringDecoder 把残缺字节留到下一块。

追问 5读出来的 JSON 文件 JSON.parse 报 Unexpected token,肉眼看内容没问题,可能是什么?
参考回答

很可能文件开头带了 BOM(\uFEFF),Windows 记事本存的文件常见。读完先 content.replace(/^\uFEFF/, '') 再解析。

# 普通读取

← 1.0 本地路径处理 path3.1 基础调试 console →

fe