【开源鸿蒙跨平台开发学习笔记】Day10:React Native 开发 OpenHarmony —— 渲染README文档
本文介绍了在ReactNative开发HarmonyOS应用时,实现GitCode仓库README.md文档渲染的完整方案。通过封装API接口获取Base64编码内容并解码,构建轻量级Markdown解析器,将Markdown转换为ReactNative原生UI组件。重点解决了HarmonyOS环境下缺少现成Markdown库的适配问题,实现了标题、段落、列表、代码块等基础语法渲染。文章详细阐述了
前言
本次参加开源鸿蒙跨平台开发学习活动,选择了 React Native 开发 HarmonyOS技术栈,在学习的同时顺便整理成一份系列笔记,记录从环境到开发的全过程。本篇作为第10篇,在前几篇文章中,我们已经完成了 React Native 项目在 OpenHarmony 上的环境搭建、基础组件使用、页面路由、仓库文件tree的获取和显示等内容。
本篇主要实现在仓库页面下半区域展示当前仓库的README.md说明文档内容,我们需要实现一个Markdown渲染器,将Markdown语法转换为原生UI组件,提供良好的阅读体验。
README 是一个项目的说明书,也是开发者判断项目质量的重要依据。
尤其在移动端应用里,如果能直接查看 README,不但更方便,也能让整个仓库浏览体验更完整。
不过在 HarmonyOS + React Native 的环境下渲染 README,并不是简单 copy PC Web 上的方案。主要难点包括:
-
GitCode / AtomGit 的 README 文件是通过 contents API 返回的 Base64 内容,需要手动解码。
-
React Native 在 HarmonyOS 环境中没有默认的 Markdown 组件,需要自定义渲染逻辑。
-
HarmonyOS 的 UI 渲染机制和 Android、iOS 有一些小差异,例如 Scroll、布局、字体等,需要稍作处理。
所以本文重点不是讲“如何用库渲染 Markdown”,
而是讲 如何构建一个适合 HarmonyOS 环境的轻量级 Markdown 渲染体系,包含:
接口封装、Base64 解码、Markdown 解析、文本排版、样式管理等完整流程。
一、准备接口与数据类型
GitCode/AtomGit 的内容接口返回数据格式遵守 GitLab API 规范,字段比较统一。
为了避免在业务层到处写 any,我将接口的关键类型先进行了定义。
为什么要先定义类型?
-
统一管理仓库、文件内容等数据结构
-
拥有更好的编辑器提示与类型检查
-
后续 API 层、组件层、页面层都有明确类型约束,降低出错率
-
对多平台版本代码(HarmonyOS/Android/iOS)有帮助,保证逻辑一致性
这些类型在项目里会被频繁使用,包括存储目录树、解析 README 内容等。
export type RepoOwner = {
id?: string | number;
login?: string;
name?: string;
avatar_url?: string;
html_url?: string;
type?: string;
};
export type Repo = {
id: string | number;
name?: string;
path?: string;
full_name?: string;
description?: string;
language?: string;
stargazers_count?: number;
watchers_count?: number;
commits_count?: number;
web_url?: string;
html_url?: string;
owner?: RepoOwner;
};
export type RepoFileContent = {
type: 'file';
encoding: 'base64';
size: number;
name: string;
path: string;
content: string;
sha: string;
url: string;
html_url: string;
download_url: string;
_links: { self: string; html: string };
};
二、封装仓库内容请求函数
GitCode 获取 README 的方式:
GET /repos/:owner/:repo/contents/:path
接口返回的数据特点:
-
文件内容是 Base64 编码的 Markdown
-
如果 README 文件不存在,会返回 404
-
网络上经常出现 “Forbidden”、“Rate limit exceeded”,因此需要处理错误提示
-
content 信息内包含 path、download_url、base64 字符串等完整信息,便于调试
在封装 API 的时候,我加入了 token、header、编码转换等处理,避免重复写同样逻辑。
import {http} from './client';
import {RepoTreeResponse} from '../types/tree';
import {RepoFileContent} from '../types/repo';
export async function fetchRepoTree(owner: string, repo: string, sha: string): Promise<RepoTreeResponse> {
const res = await http.get<RepoTreeResponse>(`repos/${owner}/${repo}/git/trees/${sha}`, {
headers: {Accept: 'application/json'},
params: {access_token: ''},
});
return res.data;
}
export async function fetchRepoContent(owner: string, repo: string, path: string): Promise<RepoFileContent> {
const res = await http.get<RepoFileContent>(`repos/${owner}/${repo}/contents/${encodeURIComponent(path)}`, {
headers: {Accept: 'application/json'},
params: {access_token: ''},
});
return res.data;
}
三、实现基础 Markdown 渲染组件
Base64 → Markdown → React Native 渲染
渲染 README 是本文的重点部分。
为何不直接使用现成的 Markdown 库?
原因很简单:
在 HarmonyOS + React Native 环境中没有可直接使用的成熟 Markdown 渲染库。
很多第三方库依赖 WebView 或使用 iOS/Android Native 实现,而这些方式在 HarmonyOS 中并不完全适配。
因此,我采用的是构建一个能满足 README 基础展示需求的轻量级 Markdown 渲染器:
-
标题(#~######)
-
普通段落
-
无序列表(- 、*)
-
代码块(``` 包围)
-
链接识别与点击
-
空行、行距、段落排版
Base64 → Markdown 解码流程
GitCode 返回的 README 内容是这样的:
{
"type": "file",
"encoding": "base64",
"content": "IyBHaXRDb2RlPC9oMT4K..."
}
其中的 content 字段需要手动 Base64 → UTF-8 转换。
Node 体系里的 Buffer 在 React Native 环境里可以正常使用,所以转换逻辑非常简单:
const decoded = Buffer.from(b64, 'base64').toString('utf-8');
解码后拿到的就是纯 Markdown 文本。
Markdown 渲染的核心原理
由于我们不使用第三方库,那么 Markdown 的渲染逻辑就需要自己拆分。
我采用的方式是:
-
将 Markdown 按行分割
-
遍历每一行,根据格式判断它属于什么类别
-
对不同类型生成不同的 React Native View/Text 结构
-
将解析后的结果加入 content 数组,最终组合成 ScrollView 渲染
例如:
-
# Title→<Text style={styles.h1}> -
- List item→<Text style={styles.li}> -
“空行” →
<View style={styles.space}> -
[Link](https://...)→ 手动用正则匹配,生成可点击的链接
通过这种方式,可以做到:
-
解析速度快
-
结构简单易维护
-
样式全部由 RN 统一控制
-
HarmonyOS 端渲染稳定
-
不依赖外部组件库,更易适配
这种轻量 Markdown 渲染器虽然没有支持全部语法,但应对项目 README 已足够。
import React from 'react';
import {View, Text, StyleSheet, Linking, ScrollView} from 'react-native';
type Props = { markdown: string; loading?: boolean; error?: string; };
function renderInline(line: string): React.ReactNode {
const parts: React.ReactNode[] = [];
const linkRegex = /\[(.+?)\]\((https?:\/\/[^\s)]+)\)/g;
let lastIndex = 0; let match: RegExpExecArray | null;
while ((match = linkRegex.exec(line))) {
const [full, text, url] = match;
if (match.index > lastIndex) parts.push(<Text key={`t-${lastIndex}`}>{line.slice(lastIndex, match.index)}</Text>);
parts.push(<Text key={`l-${match.index}`} style={styles.link} onPress={() => Linking.openURL(url)}>{text}</Text>);
lastIndex = match.index + full.length;
}
if (lastIndex < line.length) parts.push(<Text key={`t-end`}>{line.slice(lastIndex)}</Text>);
return parts;
}
export default function MarkdownView({markdown, loading, error}: Props): JSX.Element {
if (loading) return (<View style={styles.center}><Text style={styles.tip}>README 加载中...</Text></View>);
if (error) return (<View style={styles.center}><Text style={styles.error}>README 加载失败:{error}</Text></View>);
const lines = markdown.split(/\r?\n/);
const content: React.ReactNode[] = [];
let inCode = false; let codeBuf: string[] = [];
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (line.trim().startsWith('```')) {
if (!inCode) { inCode = true; codeBuf = []; }
else { inCode = false; content.push(<View key={`code-${i}`} style={styles.codeBox}><Text style={styles.codeText}>{codeBuf.join('\n')}</Text></View>); }
continue;
}
if (inCode) { codeBuf.push(line); continue; }
if (/^#{1,6}\s/.test(line)) {
const level = (line.match(/^#+/)?.[0].length) || 1;
const text = line.replace(/^#{1,6}\s*/, '');
content.push(<Text key={`h-${i}`} style={[styles.h, styles[`h${level}` as keyof typeof styles]]}>{text}</Text>);
continue;
}
if (/^\s*[-*]\s+/.test(line)) {
const text = line.replace(/^\s*[-*]\s+/, '• ');
content.push(<Text key={`li-${i}`} style={styles.li}>{renderInline(text)}</Text>);
continue;
}
if (line.trim() === '') { content.push(<View key={`sp-${i}`} style={styles.space} />); continue; }
content.push(<Text key={`p-${i}`} style={styles.p}>{renderInline(line)}</Text>);
}
return <ScrollView style={styles.container} contentContainerStyle={styles.content}>{content}</ScrollView>;
}
const styles = StyleSheet.create({
container: {flex: 1},
content: {paddingHorizontal: 16, paddingBottom: 24},
center: {flex: 1, alignItems: 'center', justifyContent: 'center'},
tip: {color: '#666', fontSize: 14}, error: {color: '#d00', fontSize: 14},
h: {fontWeight: '700', marginTop: 12},
h1: {fontSize: 22}, h2: {fontSize: 20}, h3: {fontSize: 18}, h4: {fontSize: 16}, h5: {fontSize: 15}, h6: {fontSize: 14},
p: {fontSize: 14, color: '#333', lineHeight: 20, marginTop: 8},
li: {fontSize: 14, color: '#333', lineHeight: 20, marginTop: 6},
space: {height: 8}, link: {color: '#007aff'},
codeBox: {marginTop: 10, backgroundColor: '#f6f8fa', borderRadius: 6, padding: 10},
codeText: {fontSize: 13, color: '#333', fontFamily: 'Courier'},
});
四、在探索页读取并渲染 README
有了 API 有了渲染组件后,就可以在页面加载时请求 README 并渲染。
流程大概如下:
页面加载 → 调用 fetchRepoContent → 获取 Base64 内容 → 解码为 Markdown → 传入 MarkdownView → 渲染
合理的加载策略
为了避免页面卡顿,我将 README 的加载放在 useEffect 中异步执行:
-
页面先展示“仓库信息”
-
README 加载中展示“loading”提示
-
加载成功后显示完整文档
-
加载失败给出错误提示(常见:404、权限限制)
这样的方式让用户体验更自然。
import {fetchRepoTree, fetchRepoContent} from '../api/repos';
import MarkdownView from '../components/MarkdownView';
const [readme, setReadme] = useState('');
const [readmeLoading, setReadmeLoading] = useState(false);
const [readmeError, setReadmeError] = useState('');
useEffect(() => {
setReadmeLoading(true);
setReadmeError('');
fetchRepoContent(owner, repo, 'README.md')
.then(res => {
const b64 = res.content || '';
const decoded = /* base64 -> markdown */ '';
setReadme(decoded);
})
.catch(e => setReadmeError(String(e?.message || e)))
.finally(() => setReadmeLoading(false));
}, [owner, repo]);
<View style={styles.topHalf}>
{/* 目录树 FlatList 保留 */}
</View>
<View style={styles.bottomHalf}>
<Text style={styles.readmeTitle}>README</Text>
<MarkdownView markdown={readme} loading={readmeLoading} error={readmeError} />
</View>
五、界面布局与样式处理
在 UI 布局上,我希望仓库详情页上下结构清晰:
-
上半部分展示“目录树”
-
下半部分展示 README
因此采用了两个区域:
┌──────────────────────┐
│ 仓库结构/目录树 │(flex:1)
└──────────────────────┘
│ README 内容 │(flex:1)
└──────────────────────┘
这样做的好处是:
-
用户可以同时浏览目录和 README
-
符合代码仓库常见的布局习惯
-
在 HarmonyOS 平板上效果非常好(左右增加空间)
-
适合后续扩展,例如切换 Tab(Issues / Releases / Wiki 等)
Markdown 区域使用 ScrollView 可以适应各种长度的内容。
六、运行与调试(HarmonyOS)
1. 启动 Metro
Metro 是 JS bundle 的提供者:
npm run start
2. 端口映射(必须)
HarmonyOS 模拟器或设备默认无法直接访问 8081,需要端口转发:
hdc rport tcp:8081 tcp:8081
不做这个步骤,页面就会一直停在白屏。
3. 打离线包(可选)
正式打包 HarmonyOS App 时需要构建 bundle:
npx react-native bundle-harmony --dev false
生成的 bundle 会被塞到 HAP 包中。
整个流程很简单,但要注意:每次调试前确保 Metro 与端口映射正常。

以上就是我们开发完成后的最终效果。还是不错的~~~
总结
本篇我们完整实现了 GitCode README 在 HarmonyOS 下的渲染能力。从接口封装,到 Base64 解码,再到 Markdown 渲染组件和页面布局,整个流程看似简单,但实际涉及非常多细节。
你会发现:
-
HarmonyOS 环境对 React Native 的兼容度比想象中更好
-
自己写一个轻量 Markdown 渲染器其实并不复杂
-
只要把原理梳理清楚,渲染 README 这种场景完全可以掌控
-
代码结构清晰后可扩展性也非常高(后续可以支持 TOC、图片、表格等)
最终效果是一个完全跨平台,并能在 HarmonyOS 上流畅渲染 README 的 GitCode 浏览体验。
如果你在 React Native + HarmonyOS 的开发中遇到更多适配问题,也欢迎继续交流,我会继续在系列里整理经验。
更多推荐


所有评论(0)