网关层必须介入HTML流处理,因其是第一道语义守门人;需用WebWritableStream边收边解析重写,注入lang、包裹<main>、补全无障碍属性,并校验每块chunk的语义自洽性。

HTML超大文档不能直接走网关透传,必须在网关层完成流式解析 + 语义结构校准,否则会卡死连接、拖垮后端、破坏无障碍与SEO。
为什么网关层必须介入HTML流处理
网关不是管道,而是第一道语义守门人。当后端返回一个 8MB 的 HTML 文档(比如报表页或文档导出页),若不做流式截断与结构预处理,会触发三类连锁故障:
- HTTP/1.1 下 Keep-Alive 连接被长响应阻塞,后续请求排队等待;
- 浏览器收到不完整 HTML(如
<main>开头但没闭合)时,会持续等待、延迟渲染,甚至触发错误恢复逻辑; - 缺失
lang、<main>、<nav>等语义标签的原始 HTML,经 CDN 或代理压缩后更难修复,导致 Lighthouse 无障碍评分归零。
所以不是“能不能”,而是“必须在网关层做流式注入点 + 结构钩子”。
用 htmlparser2 的 WebWritableStream 实现边收边转
别等整个响应体收完再处理——那已经晚了。要用 WebWritableStream 接入 Fetch API 的 response.body 流,实时解析并重写关键结构:
立即学习“前端免费学习笔记(深入)”;
- 监听
<html>开始后,立即注入<html lang="zh-CN">(若未声明); - 遇到第一个
<body>后,自动包裹<main>,并插入data-role="main-content"钩子供前端 JS 初始化; - 对连续出现的
<div class="header">,替换为<header data-i18n="header">,同时补上lang属性; - 所有
<img>标签未带alt时,强制设为alt=""(装饰图)或根据title回退生成。
示例片段(Node.js 网关中间件中):
const { WebWritableStream } = require('htmlparser2/lib/WebWritableStream');
const stream = new WebWritableStream({
onopentag: ({ name, attribs }) => {
if (name === 'html' && !attribs.lang) {
// 注入 lang
write('<html lang="zh-CN">');
return false; // 阻止原标签输出
}
if (name === 'body') {
write('<main data-role="main-content">');
}
},
onclosetag: (name) => {
if (name === 'body') write('</main>');
}
});
结构重构必须避开的三个硬约束
网关层的 HTML 改写不是自由编辑,它受限于浏览器解析器的容错机制和辅助技术的行为逻辑:
-
<main>不能嵌套在<article>或<aside>内部,否则 NVDA 会跳过它;网关改写时需维护 DOM 层级上下文,不能只靠正则替换; -
lang属性必须显式出现在每个语义容器上,不能只靠<html lang>继承——网关需对<section>、<p>、<h2>等逐个检查并补全; - 所有
data-i18n标记必须覆盖placeholder、title、aria-label,不能只扫textContent;网关注入时若发现这些属性存在但无对应标记,应自动添加data-i18n-${attr}。
流式注入后如何验证结构有效性
改完不验证 = 白改。网关输出前必须做轻量 DOM 校验(不用加载完整 HTML):
- 用
new DOMParser().parseFromString(chunk, 'text/html')抽样解析每 512KB 片段,检查document.querySelector('main')是否唯一且非空; - 遍历所有
querySelectorAll('[lang]'),确认值合法(如zh-HK、en-US),且未出现在<script>或<style>内; - 对含
data-i18n的元素,检查其textContent长度是否 > 0,避免空文案漏翻译。
真正难的不是解析,而是让每一次流式 chunk 都保持语义自洽——<section> 开了就得在本 chunk 或下一个 chunk 里闭合,否则浏览器会自己补全,导致结构错乱。这要求网关必须缓存未闭合标签栈,不能当成纯文本管道用。



















