Skip to content

代码高亮

为代码块添加语法高亮,提升代码可读性和展示效果。

Shiki

基本信息

特点

  • ✅ 使用 VS Code 的语法引擎,高亮精准度高
  • ✅ 支持 100+ 种编程语言
  • ✅ 内置多种 VS Code 主题
  • ✅ 支持行高亮、差异高亮
  • ✅ 零运行时依赖,生成纯 HTML + CSS
  • ✅ 支持 SSR(服务端渲染)
  • ✅ 支持自定义主题和语言
  • ✅ TypeScript 类型支持完善

安装

bash
pnpm add shiki
bash
bun add shiki
bash
npm install shiki
bash
yarn add shiki

基础用法

基本高亮

typescript
import { codeToHtml } from 'shiki'

const code = `
function hello() {
  console.log('Hello, Shiki!')
}
`

const html = await codeToHtml(code, {
  lang: 'javascript',
  theme: 'vitesse-dark'
})

console.log(html)

在 Vue 中使用

vue
<template>
  <div v-html="highlightedCode" class="rounded-lg overflow-auto"></div>
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { codeToHtml } from 'shiki'

const highlightedCode = ref('')

const code = `
const greeting = 'Hello, World!'
console.log(greeting)
`

onMounted(async () => {
  highlightedCode.value = await codeToHtml(code, {
    lang: 'typescript',
    theme: 'github-dark'
  })
})
</script>

多主题支持

typescript
import { codeToHtml } from 'shiki'

const code = 'const a = 1'

const html = await codeToHtml(code, {
  lang: 'javascript',
  themes: {
    light: 'github-light',
    dark: 'github-dark'
  }
})

// 生成的 HTML 包含两套主题的样式
// 可以通过 CSS 媒体查询切换

对应的 CSS:

css
@media (prefers-color-scheme: dark) {
  .shiki.github-light {
    display: none;
  }
}

@media (prefers-color-scheme: light) {
  .shiki.github-dark {
    display: none;
  }
}

进阶用法

行高亮

typescript
import { codeToHtml } from 'shiki'

const code = `
function add(a, b) {
  return a + b
}

const result = add(1, 2)
console.log(result)
`

const html = await codeToHtml(code, {
  lang: 'javascript',
  theme: 'nord',
  decorations: [
    {
      // 高亮第 2-3 行
      start: { line: 1, character: 0 },
      end: { line: 2, character: 0 },
      properties: { class: 'highlighted' }
    }
  ]
})

配合 CSS:

css
.highlighted {
  background-color: rgba(255, 255, 0, 0.1);
  display: block;
  margin: 0 -1rem;
  padding: 0 1rem;
}

差异高亮

typescript
import { codeToHtml } from 'shiki'

const code = `
function hello() {
  console.log('Hello')
  console.log('World')
}
`

const html = await codeToHtml(code, {
  lang: 'javascript',
  theme: 'github-dark',
  decorations: [
    {
      start: { line: 1, character: 0 },
      end: { line: 1, character: 100 },
      properties: { class: 'diff-remove' }
    },
    {
      start: { line: 2, character: 0 },
      end: { line: 2, character: 100 },
      properties: { class: 'diff-add' }
    }
  ]
})

CSS 样式:

css
.diff-remove {
  background-color: rgba(244, 63, 94, 0.15);
  display: block;
}

.diff-add {
  background-color: rgba(34, 197, 94, 0.15);
  display: block;
}

代码编辑器集成

vue
<template>
  <div class="relative w-full">
    <textarea 
      v-model="code" 
      @input="handleCodeChange"
      class="absolute inset-0 w-full h-full opacity-0 z-10 font-mono"
    ></textarea>
    <div 
      v-html="highlightedCode" 
      class="relative z-0 pointer-events-none"
    ></div>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { codeToHtml } from 'shiki'
import { debounce } from 'lodash-es'

const code = ref('console.log("Hello")')
const highlightedCode = ref('')

const updateHighlight = async (newCode: string) => {
  highlightedCode.value = await codeToHtml(newCode, {
    lang: 'javascript',
    theme: 'vitesse-dark'
  })
}

