Markdown 导出 HTML 实战:2.6MB 编辑器快照如何瘦身 71%(DOM 净化 + CSSOM 运行时裁剪)

Markdown 导出 HTML 实战:2.6MB 编辑器快照如何瘦身 71%(DOM 净化 + CSSOM 运行时裁剪)

摘要:本文记录 MarkPin 编辑器导出 HTML 的完整瘦身实战------从 2.6MB 降到 753KB(-71%)。核心思路是先把"导出=文档而非编辑器"的产品语义拍板,再做五项 DOM 净化(去光标回显/选区/活动行/双栏/行号)+ 全量 script 剥离,最后用 CSSOM 运行时按"选择器命中内容区"裁剪样式,实现零双份维护。文中还复盘了一个 Chrome 正常、ArkWeb 导出零样式的装机大坑(克隆文档 CSSOM 不可读),并沉淀出三条心得:语义先定、运行时事实优于静态清单、跨内核必须装机验证。

导出 HTML 是编辑器的高频功能,MarkPin 的实现翻过两座山:先是导出结果和渲染结果不一致 (导出文件里带着光标行的源码回显、双栏结构、选区高亮、行号栏),再是导出文件本身就不对------2.6MB 的"文档",里面 1.85MB 是编辑器自己的 JS bundle,双击打开还会执行。这篇讲完整改造:五项 DOM 净化 + 全量 script 剥离 + CSSOM 运行时裁剪,最终 2.6MB→753KB(-71%),以及一个 Chrome 上完全正常、装机后导出零样式的 ArkWeb 大坑。

一、问题定义:导出的到底应该是"文档"还是"编辑器"

MarkPin 的导出机制是渲染快照 :getRenderedHTML 对整个 document 做 cloneNode(true),内容级格式(表格/代码块/KaTeX 公式/引用/图片)随快照天然一致------这个架构决定了"所见即所得的导出"基本免费。但快照也把整个编辑器搬了进去:

  1. 编辑态残留五项:光标行的源码回显(mk-marker-reveal,导出后那一行变成源码形态)、双栏结构(.mode-pane-right/.mode-divider,甚至内容×2)、选区高亮层、活动行高亮、行号 gutter;
  2. 体积与行为问题:内联构建流程把 JS bundle 和字体全部塞进单文件------1.85MB 脚本 + 772KB 样式;打开导出文件时 bundle 会执行,二次初始化不可控。

用户拍板方案 A:导出只保留预览渲染内容,编辑器的一切都不要。

二、分析方法:先拆体积构成,再拆一致性差异

体积问题先量化:JS 移除后,剩余大头是 KaTeX 字体的 base64(39 个字体,约 748KB)------这决定了"再怎么裁 CSS,体积下限就在字体",目标定为 <800KB 是有依据的。一致性差异逐项审计:五项编辑态残留全部来自快照包含编辑器 DOM 结构这一事实,逐项列清单(类名级)再逐项设计剥离动作,比"整体重写导出"稳得多------后者意味着双份维护。

三、解决代码(一):快照净化 + script 全剥离

ts 复制代码
// editor-build/src/main.ts getRenderedHTML(真实代码,节选)
const clonedDoc: Document = document.cloneNode(true) as Document;
// 1) 活动行回显:去 reveal 类,恢复渲染形态(与其余行统一)
clonedDoc.querySelectorAll('.mk-marker-reveal').forEach((el: Element) => {
  el.classList.remove('mk-marker-reveal');
});
// 2) 选区层 / 2.1) 光标元素(内容区内会命中 CSS 裁剪保留样式,静态文档画出竖线)
clonedDoc.querySelectorAll('.cm-selectionLayer, .cm-cursor').forEach((el: Element) => el.remove());
// 3) 活动行高亮类
clonedDoc.querySelectorAll('.cm-activeLine').forEach((el: Element) => el.classList.remove('cm-activeLine'));
// 4) 双栏结构残留:移除镜像栏与分隔条;解除 is-dual(左栏恢复全宽)
clonedDoc.querySelectorAll('.mode-pane-right, .mode-divider').forEach((el: Element) => el.remove());
clonedDoc.querySelectorAll('.mode-dual').forEach((el: Element) => el.classList.remove('is-dual'));
// 5) 行号 gutter(编辑器 chrome,导出文档无需行号栏)
clonedDoc.querySelectorAll('.cm-gutters').forEach((el: Element) => el.remove());
// 6) 移除全部 <script>(纯内容导出):快照里的编辑器 bundle 打开文件时会执行
//    (二次初始化/报错不可控),导出文档必须零可执行代码
clonedDoc.querySelectorAll('script').forEach((el: Element) => el.remove());

