【UniApp进阶】手把手教你编写高性能 Markdown 渲染器 (支持代码高亮+公式)
欢迎来到UniApp 实战 AtomGit系列的第三篇!
现在,页面上半部分(Header + 目录)已经非常完美了,但下半部分还是一片空白。对于一个代码托管平台来说,README.md 就是项目的“门面”。
今天,我们要挑战的是:如何在 UniApp 中优雅、高性能地渲染 Markdown 文档,并解决图片防盗链、代码高亮等“原生”环境下的棘手问题。
目录
核心痛点:为什么不能直接用 v-html?
很多 Web 前端转过来的同学会问:“不就是把 Markdown 转成 HTML 字符串,然后用 v-html 渲染吗?”
在 UniApp (特别是小程序/App端) 中,这行不通!
-
兼容性差: 小程序端的
rich-text组件对 HTML 标签的支持非常有限(不支持<table>,<iframe>,class样式隔离等)。 -
交互缺失: 简单的 HTML 字符串无法实现代码块复制、图片点击预览等复杂交互。
-
原生渲染: 我们需要把 HTML 标签转换成 UniApp 的原生组件节点(Nodes),而不是 Webview 套壳。

直接使用的效果就是,标题和表格之类的都渲染不出来。
技术选型:站在巨人的肩膀上
我们要自己写一个 Markdown 解析器吗?当然不(除非你想写秃头)。
在 UniApp 生态中,目前最强、最稳的解决方案是 mp-html。
-
全端兼容: 完美支持 H5、微信小程序、App (NVue 除外)。
-
功能强大: 支持 Latex 公式、代码高亮、Markdown 表格、长图自动切割。
-
独立组件: 它不是一个库,而是一个可以直接使用的 Vue 组件。
实战步骤
第一步:安装依赖
由于我们使用的是 Vue 3 项目,我们需要安装 mp-html 的 Vue 3 版本(或兼容版)。同时,我们需要一个 Base64 解码库(因为 AtomGit/GitHub API 返回的 README 内容通常是 Base64 编码的)。
# 安装 mp-html (支持 Vue3)
npm install mp-html
# 安装 Base64 解码库 (小程序端没有 window.atob)
npm install js-base64
第二步:封装 ReadmeView.vue 组件
在 components 目录下新建 ReadmeView.vue。
1. Template (视图层)
结构非常简单,核心就是 <mp-html> 组件。
<template>
<view class="readme-container">
<view class="header">
<text class="icon">📖</text> README.md
</view>
<view v-if="loading" class="loading-skeleton">
<view class="line lg"></view>
<view class="line md"></view>
<view class="line sm"></view>
</view>
<view v-else class="markdown-body">
<mp-html
:content="htmlContent"
:selectable="true"
:tag-style="tagStyle"
@linktap="handleLinkTap"
/>
</view>
</view>
</template>
2. Script Setup (逻辑层)
这里有两个巨大的技术坑点需要解决:
-
Markdown 转 HTML:
mp-html接收的是 HTML 字符串,所以我们需要引入marked把 Markdown 转成 HTML。 -
图片相对路径修正: 很多 README 里的图片写的是
。在 App 里渲染时,这会变成file:///docs/img.png(报错)。我们需要把它替换成远程 CDN 地址。
<script setup>
import { ref, watch, computed } from 'vue';
import { Base64 } from 'js-base64';
import { marked } from 'marked'; // 需要 npm install marked
import mpHtml from 'mp-html/dist/uni-app/components/mp-html/mp-html'; // 根据实际路径引入
const props = defineProps({
// 仓库里的原始文件数据 (Base64)
fileData: {
type: Object,
default: null
},
// 仓库的基础 URL (用于拼接图片路径)
// 例如: https://raw.githubusercontent.com/user/repo/master/
baseUrl: {
type: String,
default: ''
}
});
const loading = ref(false);
const htmlContent = ref('');
// 定义 Markdown 样式 (类似 CSS,但以 JS 对象传入,兼容性更好)
const tagStyle = {
p: 'font-size: 28rpx; line-height: 1.8; color: #333; margin-bottom: 20rpx;',
h1: 'font-size: 40rpx; font-weight: bold; margin: 40rpx 0 20rpx; border-bottom: 1px solid #eee; padding-bottom: 10rpx;',
h2: 'font-size: 36rpx; font-weight: bold; margin: 30rpx 0 15rpx;',
code: 'background-color: #f6f8fa; padding: 4rpx 8rpx; border-radius: 6rpx; font-family: monospace; color: #d63200;',
pre: 'background-color: #f6f8fa; padding: 20rpx; border-radius: 10rpx; overflow-x: auto;',
blockquote: 'margin: 20rpx 0; padding: 0 20rpx; color: #666; border-left: 8rpx solid #ddd; background-color: #f9f9f9;'
};
// 监听数据变化,开始渲染
watch(() => props.fileData, async (newVal) => {
if (!newVal || !newVal.content) {
htmlContent.value = '<div style="color:#999;text-align:center">暂无说明文档</div>';
return;
}
loading.value = true;
try {
// 1. Base64 解码 (将接口返回的乱码转为 Markdown 源码)
// 注意处理中文乱码,Base64.decode 对 UTF8 支持较好
const rawMarkdown = Base64.decode(newVal.content);
// 2. 预处理 Markdown (关键步骤!)
// 替换相对路径图片,否则图片全裂
// 匹配  或 <img src="path">
const processedMarkdown = rawMarkdown.replace(
/!\[(.*?)\]\((.*?)\)/g,
(match, alt, url) => {
if (url.startsWith('http')) return match; // 绝对路径不管
// 拼接 CDN 前缀
const fullUrl = `${props.baseUrl}${url.replace(/^\.\//, '')}`;
return ``;
}
);
// 3. 转换: Markdown -> HTML
const html = marked.parse(processedMarkdown);
// 4. 赋值渲染
htmlContent.value = html;
} catch (e) {
console.error('解析 README 失败', e);
htmlContent.value = '解析失败';
} finally {
loading.value = false;
}
}, { immediate: true });
// 处理链接点击
const handleLinkTap = (e) => {
const url = e.href;
// 如果是外链,复制到剪贴板或者用浏览器打开
if (url.startsWith('http')) {
uni.setClipboardData({
data: url,
success: () => uni.showToast({ title: '链接已复制', icon: 'none' })
});
}
};
</script>
3. Style (样式层)
<style scoped>
.readme-container {
background-color: #fff;
margin-top: 20rpx;
padding: 30rpx;
border-radius: 16rpx 16rpx 0 0; /* 模拟卡片效果 */
min-height: 500rpx;
}
.header {
font-size: 30rpx;
font-weight: bold;
border-bottom: 1rpx solid #eee;
padding-bottom: 20rpx;
margin-bottom: 20rpx;
display: flex;
align-items: center;
}
.icon { margin-right: 10rpx; }
/* 骨架屏样式 */
.loading-skeleton .line {
background: #f2f2f2;
height: 30rpx;
margin-bottom: 20rpx;
border-radius: 4rpx;
animation: pulse 1.5s infinite;
}
.line.lg { width: 80%; }
.line.md { width: 60%; }
.line.sm { width: 40%; }
@keyframes pulse {
0% { opacity: 1; }
50% { opacity: 0.6; }
100% { opacity: 1; }
}
</style>
第三步:父组件调用 (RepoDetail.vue)
回到我们的主页面,把这个组件拼装到底部。
你需要先调用 AtomGitAPI 获取 README 的数据。
-
API:
GET /repos/{owner}/{repo}/readme
<template>
<view>
<FileTree ... />
<ReadmeView
:fileData="readmeData"
:baseUrl="rawBaseUrl"
/>
</view>
</template>
<script setup>
// ... 其他逻辑
const readmeData = ref(null);
const rawBaseUrl = ref('');
// 初始化时获取 README
const fetchReadme = async () => {
const res = await uni.request({
url: `https://gitcode.com/api/v5/repos/${owner}/${repo}/readme`
});
// 存下数据
readmeData.value = res.data;
// 计算 rawBaseUrl (用于拼接图片)
// 通常 API 会返回 download_url,我们可以从中提取前缀
// 简单粗暴拼凑法:
rawBaseUrl.value = `https://raw.gitcode.com/${owner}/${repo}/${defaultBranch}/`;
};
</script>
进阶技巧:代码高亮 (Highlight.js)
mp-html 默认是不带高亮插件的(为了包体积)。如果你想让代码块像 IDE 一样多彩,需要启用插件。
-
在
mp-html的组件源码中,找到plugins文件夹。 -
启用
highlight插件(通常只需在组件属性里配置,具体视mp-html版本而定,部分版本需要修改源码引入样式文件)。 -
最简单的方法:直接在
marked解析后的 HTML 字符串里,手动注入 highlight.js 的 CSS 样式字符串。
// 简单粗暴的高亮样式注入 (在 script setup 中)
const codeStyle = `<style>
.hljs-keyword { color: #d73a49; }
.hljs-string { color: #032f62; }
/* ...找一个 highlight.js 的 github.css 内容贴进去... */
</style>`;
// 赋值时带上样式
htmlContent.value = codeStyle + html;
总结
通过这一期,我们攻克了 AtomGit详情页的最后一块拼图。
核心知识点回顾:
-
原生渲染观: 坚决抵制
v-html,拥抱mp-html组件。 -
Base64 解码: 处理 API 返回的原始内容。
-
正则替换: 解决 Markdown 中相对路径图片的“死链”问题(这是很多练手项目的盲区!)。
至此,一个完整的、原生的、高性能的代码仓详情页架构(Info + Tree + Readme)就搭建完成了!
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)