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

# 二进制解码 string_decoder

30 秒速记

  • 解决的问题:多字节字符被切在两个 chunk 之间,分别 toString() 会出乱码
  • StringDecoder 把不完整的尾巴字节先存起来,等下一块到了拼完整再输出
  • 两个 API:write(buf) 返回能完整解码的部分,end() 吐出剩余内容
  • 平时少直接用:stream.setEncoding('utf8') 内部就是它,浏览器和新代码可用 TextDecoder + { stream: true }
  • 只解决「字符被切断」,解决不了「编码选错」

string_decoder 专门解决一件事:网络或文件流把一个汉字切成了两半,直接 toString() 会乱码。 比如「好」是 e5 a5 bd 三个字节,第一个 chunk 只带了前两个,单独解码就成了 �。StringDecoder 的做法是把凑不齐的尾巴先留在内部,下次 write 时拼上再输出,所以拼出来的字符串永远是完整的。实际项目里我很少手写它,给可读流调一下 setEncoding('utf8') 就是同样的效果,浏览器那边对应的是 TextDecoder 的流式模式。

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

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

可以用一个最小例子看出直接 toString() 和 StringDecoder 的区别:

const { StringDecoder } = require('string_decoder');

// '好' = e5 a5 bd,被切成两块
const c1 = Buffer.from([0xe4, 0xbd, 0xa0, 0xe5, 0xa5]);
const c2 = Buffer.from([0xbd]);

console.log(c1.toString() + c2.toString()); // 你��(具体几个 � 不重要,总之坏了)

const decoder = new StringDecoder('utf8');
console.log(decoder.write(c1) + decoder.write(c2) + decoder.end()); // 你好
@前端进阶之旅: 代码已经复制到剪贴板

第一块只解码出「你」,e5 a5 被留在内部;第二块到了以后补齐成 e5 a5 bd,输出「好」。最后的 end() 用来收尾,如果还有凑不齐的字节,会变成替换字符 � 返回。

同样的思路在浏览器里是 TextDecoder:

const td = new TextDecoder('utf-8');
let text = td.decode(c1, { stream: true }); // '你'
text += td.decode(c2, { stream: true });     // '好'
text += td.decode();                         // 收尾
@前端进阶之旅: 代码已经复制到剪贴板

除了 utf8,StringDecoder 也支持 utf16le(两字节对齐)和 base64(按 3 字节一组对齐)等编码,原理一样:凑不齐一组的先不输出。

面试官追问

追问 1为什么不能 chunks.map(c => c.toString()).join('')?
参考回答

每个 chunk 是按字节切的,不管字符边界。一个三字节汉字被切成 2 + 1,两边各自解码都是非法序列,会变成 �,拼起来也救不回来。要么先 Buffer.concat 再整体解码,要么用 StringDecoder 边收边解。

追问 2我直接 Buffer.concat 最后一次性解码,还需要 StringDecoder 吗?
参考回答

数据量小、能等全部收完就不需要,这样最简单。StringDecoder 的价值在于边收边处理,比如按行解析日志流、处理 SSE 推送,不能等全部到齐。

追问 3readable.setEncoding('utf8') 和自己用 StringDecoder 有区别吗?
参考回答

效果一样,setEncoding 内部就是创建了一个 StringDecoder,之后 data 事件拿到的就是字符串而不是 Buffer。注意设置之后就拿不到原始字节了,处理二进制的流别调它。

追问 4浏览器里读 fetch 的流式响应,有类似的东西吗?
参考回答

有,TextDecoder。decoder.decode(chunk, { stream: true }) 会把不完整的尾巴留着,最后再调一次不带参数的 decoder.decode() 收尾,和 write / end 是一个意思。现在 Node.js 里也能用 TextDecoder。

追问 5用了 StringDecoder 还是乱码,可能是什么原因?
参考回答

多半是编码本身选错了,比如数据是 GBK,你按 utf8 去解。StringDecoder 只负责「别把字符切断」,不负责猜编码,构造时传的编码必须和数据真实编码一致。

# 模块简介

30 秒速记

  • 用法:new StringDecoder('utf8'),然后反复 decoder.write(buf),拿到的都是完整字符
  • 内部维护一个小缓存,存着上一块末尾凑不齐的字节,最多几个字节
  • UTF-8 首字节就能看出这个字符一共几个字节,所以解码器知道还差多少
  • 典型场景:socket / HTTP 包体 / 子进程输出这种按字节切块的文本流
  • 返回空字符串 '' 不是出错,说明这块数据全是半个字符,在等下一块