配套还有导出流程切渲染形态 :导出前统一 setViewMode('preview') + 等待渲染稳定,finally 恢复原模式(成功/取消/失败所有出口)------解决源码模式导出变纯源码全文、双栏模式导出变双栏的"模式级差异"。

四、解决代码(二):CSS 运行时裁剪------按"命中内容区"保留

删了 script、删了编辑器 DOM,但完整 CSS(772KB)还在。手工维护一份"导出专用 CSS 清单"必然腐烂,我们的方案是运行时按选择器命中过滤:

ts 复制代码
// editor-build/src/main.ts getRenderedHTML(真实代码,节选)
// 【装机实证修正】遍历基于【原始 document.styleSheets】------ArkWeb 上克隆文档
//(cloneNode 产物)的 styleSheets 为空/不可读,首版遍历 clonedDoc.styleSheets
// 导致裁剪结果为空、导出 HTML 无任何样式(装机"导出没有渲染效果"根因)。
// 原文档 CSSOM 必然可用(编辑器自身渲染正常)。选择器命中判断用克隆内容区。
const contentEl: Element | null = clonedDoc.querySelector('.cm-content');
const collect = (ruleList: CSSRuleList): string[] => {
  const out: string[] = [];
  for (let ri = 0; ri < ruleList.length; ri++) {
    const rule: CSSRule = ruleList[ri];
    if (rule.type === CSSRule.STYLE_RULE) {
      const sr: CSSStyleRule = rule as CSSStyleRule;
      let keep = false;
      if (sr.selectorText.indexOf(':root') >= 0 || sr.selectorText.indexOf('body') >= 0
          || sr.selectorText.indexOf('*') >= 0) {
        keep = true;                       // 变量定义/全局排版/通配基础:保留
      } else {
        // 剥离伪元素/伪类(::before、:hover 等)后按选择器命中内容区判断
        const plain: string = sr.selectorText.replace(/::?[\w-]+(?:\([^)]*\))?/g, '').trim();
        if (plain.length > 0) {
          keep = contentEl.matches(plain) || contentEl.querySelector(plain) !== null;
        }
      }
      if (keep) { out.push(sr.cssText); }
    } else if (rule.type === CSSRule.MEDIA_RULE) {
      // @media 递归裁剪(print 等媒体块内规则同样按内容区过滤)
      const inner: string[] = collect((rule as CSSMediaRule).cssRules);
      if (inner.length > 0) {
        out.push('@media ' + (rule as CSSMediaRule).conditionText + ' {' + inner.join(' ') + '}');
      }
    }
    // @font-face/@keyframes 整类保留(动画与字体是渲染刚需)
  }
  return out;
};

效果:mk-* 渲染类、排版、KaTeX 样式因为命中 .cm-content 自动保留;gutter/tooltip/cursor/mode 布局这些编辑器 chrome 类不命中,自动剔除。零双份维护------源码样式演化时,导出子集自动同步,因为裁剪依据是"命中"这个事实,不是手工清单。防御性兜底:裁剪结果为空时降级保留原样式全文(渲染优先,宁可大不可裸)。

五、装机翻车:Chrome 正常,ArkWeb 导出零样式

