欢迎来到UniApp 实战 AtomGit系列的第三篇!

现在,页面上半部分(Header + 目录)已经非常完美了,但下半部分还是一片空白。对于一个代码托管平台来说,README.md 就是项目的“门面”。

今天,我们要挑战的是:如何在 UniApp 中优雅、高性能地渲染 Markdown 文档,并解决图片防盗链、代码高亮等“原生”环境下的棘手问题。

目录

核心痛点:为什么不能直接用 v-html?

技术选型:站在巨人的肩膀上

实战步骤

第一步:安装依赖

第二步:封装 ReadmeView.vue 组件

1. Template (视图层)

2. Script Setup (逻辑层)

3. Style (样式层)

第三步:父组件调用 (RepoDetail.vue)

进阶技巧:代码高亮 (Highlight.js)

总结


核心痛点:为什么不能直接用 v-html

很多 Web 前端转过来的同学会问:“不就是把 Markdown 转成 HTML 字符串,然后用 v-html 渲染吗?”

在 UniApp (特别是小程序/App端) 中,这行不通!

  1. 兼容性差: 小程序端的 rich-text 组件对 HTML 标签的支持非常有限(不支持 <table>, <iframe>, class样式隔离等)。

  2. 交互缺失: 简单的 HTML 字符串无法实现代码块复制、图片点击预览等复杂交互。

  3. 原生渲染: 我们需要把 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 (逻辑层)

这里有两个巨大的技术坑点需要解决:

  1. Markdown 转 HTML: mp-html 接收的是 HTML 字符串,所以我们需要引入 marked 把 Markdown 转成 HTML。

  2. 图片相对路径修正: 很多 README 里的图片写的是 ![图](./docs/img.png)。在 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 (关键步骤!)
    // 替换相对路径图片,否则图片全裂
    // 匹配 ![alt](path) 或 <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 `![${alt}](${fullUrl})`;
      }
    );

    // 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 一样多彩,需要启用插件。

  1. mp-html 的组件源码中,找到 plugins 文件夹。

  2. 启用 highlight 插件(通常只需在组件属性里配置,具体视 mp-html 版本而定,部分版本需要修改源码引入样式文件)。

  3. 最简单的方法:直接在 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详情页的最后一块拼图。

核心知识点回顾:

  1. 原生渲染观: 坚决抵制 v-html,拥抱 mp-html 组件。

  2. Base64 解码: 处理 API 返回的原始内容。

  3. 正则替换: 解决 Markdown 中相对路径图片的“死链”问题(这是很多练手项目的盲区!)。

至此,一个完整的、原生的、高性能的代码仓详情页架构(Info + Tree + Readme)就搭建完成了!

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