代码高亮库prettify实战指南:三件套用法、动态渲染与避坑排查 简介网页中展示源代码时常因缺乏语法高亮而难以阅读针对这一需求Prettify代码高亮资源包提供了一套基于CSS与JavaScript的完整方案面向初中级前端开发者、技术博主及文档编写者。压缩包共含三个文件以一个CSS样式表和两个JavaScript脚本组成整体仅十四KB其中CSS文件负责定义不同语言的关键字、字符串与注释的配色JS文件负责自动识别代码结构并执行高亮转换压缩版脚本则在保留完整功能的同时减少流量消耗便于快速加载。目前已有八百四十一人学习下载。使用过程零门槛只需在HTML中引入这组文件并为目标代码块添加约定类名即可自动高亮HTML、CSS、JavaScript、Python、Java、C等多种常见语言还支持行号显示与内联错误提示等实用功能全程无需手动配置语言规则也不影响页面原有布局。这组轻量资源特别适合个人博客、在线教程及企业技术文档场景帮助开发者快速打造美观专业的代码展示区。1. 代码高亮库 prettify三个文件零依赖老库反而省心的理由做代码高亮的第一反应往往是 highlight.js 或 Prism但 prettify 这套老方案到今天依然有明确的使用场景prettify.css、prettify.js、run_prettify.min.js 三个文件就能覆盖静态博客、文档站、旧项目里的绝大多数高亮需求。零依赖、不用构建流程、几行 HTML 就能把pre classprettyprint变成带配色的代码块对于不想引入 npm 包和打包器的团队来说它的落地成本低到几乎没有。不过它也有一些容易被忽略的边界动态渲染、语言扩展、样式覆盖下面按我实际拆包和接入的顺序把用法和坑位一次说清。这套资源适合正在做静态页面、个人博客、内部文档站或者手头有存量项目不想迁移高亮方案的人。2. 三件套怎么落地run_prettify 自动扫描与 prettyPrint 手动调用两条线2.1 三件套的分工prettify.css 管外观prettify.js 管分析run_prettify.min.js 管调度先把三个文件的作用分清后面排错才不迷糊。prettify.js 是核心引擎负责把代码文本拆成词法 token再用span classkwd这类标签把 token 包起来prettify.css 定义这些 token 类长什么样也就是颜色、粗体、斜体run_prettify.min.js 是调度器它扫描页面里所有带prettyprint类的元素在 DOM 准备就绪时调用核心逻辑还负责按 URL 参数加载额外的语言包和皮肤。三件套的依赖关系是单向的run_prettify.min.js 依赖 prettify.jsprettify.js 处理出的类名依赖 prettify.css 来表现。手动模式下你甚至可以只引入 prettify.js 和 prettify.css跳过加载器。典型的本地目录结构是这样assets/ ├── prettify.css ├── prettify.js └── run_prettify.min.js逻辑说明把三个文件放在同一目录下加载器会按约定找到同目录的 prettify.js。如果你改过文件名或目录层级加载器会失效这就是很多页面「引了 script 但高亮没反应」的第一个原因。参数说明这里没有任何配置参数目录结构就是隐式约定保持三件套同目录即可。2.2 自动模式一个 script 标签按约定扫描 prettyprint 代码块最常见的使用方式也是最省心的方式引入 CSS 和加载器然后给代码块加prettyprint类。CSS 放在 head 里做预加载run_prettify.min.js 放在 body 末尾加载器会在 DOM 解析完成后自动找出所有待高亮元素。!DOCTYPE html html langzh-CN head link relstylesheet hrefassets/prettify.css /head body pre classprettyprint lang-js linenumscodeconst list [1, 2, 3]; list.forEach((n) { console.log(n * 2); }); /code/pre script srcassets/run_prettify.min.js/script /body /html逻辑说明prettyprint类告诉加载器「这个块要处理」lang-js指定 JavaScript 语言linenums追加行号列code标签只是语义容器pre 才是 prettify 真正作用的目标。加载器在 DOMContentLoaded 时执行全量扫描整个过程不需要写一行业务代码。参数说明语言类名的格式是lang-js、lang-css、lang-html、lang-py这种短代码写成lang-javascript、language-js都不被识别这一点后面避坑章节会再展开。如果清空lang-*不写prettify 会做启发式猜测解释型语言偶尔会猜错所以稳定的做法是每个块都显式写语言。注意如果你用 innerHTML 动态拼pre代码块里面的和必须先转义否则浏览器会先把它们当标签和实体解析掉高亮结果完全错乱。转义的具体处理见 2.4 节。2.3 手动模式PR.prettyPrint 与 PR.prettyPrintOne 控制动态内容自动模式只在页面加载时扫一次之后通过 AJAX、脚本插入、Vue 渲染出来的代码块它一概不认。这时要用 prettify 暴露的 PR 全局对象。PR 是 prettify.js 挂到 window 上的命名空间核心方法两个PR.prettyPrint()全量再扫一遍PR.prettyPrintOne()只处理单段代码并返回 HTML。// 方式一手动处理页面中新出现的一批 pre PR.prettyPrint(); // 方式二精确处理单个代码串返回高亮后的 HTML const code for (let i 0; i arr.length; i) { console.log(arr[i]); }; const html PR.prettyPrintOne(code, js, true); document.getElementById(code-box).innerHTML html;逻辑说明prettyPrint()的作用范围依然限定在带prettyprint类的元素上适合一次插入多个代码块的场景prettyPrintOne(source, lang, lineNumber)的三个参数分别是纯文本代码、语言代码、是否带行号。它不做 DOM 扫描直接把文本 token 化返回带 span 的 HTML 字符串适合单个代码片段渲染。参数说明lineNumber 传布尔值 true 时输出带行号的结构false 则不带。很多团队把这两套 API 用反内容已经进入页面了还调 prettyPrintOne或者反过来拿着原始字符串调 prettyPrint结果当然空转。在 Vue 这类框架里等 DOM 真正渲染完再调全量方法this.$nextTick(() { PR.prettyPrint(); });逻辑说明$nextTick保证 DOM 更新完成后再执行扫描否则 Vue 刚更新数据、节点还没挂上扫描器等于对着一个空壳操作。如果数据是通过接口异步加载的还要在请求回调里再调一次而不是在 mounted 里调一次就完事。2.4 接口内容与 Markdown 渲染结果落地时的预处理博客站最常见的翻车点后端或 Markdown 渲染接口返回一段 JSON里面是divconst a 1;/div前端直接innerHTML塞进pre结果div标签被浏览器吃掉页面结构断开。prettify 的 tokenizer 处理的是 DOM 文本节点不是 HTML 字符串所以在写入之前必须做实体转义。function escapeHtml(str) { return str.replace(//g, amp;) .replace(//g, lt;) .replace(//g, gt;); } const raw const div div;; const box document.getElementById(code-box); box.innerHTML pre classprettyprint lang-js escapeHtml(raw) /pre; PR.prettyPrint();逻辑说明先定义转义函数把、、依次替换为实体再拼进 pre 标签里最后调PR.prettyPrint()扫描。这里有个细节必须最先替换否则第一次替换产生的amp;在后续替换中会被二次处理。参数说明转义函数是这个场景的地基适用于所有从接口或渲染器拿到的原始代码。如果你的 Markdown 渲染器已经做过转义前端就不必再做一遍判断标准是看最终 HTML 源码里是否以lt;形式出现。3. prettify.css 主题定制token 类名、换肤参数与三个高频微调3.1 prettify.css 里的 token 类名与默认配色对应关系prettify 高亮的本质是给不同词法单元打上不同的 classprettify.css 再为这些 class 配颜色。读懂这套类名你就能自己换肤、微调、甚至写一套干净的自定义样式。默认主题下常用的类名和覆盖内容如下表类名含义典型覆盖内容com注释// 注释、/* 块注释 */str字符串abc、textkwd关键字if、for、class、returntyp类型名String、Array、Objectlit字面量42、true、nullpun标点(、)、{、}、;atnHTML 属性名id、class、hrefatvHTML 属性值btn、submittagHTML 标签名div、span、apln普通文本其余未分类内容逻辑说明CSS 选择器直接按类名命中所以换肤不是去改 prettify.js而是替换或覆盖 prettify.css。这也是 prettify 把样式和逻辑拆开的好处前端可以不动一行 JS 就换掉整站配色。验证方法打开 DevTools 看任意已高亮的代码块任意一行 JS 的if关键字都会被span classkwd包住类名和上表一一对应。3.2 换肤两条路URL 参数 skin 与后端式样式覆盖换肤有两条路。第一条是自动模式下通过run_prettify.min.js的 URL 参数指定皮肤官方仓库的 styles 目录里内置了 desert、sons-of-obsidian、sunburst 等主题文件用skin参数声明即可script srcassets/run_prettify.min.js?skindesert/script逻辑说明加载器解析 script 标签的 src把skindesert映射到 styles 目录下对应的 desert.css 并注入页面。参数说明skin参数同时只能传一个想多套主题共存要么二次引入 CSS 文件要么自己写覆盖样式。第二条路是手动引入主题 CSS 文件再在上面压一层自定义样式适合改造成分较大的场景link relstylesheet hrefassets/desert.css link relstylesheet hrefassets/custom-prettify.css/* custom-prettify.css */ pre.prettyprint { background: #1e1e1e; border: none; padding: 16px; } pre.prettyprint .com { color: #6a9955; } pre.prettyprint .kwd { color: #569cd6; } pre.prettyprint .str { color: #ce9178; }逻辑说明先让皮肤文件铺底再按 3.1 的类名逐类覆盖。这里只改了com、kwd、str三类其余类名会保留 desert 默认值这是精细换肤的基本套路。参数说明覆盖样式文件必须放在皮肤文件之后CSS 同名选择器后者生效。深色页面只需要把pre.prettyprint的背景和边框处理掉代码区就能和页面融成一体。3.3 三个高频微调等宽字体、横向滚动与行号列接入 prettify 后有三处样式几乎每个项目都会调。字体和行高是最常改的默认字体在中文文章里显得松散统一成等宽字体观感立刻整齐pre.prettyprint, pre.prettyprint code { font-family: JetBrains Mono, Cascadia Code, Consolas, monospace; font-size: 14px; line-height: 1.6; }逻辑说明pre 和 code 同时设置避免外层定字体内层不继承的割裂感。参数说明字体列表按回退顺序写用户机器上没有 JetBrains Mono 会自动落到 Cascadia Code再没有就是 Consolas。长代码不换行是默认行为需要横向滚动时单独开pre.prettyprint { overflow-x: auto; white-space: pre; }逻辑说明white-space: pre保留空白和换行overflow-x: auto让超宽行滚动而不是撑破布局。参数说明如果希望小屏折行把white-space改为pre-wrap但这样代码缩进的对齐感会变差二选一看你的文章场景。行号列默认自带但样式偏素可以这样微调pre.prettyprint.linenums ol.linenums { padding-left: 2.5em; list-style: decimal; } pre.prettyprint.linenums li:nth-child(odd) { background: rgba(0, 0, 0, 0.03); }逻辑说明启用linenums之后 prettify 会把代码包装成ol.linenums列表行号通过list-style: decimal显示。nth-child(odd)做的是奇偶行斑马纹很多主题默认自带想去掉就把它的 background 覆盖成 transparent。参数说明padding-left 控制行号列宽度行号数字位数变多时适当加大否则两位数行号会被压到代码区边缘。4. 避坑排查prettify 高亮失效与样式错位的五次实战复盘4.1 动态追加的代码块永远不亮现象AJAX 返回一段 HTML里面带pre classprettyprint lang-js插入页面后整块代码只有纯文本没有任何 span 包裹。原因run_prettify.min.js 只在 DOMContentLoaded 阶段做一次全量扫描。之后插入的节点它感知不到也不会自动再扫。解决节点真正挂载到文档之后手动触发一次全量处理。const box document.createElement(div); box.innerHTML pre classprettyprint lang-jsconst s ok;/pre; document.body.appendChild(box); PR.prettyPrint();逻辑说明appendChild 完成后再调PR.prettyPrint()保证扫描时节点已经在 DOM 树里。参数说明这段代码执行完新 pre 内部会被填上 span 标签并且类名里追加 prettyprinted 标记用来防止二次处理。如果插入了多个代码块调一次全量即可不用逐个处理。4.2 代码里带和被浏览器吃掉现象展示一段 HTML 源码的代码块页面结构被切断后面的内容错位甚至消失博客正文直接崩了。原因代码里的div没转义就被浏览器当标签解析被当实体起点。prettify 处理的是 DOM 解析后的纯文本原始字符串已经被浏览器破坏掉了根本不是高亮引擎的问题。解决所有写死在 HTML 里的代码手写时把换成lt;、换成gt;、换成amp;。脚本生成的内容用 2.4 节的 escapeHtml 函数前置处理。判断标准只有一个打开页面看最终 HTML 源码代码区必须全部是实体字符一个裸都不能有。4.3 语言参数写错导致完全没高亮现象classprettyprint lang-javascript、classprettyprint lang-python页面颜色全无只有默认字体。原因prettify 的语言代码是短码注册制lang-js、lang-py、lang-css这种才对。写lang-javascript、lang-python时处理器匹配不到代码落到纯文本 handler输出只有 pln自然没有颜色。解决语言类名换成短代码如果确实是冷门语言比如 SQL、Lua、VB还需要额外引入对应语言扩展包单靠 prettify.js 的内置语言列表覆盖不了script srcassets/lang-sql.js/script script srcassets/run_prettify.min.js/script逻辑说明语言扩展文件需要在加载器之前引入或通过run_prettify.min.js的?langsql参数按需拉取取决于你下载的这版资源的文件分布。参数说明lang参数可以带多个比如?langsqllangluaskindesert加载器会逐个注入。判断语言是否加载成功直接看 DevTools 的 Network 面板里有没有对应的 lang-*.js 请求在跑。4.4 页面同时用 prettify 和 highlight.jsclass 冲突样式串味现象页面里两套高亮库都引了代码块一会儿是 prettify 的配色一会儿是 highlight.js 的配色偶尔还出现一个块被处理两次。原因两套库都要扫描 pre/codeclass 命名习惯又相似prettify 认prettyprinthighlight.js 认hljs但同一块代码可以同时带上两个类名两套处理器先后跑一遍后写入的 span 结构覆盖了前一个。解决二选一不要同页面共存。highlight.js 的自动模式可以通过hljs.highlightAll()指定容器prettify 没有范围参数它固定扫描全页面所有prettyprint元素。所以如果没法彻底移除一套就把两个库的处理范围严格分开prettify 的代码块只用prettyprint类highlight.js 的代码块只挂hljs且加载器不扫描对方区域。但实际项目里这个边界很容易被后续维护者打破最稳的做法还是同一套方案。我从第二个项目开始就不碰这种混排了排查成本远高于收益。4.5 CDN 挂了导致样式裸奔或加载器永远等不到核心文件现象页面只挂了远端 CDN 的 run_prettify.min.js某天资源加载失败整页代码区全部纯文本更隐蔽的是 CSS 没加载高亮逻辑跑完了但看不到颜色。原因三件套缺任何一件效果都不完整。CDN 不可控时等于把一个核心展示功能挂在别人的服务器上。解决直接把这套 prettify 资源内置到项目里按 2.1 的目录结构放本地没有任何外部依赖。万一还想用加速域名也可以保留 CDN 并在本地做同步降级window.PR || document.write(script srcassets/run_prettify.min.js\/script);逻辑说明这行判断当前页面有没有暴露 PR 全局对象没有就用 document.write 紧急写入本地脚本。注意 document.write 里 script 标签的结束符必须写成\/script否则会提前终止外层脚本。参数说明这属于最后的兜底策略正常项目直接把本地引入写在 HTML 里就行根本不用走这一步。既然这份资源就是本地三件套落地时直接按本地方案走最省心。5. 进阶怎么验证高亮真的生效了以及大文档不分批的卡顿解法5.1 用 prettyprinted 标记验证处理结果prettify 每处理完一个代码块会给元素追加一个prettyprinted类名。这个类既是防止重复处理的内部标记也是你判断高亮是否生效的最快抓手。验证步骤很简单打开 DevTools选中任意pre.prettyprint看类名列表里有没有prettyprinted再看内部有没有生成span.kwd、span.com这类结构化节点。如果类名里没有 prettyprinted说明加载器压根没处理这个元素如果有但配色不对问题在 prettify.css 或覆盖样式上。再配合 Network 面板确认语言扩展文件是否加载两步就能把问题定位到「逻辑层」还是「样式层」。我排查高亮不生效的问题时从来不先看代码第一步永远是按 F12 找这个类名。5.2 大文档分批高亮别让首屏一次性卡死一个页面挂几十个代码块时自动加载器会同步全量处理低端机明显卡顿体感上就是页面滚到代码区时掉帧。手动把处理拆成小块用 requestAnimationFrame 让出主线程const blocks Array.from(document.querySelectorAll(pre.prettyprint)); const tasks blocks.map((el) ({ el, text: el.textContent, lang: (el.className.match(/lang-([\w-])/) || [])[1] || , linenums: el.classList.contains(linenums) })); function processChunk(i, step) { tasks.slice(i, i step).forEach(({ el, text, lang, linenums }) { el.innerHTML PR.prettyPrintOne(text, lang, linenums); el.classList.add(prettyprinted); }); if (i step tasks.length) { requestAnimationFrame(() processChunk(i step, step)); } } processChunk(0, 5);逻辑说明先把所有待处理元素的信息缓存到 tasks 数组重点是用el.textContent取回纯文本避免 prettify 处理时把已生成的 span 再当代码读一遍。语言短码从 className 里用正则提取行号开关用classList.contains判断。processChunk 每帧只处理 5 个块处理完一帧再排下一帧期间浏览器还能响应用户滚动和点击。参数说明step 的 5 是保守值桌面端调到 10 也没问题移动端建议保持 3 到 5。这套写法适合页面代码块数量固定的场景如果代码块是动态追加的在插入回调里再调 processChunk 重新收集一次即可。从那以后我在任何页面里加代码高亮都强制走一遍自检先确认转义再对语言短码最后打开 DevTools 确认 prettyprinted 出现在类名里。这套顺序帮我避掉了后面好几次翻车资源三件套既然已经在手一次接对就是最高性价比的用法。希望帮到你。本文还有配套的精品资源点击获取