前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
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

# 流操作 stream

30 秒速记

  • 流 = 数据一块一块地处理,不用一次性全读进内存,适合大文件、网络传输
  • 四类:Readable 读、Writable 写、Duplex 双向、Transform 边读边改
  • 背压:write() 返回 false 就停,等 drain 再写;pipe 自动帮你做
  • 串流优先 pipeline():出错会传播并销毁整条链,pipe() 不管错误
  • highWaterMark 是缓冲水位不是上限:字节流按字节算,对象模式按个数算;Node 22 起默认从 16KB 提到 64KB

流就是把数据切成小块,一边生产一边消费,不用等全部数据到齐再处理。 好比用水管接水,而不是先把整池水搬过来。它的核心是背压:写入端处理不过来时 write() 会返回 false,读取端就该停一停,等 drain 事件再继续,这样内存才不会被撑爆。pipe() 会自动处理背压,但它不传播错误,中间一段挂了其他流不会关。所以实际项目里我串流一律用 pipeline(),错误能统一收到一个回调里,整条链也会被清理掉。

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

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

理解流,抓住「背压」和「错误处理」这两件事就够了。

背压是说生产者比消费者快的时候,要有机制让生产者停下来。Writable 的 write() 返回值就是这个信号:

const fs = require('fs');
const src = fs.createReadStream('big.log');
const dest = fs.createWriteStream('copy.log');

src.on('data', (chunk) => {
  const ok = dest.write(chunk);
  if (!ok) {
    src.pause();                          // 写入端缓冲满了,先停
    dest.once('drain', () => src.resume()); // 缓冲排空再继续
  }
});
src.on('end', () => dest.end());
@前端进阶之旅: 代码已经复制到剪贴板

这套逻辑 pipe() 已经帮你做了,但 pipe() 不管错误。生产环境推荐 stream/promises 里的 pipeline(Node 15 起可用):

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

async function compress() {
  try {
    await pipeline(
      fs.createReadStream('big.log'),
      zlib.createGzip(),
      fs.createWriteStream('big.log.gz'),
    );
    console.log('done');
  } catch (err) {
    console.error('failed:', err.message); // 任意一段出错都会到这里,且整条链已销毁
  }
}
@前端进阶之旅: 代码已经复制到剪贴板

消费可读流的第三种方式是异步迭代器,for await (const chunk of readable) 会在你处理完当前块之后才拉下一块,背压是天然的。三种方式选一种用,不要在同一个流上混用。

面试官追问

追问 1用 fs.readFile 读一个 2GB 的日志文件,会发生什么?
参考回答

要一次性把整个文件读进 Buffer,内存直接涨 2GB,超过 Buffer 上限还会直接报错。改成 fs.createReadStream 逐块处理,内存基本稳定在几十 KB。

追问 2a.pipe(b).pipe(c),b 报错了会怎样?
参考回答

b 的错误不会传到 a 和 c,如果没监听 error 进程直接崩,监听了的话 a 和 c 也还挂着,文件句柄泄漏。改用 await pipeline(a, b, c),任何一段出错都会销毁整条链并抛出来。

追问 3手写 src.on('data', chunk => dest.write(chunk)),写入端慢的时候内存一直涨,为什么?
参考回答

没处理背压,write() 返回 false 还在继续写,数据全堆在写入端的缓冲里。要在 false 时 src.pause(),等 dest 触发 drain 再 resume(),或者直接用 pipeline。

追问 4data 事件和 for await 能一起用吗?
参考回答

不要混用,两种都会消费数据,混着用会抢数据或者丢数据。选一种:新代码推荐 for await (const chunk of stream),它自带背压,写法也像同步代码。

追问 5highWaterMark 调大是不是就更快?
参考回答

不一定。它是缓冲水位,调大能减少暂停次数,但每个流占的内存也跟着涨,并发流多了反而危险。要不要调得看实测的吞吐和内存曲线。

# 模块概览

30 秒速记

  • 很多核心对象本身就是流:process.stdin / stdout、http 的 req / res、net.Socket、fs 读写流
  • 业务开发多数时候是「用流」,不用 require('stream');写自定义流才需要深入扩展 API
  • 最小例子:fs.createReadStream('./a.txt').pipe(process.stdout)
  • 流都继承自 EventEmitter,靠 data / end / error / finish 这些事件驱动
  • gulp、HTTP 代理、文件上传下载,本质都是流的组合

