transformstream 本身不提供过滤语义,但可通过选择性调用 controller.enqueue() 实现流式过滤,关键在于控制哪些数据块进入下游而非丢弃数据;需预先明确定义过滤目标(如敏感信息、关键词、空行等),并妥善处理跨块边界问题,例如维护缓冲区以应对手机号被切分的情况;同时必须配合 textdecoderstream 将 uint8array 流解码为字符串流,不可使用同步解码。

TransformStream 本身不提供“过滤”语义,但它能通过选择性入队(controller.enqueue())或跳过某些块,实现等效的流式过滤效果。关键在于:不是丢弃数据,而是控制哪些数据块进入下游。
明确过滤目标与数据边界
流式过滤必须提前定义清楚“什么算要过滤掉”。常见类型包括:
- 敏感信息(如手机号、身份证号)——但要注意跨块截断问题
- 特定关键词或日志级别(如
"DEBUG"行) - 空行、注释行、无效 JSON 块
- 不符合结构的二进制片段(如非 UTF-8 字节)
注意:不能依赖完整结构(如闭合 HTML 标签、合法 JSON 对象),因为 chunk 是任意大小的原始块,可能在任意位置被切开。
基础文本级过滤写法(安全场景)
适用于纯文本流、已解码、无结构依赖的过滤,例如跳过含特定前缀的日志行:
const filterLogStream = new TransformStream({
transform(chunk, controller) {
// chunk 是 string(需经 TextDecoderStream 解码后)
const lines = chunk.split(/\r\n|\n/);
for (const line of lines) {
if (!line.trim().startsWith('[DEBUG]')) {
controller.enqueue(line + '\n');
}
}
}
});
⚠️ 注意:该写法假设换行符不会跨块。若原始流可能被任意切分(如压缩传输),需自行缓存未结束的行首尾,做跨块拼接。
处理跨块边界的真实过滤逻辑
生产环境过滤手机号、邮箱等,必须应对 13912 和 345678 被拆到两个 chunk 的情况。推荐做法是维护一个缓冲区:
- 在
transform()中将当前 chunk 追加到内部 buffer - 从 buffer 中提取完整可匹配的文本段(如用正则找最长匹配)
- 只 enqueue 未被过滤的部分,把剩余未闭合内容留到下次
- 在
flush()中处理 buffer 中残留内容
示例节选(简化版手机号过滤):
let buffer = '';
const phoneFilter = new TransformStream({
transform(chunk, controller) {
buffer += chunk;
// 查找完整 11 位手机号,但不破坏跨块连续性
const re = /1[3-9]\d{9}/g;
let match;
let lastIndex = 0;
while ((match = re.exec(buffer)) !== null) {
// 入队上一段(不含匹配项)
if (match.index > lastIndex) {
controller.enqueue(buffer.slice(lastIndex, match.index));
}
// 跳过匹配项(即过滤掉),lastIndex 移到匹配末尾
lastIndex = match.index + match[0].length;
}
// 缓存未匹配完的尾部(可能含半截号码)
buffer = buffer.slice(lastIndex);
},
flush(controller) {
if (buffer) controller.enqueue(buffer);
}
});
必须搭配的前置环节
TransformStream 不接收字符串,原始 fetch().body 是 Uint8Array 流。漏掉这步会导致正则失效、chunk.replace 无反应:
- 必须先用
new TextDecoderStream()解码为字符串流 - 不能用
new TextDecoder().decode(chunk)替代——那是同步阻塞操作,会卡住流 - 典型链路:
fetch(url).body.pipeThrough(new TextDecoderStream()).pipeThrough(phoneFilter)
不支持直接过滤 DOM 或 innerHTML——那些已是完整字符串,不属于流式上下文。











