请添加图片描述

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

上一篇讲了收藏功能,这一篇来聊取消收藏。你可能会想:不就是把收藏接口换成取消收藏接口吗?

没那么简单。取消收藏有两个场景,用的接口还不一样。

两个取消收藏的场景

场景一:在文章列表页,用户点击已收藏文章的红心,取消收藏。

场景二:在收藏列表页,用户点击某篇文章的红心,取消收藏。

看起来一样的操作,但背后的数据结构不同,需要调用不同的接口。

文章列表页的取消收藏

这是我们在 ArticleCard 组件里实现的:

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) {}
};

item.collect 的判断逻辑

if (item.collect) 这个判断是整个函数的分水岭。item.collect 是一个布尔值,来自接口返回的文章数据,表示当前用户是否已收藏这篇文章。

item.collect 为 true 时,说明用户已经收藏了这篇文章,再次点击就是要取消收藏,所以调用 collectApi.uncollect

item.collect 为 false 时,说明用户还没收藏这篇文章,点击就是要收藏,所以调用 collectApi.collect

这种设计让同一个按钮实现了"切换"功能:点一下收藏,再点一下取消,再点一下又收藏……

uncollect 接口的参数

uncollect: (id: number) => api.post(`/lg/uncollect_originId/${id}/json`, {}),

注意接口路径里有 originId 这个词。这里传的 item.id 是文章的原始 ID,不是收藏记录的 ID。

WanAndroid 的设计是这样的:每篇文章有一个固定的 ID(originId),当用户收藏这篇文章时,会创建一条收藏记录,这条记录有自己的 ID。在文章列表页,我们只有文章的原始 ID,所以用 uncollect_originId 接口。

为什么用 POST 而不是 DELETE

按照 RESTful 规范,删除操作应该用 DELETE 方法。但 WanAndroid 的接口用的是 POST。这是接口设计者的选择,我们作为调用方只能遵守。

实际上很多国内的接口都不严格遵循 RESTful,增删改查全用 POST 的也不少见。重要的是和后端约定好,保持一致。

收藏列表页的取消收藏

收藏列表页的数据结构和文章列表页不同:

collectArticles.slice(0, 5).map(item => (
  <TouchableOpacity key={item.id} style={styles.collectItem} onPress={() => openLink(item.link)}>
    <Text style={[styles.collectItemTitle, {color: theme.text}]} numberOfLines={1}>
      {item.title.replace(/<[^>]+>/g, '')}
    </Text>
    <Text style={{color: theme.subText}}>›</Text>
  </TouchableOpacity>
))

收藏列表的数据来源

const loadCollectList = async () => {
  try {
    const res = await collectApi.getList(0);
    if (res.errorCode === 0) {
      setCollectArticles(res.data.datas);
    }
  } catch (e) {}
};

collectApi.getList(0) 调用收藏列表接口,参数 0 是页码(从 0 开始)。返回的数据结构和普通文章列表类似,但有一个关键区别:这里的 item.id 是收藏记录的 ID,不是文章的原始 ID。

如果要在收藏列表页取消收藏

需要用另一个接口:

// 这个接口我们没有封装,但 WanAndroid 提供了
// POST /lg/uncollect/{id}/json
// 这里的 id 是收藏记录的 ID

收藏列表返回的数据里,item.id 是收藏记录 ID,item.originId 是文章原始 ID。取消收藏时要用收藏记录 ID。

我们项目的简化处理

我们的收藏列表只展示了标题,点击是跳转到文章链接,没有做取消收藏的功能。如果要加,需要:

  1. 给每个收藏项加一个删除按钮
  2. 点击时调用 /lg/uncollect/{id}/json 接口,传收藏记录 ID
  3. 成功后刷新收藏列表

两种取消收藏的对比

相同点

都是 POST 请求。

都需要登录状态(Cookie)。

成功后都返回 errorCode: 0

不同点

接口路径不同:/lg/uncollect_originId/{id} vs /lg/uncollect/{id}

参数含义不同:文章原始 ID vs 收藏记录 ID。

使用场景不同:文章列表页 vs 收藏列表页。

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 接口

/lg/collect/list/${page}/json 获取收藏列表,分页加载。页码从 0 开始,每页返回 20 条数据(WanAndroid 的默认值)。

返回的数据结构:

{
  "errorCode": 0,
  "data": {
    "curPage": 1,
    "datas": [
      {
        "id": 123456,
        "originId": 789,
        "title": "文章标题",
        "link": "https://...",
        ...
      }
    ],
    "pageCount": 5,
    "size": 20
  }
}

注意 datas 数组里每个对象都有 id(收藏记录 ID)和 originId(文章原始 ID)两个字段。

collect 接口

/lg/collect/${id}/json 收藏文章。这里的 id 是文章原始 ID。收藏成功后,服务端会创建一条收藏记录。

uncollect 接口

/lg/uncollect_originId/${id}/json 根据文章原始 ID 取消收藏。服务端会找到对应的收藏记录并删除。

取消收藏后的界面更新

if (res.errorCode === 0) {
  Alert.alert('成功', '已取消收藏');
  onCollectChange?.();
}

弹窗提示

Alert.alert('成功', '已取消收藏') 给用户一个明确的反馈。虽然心形图标会从红变白,但弹窗能让用户更确定操作成功了。

有些 App 会用 Toast 而不是 Alert,Toast 不需要用户点击确认,几秒后自动消失,侵入性更小。React Native 没有内置 Toast,需要用第三方库或自己实现。我们用 Alert 是为了简单。

回调通知父组件

onCollectChange?.() 通知父组件数据变了。父组件收到通知后会重新加载文章列表,新数据里这篇文章的 collect 字段就变成 false 了。

然后 React 重新渲染 ArticleCard 组件,item.collect 变成 false,心形图标从 ❤️ 变成 🤍。

可选链的必要性

onCollectChange?.() 里的 ?. 是可选链操作符。因为 onCollectChange 是可选的 prop,调用方可能没传。如果不用可选链,直接写 onCollectChange(),当它是 undefined 时会报错:TypeError: onCollectChange is not a function

可选链会先检查 onCollectChange 是否存在,存在才调用,不存在就跳过。这是一种防御性编程。

收藏状态的同步问题

有一个细节问题:用户在首页收藏了一篇文章,然后切换到"我的"页面查看收藏列表,这篇文章会出现在列表里吗?

答案是:不一定。

因为收藏列表的数据是在页面加载时获取的,之后不会自动更新。用户在首页收藏文章后,收藏列表的数据还是旧的。

解决方案一:每次切换到"我的"页面时刷新

useEffect(() => {
  if (isLoggedIn) {
    loadCollectList();
  }
}, [isLoggedIn]);

我们现在的实现是监听 isLoggedIn 变化,登录后加载一次。可以改成监听页面焦点:

import {useFocusEffect} from '@react-navigation/native';

useFocusEffect(
  useCallback(() => {
    if (isLoggedIn) {
      loadCollectList();
    }
  }, [isLoggedIn])
);

每次页面获得焦点时都刷新收藏列表。

解决方案二:用全局状态管理

把收藏列表放到 Context 或 Redux 里,收藏/取消收藏时同步更新。这样不需要重新请求接口,但实现更复杂。

我们的选择

我们选择了简单方案:收藏列表有一个"刷新"按钮,用户可以手动刷新。

<TouchableOpacity onPress={loadCollectList}>
  <Text style={{color: theme.accent}}>刷新</Text>
</TouchableOpacity>

不是最优解,但够用。

错误处理的考量

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

catch 块为空的问题

现在的代码,网络错误时什么都不做。用户点了取消收藏,等了几秒,什么反应都没有,会很困惑。

更好的做法:

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

errorCode 非 0 的处理

现在只处理了 errorCode === 0 的情况。如果接口返回错误(比如文章不存在、没有权限等),也是静默失败。

更好的做法:

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

完整的取消收藏流程

  1. 用户点击红心图标
  2. 触发 handleCollect 函数
  3. 检查登录状态,未登录则提示并返回
  4. 判断 item.collect 为 true,走取消收藏分支
  5. 调用 collectApi.uncollect(item.id)
  6. 等待接口返回
  7. 检查 errorCode,为 0 则成功
  8. 弹窗提示"已取消收藏"
  9. 调用 onCollectChange 通知父组件
  10. 父组件重新加载数据
  11. 新数据传入 ArticleCard
  12. 心形图标从红变白

整个流程涉及用户交互、网络请求、状态更新、界面渲染,是一个完整的前端数据流闭环。

取消收藏和收藏是一对镜像操作,理解了一个,另一个也就懂了。


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

Logo

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

更多推荐