首版上线装机就翻车:Chrome 里导出渲染完美,模拟器里导出 HTML 打开完全无样式。环境差异定位到样式表来源------首版遍历 clonedDoc.styleSheets(克隆文档的 CSSOM),ArkWeb 上 cloneNode 产物的 styleSheets 为空/不可读 ,裁剪结果自然为空。修复就是上面代码注释里那句:改遍历原始 document.styleSheets(选择器命中判断仍用克隆内容区,结构一致)+ 空结果降级兜底。这是"Chrome 能跑 ≠ 装机能跑"的典型案例,我们后续把这类环境差异沉淀成了固定的取证方法论(后篇展开)。同批还补了 PDF 链路:导出 PDF 前同样切 preview 模式,print 样式补行号隐藏。

六、验证与效果

Chrome:导出 753KB(2.6MB→-71%,余量大头为 KaTeX 字体 base64),独立打开 DOM 断言 14 行内容全渲染(标题/任务框/引用/代码卡片/表格/KaTeX/加粗),无光标/选区/行号/双栏结构;像素验证(复选框 16×16、引用竖线、代码表格背景)。装机:模拟器三项验证(导出 HTML 渲染/PDF 纯净/导出后视图恢复)全部通过,用户确认闭环。

七、能力边界表

事项 AI 表现 我的结论
导出=渲染快照架构 内容级一致性天然免费 快照机制的代价是"什么都带走",净化要做减法而非重写
CSSOM 运行时裁剪 命中驱动方案一次成型,零双份维护 用"选择器命中"这种事实依据代替手工清单,是防腐烂的正解
ArkWeb 克隆文档 CSSOM 不可读 首版未预判,装机暴露 环境差异无法穷举预判,只能装机验证+快速修正
script 全剥离 一刀切干净,零可执行代码 导出的语义是"文档",携带执行逻辑是语义错误不只是体积问题
降级兜底(空结果保留原文) 渲染优先,宁可大不可裸 裁剪类功能的失败模式要设计成"退回原点"而非"输出空"

八、三条心得

  1. 导出的产品语义先定,技术方案跟着清晰:"导出=文档而非编辑器"这一句拍板,script 剥离、chrome 剔除、模式切换全部顺理成章;
  2. 运行时事实优于静态清单:CSS 裁剪靠选择器命中而不是维护导出样式表------清单必腐,事实不会;
  3. 跨内核功能必须装机验证:cloneNode 的 CSSOM 行为在 Chrome 与 ArkWeb 上不同,这类差异没有文档,只有装机能告诉你。

如果你也在试 AI 开发鸿蒙,或者想看后续,关注专栏,所有踩坑都会持续更新。

相关推荐
m0_738185821 小时前
Flutter 鸿蒙化实战:the_apple_sign_in 适配 OpenHarmony,Apple 登录
flutter·华为·harmonyos·鸿蒙
神奇的代码在哪里1 小时前
原生鸿蒙 GBC 模拟器开发实录:用 Anime4K 把 160×144 画面实时放大到 1080P
harmonyos·鸿蒙·超分辨率·gbc模拟器·anime4k
m0_738185822 小时前
Flutter 鸿蒙化实战:umeng_analytics_plugin 适配 OpenHarmony,友盟统计
flutter·华为·harmonyos·鸿蒙
灯澜忆梦2 小时前
【Linux】#7 | vi/vim 编辑器
linux·编辑器·vim
todoitbo2 小时前
把 XAMPP 搬到鸿蒙 PC:在鸿蒙 PC 上跑 Apache httpd 2.4.69实战项目
华为·apache·harmonyos·鸿蒙·开源鸿蒙·鸿蒙pc
JoyCong19982 小时前
即插即用,组合键可用:ToDesk 4820版本强化鸿蒙远控键鼠能力
华为·智能手机·harmonyos·远程工作·ipad
程序猿追2 小时前
漏字、换词序、空查询:用 FuzzyKot 做一次鸿蒙搜索交互排查
华为·交互·harmonyos
zd8451015002 小时前
基于 HTML 与 Modbus TCP 构建设备仪器上位机:可行性分析与代码结构设计
前端·tcp/ip·html
懿路向前2 小时前
HarmonyOS 粒子特效实战:Particle 挂载 ≠ 显示:emitRate/count 真实语义与同帧黑缝(HarmonyOS ArkUI)
华为·harmonyos