请添加图片描述

案例项目开源地址:https://atomgit.com/nutpi/wanandroid_rn_openharmony

收藏是内容类 App 的标配功能。用户看到感兴趣的文章,点一下小心心,下次就能在收藏列表里找到。

这个功能的技术实现不复杂,但要做好用户体验,需要考虑很多细节。

收藏按钮长什么样

我们用 emoji 来做收藏按钮:

<TouchableOpacity onPress={handleCollect}>
  <Text style={{fontSize: 18}}>{item.collect ? '❤️' : '🤍'}</Text>
</TouchableOpacity>

为什么用 emoji 而不是图标库

用 emoji 有几个好处。首先是简单,不需要引入额外的图标库,减少包体积。其次是跨平台一致,emoji 在各个平台上都能正常显示,不用担心字体加载问题。最后是直观,红心和白心的含义一目了然,不需要额外解释。

当然 emoji 也有缺点:不同平台的 emoji 样式略有差异,无法自定义颜色和大小(只能通过 fontSize 调整)。如果你的设计对图标有严格要求,还是用图标库更好。

条件渲染的逻辑

item.collect ? '❤️' : '🤍' 是一个三元表达式。item.collect 是布尔值,表示这篇文章是否已被当前用户收藏。已收藏显示红心,未收藏显示白心。

这个状态来自接口返回的文章数据。WanAndroid 的文章列表接口会根据当前登录用户返回每篇文章的收藏状态。

TouchableOpacity 的作用

TouchableOpacity 让这个 Text 组件可以响应点击事件。点击时会有透明度变化的反馈,让用户知道"我点到了"。

onPress={handleCollect} 绑定点击事件处理函数。注意这里传的是函数引用,不是函数调用。如果写成 onPress={handleCollect()},组件渲染时就会立刻执行,而不是等用户点击。

点击收藏的处理逻辑

const handleCollect = async () => {
  if (!isLoggedIn) {
    Alert.alert('提示', '请先登录');
    return;
  }
  try {
    if (item.collect) {
      const res = await collectApi.uncollect(item.id);
      if (res.errorCode === 0) {
        Alert.alert('成功', '已取消收藏');
        onCollectChange?.();
      }
    } else {
      const res = await collectApi.collect(item.id);
      if (res.errorCode === 0) {
        Alert.alert('成功', '收藏成功');
        onCollectChange?.();
      }
    }
  } catch (e) {}
};

这个函数做了三件事:检查登录状态、调用收藏/取消收藏接口、通知父组件刷新数据。

async 函数

async 关键字声明这是一个异步函数,里面可以使用 await 等待 Promise。收藏操作需要调用网络接口,是异步的,所以用 async 函数。

登录状态检查

if (!isLoggedIn) {
  Alert.alert('提示', '请先登录');
  return;
}

收藏功能需要登录才能使用。isLoggedIn 来自 AuthContext,表示当前是否已登录。

如果未登录,弹窗提示用户,然后 return 结束函数执行。这里没有自动打开登录弹窗,而是让用户自己去点击登录。这是一个设计选择:有些 App 会自动弹出登录框,有些只是提示。我们选择后者,侵入性更小。

为什么要在前端检查登录状态

有人可能会问:后端接口不是也会检查登录状态吗?为什么前端还要检查?

前端检查是为了更好的用户体验。如果不检查,用户点击收藏,等待网络请求,然后才看到"请先登录"的错误。前端检查可以立刻给出反馈,不用等网络。

而且,前端检查可以避免不必要的网络请求,节省流量和服务器资源。

收藏和取消收藏的分支

if (item.collect) {
  const res = await collectApi.uncollect(item.id);
  if (res.errorCode === 0) {
    Alert.alert('成功', '已取消收藏');
    onCollectChange?.();
  }
} else {
  const res = await collectApi.collect(item.id);
  if (res.errorCode === 0) {
    Alert.alert('成功', '收藏成功');
    onCollectChange?.();
  }
}

根据当前收藏状态决定调用哪个接口。已收藏就取消,未收藏就收藏。这是一个"切换"操作,点一下变成相反的状态。

两个接口的区别

collectApi.collect(item.id) 是收藏接口,把文章添加到收藏列表。

collectApi.uncollect(item.id) 是取消收藏接口,把文章从收藏列表移除。

两个接口都需要传文章 ID,让服务端知道要操作哪篇文章。

errorCode 的判断

res.errorCode === 0 是 WanAndroid 接口的成功标志。errorCode 为 0 表示操作成功,非 0 表示失败。

这里只处理了成功的情况,失败时什么都不做(静默失败)。更好的做法是提示用户:

if (res.errorCode === 0) {
  Alert.alert('成功', '收藏成功');
  onCollectChange?.();
} else {
  Alert.alert('失败', res.errorMsg || '操作失败');
}

收藏 API 的实现

export const collectApi = {
  getList: (page: number) => api.get(`/lg/collect/list/${page}/json`),
  collect: (id: number) => api.post(`/lg/collect/${id}/json`, {}),
  uncollect: (id: number) => api.post(`/lg/uncollect_originId/${id}/json`, {}),
};

三个接口

getList 获取收藏列表,分页加载。

collect 收藏文章,传入文章 ID。

uncollect 取消收藏,传入文章的原始 ID。