StringDecoder 就像一个会「等一等」的 toString:凑不成完整字符的字节先扣下,下次补齐了再交出来。 它能做到这点,是因为 UTF-8 的首字节自带长度信息,110xxxxx 表示两字节、1110xxxx 表示三字节,看到 e5 就知道后面还要两个。所以每次 write 时它检查末尾是不是一个没写完的字符,没写完就存进内部缓存,只返回前面完整的部分。处理 socket、子进程 stdout 这类按字节切块的文本流,用它就不会在边界上出乱码。

string_decoder模块用于将Buffer转成对应的字符串。使用者通过调用stringDecoder.write(buffer),可以获得buffer对应的字符串。

它的特殊之处在于,当传入的buffer不完整(比如三个字节的字符,只传入了两个),内部会维护一个internal buffer将不完整的字节cache住,等到使用者再次调用stringDecoder.write(buffer)传入剩余的字节,来拼成完整的字符。

这样可以有效避免buffer不完整带来的错误,对于很多场景,比如网络请求中的包体解析等,非常有用。

面试官追问

追问 1decoder.write(Buffer.from([0xe5])) 返回什么?
参考回答

返回空字符串 ''。e5 是一个三字节字符的开头,凑不齐就先存着,等后面两个字节到了才输出。代码里别把空字符串当成「流结束」。

追问 2解码器怎么知道一个字符还差几个字节?
参考回答

看首字节的高位:0xxxxxxx 是单字节,110xxxxx 两字节,1110xxxx 三字节,11110xxx 四字节,后续字节都是 10xxxxxx。所以看到 e5(11100101)就知道还要再来两个。

追问 3一个 StringDecoder 实例能给多个连接共用吗?
参考回答

不行,它是有状态的,内部存着上一块的尾巴。两个连接的数据混着写进去,A 的半个字符会和 B 的字节拼在一起。一条流一个实例。

追问 4子进程 stdout 打出来的中文偶尔有乱码,你会怎么修?
参考回答

child.stdout.setEncoding('utf8'),或者自己 new StringDecoder('utf8') 在 data 回调里 write。偶发乱码基本就是字符被切在了两个 chunk 之间,块大小一变就时有时无。

追问 5按行解析日志流,用了 StringDecoder 是不是就够了?
参考回答

还不够,它只保证字符完整,不保证行完整。还要自己缓存最后一段没遇到换行的文本,或者直接用 readline.createInterface({ input }),它两件事都帮你做了。

# 入门例子

30 秒速记

  • write(buf):返回能完整解出的字符,残缺的尾巴留在内部
  • end():把内部剩下的吐出来;残缺字节会变成 �,之后这个实例回到初始状态
  • end(buf) = 先 write(buf) 再 end(),返回两者拼起来的结果
  • <Buffer e4 bd a0> 解出「你」,e4 bd a0 e5 a5 只解出「你」,e5 a5 等着
  • 流结束时一定要调一次 end(),否则最后一点内容可能被吞掉

StringDecoder 常用的就两个方法:write 负责边收边解,end 负责收尾。 write 每次只返回完整的字符,比如传入「你好」的前五个字节,它返回「你」,「好」的前两个字节先扣着。end 在流结束时调,它会把内部剩下的东西全部吐出来,带参数调用就相当于先 write 再 end。我写的时候会固定在 end 事件里调一次 decoder.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
      • 模块简介
      • 入门例子
      • 例子:分多次写入多个字节
      • 例子:decoder.end()时,字节数不完整的处理
      • 相关链接
    • 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
扫码关注作者公众号:「前端进阶之旅」 每天分享技术干货
前端进阶之旅公众号二维码

二进制解码 string_decoder|Nodejs专题系列

# 二进制解码 string_decoder

30 秒速记

  • 解决的问题:多字节字符被切在两个 chunk 之间,分别 toString() 会出乱码
  • StringDecoder 把不完整的尾巴字节先存起来,等下一块到了拼完整再输出
  • 两个 API:write(buf) 返回能完整解码的部分,end() 吐出剩余内容
  • 平时少直接用:stream.setEncoding('utf8') 内部就是它,浏览器和新代码可用 TextDecoder + { stream: true }
  • 只解决「字符被切断」,解决不了「编码选错」

