news 2026/9/25 13:18:13

代码高亮库prettify实战指南:三件套用法、动态渲染与避坑排查

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
代码高亮库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 class="prettyprint">变成带配色的代码块,对于不想引入 npm 包和打包器的团队来说,它的落地成本低到几乎没有。不过它也有一些容易被忽略的边界:动态渲染、语言扩展、样式覆盖,下面按我实际拆包和接入的顺序,把用法和坑位一次说清。这套资源适合正在做静态页面、个人博客、内部文档站,或者手头有存量项目不想迁移高亮方案的人。

2. 三件套怎么落地:run_prettify 自动扫描与 prettyPrint 手动调用两条线

2.1 三件套的分工:prettify.css 管外观,prettify.js 管分析,run_prettify.min.js 管调度

先把三个文件的作用分清,后面排错才不迷糊。prettify.js 是核心引擎,负责把代码文本拆成词法 token,再用<span class="kwd">这类标签把 token 包起来;prettify.css 定义这些 token 类长什么样,也就是颜色、粗体、斜体;run_prettify.min.js 是调度器,它扫描页面里所有带prettyprint类的元素,在 DOM 准备就绪时调用核心逻辑,还负责按 URL 参数加载额外的语言包和皮肤。三件套的依赖关系是单向的:run_prettify.min.js 依赖 prettify.js,prettify.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 lang="zh-CN"> <head> <link rel="stylesheet" href="assets/prettify.css"> </head> <body> <pre class="prettyprint lang-js linenums"><code>const list = [1, 2, 3]; list.forEach((n) => { console.log(n * 2); }); </code></pre> <script src="assets/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,里面是"<div>const 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 class="prettyprint lang-js">' + escapeHtml(raw) + '</pre>'; PR.prettyPrint();

逻辑说明:先定义转义函数,把&、<、>依次替换为实体,再拼进 pre 标签里,最后调PR.prettyPrint()扫描。这里有个细节:&必须最先替换,否则第一次替换产生的&amp;在后续替换中会被二次处理。参数说明:转义函数是这个场景的地基,适用于所有从接口或渲染器拿到的原始代码。如果你的 Markdown 渲染器已经做过转义,前端就不必再做一遍,判断标准是看最终 HTML 源码里<是否以&lt;形式出现。

3. prettify.css 主题定制:token 类名、换肤参数与三个高频微调

3.1 prettify.css 里的 token 类名与默认配色对应关系

prettify 高亮的本质是给不同词法单元打上不同的 class,prettify.css 再为这些 class 配颜色。读懂这套类名,你就能自己换肤、微调、甚至写一套干净的自定义样式。默认主题下常用的类名和覆盖内容如下表:

类名含义典型覆盖内容
com注释// 注释、/* 块注释 */
str字符串'abc'、"text"
kwd关键字if、for、class、return
typ类型名String、Array、Object
lit字面量42、true、null
pun标点(、)、{、}、;
atnHTML 属性名id、class、href
atvHTML 属性值="btn"、="submit"
tagHTML 标签名div、span、a
pln普通文本其余未分类内容

逻辑说明:CSS 选择器直接按类名命中,所以换肤不是去改 prettify.js,而是替换或覆盖 prettify.css。这也是 prettify 把样式和逻辑拆开的好处,前端可以不动一行 JS 就换掉整站配色。验证方法:打开 DevTools 看任意已高亮的代码块,任意一行 JS 的if关键字都会被<span class="kwd">包住,类名和上表一一对应。

3.2 换肤两条路:URL 参数 skin 与后端式样式覆盖

换肤有两条路。第一条是自动模式下通过run_prettify.min.js的 URL 参数指定皮肤,官方仓库的 styles 目录里内置了 desert、sons-of-obsidian、sunburst 等主题文件,用skin参数声明即可:

<script src="assets/run_prettify.min.js?skin=desert"></script>