为什么取消收藏用 originId

WanAndroid 的收藏列表里,每篇文章有两个 ID:收藏记录的 ID 和文章的原始 ID(originId)。

在文章列表页取消收藏时,我们只有文章的原始 ID,所以用 uncollect_originId 接口。

在收藏列表页取消收藏时,可以用收藏记录的 ID,接口是 /lg/uncollect/{id}/json

这是 WanAndroid 接口的设计,我们按照它的规则来调用。

POST 请求的空对象

api.post(\/lg/collect/${id}/json`, {})第二个参数是空对象。虽然这个接口不需要请求体,但我们的api.post` 方法要求传一个对象,所以传空对象。

通知父组件刷新数据

onCollectChange?.();

这行代码很短,但很重要。

可选链调用

onCollectChange?.() 是可选链调用。如果 onCollectChange 是 undefined 或 null,不会报错,直接返回 undefined。如果有值,就调用它。

等价于:

if (onCollectChange) {
  onCollectChange();
}

为什么需要这个回调

收藏状态变化后,界面上的心形图标应该变化(红心变白心或反过来)。但 item.collect 是 props 传进来的,组件内部不能直接修改。

正确的做法是:通知父组件数据变了,让父组件重新获取数据,然后通过 props 传入新的数据,触发重新渲染。

这就是 React 的单向数据流:数据从父组件流向子组件,子组件想改数据,要通过回调通知父组件。

父组件怎么处理

const loadArticles = async () => {
  const res = await homeApi.getArticles(page);
  setArticles(res.data.datas);
};

<ArticleCard 
  item={item} 
  onCollectChange={loadArticles}
/>

父组件传入 loadArticles 作为回调。收藏状态变化后,重新加载文章列表,新数据里的 collect 字段就是最新的状态。

组件的 Props 定义

interface Props {
  item: Article;
  showImage?: boolean;
  onCollectChange?: () => void;
}

export const ArticleCard = ({item, showImage = false, onCollectChange}: Props) => {

TypeScript 接口

interface Props 定义了组件接受的属性类型。TypeScript 会在编译时检查传入的 props 是否符合类型要求。

必选和可选属性

item: Article 没有问号,是必选属性,调用组件时必须传。

showImage?: boolean 有问号,是可选属性,可以不传。

onCollectChange?: () => void 也是可选的。() => void 表示这是一个无参数、无返回值的函数。

默认值

showImage = false 在解构时设置默认值。如果调用方没传 showImage,就用 false。

onCollectChange 没有设置默认值,所以可能是 undefined,使用时要用可选链。

收藏状态的数据来源

export interface Article {
  id: number;
  title: string;
  author: string;
  shareUser: string;
  niceDate: string;
  link: string;
  superChapterName: string;
  chapterName: string;
  collect: boolean;  // 收藏状态
  fresh: boolean;
  envelopePic: string;
  desc: string;
  originId?: number;
}

collect 字段是布尔值,表示当前登录用户是否收藏了这篇文章。

这个字段是接口返回的,不是前端计算的。WanAndroid 的接口会根据请求头里的 Cookie 识别用户,然后在返回的文章数据里标记收藏状态。

未登录时的 collect 值

未登录时,所有文章的 collect 都是 false。因为服务端不知道你是谁,无法判断你有没有收藏。

用户体验的细节

即时反馈

点击收藏后立刻弹窗提示,让用户知道操作成功了。虽然界面上的心形图标可能要等数据刷新后才变化,但弹窗是即时的。

防止误操作

收藏和取消收藏都有弹窗确认。有些 App 会在取消收藏时加一个二次确认,防止误触。我们没有加,因为取消收藏不是不可逆的操作,用户可以再次收藏回来。

错误处理

try {
  // ...
} catch (e) {}

catch 块是空的,网络错误时静默失败。这不是最佳实践,更好的做法是提示用户:

catch (e) {
  Alert.alert('错误', '网络异常,请稍后重试');
}

完整的收藏相关代码

// ArticleCard.tsx 中的收藏逻辑
const {isLoggedIn} = useAuth();

const handleCollect = async () => {
  if (!isLoggedIn) {
    Alert.alert('提示', '请先登录');
    return;
  }
  try {
    if (item.collect) {
      const res = await collectApi.uncollect(item.id);
      if (res.errorCode === 0) {
        Alert.alert('成功', '已取消收藏');
        onCollectChange?.();
      }
    } else {
      const res = await collectApi.collect(item.id);
      if (res.errorCode === 0) {
        Alert.alert('成功', '收藏成功');
        onCollectChange?.();
      }
    }
  } catch (e) {}
};

// 收藏按钮
<TouchableOpacity onPress={handleCollect}>
  <Text style={{fontSize: 18}}>{item.collect ? '❤️' : '🤍'}</Text>
</TouchableOpacity>
// api.ts 中的收藏接口
export const collectApi = {
  getList: (page: number) => api.get(`/lg/collect/list/${page}/json`),
  collect: (id: number) => api.post(`/lg/collect/${id}/json`, {}),
  uncollect: (id: number) => api.post(`/lg/uncollect_originId/${id}/json`, {}),
};

收藏功能就是这些内容。技术上不复杂,但每个细节都影响用户体验。


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

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