前阵子在折腾 React Native for OpenHarmony 的时候,正好有个需求是给 Steam 资讯 App 加“我的收藏”。这个功能听起来不算复杂,无非就是列表展示、增删收藏、状态同步,但真正在 OpenHarmony 平台上用 RN 落地,还是有不少和 Android/iOS 不太一样的坑。这篇就把我的实现思路、代码细节和调试过程完整记录下来,给正在做 RN for OpenHarmony 适配的朋友做个参考。
如果你是第一次了解 React Native 和 OpenHarmony 的组合,也完全不用担心。我会从需求拆解、技术选型讲到代码实现,每个关键步骤都会说明“为什么这么做”,而不是只丢一段能跑的代码。这篇内容覆盖了我从零搭建收藏功能到最终在 rk3568 开发板上跑通的完整经历,既适合刚入门的跨端开发者,也适合已经在做 OpenHarmony 适配、想快速落地的老手。
1. 项目背景与整体设计思路
1.1 为什么要在 OpenHarmony 上用 React Native
先说项目背景。团队里已有成熟的 RN 应用,跑在 Android 和 iOS 上,现在要适配 OpenHarmony。如果纯用 ArkUI 重写一遍,成本太高,而且后续多端维护压力大。所以选择了 React Native for OpenHarmony(社区叫 RNOH),它是把 RN 运行时移植到 OpenHarmony 上,让同一套 JS 代码可以跑在三个平台。
RN for OpenHarmony 不是一个“实验室玩具”,它已经能支撑不少真实业务场景。我的 Steam 资讯 App 主要包含资讯流、游戏列表、详情页和收藏页。底层是 Retrofit 拉数据,前端用 React Native 渲染。收藏模块是这个 App 里比较典型的一个功能,涉及本地存储、列表渲染、跨页通信和手势交互,非常适合用来做实战拆解。
1.2 “我的收藏”功能需求拆解
用户故事很简单:我在资讯列表里看到一篇好文章,点击收藏按钮,文章进入“我的收藏”列表;我可以在收藏页查看文章、取消收藏,也可以清空全部。这个需求拆开看,包含四块:
- 收藏状态的存储:要本地持久化,App 重启后收藏不能丢。
- 收藏操作入口:资讯详情页和资讯卡片上都要有收藏按钮。
- 收藏列表页:按时间倒序展示所有收藏条目,支持左右滑动或长按删除。
- 跨页面状态同步:详情页点了收藏,回到收藏列表页要能立即看到;收藏列表页取消收藏,详情页的按钮状态也要更新。
最后这一块是很多初学者容易忽略的。如果只是简单地在每个页面分别读取存储,不做状态管理,就会出现在详情页收藏了,收藏列表页却不刷新的“灵异现象”。
1.3 技术选型:收藏数据存哪、状态怎么管
收藏数据是典型的结构化内容,数据量不大,但需要频繁读写。选型上有三条路:
- AsyncStorage:RN 官方推荐的本地 KV 存取,适合存 JSON 数组。最简单,但所有字段都挤在一个 key 里,频繁更新时性能一般。
- SQLite / RMDB:OpenHarmony 有自带的关系型数据库,适合复杂查询,但引入原生模块会增加适配成本。
- 服务端同步:把收藏同步到后端,跨设备访问。但资讯 App 的收藏往往是轻量级操作,前期没必要引入账号体系,所以先做本地收藏。
我最终选的是 AsyncStorage + React Context。原因很简单:收藏数据就是一组 ID 和对应的文章摘要,不需要数据库,KV 足够;Context 配合 useReducer 可以在所有页面共享收藏状态,不用引入 Redux 这样重的方案。后续如果要做多设备云同步,再在存储层换实现也不迟。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心细节解析与实操要点
2.1 数据层:自己封装一个收藏存储模块
直接用 AsyncStorage 的话,所有收藏逻辑会散落在各个组件里,后面改起来很麻烦。我习惯在数据层做一层封装,对外暴露 getFavorites()、saveFavorite(item)、removeFavorite(id)、clearFavorites() 四个方法。
封装时有个细节要注意:AsyncStorage 存的是字符串,所以每次读取后需要 JSON.parse,写入前需要 JSON.stringify。而且不要每次更新都读一次全量数组再写回,正确做法是先把完整数组读出来,在内存里修改,然后再写回。否则容易出现并发写覆盖的问题。
typescript复制// services/FavoriteStorage.ts
import AsyncStorage from '@react-native-async-storage/async-storage';
const FAVORITE_KEY = 'FAVORITE_NEWS_LIST';
export interface FavoriteItem {
id: string;
title: string;
summary: string;
thumbnail: string;
createdAt: number;
}
export async function getFavorites(): Promise<FavoriteItem[]> {
const raw = await AsyncStorage.getItem(FAVORITE_KEY);
return raw ? JSON.parse(raw) : [];
}
export async function saveFavorite(item: FavoriteItem): Promise<void> {
const list = await getFavorites();
const index = list.findIndex((it) => it.id === item.id);
if (index >= 0) {
list[index] = item;
} else {
list.unshift(item);
}
await AsyncStorage.setItem(FAVORITE_KEY, JSON.stringify(list));
}
export async function removeFavorite(id: string): Promise<void> {
const list = await getFavorites();
const next = list.filter((it) => it.id !== id);
await AsyncStorage.setItem(FAVORITE_KEY, JSON.stringify(next));
}
export async function clearFavorites(): Promise<void> {
await AsyncStorage.removeItem(FAVORITE_KEY);
}
这里我用了 unshift 而不是 push,是为了让新收藏的条目排在列表最顶部,更符合用户习惯。时间戳 createdAt 用来做排序,也方便以后做“按收藏时间筛选”。
2.2 状态管理:用 Context 做跨页面同步
收藏状态需要在详情页、列表页、首页多个地方共享。我用 React Context 加 useReducer 来管理,效果不错。
Provider 做的事很简单:启动时从 AsyncStorage 加载数据到内存,之后所有增删操作同时更新内存和存储。组件层通过 useContext 拿到收藏列表和操作方法,页面 UI 就会自动响应变化。
tsx复制// context/FavoritesContext.tsx
import React, { createContext, useContext, useEffect, useReducer } from 'react';
import { getFavorites, saveFavorite, removeFavorite, FavoriteItem } from '../services/FavoriteStorage';
const FavoritesContext = createContext<any>(null);
type State = { list: FavoriteItem[]; loaded: boolean };
type Action =
| { type: 'LOAD'; payload: FavoriteItem[] }
| { type: 'ADD'; payload: FavoriteItem }
| { type: 'REMOVE'; payload: string };
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'LOAD':
return { list: action.payload, loaded: true };
case 'ADD':
return { list: [action.payload, ...state.list], loaded: true };
case 'REMOVE':
return { list: state.list.filter((it) => it.id !== action.payload), loaded: true };
default:
return state;
}
}
export function FavoritesProvider({ children }) {
const [state, dispatch] = useReducer(reducer, { list: [], loaded: false });
useEffect(() => {
getFavorites().then((list) => dispatch({ type: 'LOAD', payload: list }));
}, []);
const add = (item: FavoriteItem) => {
saveFavorite(item);
dispatch({ type: 'ADD', payload: item });
};
const remove = (id: string) => {
removeFavorite(id);
dispatch({ type: 'REMOVE', payload: id });
};
return (
<FavoritesContext.Provider value={{ list: state.list, loaded: state.loaded, add, remove }}>
{children}
</FavoritesContext.Provider>
);
}
export function useFavorites() {
return useContext(FavoritesContext);
}
这里有个实战体会:add 和 remove 里我按顺序先写存储、再 dispatch,这看起来有点冗余,实际上能避免一个极端情况——如果用户在快速点击收藏和取消收藏之间切换,就可能出现存储已经更新了,但内存里还是旧数据的错乱。先同步写存储、再更新内存,可以最大程度保证数据一致。代价是每次操作都有一点延迟,但对本地 KV 来说完全可接受。
2.3 交互细节:点击页面其他区域关闭弹窗
做收藏列表页时,我加了一个“长按收藏条目弹出操作菜单”的能力。菜单里有“取消收藏”和“查看详情”。这个需求本身不难,但有一个交互细节很考验 RN 功底:点击弹窗以外的区域,弹窗要自动关闭。
在 Android 上,RN 的 TouchableWithoutFeedback 一般能覆盖整个屏幕,再配合 onPress 关闭弹窗。但在 OpenHarmony 的 RN 实现里,我遇到了一个坑:外层容器的 onPress 在内层弹窗展开后不触发,或者在点击弹窗内部时也会触发关闭。
排查下来,原因是 OpenHarmony 上事件处理对 TouchableWithoutFeedback 的支持有差异。我不再依赖外层点击事件,而是改用监听“触摸开始位置 + 判断点击目标是否在弹窗区域内”。具体的做法是:
- 在页面根节点用
View的onStartShouldSetResponder={() => true}拦截所有触摸响应。 - 在
onResponderRelease事件里取到触摸点的绝对坐标ev.nativeEvent.locationX和locationY,判断该点是否落在弹窗组件范围内。 - 如果在弹窗范围外,就关闭弹窗。
tsx复制const [menuVisible, setMenuVisible] = useState(false);
const [selectedItem, setSelectedItem] = useState<FavoriteItem | null>(null);
const menuRef = useRef<View>(null);
const closeMenuIfOutside = (event: GestureResponderEvent) => {
if (!menuVisible) return;
const { locationX, locationY } = event.nativeEvent;
menuRef.current?.measure((x, y, width, height, pageX, pageY) => {
const inside =
locationX >= pageX &&
locationX <= pageX + width &&
locationY >= pageY &&
locationY <= pageY + height;
if (!inside) {
setMenuVisible(false);
}
});
};
这种方式比我一开始用 TouchableWithoutFeedback 可靠得多。它本质上就是用“命中测试”替代“事件冒泡”,在 OpenHarmony 这种新平台适配期,这种方案兼容性更好。
2.4 跨端差异:RN for OpenHarmony 与 Android/iOS 的差异
在做这个功能时,我明显感觉到 RN for OpenHarmony 虽然兼容大部分 RN API,但还没有做到 100% 一致。比如:
AsyncStorage不能直接用社区原包,需要安装@react-native-oh-tpl/async-storage这个 fork 版本。- 部分组件的
onLayout和事件流在 OpenHarmony 上时序不同,容易出现先渲染后量度的抖动。 - 图片加载方面,
Image组件在某些情况下需要显式指定resizeMode,否则在真机上会出现拉伸或空白。 - 手势响应链不如 Android 成熟,所以自定义手势交互时建议多测试真机。
这些差异并不致命,但是如果你是从 Android 项目直接搬代码过来,大概率会遇到一些“明明代码没错就是不工作”的情况。提前知道这些,能省很多排错时间。
3. 实操过程与核心环节实现
3.1 环境准备:编译 OpenHarmony 版 RN 工程
我手里的开发设备是 rk3568 开发板,系统是 OpenHarmony 标准版。开发机是 Ubuntu,装了 DevEco Studio 和配套的 SDK。
第一步,先确认系统版本。这里用 hdc 工具(OpenHarmony 的调试桥梁,对应 Android 的 adb)来查看:
bash复制hdc shell param get const.product.name
hdc shell param get const.product.version
这两条命令分别拿到设备型号和系统版本。我在实施前用 const.product.name 确认了当前设备是 rk3568,避免后面装错了系统架构的 HaP 包。
然后创建 RN 工程。我使用的是 @react-native-oh/react-native-harmony 这个脚手架,安装命令是:
bash复制npx @react-native-oh/react-native-harmony@latest init SteamNewsApp
创建出来的工程会自带 harmony 目录,里面是 OpenHarmony 的原生工程入口。接下来把 @react-native-oh-tpl/async-storage 安装并链接进 harmony 工程:
bash复制npm install @react-native-oh-tpl/async-storage
然后在 harmony 目录下执行编译,生成 HaP 包:
bash复制hvigorw assembleHap
这里有个非常重要的注意事项:在编译之前,务必在 OpenHarmony 工程里检查 API 版本和 NDK 版本。compileSdkVersion 要跟你开发板的系统版本匹配,否则装不上。我一开始没注意,编译出来的 HaP 在 rk3568 上报“install failed due to incompatible api”,后来把 compileSdkVersion 调到和设备匹配的版本才解决。
安装应用到设备上,用 hdc:
bash复制hdc install entry-default-signed.hap
如果设备已经运行了旧版本,需要先卸载:
bash复制hdc uninstall com.example.steamnews
跑起来后,怀疑哪一步有问题就抓日志,我一般用 hdc hilog 配合 grep 过滤 RN 相关日志:
bash复制hdc hilog | grep ReactNativeJS
这些命令是 OpenHarmony 开发的基础,但对刚转过来的 RN 开发者来说,属于必须记的第一批工具。
3.2 实现收藏数据管理模块
我习惯先把数据模块写好,再接界面。数据模块就是前面贴的 FavoriteStorage.ts,它负责和 AsyncStorage 打交道。
写的时候要注意一个细节:在 App 启动时,AsyncStorage 是异步加载,不能期待在构造函数里就能拿到数据。所以 FavoritesProvider 在 useEffect 里去加载,并维护一个 loaded 字段。这个字段很关键,在收藏列表页可以配合启动加载动画,避免闪一下空列表再跳成有数据的列表。
我给收藏列表页加了一个 loading 状态:
tsx复制if (!loaded) {
return <ActivityIndicator style={{ flex: 1 }} size="large" color="#1a9fff" />;
}
这样用户感知是“进入收藏页,停顿一下,然后内容出现”,而不是“先看到空白,再看到列表”。
3.3 实现“我的收藏”列表页
收藏列表页是功能的核心展示界面。我用了 FlatList,因为收藏数量可能上百条,长列表用 FlatList 有懒加载和回收机制,不会像 ScrollView 那样一次性渲染所有子项。
每个列表项是一个卡片,展示缩略图、标题、摘要和收藏时间,长按弹出“取消收藏”菜单。
tsx复制// screens/FavoritesScreen.tsx
import { FlatList, Text, View, TouchableOpacity, ActivityIndicator } from 'react-native';
import { useFavorites } from '../context/FavoritesContext';
export function FavoritesScreen({ navigation }) {
const { list, loaded, remove } = useFavorites();
const renderItem = ({ item }) => (
<TouchableOpacity
style={styles.card}
onPress={() => navigation.navigate('Detail', { id: item.id })}
onLongPress={() => openMenu(item)}
>
<Image source={{ uri: item.thumbnail }} style={styles.thumbnail} resizeMode="cover" />
<View style={styles.info}>
<Text style={styles.title} numberOfLines={2}>{item.title}</Text>
<Text style={styles.summary} numberOfLines={3}>{item.summary}</Text>
<Text style={styles.time}>{new Date(item.createdAt).toLocaleDateString()}</Text>
</View>
</TouchableOpacity>
);
return (
<View style={{ flex: 1 }}>
<FlatList
data={list}
keyExtractor={(item) => item.id}
renderItem={renderItem}
contentContainerStyle={list.length === 0 ? styles.emptyContainer : styles.listContainer}
ListEmptyComponent={loaded ? <EmptyView /> : null}
/>
</View>
);
}
这里我踩过一个坑:ListEmptyComponent 的条件判断。如果 loaded 为 false,就不显示空状态,否则用户会看到一瞬间“暂无收藏”的假信息。等 AsyncStorage 加载完成后再根据数据量显示空态或者列表,体验会好很多。
3.4 实现收藏/取消收藏交互
详情页的收藏按钮是个人最常用到的入口。我用了 useFavorites() 里的 list 来判断当前资讯是否已收藏,然后切换按钮图标和文案。
tsx复制// components/FavoriteButton.tsx
import { TouchableOpacity, Text } from 'react-native';
import { useFavorites } from '../context/FavoritesContext';
export function FavoriteButton({ article }) {
const { list, add, remove } = useFavorites();
const isFavorite = list.some((it) => it.id === article.id);
const toggle = () => {
if (isFavorite) {
remove(article.id);
} else {
add({
id: article.id,
title: article.title,
summary: article.summary,
thumbnail: article.thumbnail,
createdAt: Date.now(),
});
}
};
return (
<TouchableOpacity onPress={toggle} style={styles.button}>
<Text style={{ color: isFavorite ? '#ff9500' : '#666' }}>
{isFavorite ? '已收藏' : '收藏'}
</Text>
</TouchableOpacity>
);
}
这个组件看起来功能简单,其实包含了“组件订阅状态”这一层。因为 useFavorites 返回的是整个收藏数组,每次收藏数据变化,所有调用它的组件都会重新渲染。如果整个 App 有几十个入口都订阅同一个 context,性能会受影响。
优化思路是拆分外层:把 isFavorite 判断放到一个只负责读取的 memo 子组件里,或者干脆用 useSelector 之类的库。我们这个 App 收藏入口不多,暂时没有做细粒度拆分,但如果你在开发大型 App,一定要提前考虑这一点。
3.5 长按菜单与点击外部关闭的完整闭环
收藏列表页的长按菜单我用一个绝对定位的浮层实现。点击卡片长按后,通过 measure 获取菜单位置,显示在选中的卡片附近。然后通过前面介绍的“点击外部区域关闭”逻辑,让菜单在点击其他区域时消失。
菜单本身的样式、动画可以做得朴素一些,重点是交互逻辑闭环:
- 长按卡片 -> 打开菜单
- 点击菜单里的“取消收藏” -> 删除数据,菜单消失
- 点击菜单里的“查看详情” -> 跳转详情页,菜单消失
- 点击菜单外区域 -> 菜单消失
我在菜单打开时,还给背景加了一层半透明遮罩,这样用户在视觉上会自然理解“这是浮层,点别处会关”。这层遮罩也有一个 onTouchStart 处理器来关闭菜单,避免在菜单打开时误触列表项造成跳转。
4. 常见问题与排查技巧实录
4.1 hdc 调试命令:查看系统版本与安装调试包
调试过程中,hdc 是天天都要用的工具。很多刚从 Android 转过来的同学喜欢用 adb,但 OpenHarmony 的 hdc 命令格式有些许不同。我常用的几条命令:
bash复制# 查看设备列表
hdc list targets
# 查看系统版本信息
hdc shell param get const.product.version
# 查看设备名称
hdc shell param get const.product.name
# 安装应用
hdc install entry-default-signed.hap
# 卸载应用
hdc uninstall com.example.steamnews
# 查看实时日志(过滤 JS 层日志)
hdc hilog | grep ReactNativeJS
这里有一个很实用的技巧:当你拿到一台新开发板时,第一件事就是用 param get const.product.name 和 param get const.product.version 确认设备型号和系统版本。因为不同设备对应的 SDK 版本、屏幕密度、CPU 架构可能完全不同。如果开发板是 rk3568,但你的 HarmonyOS SDK 版本过新,编译出的应用可能无法安装。
4.2 收藏状态不同步:Context 更新后 FlatList 不刷新
这是我调试时花时间最多的问题。现象是:在详情页收藏了一篇文章,回到“我的收藏”页,列表没有立刻出现新内容;但杀掉 App 重进,收藏内容却在。
后来定位到原因:FavoritesScreen 用了 useFavorites() 取 list,但那个组件被写成了纯组件,没有正确订阅 context 的变化。奇怪的是,我在 FlatList 的 data={list} 中明明引用了。后来才发现是 React.memo 惹的祸,我用 React.memo 包裹了列表组件,导致 context 变化时没有触发重渲染。
解决办法:不要在 FavoritesScreen 外层加 React.memo,或者在使用 context 的地方单独拆一个子组件。收藏状态属于全局数据,这种组件不适合做 memo 优化。
4.3 列表刷新卡顿与内存优化
一开始我的收藏列表卡片直接渲染完整摘要,每个卡片还有一张网络图片。滚动时在 rk3568 开发板上能明显感觉到掉帧。后来做了几个优化,效果明显:
- 摘要文本从“显示 3 行”改为“显示 2 行”,减少无用渲染。
- 图片用
getImageSize预取,固定卡片高度,避免滚动时不断重新计算高度。 - 给
FlatList设置windowSize={5},减少离屏渲染范围。
实测从 rk3568 设备表现看,优化前后滚动帧率从 40fps 左右提升到 55fps 以上。对于这种轻量级列表,性能瓶颈往往不在 RN 本身,而在无谓的重计算和布局抖动。
4.4 OpenHarmony 上 AsyncStorage 失效问题
使用社区原版 @react-native-async-storage/async-storage 时,在 OpenHarmony 上会直接报“Native module cannot be null”,这是因为原包的 Android 原生代码无法在 OpenHarmony 上运行。必须换成 @react-native-oh-tpl/async-storage,并在编译前检查是否已在 harmony 工程中正确配置了 native module。
如果你不想依赖这个 fork,也可以用 OpenHarmony 自身的轻量级偏好数据库。但那样做把业务和系统绑定太深,失去了跨端统一存储的意义,所以我最终选择 fork 包。
安装后还有一个坑:这个包在某些版本要求你必须在原生工程里手动修改 module.json5,添加存储权限声明,否则写入时静默失败。
json5复制{
"module": {
"requestPermissions": [
{
"name": "ohos.permission.STORAGE"
}
]
}
}
这个坑非常隐蔽,因为代码不报错,但数据就是写不进去。后来我在 hdc hilog 里看到一条 ERR_NO_TOKEN 的权限日志,才定位到问题。建议大家安装完依赖后,第一时间检查这个权限声明。
4.5 点击事件失效与坐标计算
前面提到的“点击外部关闭菜单”,在 OpenHarmony 上还遇到过 measure 回调拿到的坐标不准的情况。具体表现是:点击弹窗外部区域,但菜单仍然不消失,因为拿到的 pageY 偏了。
排查后发现,是因为弹窗本身做了一段位移动画,动画过程中 measure 拿到的位置是动画开始前的旧位置。解决方法是等动画结束再绑定外部点击,或者在动画结束后重新测量一次坐标。我在代码里用 onAnimationEnd 回调做了同步。
tsx复制// 菜单显示后,等待动画结束再绑定外部点击
onShow={() => {
setTimeout(() => {
setMenuReady(true);
}, 250);
}}
这样虽然代码上多了一处 setTimeout 的 hack,但在当前版本 RNOH 上是最稳的兼容做法。
5. 实用心得与后续扩展建议
5.1 推荐一个调试“组合拳”习惯
调试 RN for OpenHarmony 时,我习惯同时在电脑开三个终端窗口:一个实时跑 hdc hilog | grep ReactNativeJS,一个跑 Metro,还有一个用来跑打包和安装命令。这种组合拳能让我在问题发生时第一时间拿到 JS 层和原生层的日志,不至于翻半天历史记录。
如果你的页面完全白屏,优先看 Metro 日志;如果应用闪退,优先看 hilog 里的 FATAL 级别日志;如果数据不刷新,检查 context 和异步时序。这么多年跨端调试下来,我觉得定位问题的速度比写代码的速度更能决定项目成败。
5.2 接下来可以试试的扩展方向
“我的收藏”这个功能目前是纯本地数据。我做了一些思考,后续可以扩展的方向有:
- 收藏数据云同步:登录后把收藏列表同步到服务端,这样换设备也能看到。可以在
FavoriteStorage这层再包一个 sync provider。 - 收藏分类管理:按游戏分类、资讯类型等打标签,列表页支持筛选。这需要存储结构从一维数组扩展成带 tag 的对象。
- 收藏推送提醒:对某个游戏的新闻设置了“新消息提醒”,如果收藏了该游戏的资讯,有新动态时可以走 Push 通知。这个需要接入 OpenHarmony 的 Push Kit,RN 侧也可以封装。
从项目整体来看,FavoritesProvider 这套 Context 模式可以复用到其他全局状态,比如用户设置、浏览历史,也算是一种基建沉淀了。
5.3 最后再分享一个小技巧
在收藏列表卡片里,我最终加了一个“收藏序号”的小角标,数字从 1 开始递增,表示收藏顺序。这本来是产品上没要求的东西,但加上之后,用户能直观感受到“我收藏了 12 篇资讯”,比单纯看列表更有人情味。这种小细节,往往能给 App 增加很多好感度。
实现上完全不用改存储结构,直接在 renderItem 里用 index + 1 就行:
tsx复制<Text style={styles.badge}>{index + 1}</Text>
但要注意 FlatList 的数据变化后 index 是动态的,如果你希望序号代表“历史第几个收藏”,就要存储时保留一个全局自增序号。考虑到目前需求只是“展示顺序”,动态 index 已经够用。
做这样一个功能,从需求分析到落地调试,我觉得最大的体会是:React Native for OpenHarmony 的生态还在快速完善期,遇到问题不要慌,多用 hdc 拿原生日志、多对照 RN 原生 API 的行为,通常都能找到对应的替代方案。收藏功能只是一个起点,接下来我会继续把资讯流的其他模块也搬到 OpenHarmony 上,到时候有新坑再继续分享。