// 防抖处理,避免频繁高亮
const handleCodeChange = debounce(async () => {
  await updateHighlight(code.value)
}, 300)

// 初始化
updateHighlight(code.value)
</script>

自定义主题

typescript
import { codeToHtml } from 'shiki'

const customTheme = {
  name: 'custom-theme',
  colors: {
    'editor.background': '#1e1e1e',
    'editor.foreground': '#d4d4d4'
  },
  tokenColors: [
    {
      scope: ['comment'],
      settings: {
        foreground: '#6A9955'
      }
    },
    {
      scope: ['string'],
      settings: {
        foreground: '#CE9178'
      }
    },
    {
      scope: ['keyword'],
      settings: {
        foreground: '#569CD6'
      }
    }
  ]
}

const html = await codeToHtml('const a = 1', {
  lang: 'javascript',
  theme: customTheme
})

完整示例

点击查看完整的代码展示组件
vue
<template>
  <div class="border border-gray-200 rounded-lg overflow-hidden">
    <div class="flex gap-2.5 p-3 bg-gray-50 border-b border-gray-200">
      <select 
        v-model="selectedLang" 
        @change="updateHighlight"
        class="px-3 py-1.5 border border-gray-300 rounded bg-white cursor-pointer"
      >
        <option value="javascript">JavaScript</option>
        <option value="typescript">TypeScript</option>
        <option value="vue">Vue</option>
        <option value="python">Python</option>
        <option value="go">Go</option>
      </select>
      
      <select 
        v-model="selectedTheme" 
        @change="updateHighlight"
        class="px-3 py-1.5 border border-gray-300 rounded bg-white cursor-pointer"
      >
        <option value="github-dark">GitHub Dark</option>
        <option value="github-light">GitHub Light</option>
        <option value="vitesse-dark">Vitesse Dark</option>
        <option value="nord">Nord</option>
        <option value="one-dark-pro">One Dark Pro</option>
      </select>

      <button 
        @click="copyCode" 
        class="px-3 py-1.5 border border-gray-300 rounded bg-white cursor-pointer hover:bg-gray-100"
      >
        {{ copied ? '已复制' : '复制代码' }}
      </button>
    </div>

    <div v-html="highlightedCode" class="overflow-auto max-h-[500px] [&_pre]:m-0 [&_pre]:p-4"></div>
  </div>
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { codeToHtml } from 'shiki'

const selectedLang = ref('javascript')
const selectedTheme = ref('github-dark')
const highlightedCode = ref('')
const copied = ref(false)

const code = ref(`
function fibonacci(n) {
  if (n <= 1) return n
  return fibonacci(n - 1) + fibonacci(n - 2)
}

const result = fibonacci(10)
console.log('Fibonacci(10):', result)
`)

const updateHighlight = async () => {
  try {
    highlightedCode.value = await codeToHtml(code.value, {
      lang: selectedLang.value,
      theme: selectedTheme.value
    })
  } catch (error) {
    console.error('高亮失败:', error)
  }
}

const copyCode = async () => {
  try {
    await navigator.clipboard.writeText(code.value)
    copied.value = true
    setTimeout(() => {
      copied.value = false
    }, 2000)
  } catch (error) {
    console.error('复制失败:', error)
  }
}

onMounted(() => {
  updateHighlight()
})
</script>

最佳实践

1. 性能优化

使用单例模式

typescript
import { createHighlighter } from 'shiki'

let highlighter: Awaited<ReturnType<typeof createHighlighter>> | null = null

export async function getHighlighter() {
  if (!highlighter) {
    highlighter = await createHighlighter({
      themes: ['github-dark', 'github-light'],
      langs: ['javascript', 'typescript', 'vue', 'python']
    })
  }
  return highlighter
}
typescript
import { getHighlighter } from './highlighter'

const highlighter = await getHighlighter()
const html = highlighter.codeToHtml(code, {
  lang: 'javascript',
  theme: 'github-dark'
})

2. 按需加载语言

typescript
import { createHighlighter } from 'shiki'

const highlighter = await createHighlighter({
  themes: ['github-dark'],
  langs: [] // 初始不加载任何语言
})