string_decoder 专门解决一件事:网络或文件流把一个汉字切成了两半,直接 toString() 会乱码。 比如「好」是 e5 a5 bd 三个字节,第一个 chunk 只带了前两个,单独解码就成了 �。StringDecoder 的做法是把凑不齐的尾巴先留在内部,下次 write 时拼上再输出,所以拼出来的字符串永远是完整的。实际项目里我很少手写它,给可读流调一下 setEncoding('utf8') 就是同样的效果,浏览器那边对应的是 TextDecoder 的流式模式。

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

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

可以用一个最小例子看出直接 toString() 和 StringDecoder 的区别:

const { StringDecoder } = require('string_decoder');

// '好' = e5 a5 bd,被切成两块
const c1 = Buffer.from([0xe4, 0xbd, 0xa0, 0xe5, 0xa5]);
const c2 = Buffer.from([0xbd]);

console.log(c1.toString() + c2.toString()); // 你��(具体几个 � 不重要,总之坏了)

const decoder = new StringDecoder('utf8');
console.log(decoder.write(c1) + decoder.write(c2) + decoder.end()); // 你好
@前端进阶之旅: 代码已经复制到剪贴板

第一块只解码出「你」,e5 a5 被留在内部;第二块到了以后补齐成 e5 a5 bd,输出「好」。最后的 end() 用来收尾,如果还有凑不齐的字节,会变成替换字符 � 返回。

同样的思路在浏览器里是 TextDecoder:

const td = new TextDecoder('utf-8');
let text = td.decode(c1, { stream: true }); // '你'
text += td.decode(c2, { stream: true });     // '好'
text += td.decode();                         // 收尾
@前端进阶之旅: 代码已经复制到剪贴板

除了 utf8,StringDecoder 也支持 utf16le(两字节对齐)和 base64(按 3 字节一组对齐)等编码,原理一样:凑不齐一组的先不输出。

面试官追问

追问 1为什么不能 chunks.map(c => c.toString()).join('')?
参考回答

每个 chunk 是按字节切的,不管字符边界。一个三字节汉字被切成 2 + 1,两边各自解码都是非法序列,会变成 �,拼起来也救不回来。要么先 Buffer.concat 再整体解码,要么用 StringDecoder 边收边解。

追问 2我直接 Buffer.concat 最后一次性解码,还需要 StringDecoder 吗?
参考回答

数据量小、能等全部收完就不需要,这样最简单。StringDecoder 的价值在于边收边处理,比如按行解析日志流、处理 SSE 推送,不能等全部到齐。

追问 3readable.setEncoding('utf8') 和自己用 StringDecoder 有区别吗?
参考回答

效果一样,setEncoding 内部就是创建了一个 StringDecoder,之后 data 事件拿到的就是字符串而不是 Buffer。注意设置之后就拿不到原始字节了,处理二进制的流别调它。

追问 4浏览器里读 fetch 的流式响应,有类似的东西吗?
参考回答

有,TextDecoder。decoder.decode(chunk, { stream: true }) 会把不完整的尾巴留着,最后再调一次不带参数的 decoder.decode() 收尾,和 write / end 是一个意思。现在 Node.js 里也能用 TextDecoder。

追问 5用了 StringDecoder 还是乱码,可能是什么原因?
参考回答

多半是编码本身选错了,比如数据是 GBK,你按 utf8 去解。StringDecoder 只负责「别把字符切断」,不负责猜编码,构造时传的编码必须和数据真实编码一致。

# 模块简介

30 秒速记

  • 用法:new StringDecoder('utf8'),然后反复 decoder.write(buf),拿到的都是完整字符
  • 内部维护一个小缓存,存着上一块末尾凑不齐的字节,最多几个字节
  • UTF-8 首字节就能看出这个字符一共几个字节,所以解码器知道还差多少
  • 典型场景:socket / HTTP 包体 / 子进程输出这种按字节切块的文本流
  • 返回空字符串 '' 不是出错,说明这块数据全是半个字符,在等下一块

StringDecoder 就像一个会「等一等」的 toString:凑不成完整字符的字节先扣下,下次补齐了再交出来。 它能做到这点,是因为 UTF-8 的首字节自带长度信息,110xxxxx 表示两字节、1110xxxx 表示三字节,看到 e5 就知道后面还要两个。所以每次 write 时它检查末尾是不是一个没写完的字符,没写完就存进内部缓存,只返回前面完整的部分。处理 socket、子进程 stdout 这类按字节切块的文本流,用它就不会在边界上出乱码。

string_decoder模块用于将Buffer转成对应的字符串。使用者通过调用stringDecoder.write(buffer),可以获得buffer对应的字符串。

它的特殊之处在于,当传入的buffer不完整(比如三个字节的字符,只传入了两个),内部会维护一个internal buffer将不完整的字节cache住,等到使用者再次调用stringDecoder.write(buffer)传入剩余的字节,来拼成完整的字符。

这样可以有效避免buffer不完整带来的错误,对于很多场景,比如网络请求中的包体解析等,非常有用。

面试官追问

追问 1decoder.write(Buffer.from([0xe5])) 返回什么?
参考回答

返回空字符串 ''。e5 是一个三字节字符的开头,凑不齐就先存着,等后面两个字节到了才输出。代码里别把空字符串当成「流结束」。

追问 2解码器怎么知道一个字符还差几个字节?
参考回答

看首字节的高位:0xxxxxxx 是单字节,110xxxxx 两字节,1110xxxx 三字节,11110xxx 四字节,后续字节都是 10xxxxxx。所以看到 e5(11100101)就知道还要再来两个。

追问 3一个 StringDecoder 实例能给多个连接共用吗?
参考回答

不行,它是有状态的,内部存着上一块的尾巴。两个连接的数据混着写进去,A 的半个字符会和 B 的字节拼在一起。一条流一个实例。

追问 4子进程 stdout 打出来的中文偶尔有乱码,你会怎么修?
参考回答

child.stdout.setEncoding('utf8'),或者自己 new StringDecoder('utf8') 在 data 回调里 write。偶发乱码基本就是字符被切在了两个 chunk 之间,块大小一变就时有时无。

追问 5按行解析日志流,用了 StringDecoder 是不是就够了?
参考回答

还不够,它只保证字符完整,不保证行完整。还要自己缓存最后一段没遇到换行的文本,或者直接用 readline.createInterface({ input }),它两件事都帮你做了。

# 入门例子

30 秒速记

  • write(buf):返回能完整解出的字符,残缺的尾巴留在内部
  • end():把内部剩下的吐出来;残缺字节会变成 �,之后这个实例回到初始状态
  • end(buf) = 先 write(buf) 再 end(),返回两者拼起来的结果
  • <Buffer e4 bd a0> 解出「你」,e4 bd a0 e5 a5 只解出「你」,e5 a5 等着
  • 流结束时一定要调一次 end(),否则最后一点内容可能被吞掉

StringDecoder 常用的就两个方法:write 负责边收边解,end 负责收尾。 write 每次只返回完整的字符,比如传入「你好」的前五个字节,它返回「你」,「好」的前两个字节先扣着。end 在流结束时调,它会把内部剩下的东西全部吐出来,带参数调用就相当于先 write 再 end。我写的时候会固定在 end 事件里调一次 decoder.end(),这是最容易漏的一步,漏了不一定立刻看出来,但残缺数据就悄悄丢了。

这节分别演示了decode.write(buffer)、decode.end([buffer])两个主要API的用法。

例子一:

decoder.write(buffer)调用传入了Buffer对象<Buffer e4 bd a0>,相应的返回了对应的字符串你;

const StringDecoder = require('string_decoder').StringDecoder;
const decoder = new StringDecoder('utf8');

// Buffer.from('你') => <Buffer e4 bd a0>
const str = decoder.write(Buffer.from([0xe4, 0xbd, 0xa0]));
console.log(str);  // 你
@前端进阶之旅: 代码已经复制到剪贴板

例子二:

当decoder.end([buffer])被调用时,内部剩余的buffer会被一次性返回。如果此时带上buffer参数,那么相当于同时调用decoder.write(buffer)和decoder.end()。

const StringDecoder = require('string_decoder').StringDecoder;
const decoder = new StringDecoder('utf8');

// Buffer.from('你好') => <Buffer e4 bd a0 e5 a5 bd>
let str = decoder.write(Buffer.from([0xe4, 0xbd, 0xa0, 0xe5, 0xa5]));
console.log(str);  // 你

str = decoder.end(Buffer.from([0xbd]));
console.log(str);  // 好
@前端进阶之旅: 代码已经复制到剪贴板

← 8.1 二进制数据 buffer9.1 事件机制 events →

fe