vue3中使用shiki实现代码高亮和注意事项
前言
最近在写AI训练器的UI界面,有一个需求就是展示TOML配置,为此就需要用到代码高亮,我之前用过 highlight.js
和 PrismJS
,但是在撰写VitePress项目配置时发现了一个新的代码高亮库:shiki
。
shiki可以省去维护css和html,并且原生就是ESM按需加载,支持多种主题等,已经满足了我对一个代码高亮的需求,为此我去尝试用了下这个,于是有了本篇文章。
插件文档:Shiki语法高亮器
github:shiki
注意事项
shiki是按需加载的,它的实现方式就是除了一个主体的js文件,在打包后会有非常多的不同代码语言的解析器脚本文件,但是不用担心,这些脚本文件只有在真实使用的时候会加载,只是预先生成了罢了。
如果你使用了 vite-bundle-analyzer
这类的分析库,可能会导致你看到的包体大小非常大,解决办法就是你可以使用CDN的方式来做,具体官方有文档,可以自行研究,我个人觉得这不是什么大的问题。
效果图
教程
我们先安装插件:
pnpm add -D shiki
安装完成后就可以创建vue组件来使用代码高亮,这里我直接贴出我的组件代码:
<template>
<div class="toml-preview">
<div class="toml-preview-content" v-html="tomlHtml"></div>
</div>
</template>
<script setup lang="ts">
import { createHighlighter } from "shiki";
import type { Highlighter } from "shiki";
import { useAppStore } from "@/stores";
export interface TomlPreviewProps {
toml: string;
}
const instance = getCurrentInstance();
const props = defineProps<TomlPreviewProps>();
const appStore = useAppStore();
const isDark = storeToRefs(appStore).isDark;
const highlighter = ref<Highlighter>();
const tomlHtml = ref("");
const isInitializing = ref(false);
/** 初始化 */
async function init() {
try {
isInitializing.value = true;
highlighter.value = await createHighlighter({
themes: ["github-light", "github-dark"],
langs: ["toml"]
});
isInitializing.value = false;
if (instance?.isUnmounted) highlighterDispose();
highlighterToml();
} catch (error) {
isInitializing.value = false;
console.error("初始化Toml预览失败:", error);
}
}
/** 销毁 */
function highlighterDispose() {
highlighter.value?.dispose();
highlighter.value = void 0;
}
/** 生成高亮html */
function highlighterToml() {
if (!highlighter.value) return;
tomlHtml.value = highlighter.value.codeToHtml(props.toml, {
lang: "toml",
theme: isDark.value ? "github-dark" : "github-light"
});
}
/** watch防抖 */
const watchFn = useDebounceFn(async () => {
if (isInitializing.value) return;
if (!highlighter.value) {
await init();
if (!highlighter.value) return; // Still no highlighter available
}
highlighterToml();
}, 300);
watch([() => props.toml, isDark], watchFn);
init();
onUnmounted(() => {
highlighterDispose();
});
</script>
<style lang="scss" scoped>
.toml-preview {
height: 100%;
background-color: var(--zl-toml-preview-bg);
border-radius: $zl-border-radius;
padding: $zl-padding 0 $zl-padding $zl-padding;
}
.toml-preview-content {
height: 100%;
overflow: auto;
}
.toml-preview-content :deep(.shiki) {
font-size: 14px;
line-height: 1.5;
min-height: 4em;
white-space: pre;
border-width: 1px;
border-color: #9ca3af4d;
border-radius: 0.25rem;
background-color: var(--zl-toml-preview-code-bg) !important;
}
</style>
首先声明了一个props类型,要求传入 toml 代码文本,组件会将这个代码文本转换成代码高亮的html元素并渲染。
由于我的项目是支持 dark/light 两种主题模式,所以我从pinia仓库引入了useAppStore
来获取当前主题模式是dark还是light。
剩下的就是代码高亮的逻辑,创建了 tomlHtml
变量接收高亮后的html元素文本,再通过 v-html
渲染。
组件watch监听toml文本或者isDark是否发生变化,变化了就重新生代码高亮文本成并渲染。
需要注意的是 createHighlighter
创建的 shiki 实例是可以重复使用的,官方也推荐你复用这一个实例,所以我会判断,如果实例存在就不会重复 init
初始化了,反之,如果组件被销毁了就会将实例也进行销毁。
这里比较有意思的就是 createHighlighter
它是一个异步的操作,所以我们除了在 onUnmounted
做实例销毁的处理,还需要在 init 中判断组件是否销毁,因为有可能用户显示了这个组件又很快的不显示了,这时候 init 触发了,init调用createHighlighter又是一个异步的,导致组件不显示后这个异步才完成,但是在完成之前onUnmounted就已经被触发了,这就导致了这个创建的实例无法被销毁。
为了解决这个问题,我们可以通过 getCurrentInstance
获取当前组件实例对象,通过对象上的 isUnmounted
来判断组件是不是被卸载了,卸载了我们就进行销毁实例处理。
基本逻辑就是这样了,这也是我第一次使用 isUnmounted
这种属性,以防有其他人也碰到这样的问题,特此记录一下。
本文系作者 @木灵鱼儿 原创发布在木灵鱼儿站点。未经许可,禁止转载。
暂无评论数据