逻辑说明:加载器解析 script 标签的 src,把skin=desert映射到 styles 目录下对应的 desert.css 并注入页面。参数说明:skin参数同时只能传一个,想多套主题共存要么二次引入 CSS 文件,要么自己写覆盖样式。第二条路是手动引入主题 CSS 文件,再在上面压一层自定义样式,适合改造成分较大的场景:

<link rel="stylesheet" href="assets/desert.css"> <link rel="stylesheet" href="assets/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 class="prettyprint lang-js">,插入页面后整块代码只有纯文本,没有任何 span 包裹。

原因:run_prettify.min.js 只在 DOMContentLoaded 阶段做一次全量扫描。之后插入的节点它感知不到,也不会自动再扫。

解决:节点真正挂载到文档之后,手动触发一次全量处理。

const box = document.createElement('div'); box.innerHTML = '<pre class="prettyprint lang-js">const 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 语言参数写错导致完全没高亮

现象:class="prettyprint lang-javascript"、class="prettyprint lang-python",页面颜色全无,只有默认字体。

原因:prettify 的语言代码是短码注册制,lang-js、lang-py、lang-css这种才对。写lang-javascript、lang-python时处理器匹配不到,代码落到纯文本 handler,输出只有 pln,自然没有颜色。

解决:语言类名换成短代码;如果确实是冷门语言,比如 SQL、Lua、VB,还需要额外引入对应语言扩展包,单靠 prettify.js 的内置语言列表覆盖不了:

<script src="assets/lang-sql.js"></script> <script src="assets/run_prettify.min.js"></script>

逻辑说明:语言扩展文件需要在加载器之前引入,或通过run_prettify.min.js的?lang=sql参数按需拉取,取决于你下载的这版资源的文件分布。参数说明:lang参数可以带多个,比如?lang=sql&lang=lua&skin=desert,加载器会逐个注入。判断语言是否加载成功,直接看 DevTools 的 Network 面板里有没有对应的 lang-*.js 请求在跑。

4.4 页面同时用 prettify 和 highlight.js,class 冲突样式串味

现象:页面里两套高亮库都引了,代码块一会儿是 prettify 的配色,一会儿是 highlight.js 的配色,偶尔还出现一个块被处理两次。

原因:两套库都要扫描 pre/code,class 命名习惯又相似,prettify 认prettyprint,highlight.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 src="assets/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 出现在类名里。这套顺序帮我避掉了后面好几次翻车,资源三件套既然已经在手,一次接对就是最高性价比的用法。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/25 13:17:20

人型机器人ZMP零力矩点控制:从倒立摆模型到动态步态稳定性实战

1. 从零力矩点说起&#xff1a;人型机器人为什么离不开ZMP人型机器人走路这件事&#xff0c;外行看热闹&#xff0c;内行看门道。很多人第一次接触双足机器人控制&#xff0c;脑子里想的都是关节怎么转、步态怎么规划&#xff0c;但真正上手之后才会发现&#xff0c;最核心的问…

作者头像 李华
网站建设 2026/9/25 13:16:01

J4125 与 I3-6100U 性能对比:用 TaoToken 统一 Key 跑通本地 AI 工具配置

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/25 13:13:43

Atlas 300V推理卡实战:从YOLO模型迁移到昇腾NPU部署全攻略

华为的 Atlas 系列这几年在国产 AI 加速卡里出镜率越来越高&#xff0c;尤其是 Atlas 300V 推理卡&#xff0c;经常在安防、工业质检、智慧零售这些落地场景里看到。我最早接触 Atlas 是因为客户那边要搞国产化替代&#xff0c;手头一批 YOLO 检测模型要从 GPU 迁到昇腾平台&am…

作者头像 李华
网站建设 2026/9/25 13:10:05

OpenClaw本地部署全攻略:从飞书接入到Ollama大模型配置

1. 为什么要做 OpenClaw 本地部署&#xff1a;需求分析比安装更优先1.1 OpenClaw到底是什么&#xff1a;一个能跑在你自己电脑上的 Agent 运行时先说结论&#xff1a;OpenClaw 并不是一个简单的聊天机器人&#xff0c;而是一套开源 AI Agent 运行时环境。把它部署到本地后&…

作者头像 李华