写技术博客时,代码块如果只是黑白文字,读起来会比较吃力。尤其是 JavaScript、SQL、HTML 混在一篇文章里时,有没有语法高亮,阅读体验差别很大。
前端常见的方案包括 Prism.js、highlight.js 和 Shiki。它们都可以给代码上色,但实现思路、适用场景和配置方式并不完全相同。本文先以 Prism.js 为主线,做一套实际可用的博客配置,再讨论应该如何选型。
先看结论:技术博客通常优先选择 Prism.js;语言来源复杂、希望自动检测时考虑 highlight.js;构建时生成高亮 HTML 时优先评估 Shiki。
- WordPress、普通博客:Prism.js,语言和插件都容易控制。
- 代码来源复杂、语言不固定:highlight.js,可使用自动检测。
- Astro、VitePress、Next.js 等预渲染站点:Shiki,适合构建时输出。
- 需要让用户直接编辑代码:Monaco Editor 或 CodeMirror。
本文示例以 Prism.js 1.x(CDN 示例固定为 1.29.0)为基础。所列插件及 API 应按项目实际安装版本核对。语法高亮仅用于展示代码,不等于在线代码编辑器。
一、语法高亮到底做了什么?
下面是一段普通代码:
const user = { name: 'Kakashi', active: true };
console.log(user.name);
语法高亮器会识别出关键字、字符串、属性、注释等结构,为不同部分生成带样式的标记。例如 Prism.js 的输出通常包含类似 <span class="token string"> 的 HTML,然后交给 CSS 控制颜色。
- 语法高亮:把已有代码按语法分类着色,例如 Prism.js、highlight.js、Shiki。
- Markdown 解析:把 Markdown 转换成 HTML,例如 markdown-it、marked。解析器不一定自带语法高亮。
- 代码编辑器:允许用户输入、选择、编辑代码,例如 Monaco Editor、CodeMirror。Prism.js 本身不负责编辑体验。
二、Prism.js 的基本原理
Prism.js 官网将它定位为轻量、可扩展的语法高亮库。主要结构如下:
| 组成部分 | 作用 | 常见文件 |
|---|---|---|
| Core | 分析代码并执行高亮 | prism-core.min.js |
| Language | 定义不同编程语言的语法 | prism-javascript.min.js、prism-css.min.js |
| Theme | 控制代码块颜色与外观 | prism-tomorrow.min.css |
| Plugin | 扩展行号、复制、工具栏等功能 | prism-line-numbers.min.js 等 |
代码块的标准写法为:
<pre><code class="language-javascript">const count = 10;</code></pre>
其中:
<pre>保留换行和空格,适合多行代码。<code>表示里面的内容是代码。language-javascript用来明确指定高亮语言,也支持lang-javascript。
单行代码也可以只使用 <code class="language-css">。
三、直接用 CDN:几分钟搭建一个可运行页面
如果只是普通 HTML 网站,先用 CDN 是最容易理解的方案。以下示例使用固定版本,避免以后 CDN 自动升级导致行为变化。
保存为 index.html,浏览器打开即可运行:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Prism.js Demo</title>
<!-- 代码块主题 -->
<link rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism-tomorrow.min.css" />
</head>
<body>
<h1>Prism.js 代码高亮示例</h1>
<pre><code class="language-javascript">const sum = (a, b) => a + b;
console.log(sum(10, 20));</code></pre>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-core.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/autoloader/prism-autoloader.min.js"></script>
</body>
</html>
这里使用 Autoloader 插件,它会按代码块使用的语言,加载缺少的语言定义。对于一篇文章混合 JavaScript、HTML、SQL、Bash 的博客,比手动列出所有语言文件更省事。
但它有代价:首次看到某种语言时,浏览器可能还需要额外发起请求。如果已经知道网站只展示哪几种语言,打包时明确引入语言包通常更便于控制请求和产物体积。
不使用 Autoloader 可以吗?
当然可以。例如只需要 JavaScript 和 CSS:
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-core.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-clike.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-javascript.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-css.min.js"></script>
注意 部分语言定义有依赖关系。例如 JavaScript 依赖 C-like 语法,所以要先加载 prism-clike.min.js。正式项目可以使用官网提供的打包下载工具,减少手动维护依赖的麻烦。
四、实用插件:让代码块更像成熟技术博客
Prism.js 的价值不仅是关键字着色,更在于比较完善的插件体系。
4.1 显示行号(Line Numbers)
引入以下 CSS 和 JS:
<link rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/line-numbers/prism-line-numbers.min.css" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/line-numbers/prism-line-numbers.min.js"></script>
然后在 <pre> 添加 line-numbers:
<pre class="line-numbers"><code class="language-sql">SELECT id, name
FROM users
WHERE active = 1;</code></pre>
排查报错时,别人可以直接告诉你“看第 3 行”。对于较长的 SQL 或配置文件尤其有用。行号属于展示层,不会自动修改原始代码内容。
4.2 一键复制代码(Toolbar + Copy to Clipboard)
官方复制插件需要先有 Toolbar。JS 的顺序建议写成:Core → Autoloader → Toolbar → Copy 插件。
<link rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/toolbar/prism-toolbar.min.css" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/toolbar/prism-toolbar.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/copy-to-clipboard/prism-copy-to-clipboard.min.js"></script>
使用普通的 Prism 代码块即可:
<pre><code class="language-bash">npm install prismjs</code></pre>
插件会自动生成复制按钮。页面还可以通过 data-prismjs-copy、data-prismjs-copy-success 等属性设置按钮文案。复制依赖浏览器剪贴板能力,部署时优先使用 HTTPS;最终行为应在目标浏览器测试。
4.3 标注文件名(Show Language / File Highlight 等)
对技术博客来说,文件名往往比“JavaScript”标签更重要。例如:
src/components/CodeBlock.vue
src/utils/highlight.js
<pre data-label="src/main.js"><code class="language-javascript">console.log('hello');</code></pre>
data-label 的呈现依赖 Toolbar 的标签功能;仅加载核心库或复制插件,不会保证文件名自动显示。
4.4 高亮特定行(Line Highlight)
想说明 Bug 的关键代码时,可以突出第 2~3 行:
<link rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/line-highlight/prism-line-highlight.min.css" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/line-highlight/prism-line-highlight.min.js"></script>
<pre data-line="2-3"><code class="language-javascript">const user = getUser();
if (!user) {
throw new Error('User not found');
}
render(user);</code></pre>
如果同时启用了行号插件,请一并测试主题 CSS、data-start 和行高等设置,以免高亮框位置偏移。
五、在 npm / Vite / Vue 项目中使用
对于有构建工具的站点,最好把依赖纳入 npm 管理,不必每页手动塞 CDN。
npm install prismjs
下面是一种仅加载需要语言的方式:
// src/utils/prism.js
import Prism from 'prismjs';
import 'prismjs/themes/prism-tomorrow.css';
import 'prismjs/components/prism-markup';
import 'prismjs/components/prism-css';
import 'prismjs/components/prism-clike';
import 'prismjs/components/prism-javascript';
import 'prismjs/components/prism-sql';
export default Prism;
提醒:Prism.js 1.x 的语言模块存在依赖与加载顺序问题。上述例子显式导入 C-like,实际项目应核对所用版本的语言依赖。需要很多语言时,考虑构建时的按需加载方案。
5.1 Vue 2 / Vue 3:页面更新之后重新高亮
博客详情页常见流程:请求 API → 获取 Markdown → 转成 HTML → 更新 DOM。Prism.js 首次加载时可能页面还是空的,因此必须在代码块插入 DOM 之后执行高亮。
Vue 3 示例:
<template>
<article ref="articleRef" v-html="safeHtml"></article>
</template>
<script setup>
import { ref, nextTick, watch } from 'vue';
import Prism from '@/utils/prism';
const props = defineProps({
safeHtml: { type: String, required: true }
});
const articleRef = ref(null);
watch(
() => props.safeHtml,
async () => {
await nextTick();
if (articleRef.value) {
Prism.highlightAllUnder(articleRef.value);
}
},
{ immediate: true, flush: 'post' }
);
</script>
这里的 safeHtml 必须是可信或经过适当净化的 HTML。v-html 不会自动过滤恶意内容;Markdown 转成 HTML 也不意味着它一定安全。不要直接把不可信输入交给 v-html。
Vue 2 里同样可以在更新后调用:
this.$nextTick(() => {
const container = this.$refs.article;
if (container) Prism.highlightAllUnder(container);
});
建议用 highlightAllUnder(container) 而不是每次 highlightAll():前者只扫描当前文章容器,不会无谓处理整个网页。
5.2 为什么不能直接在 created() 里执行?
created() 时组件 DOM 尚未完成挂载。即便在 mounted() 调用一次,如果文章 HTML 是通过异步接口稍后插入,也可能拿不到代码块。关键不是某个生命周期名字,而是 调用时代码块是否已经真实存在于 DOM。
如果文章由异步组件、分页切换或客户端路由加载,应该在每次内容实际更新后进行高亮,并限定处理范围。
六、Markdown 博客里的 Prism.js:完整数据流
写博客通常不是手动写 <pre><code>,而是写:
```javascript
const message = 'Hello Prism';
console.log(message);
```
实际显示依赖这样的流程:
Markdown 原文
↓
Markdown 解析器(markdown-it / marked 等)
↓
HTML:<pre><code class="language-javascript">...</code></pre>
↓
Prism.js 识别语言并生成高亮标记
↓
CSS 负责配色、换行和代码块样式
最重要的是检查解析后的 HTML。 不同 Markdown 渲染器生成的 class 可能不同,甚至可能已经包含高亮后的 <span>。如果已有 Shiki、highlight.js 或其他插件负责高亮,就不要再让 Prism.js 重复修改同一段代码。
6.1 使用 markdown-it 输出代码块
先安装:
npm install markdown-it prismjs
如果你希望在解析 Markdown 的时候就高亮,可以配置 highlight 函数:
import MarkdownIt from 'markdown-it';
import Prism from 'prismjs';
import 'prismjs/components/prism-clike';
import 'prismjs/components/prism-javascript';
import 'prismjs/components/prism-sql';
const md = new MarkdownIt({
html: false,
highlight(code, lang) {
if (lang && Prism.languages[lang]) {
const highlighted = Prism.highlight(code, Prism.languages[lang], lang);
return `<pre class="language-${lang}"><code class="language-${lang}">${highlighted}</code></pre>`;
}
// 返回空字符串,让 markdown-it 用自身的安全转义结果渲染
return '';
}
});
const html = md.render('```javascript\nconsole.log("Hello");\n```');
这个简短版本仅适合语言名可控的内容。若 Markdown 来自用户投稿,不能未经校验就把 lang 拼进 HTML 属性:应对语言名使用严格白名单,或做符合 HTML 属性上下文的转义。html: false 也不是对所有不可信内容的一站式安全保证,渲染链若允许原始 HTML 或其他扩展,还要采取适当的 HTML 净化措施。
七、HTML 转义:最容易踩的坑
希望网页显示下面的代码:
<div class="notice">Hello</div>
如果直接把它写进 HTML 的 <code> 标签中:
<pre><code class="language-html"><div class="notice">Hello</div></code></pre>
浏览器会把内层 <div> 当作真正标签,而不是代码文本。正确做法是:
<pre><code class="language-html"><div class="notice">Hello</div></code></pre>
常用字符处理规则:
| 原始字符 | HTML 文本中的安全表示 |
|---|---|
& | & |
< | < |
> | >(通常也会一起处理) |
" | "(在属性值等上下文尤其重要) |
如果从字符串动态生成 HTML,应该使用可信的 Markdown 渲染器或正确的 HTML 转义方法,而不是随便用 innerHTML 拼接代码。
顺便澄清:Prism.js 负责高亮,不负责替你完成整个网站的 XSS 防护。
八、主题样式和移动端体验
Prism.js 自带多个主题,例如 prism.css、prism-tomorrow.css、prism-okaidia.css。选择时不仅要考虑颜色,更要注意与网站本身的亮色/暗色主题是否匹配。
下面是一套博客常用的补充样式:
.article pre[class*="language-"] {
max-width: 100%;
overflow-x: auto;
border-radius: 10px;
padding: 1.25rem;
font-size: 0.9rem;
line-height: 1.7;
tab-size: 2;
}
.article pre[class*="language-"] code {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco,
Consolas, "Liberation Mono", monospace;
}
.article :not(pre) > code {
overflow-wrap: anywhere;
padding: 0.15em 0.35em;
border-radius: 4px;
background: rgba(127, 127, 127, 0.12);
}
这里把横向滚动交给代码块本身,尽量避免很长的 SQL、JSON 或命令行把整个页面撑宽。注意主题 CSS 也可能给代码块指定 padding、overflow 等规则,最终样式应按实际加载顺序检查。
九、Prism.js、highlight.js、Shiki 有什么区别?
先看结论:三者都能完成博客中的代码高亮,但不适合简单地按“谁更先进”排名。
| 对比项 | Prism.js | highlight.js | Shiki |
|---|---|---|---|
| 核心方式 | 语言规则与 Token 解析 | 语言规则及相关性检测 | TextMate 语法与主题体系 |
| 自动猜测语言 | 不是主要设计方向 | 内置自动检测能力 | 通常显式指定语言 |
| 客户端使用 | 非常方便 | 非常方便 | 可以,但需考虑加载和初始化成本 |
| 预生成 HTML | 支持 | 支持 | 尤其适合构建时或服务端生成 |
| 主题方式 | CSS 主题 | CSS 主题 | VS Code 风格主题体系,可输出颜色信息 |
| 插件 / 扩展 | 官方 DOM 插件丰富 | API、插件生态及自定义能力 | Transformers 扩展生成结果 |
| 常见场景 | 传统博客、文档站、DOM 代码块 | 多来源、语言不固定的代码片段 | 静态站点、文档站、追求编辑器配色一致性 |
9.1 Prism.js:适合可控的博客代码块
Prism.js 的好处是上手直接,代码块写 language-javascript,再配好 CSS 和 JS,就能显示。行号、复制代码、指定行高亮等插件也很实用。
代价是:需要考虑语言包依赖、异步插入后重跑高亮、插件样式兼容。对于博客这种“文章编辑者知道代码语言”的场景,手动标注语言并不算问题。
9.2 highlight.js:不用事先知道语言也能尝试高亮
highlight.js 的特色之一是语言自动检测。
<link rel="stylesheet" href="/assets/highlightjs/styles/github-dark.min.css" />
<script src="/assets/highlightjs/highlight.min.js"></script>
<script>hljs.highlightAll();</script>
当代码块使用 language-javascript 时,可以明确指定语言;没有标注语言时,则可以尝试自动识别。上面示例中的 /assets/... 是需要自行部署的资源路径,不能直接复制到一个没有这些文件的网站上。
如果使用 npm:
npm install highlight.js
import hljs from 'highlight.js/lib/core';
import javascript from 'highlight.js/lib/languages/javascript';
import sql from 'highlight.js/lib/languages/sql';
import 'highlight.js/styles/github-dark.css';
hljs.registerLanguage('javascript', javascript);
hljs.registerLanguage('sql', sql);
// DOM 已完成更新之后执行
hljs.highlightAll();
9.3 Shiki:接近 VS Code 的高亮效果
Shiki 使用 TextMate 语法和主题,适合想让博客中的代码颜色接近编辑器的项目。安装:
npm install shiki
示例:
import { codeToHtml } from 'shiki';
const html = await codeToHtml(
'const message: string = "Hello Shiki";',
{
lang: 'typescript',
theme: 'github-dark'
}
);
html 是已经包含高亮信息的 HTML,适合在构建期间或服务端生成后输出到页面。Shiki 也能在浏览器中使用,但要考虑语法、主题等资源的加载开销,不能简单认为它在所有环境下都比 Prism.js 快。
对于静态生成的博客,Shiki 的优势很明显:网页访问者无需在加载后再运行一遍语法分析(前提是 HTML 已在构建期间生成)。不过,如果你需要复制按钮或交互式工具栏,仍然需要额外处理。
十、Monaco Editor 和 CodeMirror 是同类工具吗?
严格来说,它们解决的是另一类问题。
| 工具 | 主要用途 |
|---|---|
| Monaco Editor | 接近 VS Code 的浏览器代码编辑体验 |
| CodeMirror | 可定制的 Web 文本 / 代码编辑器 |
| Prism.js / highlight.js / Shiki | 展示已有代码的语法高亮 |
例如在线 SQL 编辑器、JSON 编辑器、可修改代码的 Playground,就不应该只因为 Prism.js 可以上色便拿它替代完整编辑器。语法高亮只是编辑器能力中的一部分,光标、选择、撤销、输入法、补全、折叠等都需要额外的系统支持。
十一、实际选型:不同网站应该用哪个?
- 普通 HTML / WordPress / 简单个人博客:如果只想为文章加高亮、行号和复制按钮,选择 Prism.js。配置简单,已有主题和插件基本够用。
- 大量转载代码、语言不统一:可以考虑 highlight.js 的自动检测能力;能确定语言时仍建议传入
language-xxx。 - Astro、VitePress、Next.js 或预渲染文档站:希望构建时生成高亮 HTML,并保持接近 VS Code 的主题效果时,优先评估 Shiki。
- 用户需要直接在网页上写代码:选择 Monaco Editor 或 CodeMirror;语法高亮库不能替代完整编辑器。
十二、实战检查清单:上线前确认这些问题
- 代码没有颜色:检查 CSS 是否加载,语言定义是否存在,
language-xxx是否写对。 - 动态文章没高亮:确认代码块已经进入 DOM,之后再调用
highlightAllUnder()。 - HTML 标签消失:检查
<、&是否正确转义。 - 代码复制按钮不出现:检查 Toolbar 和 Copy 插件是否都加载,顺序是否正确。
- 页面被代码撑宽:给
<pre>设置overflow-x: auto,检查父级布局限制。 - 同一代码块颜色异常:检查是否同时启用了 Prism.js、highlight.js、Shiki 或 Markdown 插件的高亮功能。
- 页面加载慢:不要无差别引入所有语言包;控制客户端高亮数量,必要时构建时预渲染。
- 文章来源不可信:明确 HTML 转义和净化边界。切勿将来自用户的原始 HTML 无过滤地插入 DOM。
- 代码块很多:优先避免重复高亮,缩小 DOM 扫描范围;必要时再评估分段渲染。
- 升级了依赖:检查语言包、主题和插件版本兼容,固定版本并进行回归测试。
总结
如果目标是给个人技术博客的代码块上色,Prism.js 依然是简单、实用的选择。它的核心并不复杂:<pre><code class="language-xxx">、语言定义和主题 CSS。真正需要花时间的是 Markdown 解析链、动态内容、HTML 转义,以及行号、复制等插件之间的配合。
我的选型原则是:
- 传统博客、希望自己控制 DOM 和插件:优先考虑 Prism.js。
- 希望自动识别语言、快速处理来源复杂的代码:考虑 highlight.js。
- 构建时生成文章,追求编辑器风格的语法高亮:考虑 Shiki。
- 要让用户编辑代码:直接考虑 Monaco Editor 或 CodeMirror。