// 按需加载
await highlighter.loadLanguage('javascript')
await highlighter.loadLanguage('python')

3. 服务端渲染

typescript
// 在服务端预渲染
import { codeToHtml } from 'shiki'

export async function generateCodeHTML(code: string, lang: string) {
  return await codeToHtml(code, {
    lang,
    theme: 'github-dark'
  })
}

4. 错误处理

typescript
import { codeToHtml } from 'shiki'

async function highlightCode(code: string, lang: string) {
  try {
    return await codeToHtml(code, {
      lang,
      theme: 'github-dark'
    })
  } catch (error) {
    console.error('高亮失败:', error)
    // 降级处理:返回纯文本
    return `<pre><code>${escapeHtml(code)}</code></pre>`
  }
}

function escapeHtml(text: string) {
  return text
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#039;')
}

5. 缓存策略

typescript
const codeCache = new Map<string, string>()

async function highlightWithCache(code: string, lang: string, theme: string) {
  const key = `${lang}-${theme}-${code}`
  
  if (codeCache.has(key)) {
    return codeCache.get(key)!
  }
  
  const html = await codeToHtml(code, { lang, theme })
  codeCache.set(key, html)
  
  return html
}

Highlight.js

基本信息

特点

  • ✅ 支持 190+ 种编程语言
  • ✅ 自动语言检测
  • ✅ 90+ 种配色方案
  • ✅ 轻量级,核心库仅 ~12KB (gzipped)
  • ✅ 支持 Node.js 和浏览器
  • ✅ 无依赖
  • ✅ 支持自定义语言和样式
  • ✅ 活跃的社区维护

安装

bash
pnpm add highlight.js
bash
bun add highlight.js
bash
npm install highlight.js
bash
yarn add highlight.js

基础用法

基本高亮

typescript
import hljs from 'highlight.js'
import 'highlight.js/styles/github-dark.css'

const code = `
function hello() {
  console.log('Hello, Highlight.js!')
}
`

const highlighted = hljs.highlight(code, { 
  language: 'javascript' 
}).value

console.log(highlighted)

在 Vue 中使用

vue
<template>
  <div>
    <pre><code 
      ref="codeRef" 
      class="language-javascript"
    >{{ code }}</code></pre>
  </div>
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue'
import hljs from 'highlight.js'
import 'highlight.js/styles/atom-one-dark.css'

const codeRef = ref<HTMLElement>()
const code = `
const greeting = 'Hello, World!'
console.log(greeting)
`

onMounted(() => {
  if (codeRef.value) {
    hljs.highlightElement(codeRef.value)
  }
})
</script>

自动语言检测

typescript
import hljs from 'highlight.js'

const code = 'const a = 1'

// 自动检测语言
const result = hljs.highlightAuto(code)

console.log('检测到的语言:', result.language)
console.log('高亮结果:', result.value)

按需引入语言

typescript
// 只引入需要的语言,减小打包体积
import hljs from 'highlight.js/lib/core'
import javascript from 'highlight.js/lib/languages/javascript'
import python from 'highlight.js/lib/languages/python'
import 'highlight.js/styles/github-dark.css'

hljs.registerLanguage('javascript', javascript)
hljs.registerLanguage('python', python)

const code = 'console.log("Hello")'
const highlighted = hljs.highlight(code, { language: 'javascript' }).value

进阶用法

全局自动高亮

vue
<template>
  <div class="content" v-html="htmlContent"></div>
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue'
import hljs from 'highlight.js'
import 'highlight.js/styles/monokai.css'

const htmlContent = ref(`
  <pre><code class="language-javascript">
    function test() {
      console.log('test')
    }
  </code></pre>
`)

onMounted(() => {
  // 高亮所有代码块
  document.querySelectorAll('pre code').forEach((block) => {
    hljs.highlightElement(block as HTMLElement)
  })
})
</script>

行号支持

vue
<template>
  <div class="flex bg-[#1e1e1e] rounded-lg overflow-hidden">
    <div class="flex flex-col py-4 px-2 bg-[#252525] text-[#858585] text-right select-none font-mono text-sm leading-6">
      <span v-for="n in lineCount" :key="n" class="block">{{ n }}</span>
    </div>
    <pre class="m-0 flex-1"><code 
      ref="codeRef"
      class="language-javascript block p-4"
    >{{ code }}</code></pre>
  </div>
