前言

最近在写AI训练器的UI界面,有一个需求就是展示TOML配置,为此就需要用到代码高亮,我之前用过 highlight.jsPrismJS,但是在撰写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 这种属性,以防有其他人也碰到这样的问题,特此记录一下。

分类: vue 项目实战 标签: 代码高亮异步vue3shikiESM

评论

暂无评论数据

暂无评论数据

目录