ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

代码高亮方案怎么选?从Prism.js到文档站工程化实践

代码高亮方案怎么选?从Prism.js到文档站工程化实践 前段时间整理内部文档站上千个代码块没有高亮。SQL、JSON、Shell 混在一起页面看起来像一大片纯文本读者要在密集字符里自己辨认关键字。最难受的是用户复制我们给出的命令时经常会多复制一个行号或者少复制一个引号然后跑出一堆莫名其妙的报错。我一开始觉得这个问题的解法很简单找一个代码高亮插件挂上去就行。但真正动手后发现事情没有想象中顺利。不同高亮库之间体积、语言覆盖、主题适配、动态加载、转义规则、插件机制都有差异。单次接入可以很快但想要长期稳定地用在文档站、博客、组件演示页里需要把这些细节全部理清。这篇文章会从一个常见场景出发尽量把代码高亮这一个小工具讲透为什么需要它、选型时要关注什么、如何最小接入、如何工程化、哪些问题最容易翻车以及怎么判断自己是不是已经做得过度了。1. 高亮不是美化是降低阅读和复制的成本很多人把代码高亮理解为“好看”。这个判断没有错但它低估了高亮在技术内容里扮演的角色。我自己在排查问题时经常经历这样的过程页面上一段日志没有高亮里面有一串 URL、几个 IP、一段堆栈信息。我需要盯着毫秒级的时间戳和Exception之间的空格手动定位出错位置。如果高亮正确把错误类型、方法名、参数标识出来视觉焦点可以很快落到“异常种类”和“行号”上效率完全不同。对技术文档、教程博客、API 示例而言代码高亮承担的核心职责是这三个把词法单元分级让读者能按颜色快速扫读。让字符串、注释、关键字和变量之间的边界更清晰减少误解。让代码块在长文中成为稳定的视觉锚点。但代码高亮同时还有一个很容易被忽略的价值它会影响“复制”。很多高亮方案会在屏幕上渲染出带修饰的span标签如果复制逻辑处理不好用户复制出来的内容会带上多余的颜色标记或者行号。于是产生了更底层的需求高亮必须和“干净的复制”协同工作不能只负责视觉层。所以我在重构文档站时的第一个判断是高亮不是可选的装饰而是阅读体验的基本构成。如果说文章结构负责提供“信息路径”那么代码高亮负责提供“视觉语法”。两者缺一个读者都要在前额叶里多完成一层解码工作。1.1 从一次文档重构说起触发这次重构的是一个特别不起眼的场景。团队里有位新同事第一次部署服务按照我们文档里的命令执行安装。他复制到终端后提示command not found反复比对了三次才发现复制的内容里夹杂了一个看不见的隐藏字符来自行号和代码之间的空白区域。这个问题的根源在于代码块本身没有处理好。不是高亮库能解决的问题而是展示层的复制策略和高亮方案需要一起设计。但我在调研时意识到如果当时能早一点选一个成熟的高亮工具也许就不会拖到需要整体重构。于是我把当时的阅读场景做了拆解读者会在页面上停留多久他需要从代码块里找出哪几个关键参数他是否会复制命令去执行文档是否要支持暗色主题页面是静态生成还是服务端渲染代码块的体积大致多少是短片段还是长文件这些问题看起来和选型无关但它们决定了方案的体积要求、渲染时机、主题能力和插件复杂度。对于轻量级文档站我更倾向于选“足够用、少依赖、易定制”的方案而不是功能越多越好。1.2 高亮工具到底在解决什么高亮工具本质上是一个“词法分析器 渲染器”。它把一段源代码按语言规则切成不同类型的 token比如关键字、字符串、函数名、注释、数字、运算符然后通过 CSS 给每类 token 分配颜色或字体样式。但一个优秀的开源高亮库会在这套机制之上额外解决三类问题语言定义是否齐全常见语言覆盖率如何遇到冷门语言或自定义 DSL 时能不能扩展。渲染性能是否可控同时出现在页面上的代码块多时浏览器会不会卡顿。产物体积是否透明只高亮一两种语言时需不需要把上千种语言的解析器全打包进去。在真实项目中这三个问题往往比“配色好不好看”更致命。2. 为什么我会把目光从重型方案移到 Prism.js在最初调研时我先试了试常见的重型方案。它们能开箱解决“多数语言高亮”这件事配置文件也相对简单。但当我把页面打包产物拉出来看体积时还是倒吸一口冷气。为了两个代码示例引入了大几十 KB 甚至上百 KB 的运行时而且很难按语言做裁剪。后来在整理开源知识库时注意到 Prism.js。它最显著的特点是模块化和插件化。核心包体积小语言定义可以通过手动挑选的方式按需加载。它不追求一个巨大仓库搞定所有事情而是把语法定义、主题、插件拆成可以自由组装的零件。这种做法非常契合文档站、博客这类页面往往只会用到少量语言却想要丰富的行号、复制按钮、自动保持滚动位置等特性。还有一点是渲染时机的灵活性。Prism 可以在页面加载后对code元素做高亮也可以在 DOM 动态变化后主动调用highlightElement或highlightAllUnder。这个能力对异步渲染很友好比如搜索面板里出现在弹窗中的代码就不需要整页重刷。当然这不是说 Prism 在所有场景都优于别的工具。如果是大型在线编辑器需要极细粒度、持续变化的词法分析那应该用 CodeMirror 或 Monaco Editor。如果内容是服务端产出的静态 HTML需要一次性输出高亮结果Shiki 的生成式方案也很合适。我选择 Prism 是把它定位在“轻量级内容站、文档站、组件文档”这个区间里。2.1 Prism.js 设计里值得学习的三个选择第一个选择是“语言需要显式声明”。Prism 不会试图从代码内容里猜语言而是在code classlanguage-sql上明确标注。这样带来了两个好处一是省去自动嗅探的成本二是让使用者清楚知道“只要类名写错了这段代码就不会高亮”。第二个选择是“插件要自己拼”。比如行号、显示语言、高亮指定行、复制按钮、工具栏这些都是独立插件。需要哪个就引入哪个。这个约束会增加一点配置成本但换来的是按需加载的灵活性和更清晰的依赖关系。第三个选择是“主题用 CSS 变量驱动”。官方提供多套主题也可以自己写 CSS 覆盖token系列类。这让我能够把高亮配色嵌入到文档站本身的暗色/亮色主题切换里而不是在主题之间做生硬跳转。2.2 和常见备选方案的初步对比那时候我顺手做了个对比表用于判断哪种方案适合这篇文档站的场景维度Prism.jshighlight.jsShiki运行方式浏览器 / Node浏览器 / NodeNode 生成静态 HTML按需加载语言可独立选择需要手动注册或全量导入基于 TextMate 语法按语言加载体积控制细颗粒度中等较大但输出无运行时动态内容高亮支持手动 API支持主要面向构建期主题定制CSS 变量简单支持 CSS 覆盖依赖 VS Code 主题转换常见问题动态内容要手动初始化类名冲突构建集成成本较高表格并不是要证明哪一种绝对更好而是提醒自己选型必须落到“我的页面从哪里生成、有没有动态插入、最终产物在哪里分发”这些实际条件上。对我当时的场景来说文档站是静态生成但页面里有很多交互组件会动态渲染代码示例。Reveal 层是浏览器端Prism 的手动初始化 API 非常适合这种模型。如果换成纯构建期生成高亮动态示例就需要额外处理。这也是我没有继续使用逐字高亮方案的原因之一。3. 最小接入先把一条代码点亮再想扩大很多人接 Prism 时会直接走“安装 npm 包、进入打包器、写全局引入”这条路。但对一个简单的静态页面或博客来说最快验证的方式其实是先本地下载一小套文件观察高亮效果是否满足需求再迁移到工程里。我一般的验证顺序是这样的下载prism.js核心文件和一个主题 CSS。在 HTML 里引入。写一个包含 SQL、JSON、JavaScript 三种代码块的最小页面。给code添加正确的language-*类并放入pre。在浏览器里检查组件的控制台报错、高亮类和最终渲染效果。3.1 一个基础但不缺细节的示例用原生 HTML 搭配 CDN 文件演示时结构大致如下!DOCTYPE html html langzh-CN head meta charsetUTF-8 titlePrism 最小示例/title link relstylesheet href./prism.css / /head body precode classlanguage-sql SELECT user_id, COUNT(*) AS login_count FROM login_log WHERE created_at DATE_SUB(NOW(), INTERVAL 7 DAY) GROUP BY user_id ORDER BY login_count DESC; /code/pre script src./prism.js/script /body /html这里最关键的是code元素必须放在pre里且class要写language-sql不能只写sql。prism.js必须在要高亮的内容渲染完成后加载或者放在页面底部。主题 CSS 要放在页面头部和普通样式一起加载避免高亮后文本闪烁。如果是在 Vue 或 React 项目里就不建议在 HTML 里写死代码块。更常见的做法是提供一个CodeBlock组件组件内部接收language和code然后在挂载后调用 Prism API。import { useEffect, useRef } from react; import Prism from prismjs; import prismjs/themes/prism-tomorrow.css; export default function CodeBlock({ language, code }) { const ref useRef(); useEffect(() { if (ref.current) { Prism.highlightElement(ref.current); } }, [code, language]); return ( pre code ref{ref} className{language-${language}} {code} /code /pre ); }这个组件是“最小可运行”状态。它不关心代码来源只负责在实例挂载时高亮。如果code是异步请求来的只要code变化后重新执行useEffect高亮也会更新。3.2 如何决定引入哪些语言定义Prism 官网提供按语言下载也支持在核心包基础上单独追加语言定义。如果你的项目用的是打包器常见写法是这样import Prism from prismjs; import prismjs/components/prism-sql; import prismjs/components/prism-json; import prismjs/components/prism-bash; import prismjs/components/prism-yaml;需要注意Prism 的组件文件有一些内部依赖关系。比如prism-markup通常会被许多语言引用所以不要在页面里随意删减同名的依赖文件。更稳妥的做法是先按官网提供的方式引入一组语言再通过打包分析确认体积组成。如果是从浏览器端用全局脚本引入没有打包器那要尽量只加载自己真正需要的语言文件。全量引入所有语言会带来体积负担也会让初始执行时间变长。4. 真正进入工程化控制体积、统一主题、处理动态内容当代码块的数量和页面形态变复杂单靠“写一个precode”是不够的。你需要建立一个统一的渲染约束。工程化不代表一定要引入复杂构建链而是让工具的接入方式在团队内稳定可复制。我实际推进时会按三个层次来做。4.1 第一层用函数封装渲染逻辑如果页面里存在多处需要手动调用高亮的地方最好把“传入代码和语言输出高亮后的 DOM”封装成一个函数。function highlightCode(code, language) { // 如果语言不存在Prism 会按纯文本处理 const grammar Prism.languages[language]; if (!grammar) { return escapeHtml(code); } return Prism.highlight(code, grammar, language); }这个封装的额外价值是提供一个兜底逻辑。当某种语言没有被加载时代码依然能被安全转义而不至于被当作 HTML 渲染导致标签丢失或脚本注入。4.2 第二层用体积约束替代“能跑就行”在静态站里语言种类往往不超过十种JavaScript、TypeScript、HTML、CSS、SQL、JSON、Bash、YAML、Python 等。完全可以按需引入而不是把全部语言定义塞进来。我建议每个项目都在代码里维护一个明确语言列表const supportedLanguages [ javascript, typescript, sql, json, bash, yaml, python ];在编写动态内容时如果遇到列表之外的语言就不要盲目加载。要么放弃高亮要么提示内容作者补充语言定义。这种“显式白名单”思维能避免团队中有人不小心把一个巨大的语言包引入页面拖慢首屏。4.3 第三层让高亮集成到内容生产流程在高亮工具可以被反复使用之后要解决的问题就变成了“内容作者如何少犯错”。我到后期会给文档站的代码块开发一个简短的标签约定。比如sql SELECT user_id FROM login_log LIMIT 10;内容作者用 Markdown 写代码交给构建流程转换。这样他们不需要理解 Prism 类名只需要记得语言标识。而构建层需要保证把 language-sql 正确输出到最终的 HTML 标签上。 如果内容动态渲染在浏览器端那么组件层需要根据 Markdown 解析结果生成 code 的 className。如果内容是静态站点生成的也可以选择在构建期先调一次 Prism生成带高亮标记的 HTML页面加载时就不需要再执行高亮逻辑。 这两条路径都有适用场景。我的建议是优先考虑静态生成方案因为在构建期完成高亮可以减少浏览器端 JS也更容易做缓存。但如果页面里的大量代码块要在运行时按用户输入动态拼接那就用浏览器端 API。 ## 5. 那些看起来不起眼但一定会踩的坑 接入 Prism 之后我踩过不少坑也在社区文档里看到过很多同类问题。如果按出现频率排序最普遍的并不是“高亮没生效”而是“生效了但行为变得很奇怪”。 ### 5.1 动态插入内容后没有重新高亮 很多单页应用会把代码块通过 innerHTML、fetch、路由切换等方式插到页面里。如果初始页面加载时已经执行了 Prism.highlightAll()动态插入的新代码自然不会被处理。 这类问题不会直接抛错只会表现为“后来出现的代码块没有颜色”。排查时要先确认这些代码块是否在 highlightAll() 之后进入 DOM。如果是需要在新节点插入后调用一次 Prism.highlightElement(node)。 一个常见做法是在 MutationObserver 里监听需要高亮的容器变化。但不要为了省事在每次 DOM 变化时都调用 highlightAll因为会重复高亮已经处理过的节点带来不必要开销。更好的方案是在节点插入的完成回调里精确初始化。 ### 5.2 代码内容里的特殊字符被意外转义 如果代码块里包含 script、style 或者 div 这类标签直接在 HTML 里插进去会被浏览器解析为真实标签。最常见的行为是标签被吃掉代码显示不完整甚至影响页面布局。 Prism 本身主要负责把 token 包进带类名的 span 里。它不会替你完成“用户输入是否安全”这件事。所以在把原始代码字符串交给 Prism 之前要先对 , , 等字符做 HTML 转义。 在 React/Vue 这类框架中把代码作为文本内容而不是 HTML 插进去通常能规避大部分风险。我建议明确区分前端组件内部的高亮处理和用户自定义 HTML 的渲染边界。 ### 5.3 行号插件与横向滚动冲突 如果你启用了行号插件 line-numbers默认方式会给 pre 添加一个 line-numbers-rows 之类的结构。当代码很长并需要横向滚动时行号区域如果被固定在代码左侧很容易出现行号与代码行不对齐的情况。 解决方法通常有三种 1. 给 pre 设置足够宽度的内边距。 2. 让行号区域不随代码横向滚动。 3. 如果代码需要横向滚动可以关闭行号插件或者把行号放到代码块主体外部。 从实际体验看长代码更适合做纵向滚动加“自动换行”模式而不是强行让读者横向拖动。Prism 的兼容性再强也不能替你决定代码块的阅读策略。 ### 5.4 无法识别的语言会被静默忽略 当我第一次在文档里加一种自定义 DSL 时写完类名 language-mydsl 后页面长时间不生效最后才发现是因为该语言没有在 Prism 中注册。Prism 不会因为语言不存在就抛错它会很安静地保留原文本。 所以对“文档里可以高亮哪些语言”这件事一定要有一套可见的规则。比如在后台维护一份“文档代码语言清单”新增语言前必须先验证能否正确加载。否则就会有人在文档里写了一个漂亮的 language-rust但因为缺少语言定义而看不出任何效果。 ### 5.5 主题覆盖和字体大小不一致 高亮颜色会受普通过样式继承影响。如果文档页面的 pre 或 code 上设置了全局字体、字号或背景色Prism 主题里的 background 和 color 可能被覆盖得七七八八。 我遇到的情况是网站统一样式里给所有 code 设置了深灰色背景和内联样式导致 Prism 的彩色 token 全部挤在一个不合适的背景上可读性反而下降。要让高亮生效应该给代码块建立一套独立样式作用域而不是依赖全局样式自然继承。 ## 6. 给代码块做一次体验复盘高亮之外还剩什么 当高亮终于稳定生效后我会问自己一个问题读者在这段代码上能否顺利完成“阅读、复制、执行、粘贴结果”这个闭环高亮只是其中一个环节。 ### 6.1 先检查复制是否干净 最影响实际体验的是复制。无论你的高亮方案多好看如果用户复制出来的代码多了一个行号、少了一个空格效果会大打折扣。 检查方法很简单在浏览器里选中代码块的一部分或全部复制到 vim 或纯文本编辑器里看内容是否与原始代码一致。如果安装了“复制按钮”插件还要验证按钮复制的内容是否来自原始代码字符串而不是来自 DOM 中经过渲染的富文本。 ### 6.2 再检查空行和缩进是否保留 有的高亮处理会把代码块首尾的空格去掉或者把前置缩进折叠起来。在展示 YAML 和 Python 这类对缩进敏感的语言时这是致命的。 这种问题不是 Prism 造成而是页面布局把空白折叠了。要确保 pre 标签的 white-space 是 pre 或 pre-wrap并且没有全局 CSS 把它覆盖成 normal。 ### 6.3 最后检查暗色主题是否协调 很多文档站在亮色模式下一切正常一旦切换到暗色模式代码块就变成一块刺眼的白色或深蓝色。建议为代码高亮主题绑定站点整体的主题变量。 Prism 的 CSS token 类通常可以映射成自定义变量这样切换主题时只需替换变量值。不要在同一页面混用多套不同明度的代码主题否则视觉上会显得很凌乱。 ### 6.4 警惕“高亮仪式”过度设计 代码高亮一旦跑通“添加更多插件”的诱惑会变大。我在很多博客里看到工具栏、行号、复制按钮、代码标题、语言标签、折叠展开全都堆在同一个代码块上。这种效果乍一看功能丰富但阅读长文时每个代码块周围的信息噪声反而增加了。 我自己的边界是 - 行号适合需要讲解“上方第几行”的长代码短代码无须显示。 - 语言标签对读者有帮助但如果代码块本身就是按语言分区的可能不需要每个块都重复写。 - 复制按钮建议只出现在可执行的命令或需要复制的长段代码上。 - 标题栏如果一段代码带上下文标题有用如果只是普通片段会显得臃肿。 这些判断没有绝对标准但建议你从“读者进入代码块后的第一眼舒适度”出发而不是从“我把能力堆齐了”出发。 ## 7. 真正需要考虑的长期维护策略 一个看似简单的高亮方案在长期维护中也会有自己的生命周期。它的核心风险不在软件功能而在依赖版本、文档内容和业务形式的漂移。 ### 7.1 版本锁定与安全更新 如果直接使用 CDN 上的最新版文件一旦上游改动语言规则或插件行为也许你第二天打开页面发现行为不一样了。建议把 Prism 文件锁定到具体版本号或使用 npm 等包管理器固定依赖版本。安全更新和功能更新要分开处理。 ### 7.2 语言列表的维护责任 随着项目增多团队成员可能会往文档中加入 Go、Rust、GraphQL、Dockerfile 等语言。如果不及时补全语言定义就会出现有些高亮成功、有些高亮失败的情况。 可以写一个自动检查脚本扫描 Markdown 或组件中的代码语言标签对比 Prism 已加载语言列表输出缺失清单。这样在内容提交时就能提前暴露问题而不是等读者反馈颜色不对。 ### 7.3 把高亮从“一次性配置”变成“可复用规范” 我最终把代码高亮做成一个内部组件库的 CodeBlock 组件。所有文档站、组件示例、教学文章都统一使用它。组件内部负责语言白名单、动态高亮、复制按钮、主题适配、错误兜底。对外暴露的只有一个简单的 language 和 code 属性。 这个过程让我意识到很多工具类功能最大的投入不是在“第一次接入成功”而是在“把它封装成稳定接口”。一旦接口稳定团队写文档的人不需要了解 Prism 细节维护的人也不需要担心某个页面私自使用了不同写法。 ## 8. 从一个工具学到的一种流程思维方式 回顾这次重构我收获的不只是代码高亮本身还有一个更通用的处理路径。 第一步先跑通最小用例。任何工具都不应该在还不会用的时候直接做复杂定制。 第二步观察真实使用场景。代码高亮的真实场景不是截图好看而是读者能否快速定位、复制、理解。这决定了哪些坑需要优先处理。 第三步做体积和性能约束。前端工具一旦进入团队级项目体积失控是迟早的事要在入口处做好白名单和裁剪。 第四步把体验闭环补齐。高亮、复制、暗色主题、横向滚动、编译异常时的兜底共同构成用户对代码块的最终体验。 第五步沉淀成规范。让普通内容作者通过简单的写法获得一致结果而不是每次都要深入工具实现。 这条路径可以迁移到很多“小工具大使用”的场景中。与其不停更换更酷的方案不如把当前已经跑通的方案做成稳定、可扩展、有边界的工程能力。代码高亮看似是个小问题但它背后涉及内容生产、前端性能、阅读体验和团队协作。处理得足够扎实整套文档系统都会更可信。 如果你想在博客或文档站里引入 Prism我的建议是从一个最小代码块开始先确认它能在你的页面里正确渲染 SQL 或 JSON再考虑行号、复制按钮、主题切换这类增强功能。先把流程跑通把边界确认清楚剩下的再一点点加。
返回列表