</template>

<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import hljs from 'highlight.js'
import 'highlight.js/styles/github-dark.css'

const codeRef = ref<HTMLElement>()
const code = `function hello() {
  console.log('Hello')
  return true
}

const result = hello()`

const lineCount = computed(() => code.split('\n').length)

onMounted(() => {
  if (codeRef.value) {
    hljs.highlightElement(codeRef.value)
  }
})
</script>

代码高亮插件

typescript
// 自定义插件:添加复制按钮
import hljs from 'highlight.js'

hljs.addPlugin({
  'after:highlightElement': ({ el, result }) => {
    const wrapper = document.createElement('div')
    wrapper.className = 'code-wrapper'
    
    const copyBtn = document.createElement('button')
    copyBtn.className = 'copy-button'
    copyBtn.textContent = '复制'
    copyBtn.onclick = () => {
      navigator.clipboard.writeText(el.textContent || '')
      copyBtn.textContent = '已复制'
      setTimeout(() => {
        copyBtn.textContent = '复制'
      }, 2000)
    }
    
    el.parentNode?.insertBefore(wrapper, el)
    wrapper.appendChild(copyBtn)
    wrapper.appendChild(el)
  }
})

自定义语言定义

typescript
import hljs from 'highlight.js/lib/core'

// 定义自定义语言
hljs.registerLanguage('mylang', (hljs) => {
  return {
    keywords: 'if else while for function',
    contains: [
      hljs.QUOTE_STRING_MODE,
      hljs.C_LINE_COMMENT_MODE,
      hljs.C_BLOCK_COMMENT_MODE,
      {
        className: 'number',
        begin: '\\b\\d+(\\.\\d+)?'
      }
    ]
  }
})

const code = 'function test() { if (true) { } }'
const result = hljs.highlight(code, { language: 'mylang' })

完整示例

点击查看完整的代码查看器组件
vue
<template>
  <div class="border border-[#3a3a3a] rounded-lg overflow-hidden bg-[#282c34]">
    <div class="flex justify-between items-center bg-[#21252b] border-b border-[#3a3a3a] p-2 px-3">
      <div class="flex gap-1">
        <button
          v-for="file in files"
          :key="file.name"
          :class="[
            'px-3 py-1.5 bg-transparent border-none cursor-pointer rounded transition-all',
            currentFile === file.name 
              ? 'bg-[#282c34] text-[#61afef]' 
              : 'text-[#abb2bf] hover:bg-[#2c313a]'
          ]"
          @click="currentFile = file.name"
        >
          {{ file.name }}
        </button>
      </div>
      <button 
        @click="copyCurrentCode" 
        class="px-3 py-1 bg-[#3a3f4b] border border-[#4b5263] text-[#abb2bf] rounded cursor-pointer text-xs hover:bg-[#4b5263]"
      >
        {{ copied ? '✓ 已复制' : '复制' }}
      </button>
    </div>

    <div class="flex overflow-x-auto">
      <div class="flex flex-col py-4 px-2 bg-[#21252b] text-[#5c6370] text-right select-none font-mono text-sm leading-6 min-w-[40px]">
        <span v-for="n in currentLineCount" :key="n" class="block">{{ n }}</span>
      </div>
      <pre class="m-0 flex-1 bg-[#282c34]"><code 
        ref="codeRef"
        :class="`language-${currentLanguage} block p-4 font-mono text-sm leading-6`"
      >{{ currentCode }}</code></pre>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, computed, watch, onMounted } 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 css from 'highlight.js/lib/languages/css'
import 'highlight.js/styles/atom-one-dark.css'

hljs.registerLanguage('javascript', javascript)
hljs.registerLanguage('typescript', typescript)
hljs.registerLanguage('css', css)

interface CodeFile {
  name: string
  language: string
  code: string
}

