Uniapp 鸿蒙实战之 AtomGit APP - 代码高亮与 Markdown 渲染
·

目录
一、功能需求分析
1.1 核心功能
在开发 AtomGit APP 时,代码高亮和 Markdown 渲染是最核心的功能之一。用户需要能够流畅地浏览代码文件,并且能够阅读格式化的 Markdown 文档。具体需求包括:
代码高亮需求:
- 支持主流编程语言(JavaScript、Python、Java、Go、Rust 等 100+ 种语言)
- 代码行号显示,方便定位和讨论
- 一键复制代码功能,提升开发效率
- 支持主题切换(明暗模式)
Markdown 渲染需求:
- 支持 GitHub Flavored Markdown(GFM)规范
- 正确渲染代码块、表格、列表等复杂元素
- 支持数学公式、流程图等扩展语法
- 防止 XSS 攻击,确保内容安全
1.2 用户体验目标
根据用户反馈和行业最佳实践,我们设定了以下体验目标:
- 代码渲染速度 < 500ms(针对 1000 行以内的文件)
- 支持 100+ 种编程语言
- 内存占用优化,避免大文件导致应用卡顿
- 主题切换流畅,无闪烁
二、技术选型对比
2.1 代码高亮库对比
在技术选型阶段,我们对比了主流的代码高亮库:
| 特性 | Prism.js | Highlight.js | Shiki |
|---|---|---|---|
| 体积 | 小(按需加载) | 中等 | 大 |
| 语言支持 | 200+ | 190+ | 180+ |
| 主题 | 丰富(8 个官方主题) | 较多 | VSCode 主题 |
| 插件生态 | 丰富 | 中等 | 少 |
| 性能 | 优秀 | 良好 | 优秀 |
| 浏览器兼容性 | 优秀 | 优秀 | 中等 |
经过综合评估,我们选择 Prism.js,原因如下:
- 体积优势:支持按需加载语言包,初始加载体积小
- 插件丰富:提供行号显示、工具栏、复制按钮等插件
- 性能优异:纯 JavaScript 实现,渲染速度快
- 社区活跃:文档完善,问题容易解决
2.2 Markdown 解析器对比
| 特性 | marked.js | markdown-it | remark |
|---|---|---|---|
| 体积 | 小 | 中等 | 大 |
| 性能 | 快 | 中等 | 中等 |
| 扩展性 | 好 | 优秀 | 优秀 |
| GFM 支持 | 好 | 需插件 | 需插件 |
| 学习曲线 | 低 | 中 | 高 |
我们选择 marked.js,因为它体积小、性能好、易于集成。同时配合 DOMPurify 进行 HTML 净化,确保安全性。
三、代码高亮组件实现
3.1 基础组件封装
首先,我们创建一个可复用的代码高亮组件 components/code-highlight.vue:
<template>
<view class="code-container">
<!-- 代码头部:语言标签和复制按钮 -->
<view class="code-header">
<view class="language-tag">
<text class="language-name">{{ language }}</text>
<text class="line-count">{{ lineCount }} 行</text>
</view>
<button class="copy-btn" @click="handleCopy">
<text class="btn-icon">{{ copyIcon }}</text>
<text class="btn-text">{{ copyText }}</text>
</button>
</view>
<!-- 代码内容区域 -->
<scroll-view class="code-wrapper" scroll-x="true">
<view class="code-content">
<rich-text :nodes="highlightedCode"></rich-text>
</view>
</scroll-view>
</view>
</template>
<script>
import Prism from 'prismjs'
import 'prismjs/themes/prism-tomorrow.css'
// 导入行号插件
import 'prismjs/plugins/line-numbers/prism-line-numbers.css'
import 'prismjs/plugins/line-numbers/prism-line-numbers.js'
export default {
name: 'CodeHighlight',
props: {
// 代码内容
code: {
type: String,
required: true
},
// 编程语言
language: {
type: String,
default: 'javascript'
},
// 是否显示行号
showLineNumbers: {
type: Boolean,
default: true
}
},
data() {
return {
copyText: '复制',
copyIcon: '📋'
}
},
computed: {
// 高亮后的代码
highlightedCode() {
if (!this.code) return ''
try {
const grammar = Prism.languages[this.language] || Prism.languages.javascript
const highlighted = Prism.highlight(this.code, grammar, this.language)
// 如果需要行号,添加 line-numbers 类
if (this.showLineNumbers) {
return `<pre class="line-numbers"><code class="language-${this.language}">${highlighted}</code></pre>`
}
return `<pre><code class="language-${this.language}">${highlighted}</code></pre>`
} catch (error) {
console.error('代码高亮失败:', error)
return `<pre><code>${this.code}</code></pre>`
}
},
// 代码行数
lineCount() {
return this.code ? this.code.split('\n').length : 0
}
},
methods: {
// 复制代码
async handleCopy() {
try {
await uni.setClipboardData({
data: this.code
})
this.copyText = '已复制'
this.copyIcon = '✅'
uni.showToast({
title: '代码已复制',
icon: 'success',
duration: 2000
})
// 2秒后恢复
setTimeout(() => {
this.copyText = '复制'
this.copyIcon = '📋'
}, 2000)
} catch (error) {
console.error('复制失败:', error)
uni.showToast({
title: '复制失败',
icon: 'none'
})
}
}
}
}
</script>
3.2 动态语言加载
为了优化初始加载性能,我们实现了按需加载语言包的功能:
// utils/language-loader.js
// 语言映射表
const languageMap = {
javascript: () => import('prismjs/components/prism-javascript'),
js: () => import('prismjs/components/prism-javascript'),
typescript: () => import('prismjs/components/prism-typescript'),
ts: () => import('prismjs/components/prism-typescript'),
python: () => import('prismjs/components/prism-python'),
py: () => import('prismjs/components/prism-python'),
java: () => import('prismjs/components/prism-java'),
go: () => import('prismjs/components/prism-go'),
rust: () => import('prismjs/components/prism-rust'),
c: () => import('prismjs/components/prism-c'),
cpp: () => import('prismjs/components/prism-cpp'),
'c++': () => import('prismjs/components/prism-cpp'),
css: () => import('prismjs/components/prism-css'),
html: () => import('prismjs/components/prism-markup'),
vue: () => import('prismjs/components/prism-vue'),
react: () => import('prismjs/components/prism-jsx'),
json: () => import('prismjs/components/prism-json'),
yaml: () => import('prismjs/components/prism-yaml'),
markdown: () => import('prismjs/components/prism-markdown'),
bash: () => import('prismjs/components/prism-bash'),
shell: () => import('prismjs/components/prism-bash')
}
// 已加载的语言缓存
const loadedLanguages = new Set()
/**
* 动态加载语言包
* @param {string} language - 语言名称
* @returns {Promise<boolean>} - 加载结果
*/
export async function loadLanguage(language) {
const normalizedLang = language.toLowerCase()
// 如果已经加载过,直接返回
if (loadedLanguages.has(normalizedLang)) {
return true
}
// 查找加载函数
const loader = languageMap[normalizedLang]
if (!loader) {
console.warn(`不支持的语言: ${language}`)
return false
}
try {
await loader()
loadedLanguages.add(normalizedLang)
console.log(`语言包加载成功: ${language}`)
return true
} catch (error) {
console.error(`语言包加载失败: ${language}`, error)
return false
}
}
/**
* 批量加载语言包
* @param {string[]} languages - 语言列表
*/
export async function loadLanguages(languages) {
const promises = languages.map(lang => loadLanguage(lang))
await Promise.allSettled(promises)
}
3.3 使用示例
在页面中使用代码高亮组件:
<template>
<view class="code-page">
<code-highlight
:code="codeContent"
:language="language"
:show-line-numbers="true"
/>
</view>
</template>
<script>
import CodeHighlight from '@/components/code-highlight.vue'
import { loadLanguage } from '@/utils/language-loader'
export default {
components: {
CodeHighlight
},
data() {
return {
codeContent: '',
language: 'javascript'
}
},
async onLoad(options) {
// 获取文件内容
this.codeContent = await this.fetchFileContent(options.fileUrl)
this.language = this.detectLanguage(options.fileName)
// 动态加载语言包
await loadLanguage(this.language)
},
methods: {
// 检测语言类型
detectLanguage(fileName) {
const ext = fileName.split('.').pop().toLowerCase()
const langMap = {
js: 'javascript',
ts: 'typescript',
py: 'python',
java: 'java',
go: 'go',
rs: 'rust'
}
return langMap[ext] || 'javascript'
}
}
}
</script>
四、Markdown 渲染实现
4.1 基础配置
首先安装必要的依赖:
npm install marked dompurify
创建 Markdown 渲染组件 components/markdown-renderer.vue:
<template>
<view class="markdown-container">
<rich-text :nodes="renderedContent" class="markdown-content"></rich-text>
</view>
</template>
<script>
import { marked } from 'marked'
import DOMPurify from 'dompurify'
// 配置 marked
marked.setOptions({
breaks: true, // 支持 GitHub 风格的换行
gfm: true, // 启用 GitHub Flavored Markdown
headerIds: true, // 为标题添加 ID
mangle: false, // 不混淆邮箱地址
sanitize: false // 使用 DOMPurify 进行净化
})
export default {
name: 'MarkdownRenderer',
props: {
content: {
type: String,
required: true
}
},
computed: {
renderedContent() {
if (!this.content) return ''
try {
// 解析 Markdown
const rawHtml = marked(this.content)
// 使用 DOMPurify 净化 HTML,防止 XSS 攻击
const cleanHtml = DOMPurify.sanitize(rawHtml, {
ALLOWED_TAGS: [
'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
'p', 'br', 'hr',
'ul', 'ol', 'li',
'blockquote', 'pre', 'code',
'strong', 'em', 'del', 'ins',
'a', 'img',
'table', 'thead', 'tbody', 'tr', 'th', 'td',
'div', 'span'
],
ALLOWED_ATTR: [
'href', 'src', 'alt', 'title', 'class', 'id',
'target', 'rel'
]
})
return cleanHtml
} catch (error) {
console.error('Markdown 渲染失败:', error)
return `<p>${this.content}</p>`
}
}
}
}
</script>
4.2 样式优化
为了提供更好的阅读体验,我们需要为 Markdown 内容添加样式:
<style lang="scss" scoped>
.markdown-container {
padding: 32rpx;
background-color: #ffffff;
line-height: 1.8;
font-size: 28rpx;
color: #333333;
.markdown-content {
// 标题样式
::v-deep h1 {
font-size: 48rpx;
font-weight: bold;
margin: 40rpx 0 20rpx;
padding-bottom: 16rpx;
border-bottom: 2rpx solid #e0e0e0;
}
::v-deep h2 {
font-size: 40rpx;
font-weight: bold;
margin: 32rpx 0 16rpx;
}
::v-deep h3 {
font-size: 36rpx;
font-weight: bold;
margin: 24rpx 0 12rpx;
}
// 段落样式
::v-deep p {
margin: 16rpx 0;
text-align: justify;
}
// 代码块样式
::v-deep code {
background-color: #f5f5f5;
padding: 4rpx 8rpx;
border-radius: 4rpx;
font-family: 'Courier New', Courier, monospace;
font-size: 24rpx;
color: #e83e8c;
}
::v-deep pre {
background-color: #2d2d2d;
padding: 24rpx;
border-radius: 8rpx;
overflow-x: auto;
margin: 24rpx 0;
code {
background: none;
padding: 0;
color: #f8f8f2;
}
}
// 列表样式
::v-deep ul, ::v-deep ol {
padding-left: 48rpx;
margin: 16rpx 0;
}
::v-deep li {
margin: 8rpx 0;
}
// 引用块样式
::v-deep blockquote {
border-left: 8rpx solid #007AFF;
padding-left: 24rpx;
margin: 24rpx 0;
color: #666666;
background-color: #f9f9f9;
padding: 16rpx 24rpx;
border-radius: 0 8rpx 8rpx 0;
}
// 链接样式
::v-deep a {
color: #007AFF;
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
// 表格样式
::v-deep table {
width: 100%;
border-collapse: collapse;
margin: 24rpx 0;
th, td {
border: 1rpx solid #e0e0e0;
padding: 12rpx 16rpx;
text-align: left;
}
th {
background-color: #f5f5f5;
font-weight: bold;
}
tr:nth-child(even) {
background-color: #fafafa;
}
}
// 图片样式
::v-deep img {
max-width: 100%;
height: auto;
display: block;
margin: 24rpx auto;
border-radius: 8rpx;
}
// 分割线样式
::v-deep hr {
border: none;
border-top: 2rpx solid #e0e0e0;
margin: 32rpx 0;
}
}
}
</style>
五、性能优化策略
5.1 虚拟滚动优化
对于大文件(超过 1000 行),我们使用虚拟滚动技术优化性能:
// 计算可视区域
const calculateVisibleRange = (scrollTop, itemHeight, visibleCount, totalCount) => {
const startIndex = Math.floor(scrollTop / itemHeight)
const endIndex = Math.min(startIndex + visibleCount, totalCount)
return { startIndex, endIndex }
}
// 渲染可视区域的代码行
const renderVisibleLines = (startIndex, endIndex, allLines) => {
return allLines.slice(startIndex, endIndex)
}
5.2 缓存机制
为了提升二次加载速度,我们实现了代码缓存:
// utils/code-cache.js
class CodeCache {
constructor() {
this.cache = new Map()
this.maxSize = 50 // 最多缓存 50 个文件
}
set(key, value) {
if (this.cache.size >= this.maxSize) {
// 删除最早的缓存
const firstKey = this.cache.keys().next().value
this.cache.delete(firstKey)
}
this.cache.set(key, value)
}
get(key) {
return this.cache.get(key)
}
has(key) {
return this.cache.has(key)
}
clear() {
this.cache.clear()
}
}
export const codeCache = new CodeCache()
5.3 懒加载语言包
只在用户打开特定语言的代码文件时,才加载对应的语言包:
// 在组件中使用
async mounted() {
const lang = this.detectLanguage(this.fileName)
// 先显示未高亮的代码
this.displayCode = this.code
// 异步加载语言包并高亮
await loadLanguage(lang)
this.highlightCode()
}
六、实际效果展示
6.1 代码高亮效果
通过 Prism.js,我们实现了专业的代码高亮效果,支持多种主题切换:
- 明亮主题:适合白天阅读
- 暗黑主题:适合夜间阅读,减少眼睛疲劳
6.2 Markdown 渲染效果
正确渲染了以下复杂元素:
- 多级标题、列表、引用
- 代码块(带语法高亮)
- 表格、链接、图片
- 数学公式(通过扩展插件)
七、总结与优化建议
本文详细介绍了 AtomGit APP 中代码高亮与 Markdown 渲染功能的实现。通过 Prism.js 和 marked.js 的深度集成,我们实现了专业级的代码阅读体验。
7.1 核心要点
- 技术选型:选择 Prism.js 和 marked.js,平衡了体积、性能和功能
- 组件封装:创建了可复用的代码高亮和 Markdown 渲染组件
- 动态加载:按需加载语言包,优化初始加载性能
- 安全防护:使用 DOMPurify 防止 XSS 攻击
- 性能优化:通过虚拟滚动、缓存机制提升大文件渲染性能
7.2 后续优化方向
- 支持更多编程语言和主题
- 优化超大文件(10000+ 行)的渲染性能
- 添加代码折叠、搜索等高级功能
- 支持自定义主题配置
7.3 参考资料
更多推荐



所有评论(0)