流在 Node.js 里无处不在,你没写 require('stream') 不代表没在用它。 HTTP 服务里的 req 是可读流、res 是可写流,process.stdout 是可写流,net.Socket 是双工流。所以大部分人日常是在「用流」,需要搞懂的是它的运行机制:数据分块流动、靠事件驱动、有背压。只有像 gulp 插件那样要自己造一个流的时候,才需要去研究 Readable、Transform 这些类怎么继承实现。

nodejs的核心模块,基本上都是stream的的实例,比如process.stdout、http.clientRequest。

对于大部分的nodejs开发者来说,平常并不会直接用到stream模块,只需要了解stream的运行机制即可(非常重要)。

而对于想要实现自定义stream实例的开发者来说,就得好好研究stream的扩展API了,比如gulp的内部实现就大量用到了自定义的stream类型。

来个简单的例子镇楼,几行代码就实现了读取文件内容,并打印到控制台:

const fs = require('fs');

fs.createReadStream('./sample.txt').pipe(process.stdout);
@前端进阶之旅: 代码已经复制到剪贴板

面试官追问

追问 1HTTP 服务里要把一个大文件返回给前端,怎么写最省内存?
参考回答

res 本身就是可写流,直接 await pipeline(fs.createReadStream(file), res)。别先 readFile 再 res.end(data),并发一多内存就扛不住。

追问 2fs.createReadStream('./a.txt') 在 CI 里报 ENOENT,本地正常,先看什么?
参考回答

相对路径是相对 process.cwd() 解析的,不是相对当前文件。CI 的工作目录不同就找不到,改成 path.join(__dirname, 'a.txt')。

追问 3用户下载到一半关掉了页面,服务端的读文件流会自动停吗?
参考回答

用 pipe 的话不会,读取流可能还挂着。pipeline 会在 res 被销毁时把源流一起销毁,这也是我推荐它的原因之一。

追问 4Express 里怎么拿到上传文件而不把整个文件读进内存?
参考回答

req 就是可读流,用 busboy 这类库解析 multipart,拿到每个文件的流直接 pipeline 到磁盘或对象存储。body-parser 之类会把整个请求体读进内存,不适合大文件。

# Stream分类

30 秒速记

  • Readable:只出数据,fs.createReadStream()、http 的 req、process.stdin
  • Writable:只收数据,fs.createWriteStream()、http 的 res、process.stdout
  • Duplex:能读能写,两个方向互不相干,典型是 net.Socket
  • Transform:特殊的 Duplex,读出来的是写进去的加工结果,zlib、crypto
  • 判断 Duplex 还是 Transform:看输出是不是由输入算出来的

Node.js 的流分四类,记住「数据往哪走」就好分:只出的是 Readable,只进的是 Writable,进出都有的是 Duplex,进去的数据加工后再出来的是 Transform。 Duplex 和 Transform 最容易混。net.Socket 是 Duplex

基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
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
      • 模块概览
      • Stream分类
      • Readable Stream
      • Writable Stream
      • Duplex Stream
      • Transform 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
扫码关注作者公众号:「前端进阶之旅」 每天分享技术干货
前端进阶之旅公众号二维码

流操作 stream|Nodejs专题系列

# 流操作 stream

30 秒速记

  • 流 = 数据一块一块地处理,不用一次性全读进内存,适合大文件、网络传输
  • 四类:Readable 读、Writable 写、Duplex 双向、Transform 边读边改
  • 背压:write() 返回 false 就停,等 drain 再写;pipe 自动帮你做
  • 串流优先 pipeline():出错会传播并销毁整条链,pipe() 不管错误
  • highWaterMark 是缓冲水位不是上限:字节流按字节算,对象模式按个数算;Node 22 起默认从 16KB 提到 64KB

流就是把数据切成小块,一边生产一边消费,不用等全部数据到齐再处理。 好比用水管接水,而不是先把整池水搬过来。它的核心是背压:写入端处理不过来时 write() 会返回 false,读取端就该停一停,等 drain 事件再继续,这样内存才不会被撑爆。pipe() 会自动处理背压,但它不传播错误,中间一段挂了其他流不会关。所以实际项目里我串流一律用 pipeline(),错误能统一收到一个回调里,整条链也会被清理掉。

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

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

理解流,抓住「背压」和「错误处理」这两件事就够了。

背压是说生产者比消费者快的时候,要有机制让生产者停下来。Writable 的 write() 返回值就是这个信号:

const fs = require('fs');
const src = fs.createReadStream('big.log');
const dest = fs.createWriteStream('copy.log');

src.on('data', (chunk) => {
  const ok = dest.write(chunk);
  if (!ok) {
    src.pause();                          // 写入端缓冲满了,先停
    dest.once('drain', () => src.resume()); // 缓冲排空再继续
  }
});
src.on('end', () => dest.end());
@前端进阶之旅: 代码已经复制到剪贴板

这套逻辑 pipe() 已经帮你做了,但 pipe() 不管错误。生产环境推荐 stream/promises 里的 pipeline(Node 15 起可用):

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

async function compress() {
  try {
    await pipeline(
      fs.createReadStream('big.log'),
      zlib.createGzip(),
      fs.createWriteStream('big.log.gz'),
    );
    console.log('done');
  } catch (err) {
    console.error('failed:', err.message); // 任意一段出错都会到这里,且整条链已销毁
  }
}
@前端进阶之旅: 代码已经复制到剪贴板

消费可读流的第三种方式是异步迭代器,for await (const chunk of readable) 会在你处理完当前块之后才拉下一块,背压是天然的。三种方式选一种用,不要在同一个流上混用。

面试官追问

追问 1用 fs.readFile 读一个 2GB 的日志文件,会发生什么?
参考回答

要一次性把整个文件读进 Buffer,内存直接涨 2GB,超过 Buffer 上限还会直接报错。改成 fs.createReadStream 逐块处理,内存基本稳定在几十 KB。

追问 2a.pipe(b).pipe(c),b 报错了会怎样?
参考回答

b 的错误不会传到 a 和 c,如果没监听 error 进程直接崩,监听了的话 a 和 c 也还挂着,文件句柄泄漏。改用 await pipeline(a, b, c),任何一段出错都会销毁整条链并抛出来。

追问 3手写 src.on('data', chunk => dest.write(chunk)),写入端慢的时候内存一直涨,为什么?
参考回答

没处理背压,write() 返回 false 还在继续写,数据全堆在写入端的缓冲里。要在 false 时 src.pause(),等 dest 触发 drain 再 resume(),或者直接用 pipeline。

追问 4data 事件和 for await 能一起用吗?
参考回答

不要混用,两种都会消费数据,混着用会抢数据或者丢数据。选一种:新代码推荐 for await (const chunk of stream),它自带背压,写法也像同步代码。

追问 5highWaterMark 调大是不是就更快?
参考回答

不一定。它是缓冲水位,调大能减少暂停次数,但每个流占的内存也跟着涨,并发流多了反而危险。要不要调得看实测的吞吐和内存曲线。

# 模块概览

30 秒速记

  • 很多核心对象本身就是流:process.stdin / stdout、http 的 req / res、net.Socket、fs 读写流
  • 业务开发多数时候是「用流」,不用 require('stream');写自定义流才需要深入扩展 API
  • 最小例子:fs.createReadStream('./a.txt').pipe(process.stdout)
  • 流都继承自 EventEmitter,靠 data / end / error / finish 这些事件驱动
  • gulp、HTTP 代理、文件上传下载,本质都是流的组合

流在 Node.js 里无处不在,你没写 require('stream') 不代表没在用它。 HTTP 服务里的 req 是可读流、res 是可写流,process.stdout 是可写流,net.Socket 是双工流。所以大部分人日常是在「用流」,需要搞懂的是它的运行机制:数据分块流动、靠事件驱动、有背压。只有像 gulp 插件那样要自己造一个流的时候,才需要去研究 Readable、Transform 这些类怎么继承实现。

nodejs的核心模块,基本上都是stream的的实例,比如process.stdout、http.clientRequest。

对于大部分的nodejs开发者来说,平常并不会直接用到stream模块,只需要了解stream的运行机制即可(非常重要)。

而对于想要实现自定义stream实例的开发者来说,就得好好研究stream的扩展API了,比如gulp的内部实现就大量用到了自定义的stream类型。

来个简单的例子镇楼,几行代码就实现了读取文件内容,并打印到控制台:

const fs = require('fs');

fs.createReadStream('./sample.txt').pipe(process.stdout);
@前端进阶之旅: 代码已经复制到剪贴板

面试官追问

追问 1HTTP 服务里要把一个大文件返回给前端,怎么写最省内存?
参考回答

res 本身就是可写流,直接 await pipeline(fs.createReadStream(file), res)。别先 readFile 再 res.end(data),并发一多内存就扛不住。

追问 2fs.createReadStream('./a.txt') 在 CI 里报 ENOENT,本地正常,先看什么?
参考回答

相对路径是相对 process.cwd() 解析的,不是相对当前文件。CI 的工作目录不同就找不到,改成 path.join(__dirname, 'a.txt')。

追问 3用户下载到一半关掉了页面,服务端的读文件流会自动停吗?
参考回答

用 pipe 的话不会,读取流可能还挂着。pipeline 会在 res 被销毁时把源流一起销毁,这也是我推荐它的原因之一。

追问 4Express 里怎么拿到上传文件而不把整个文件读进内存?
参考回答

req 就是可读流,用 busboy 这类库解析 multipart,拿到每个文件的流直接 pipeline 到磁盘或对象存储。body-parser 之类会把整个请求体读进内存,不适合大文件。

# Stream分类

30 秒速记

  • Readable:只出数据,fs.createReadStream()、http 的 req、process.stdin
  • Writable:只收数据,fs.createWriteStream()、http 的 res、process.stdout
  • Duplex:能读能写,两个方向互不相干,典型是 net.Socket
  • Transform:特殊的 Duplex,读出来的是写进去的加工结果,zlib、crypto
  • 判断 Duplex 还是 Transform:看输出是不是由输入算出来的

Node.js 的流分四类,记住「数据往哪走」就好分:只出的是 Readable,只进的是 Writable,进出都有的是 Duplex,进去的数据加工后再出来的是 Transform。 Duplex 和 Transform 最容易混。net.Socket 是 Duplex,你往里写的数据发给了对方,读出来的是对方发回来的,两个方向没有关系。zlib.createGzip() 是 Transform,你写进原文,读出来的就是压缩后的这份原文。一个文件压缩任务就是 Readable → Transform → Writable 三段串起来。

在nodejs中,有四种stream类型:

  • Readable:用来读取数据,比如 fs.createReadStream()。
  • Writable:用来写数据,比如 fs.createWriteStream()。
  • Duplex:可读+可写,比如 net.Socket()。
  • Transform:在读写的过程中,可以对数据进行修改,比如 zlib.createDeflate()(数据压缩/解压)。

面试官追问

追问 1net.Socket 能读能写,为什么不算 Transform?
参考回答

它读出来的数据是对端发来的,跟你写进去的内容没有对应关系。Transform 要求输出由输入加工而来,比如写进原文、读出压缩结果。

追问 2要把一个大文件压缩后存盘,用哪几类流?
参考回答

fs.createReadStream 是 Readable,zlib.createGzip 是 Transform,fs.createWriteStream 是 Writable,用 pipeline(src, gzip, dest) 串起来。

追问 3http 的 req 和 res 分别是什么流?在客户端那边呢?
参考回答

服务端的 req 是 Readable(IncomingMessage),res 是 Writable。客户端 http.request 返回的 ClientRequest 是 Writable,响应对象又是 Readable,方向正好反过来。

追问 4自己写一个把文本转大写的流,继承哪个类?
参考回答

继承 Transform,实现 transform(chunk, enc, cb),里面 cb(null, chunk.toString().toUpperCase())。不用自己管读写两端的缓冲,这是 Transform 比自己实现 Duplex 省事的地方。

# Readable Stream

← 5.1 URL查询字符串 querystring6.2 逐行读取 readline →

fe