const files = ref<CodeFile[]>([
  {
    name: 'index.ts',
    language: 'typescript',
    code: `import { createApp } from 'vue'
import App from './App.vue'

const app = createApp(App)
app.mount('#app')`
  },
  {
    name: 'App.vue',
    language: 'javascript',
    code: `<template>
  <div class="app">
    <h1>Hello Vue!</h1>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'

const message = ref('Hello')
</script>`
  },
  {
    name: 'style.css',
    language: 'css',
    code: `.app {
  padding: 20px;
  font-family: sans-serif;
}

h1 {
  color: #42b983;
}`
  }
])

const currentFile = ref('index.ts')
const codeRef = ref<HTMLElement>()
const copied = ref(false)

const currentFileData = computed(() => 
  files.value.find(f => f.name === currentFile.value)!
)

const currentCode = computed(() => currentFileData.value.code)
const currentLanguage = computed(() => currentFileData.value.language)
const currentLineCount = computed(() => currentCode.value.split('\n').length)

const highlightCode = () => {
  if (codeRef.value) {
    hljs.highlightElement(codeRef.value)
  }
}

const copyCurrentCode = async () => {
  try {
    await navigator.clipboard.writeText(currentCode.value)
    copied.value = true
    setTimeout(() => {
      copied.value = false
    }, 2000)
  } catch (error) {
    console.error('复制失败:', error)
  }
}

watch(currentFile, () => {
  setTimeout(highlightCode, 0)
})

onMounted(() => {
  highlightCode()
})
</script>

最佳实践

1. 按需加载

typescript
// 只加载需要的语言
import hljs from 'highlight.js/lib/core'
import javascript from 'highlight.js/lib/languages/javascript'
import typescript from 'highlight.js/lib/languages/typescript'

hljs.registerLanguage('javascript', javascript)
hljs.registerLanguage('typescript', typescript)

2. 性能优化

typescript
// 使用 Web Worker 处理大文件
const worker = new Worker('/highlight-worker.js')

worker.postMessage({
  code: largeCodeString,
  language: 'javascript'
})

worker.onmessage = (e) => {
  const highlighted = e.data
  // 更新 UI
}

3. 主题切换

vue
<script setup lang="ts">
import { ref, watch } from 'vue'

const theme = ref<'light' | 'dark'>('dark')

watch(theme, (newTheme) => {
  // 动态加载主题
  const link = document.getElementById('hljs-theme') as HTMLLinkElement
  if (link) {
    link.href = newTheme === 'dark' 
      ? '/node_modules/highlight.js/styles/atom-one-dark.css'
      : '/node_modules/highlight.js/styles/atom-one-light.css'
  }
})
</script>

4. 安全处理

typescript
import hljs from 'highlight.js'

// 转义 HTML,防止 XSS
function escapeHtml(text: string) {
  return text
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
}

const userCode = getUserInput() // 用户输入的代码
const safeCode = escapeHtml(userCode)
const highlighted = hljs.highlight(safeCode, { language: 'javascript' }).value

5. 语言别名

typescript
import hljs from 'highlight.js/lib/core'
import javascript from 'highlight.js/lib/languages/javascript'

hljs.registerLanguage('javascript', javascript)
hljs.registerLanguage('js', javascript) // 添加别名
hljs.registerAliases('jsx', { languageName: 'javascript' })

对比总结

Shiki vs Highlight.js

特性ShikiHighlight.js
高亮精度⭐⭐⭐⭐⭐ 使用 VS Code 引擎⭐⭐⭐⭐ 基于正则表达式
主题VS Code 主题90+ 预设主题
语言支持100+190+
包体积较大 (~500KB)小 (~12KB core)
运行时零运行时 (生成 HTML)需要运行时高亮
SSR✅ 完美支持⚠️ 需要特殊处理
自动检测❌ 不支持✅ 支持
性能构建时高亮快运行时高亮快
学习曲线中等简单

使用建议

选择 Shiki 的场景

  • 需要最精准的语法高亮
  • 使用 SSR/SSG(如 VitePress、Nuxt)
  • 喜欢 VS Code 主题风格
  • 代码在构建时已知

选择 Highlight.js 的场景

  • 需要自动语言检测
  • 运行时动态高亮
  • 对包体积敏感
  • 需要简单快速集成
  • 用户生成的内容高亮

相关资源

Shiki:

Highlight.js: