ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Vue代码块高亮方案对比与组件封装实践:从highlight.js到prismjs

Vue代码块高亮方案对比与组件封装实践:从highlight.js到prismjs 写Vue项目的时候遇到“页面里要展示一段代码”这种需求太常见了。不管是做技术文档、博客网站还是后台系统里要给用户展示接入示例代码块都是绕不开的组件。但很多新手朋友一开始会直接丢一个precode上去结果页面丑得没法看缩进乱、没有高亮、复制不方便和人家那些漂亮的文档站一比差距非常明显。这篇博文我把自己在Vue 2和Vue 3里做代码块样式和代码高亮的完整经验整理出来从方案选型到组件封装从行号到复制按钮再到动态加载和性能优化尽量讲透帮助大家少走弯路。1. 方案选型先弄清楚主流高亮库的差异选库之前先明确一个道理代码高亮的本质是分词和着色。不同的库在分词精度、体积、插件生态上差别很大选错了后面坑很多。我前前后后用过highlight.js、prismjs、shiki这三个分别说下适用场景。1.1 highlight.js和prismjs怎么选highlight.js是被用最多的一个特点是开箱即用支持的语言有191种能自动检测语言类型。如果你的页面内容比较杂游客粘贴什么代码你无法预判用highlight.js会比较省心因为它有highlightAuto()方法可以猜语言。prismjs的定位则不太一样它更强调“按需加载”。默认核心很小语言语法、插件、主题都靠手动引入适合对包体积敏感的工程化项目。比如你做的组件库只需要展示JavaScript和CSS那prismjs可以只加载这几个语言模块体积控制相当可观。在Vue项目里面如果只是写个博客、做个临时后台页面我建议直接用highlight.js如果是构建正式的前端文档站、组件库文档、代码分享平台需要定制插件和完善的扩展机制那用prismjs更合适。shiki是后起之秀基于VS Code的TextMate语法高亮结果几乎和编辑器里一模一样但是体积偏大一般用在构建期静态生成运行期动态使用需要额外处理这块后面展开聊。1.2 为什么先考虑体积和加载方式选型时最容易忽略的是加载方式。很多朋友npm install完直接在main.js里全局import highlight.js/styles/github.css还用了全量语言包这样做页面能用但在低端设备上首屏会明显变慢。全量highlight.js的压缩体积大约在200KB左右语言包全部注册后内存占用也不小。更合理的思路是区分“构建期高亮”和“运行期高亮”。文档站的代码是死的可以在构建时就把高亮后的HTML生成出来运行时直接渲染零开销。后台管理系统的代码是动态从接口拿的只能运行时高亮那就需要按需加载语言包、动态注册。我在做技术卡片模块时就是让用户选择代码语言然后前端只有语言变化时才去加载对应的语言包这个优化对页面响应速度的提升非常明显。选库之前先想清楚自己的场景属于哪一种后面细节才好落地。2. 基于highlight.js的完整实现从安装到封装组件先拿highlight.js开刀因为它是大多数人最容易上手的选择。下面这套方案我实测过可以直接在Vue 3 Vite工程里用Vue 2写法逻辑相同只是生命周期和响应式写法要对应调整。2.1 安装与基础配置先装依赖建议把highlight.js装成运行时依赖npm install highlight.js在main.js里引入基础样式和一个默认主题我推荐github.css因为白底黑字阅读体验最舒服和绝大多数页面风格都能搭import { createApp } from vue import App from ./App.vue import highlight.js/styles/github.css createApp(App).mount(#app)这里有个容易被忽视的点样式的引入顺序。如果你是做组件库或者二次开发一定要确保高亮主题样式在业务自定义样式之前引入否则业务样式很容易覆盖掉高亮的配色。我遇到过好几次这种情况排查半天才发现是样式覆盖的问题。2.2 封装一个可复用的CodeBlock组件基础能力封装成一个组件比较合理。组件接收两个propscode表示要展示的代码文本language表示语言类型。核心机制是在模板里用v-html渲染高亮后的HTML同时在内部处理好代码变化的监听。下面是一个可以直接用的组件示例template div classcode-block div classcode-block__header span classcode-block__lang{{ language || text }}/span button classcode-block__copy clickhandleCopy复制/button /div precode v-htmlhighlightedCode/code/pre /div /template script setup import { ref, computed, watch } from vue import hljs from highlight.js/lib/core import javascript from highlight.js/lib/languages/javascript import typescript from highlight.js/lib/languages/typescript import xml from highlight.js/lib/languages/xml import css from highlight.js/lib/languages/css import bash from highlight.js/lib/languages/bash hljs.registerLanguage(javascript, javascript) hljs.registerLanguage(typescript, typescript) hljs.registerLanguage(xml, xml) hljs.registerLanguage(css, css) hljs.registerLanguage(bash, bash) const props defineProps({ code: { type: String, default: }, language: { type: String, default: text } }) const highlightedCode computed(() { if (!props.code) return if (props.language hljs.getLanguage(props.language)) { try { return hljs.highlight(props.code, { language: props.language }).value } catch (e) { console.error(代码高亮失败:, e) return escapeHtml(props.code) } } return escapeHtml(props.code) }) function escapeHtml(str) { return str .replace(//g, amp;) .replace(//g, lt;) .replace(//g, gt;) .replace(//g, quot;) .replace(//g, #039;) } function handleCopy() { navigator.clipboard.writeText(props.code).then(() { // 可以在这里做一个复制成功的轻提示 }) } /script注意我这里用的是hljs/ lib/core而不是直接import hljs from highlight.js因为后者会把190多种语言全部加载进来。用core模式后只手动注册常用语言包体积小了很多后面还要新增语言就在registerLanguage那里继续加即可。2.3 样式定制主题切换、行号、复制按钮高亮样式只是第一步代码块好不好看还得看整体设计。我做了三件事给代码块加圆角和阴影、设置内部的字体和行高、处理横向滚动。关键CSS参考如下.code-block { border-radius: 8px; overflow: hidden; background: #ffffff; border: 1px solid #e5e7eb; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); margin: 16px 0; } .code-block pre { margin: 0; padding: 16px; overflow-x: auto; font-family: JetBrains Mono, Fira Code, Consolas, Monaco, monospace; font-size: 14px; line-height: 1.7; } .code-block code { font-family: inherit; background: transparent; } .code-block__header { display: flex; justify-content: space-between; align-items: center; padding: 8px 16px; background: #f8f9fa; border-bottom: 1px solid #e5e7eb; font-size: 13px; color: #6b7280; } .code-block__copy { border: none; background: transparent; color: #6b7280; cursor: pointer; font-size: 13px; }行号这块比较讲究。最简单的方式是给每一行包一个span然后CSS里用counter做行号计数但这样行号和高亮标签会交叉嵌套处理起来很麻烦。实测下来更好用的是在pre里生成一个行号的兄弟节点两边一行对一行排列用flex或grid布局。这个后面在prismjs方案里会提到更成熟的做法因为prism有现成插件。复制按钮需要注意兼容性。navigator.clipboard.writeText要求页面在安全上下文里运行如果你的站点是http协议或者iframe内嵌可能会拿到undefined。遇到这种情况我一般用兜底的document.execCommand(copy)方案来实现核心思路是创建一个隐藏的textarea把代码放进去选中后执行copy命令。function handleCopy() { if (navigator.clipboard window.isSecureContext) { navigator.clipboard.writeText(props.code) } else { const textarea document.createElement(textarea) textarea.value props.code textarea.style.position fixed textarea.style.opacity 0 document.body.appendChild(textarea) textarea.select() document.execCommand(copy) document.body.removeChild(textarea) } }这个兜底方案是网上老代码里非常经典的写法我实测在大部分老旧浏览器和部分webview里都能正常运作建议直接抄进去。3. 基于prismjs的实现与差异对比如果你的项目对页面体积敏感或者需要行号、显示语言标签、行高亮、复制按钮这些增强功能prismjs会让你舒服很多因为这些东西在prism里都是现成插件不用自己造轮子去排版。3.1 prismjs的安装和配置要点安装命令npm install prismjs与highlight.js不同prismjs默认没有把所有语言包含进来。你想支持哪种语言就必须手动引入对应的语法文件。以Vue组件为例template pre classline-numberscode v-htmlhighlightedCode/code/pre /template script setup import { computed } from vue import Prism from prismjs import prismjs/components/prism-javascript import prismjs/components/prism-typescript import prismjs/components/prism-css import prismjs/components/prism-markup import prismjs/plugins/line-numbers/prism-line-numbers import prismjs/themes/prism-tomorrow.css const props defineProps({ code: { type: String, default: }, language: { type: String, default: javascript } }) const highlightedCode computed(() { if (!props.code) return const lang props.language || javascript if (Prism.languages[lang]) { return Prism.highlight(props.code, Prism.languages[lang], lang) } return escapeHtml(props.code) }) function escapeHtml(str) { return str.replace(//g, amp;).replace(//g, lt;).replace(//g, gt;) } /script这里要特别提醒prism的CSS插件文件prism-line-numbers.css一定要记得引入否则line-numbers类名不会生效。很多人引入插件JS却忘了CSS最后跑来问我为什么没有行号排查半天发现是样式漏了。3.2 行号插件和自定义样式prism-line-numbers插件的实现原理是给每个code元素用::before伪元素做行号渲染它会把代码按行拆分成单独的块然后让行号和代码行严格对齐。但这个插件有一个不太完美的地方当代码块出现横向滚动时行号区域可能跟着滚动走视觉上会错位。解决办法通常是给code加padding-left并把white-space设置成pre-wrap或者用CSS自定义覆盖行号区域的定位方式。我自己的做法是用一个overflow: hidden的外部容器和一个overflow: auto的滚动容器来嵌套行号区域固定在左侧不动代码区域独立滚动。这个方案在长代码行特别多的场景下效果明显比默认行为好但结构上会复杂一些适合有追求的组件库场景。3.3 两个库的对比总结对比维度highlight.jsprismjs开箱即用好190语言内置一般语言需要手动注册自动语言检测支持不支持体积控制平均水平有按需子模块优秀核心小按需加载插件生态较少丰富行号/高亮行/工具栏等主题风格多多实测推荐普通项目/博客文档站/组件库更直白一点如果时间紧任务重直接highlight.js如果后面有复杂的代码展示需求果断prismjs。两个库的API风格差异很大中途切换成本高选定了就尽量不要换。4. 进阶特性与性能优化让代码块真正好用高亮只是底座真正拉开体验差距的是行号、复制、语言标签这些细节还有动态加载和性能层面的优化。这一节讲点我自己踩坑后沉淀下来的实用方案。4.1 动态加载语言包减小体积很多Vue页面里的代码语言类型是由后端下发的前端不可能把所有语言一次性全部注册。这时可以结合Vue的异步组件特性在拿到语言类型后再去加载对应的语言包。以highlight.js为例可以在渲染前动态判断语言是否已注册没有注册就用动态import加载async function ensureLanguage(lang) { if (!lang) return false if (hljs.getLanguage(lang)) return true const languageMap { javascript: javascript, typescript: typescript, python: python, java: java, go: go, rust: rust } const moduleName languageMap[lang] if (!moduleName) return false try { const module await import(highlight.js/lib/languages/${moduleName}) hljs.registerLanguage(lang, module.default) return true } catch (e) { console.warn(语言 ${lang} 加载失败, e) return false } }用这种按需注册的方式后代码块的逻辑变为渲染前先await ensureLanguage(props.language)注册成功后再计算高亮HTML。首屏不需要的语言一个都不会加载对组件库和页面性能是非常友好的。4.2 代码复制功能的可靠性细节复制功能看似简单但涉及异步写入、用户反馈、异常处理三个环节任何一个没做好都会显得很粗糙。我在生产环境里的完整实现会考虑三件事第一复制成功之后给出视觉反馈。最简单的方式是按钮文字从“复制”变成“已复制”配合样式改变1.5秒后恢复。第二要捕获复制失败的情况在Promise的catch里提示操作失败。第三如果代码是异步加载或动态拼接的复制的时候要用当前最新的props.code避免用户看到的是A内容复制出来却是B内容。另外一个小建议复制内容最好保留原始代码文本而不是复制高亮后的innerHTML。因为高亮后的HTML里塞了一大堆span标签复制到编辑器里全是垃圾样式。我做过的版本里还见过直接把富文本带样式复制导致微信编辑器排版错乱的所以说这个坑真不是小问题。4.3 高阶定制shiki方案前瞻如果追求极致的渲染质量shiki是我目前推荐的方向。它使用VS Code同款的TextMate语法分词支持几乎所有eclipse词法描述渲染效果和编辑器里几乎无差异。但它不适合运行时同步调用因为初始化成本高语法文件大更推荐两条路一是构建时高亮比如在Vite插件里把Markdown中的代码块一次性转成带高亮类名的静态HTML运行时零成本。二是使用Web Worker异步高亮把shiki初始化放到后台线程避免阻塞主线程渲染代价是接入复杂度明显增加。我的个人判断是对于普通Vue应用prismjs已经是能力上限附近的最优解如果你维护的是文档站或组件库值得为shiki多花点时间因为静态站点构建时用shiki是零成本白嫖VS Code渲染质量这种体验提升是prismjs给不了的。5. 常见问题与排查技巧实录这是很多朋友私信问得最多的部分我把实际操作中遇到的高频问题整理出来基本覆盖了从部署到运行的重点场景。5.1 样式不生效的几大原因高亮样式不生效排查顺序非常重要。第一步先打开浏览器开发者工具看code元素上有没有生成类似hljs-keyword的类名。如果没有说明高亮逻辑根本没执行如果有但颜色不对就是CSS被覆盖或者主题样式没加载。我遇到过的最典型的场景是这样的组件里写了scoped样式而highlight.js生成的高亮类名是动态添加的scoped会把这些类名加上>
返回列表