请添加图片描述

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

退出登录看起来是个简单功能,点一下按钮就完事了。但仔细想想,这个"点一下"背后要做的事情还挺多的:调接口、清 Cookie、重置状态、刷新界面、给用户反馈……

今天就来把这个流程拆开看看。

用户点击退出按钮

故事从这里开始:

<TouchableOpacity style={[styles.logoutBtn, {borderColor: theme.danger}]} onPress={logout}>
  <Text style={{color: theme.danger}}>退出</Text>
</TouchableOpacity>

用户点击按钮,触发 onPress 回调,调用 logout 函数。

这个 logout 从哪来?从 AuthContext:

const {isLoggedIn, userInfo, logout} = useAuth();

useAuth 是我们自定义的 Hook,返回认证相关的状态和方法。

logout 函数的实现

const logout = async () => {
  try {
    await userApi.logout();
    clearAuthCookie();
    setIsLoggedIn(false);
    setUserInfo(null);
    Alert.alert('成功', '已退出登录');
  } catch (e) {}
};

五行代码,五件事情:

  1. 调用登出接口
  2. 清除本地 Cookie
  3. 设置登录状态为 false
  4. 清空用户信息
  5. 弹窗提示用户

我们一个一个看。

第一步:调用登出接口

await userApi.logout();

userApi.logout 的实现:

export const userApi = {
  logout: () => api.get('/user/logout/json'),
};

就是发一个 GET 请求到 /user/logout/json

为什么要调接口

有人可能会问:直接清除本地状态不就行了,为什么还要调接口?

因为服务端也维护着登录状态。WanAndroid 用 Cookie 来识别用户,服务端会记录哪些 Cookie 是有效的。调用登出接口,服务端会把这个 Cookie 标记为失效。

如果不调接口,本地是退出了,但 Cookie 还是有效的。万一 Cookie 泄露,别人拿着这个 Cookie 还能冒充你操作。

await 的作用

await 等待接口返回。虽然我们没有用返回值,但等待是有意义的:确保服务端处理完成后再进行下一步。

不过这里有个问题:如果网络不好,接口超时了怎么办?用户要等很久才能退出?

实际上,即使接口失败,后面的清理逻辑还是会执行(因为我们没有在 catch 里 return)。所以用户体验上不会卡住。只是服务端的 Cookie 可能没有及时失效,但这个影响不大,Cookie 本身也有过期时间。

第二步:清除本地 Cookie

clearAuthCookie();

这个函数在 api.ts 里定义:

let authCookie = '';

export const clearAuthCookie = () => {
  authCookie = '';
};

我们用一个模块级变量 authCookie 存储登录后获取的 Cookie。清除就是把它设为空字符串。

Cookie 是怎么来的

登录成功时,服务端会在响应头里返回 Set-Cookie

export const api = {
  async post(url: string, data: Record<string, string> = {}) {
    const res = await fetch(`${BASE_URL}${url}`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded',
        ...(authCookie ? {Cookie: authCookie} : {}),
      },
      body: formData.toString(),
    });
    const setCookie = res.headers.get('set-cookie');
    if (setCookie) {
      authCookie = setCookie;
    }
    return res.json();
  },
};

登录请求返回时,我们从响应头取出 Cookie 并保存。后续请求会在请求头里带上这个 Cookie。

为什么不用 AsyncStorage

有人可能会问:为什么不把 Cookie 存到 AsyncStorage 里,这样关闭 App 再打开还能保持登录?

可以这么做,但我们这个项目选择了简单方案:每次打开 App 都需要重新登录。这样实现简单,也更安全。

如果要实现"记住登录状态",需要:

  1. 登录成功后把 Cookie 存到 AsyncStorage
  2. App 启动时从 AsyncStorage 读取 Cookie
  3. 退出登录时从 AsyncStorage 删除 Cookie
  4. 处理 Cookie 过期的情况

第三步:设置登录状态为 false

setIsLoggedIn(false);

isLoggedIn 是 AuthContext 里的状态:

const [isLoggedIn, setIsLoggedIn] = useState(false);

设为 false 后,所有使用 isLoggedIn 的组件都会重新渲染。

哪些地方用到了 isLoggedIn

用户信息卡片:

{isLoggedIn ? (
  <View style={styles.userInfo}>...</View>
) : (
  <TouchableOpacity onPress={onShowLogin}>
    <Text>点击登录</Text>
  </TouchableOpacity>
)}

isLoggedIn 变成 false 后,用户信息区域会变成"点击登录"。

收藏按钮:

const handleCollect = async () => {
  if (!isLoggedIn) {
    Alert.alert('提示', '请先登录');
    return;
  }
  // ...
};

退出后点击收藏,会提示"请先登录"。

收藏列表:

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

isLoggedIn 变化会触发这个 effect。变成 false 时不会加载收藏列表。

第四步:清空用户信息

setUserInfo(null);

userInfo 存储用户的详细信息:

interface UserInfo {
  id: number;
  username: string;
  nickname: string;
  coinCount: number;
  level: number;
  rank: string;
}

设为 null 后,用户信息卡片的头像会变成问号,用户名和积分也不再显示。

为什么要单独清空 userInfo

理论上,isLoggedIn 为 false 时,界面上就不会显示用户信息了。但清空 userInfo 还是有必要的:

  1. 释放内存,虽然用户信息很小,但养成好习惯
  2. 下次登录时,不会短暂显示上一个用户的信息
  3. 代码逻辑更清晰,登出就是"什么都没有"的状态

第五步:弹窗提示

Alert.alert('成功', '已退出登录');

给用户一个明确的反馈:操作完成了。

为什么需要提示

退出登录后,界面会变化(用户信息变成"点击登录"),用户能看到变化。那还需要弹窗吗?

需要。因为:

  1. 界面变化可能不够明显,用户可能没注意到
  2. 弹窗是一个明确的"句号",告诉用户"这件事做完了"
  3. 如果退出失败,也可以通过弹窗告诉用户

Alert.alert 的参数

Alert.alert(title, message, buttons, options);

我们只用了前两个参数:标题和内容。

如果想加按钮,可以这样:

Alert.alert(
  '确认退出',
  '确定要退出登录吗?',
  [
    {text: '取消', style: 'cancel'},
    {text: '确定', onPress: () => doLogout()},
  ]
);

不过我们的退出按钮已经很明确了,不需要二次确认。

错误处理

const logout = async () => {
  try {
    await userApi.logout();
    clearAuthCookie();
    setIsLoggedIn(false);
    setUserInfo(null);
    Alert.alert('成功', '已退出登录');
  } catch (e) {}
};

catch 块是空的,意味着即使接口调用失败,也不会有任何提示。

这样设计是有意的:退出登录是用户主动的操作,即使服务端没有正确处理,本地状态也应该清除。用户的意图是"我要退出",我们应该尊重这个意图。

如果想更严谨一些,可以这样:

const logout = async () => {
  try {
    await userApi.logout();
  } catch (e) {
    console.warn('登出接口调用失败', e);
  } finally {
    clearAuthCookie();
    setIsLoggedIn(false);
    setUserInfo(null);
    Alert.alert('成功', '已退出登录');
  }
};

finally 确保清理逻辑一定会执行,同时在 catch 里记录日志方便排查问题。

完整的 AuthContext 代码

import React, {createContext, useContext, useState, ReactNode} from 'react';
import {Alert} from 'react-native';
import {UserInfo} from '../types';
import {userApi, clearAuthCookie} from '../services/api';

interface AuthContextType {
  isLoggedIn: boolean;
  userInfo: UserInfo | null;
  login: (username: string, password: string) => Promise<boolean>;
  register: (username: string, password: string, repassword: string) => Promise<boolean>;
  logout: () => Promise<void>;
}

const AuthContext = createContext<AuthContextType | undefined>(undefined);

export const AuthProvider = ({children}: {children: ReactNode}) => {
  const [isLoggedIn, setIsLoggedIn] = useState(false);
  const [userInfo, setUserInfo] = useState<UserInfo | null>(null);

  const login = async (username: string, password: string): Promise<boolean> => {
    try {
      const res = await userApi.login(username, password);
      if (res.errorCode === 0) {
        setIsLoggedIn(true);
        setUserInfo(res.data);
        return true;
      } else {
        Alert.alert('登录失败', res.errorMsg || '请检查用户名和密码');
        return false;
      }
    } catch (e) {
      Alert.alert('错误', '网络错误');
      return false;
    }
  };

  const register = async (username: string, password: string, repassword: string): Promise<boolean> => {
    try {
      const res = await userApi.register(username, password, repassword);
      if (res.errorCode === 0) {
        setIsLoggedIn(true);
        setUserInfo(res.data);
        return true;
      } else {
        Alert.alert('注册失败', res.errorMsg || '请检查输入信息');
        return false;
      }
    } catch (e) {
      Alert.alert('错误', '网络错误');
      return false;
    }
  };

  const logout = async () => {
    try {
      await userApi.logout();
      clearAuthCookie();
      setIsLoggedIn(false);
      setUserInfo(null);
      Alert.alert('成功', '已退出登录');
    } catch (e) {}
  };

  return (
    <AuthContext.Provider value={{isLoggedIn, userInfo, login, register, logout}}>
      {children}
    </AuthContext.Provider>
  );
};

export const useAuth = () => {
  const context = useContext(AuthContext);
  if (!context) {
    throw new Error('useAuth must be used within AuthProvider');
  }
  return context;
};

退出按钮的样式

logoutBtn: {
  paddingHorizontal: 16,
  paddingVertical: 8,
  borderRadius: 8,
  borderWidth: 1
},

用边框样式而不是填充背景,看起来更轻量。配合 theme.danger(红色),暗示这是一个需要谨慎的操作。

流程回顾

用户点击退出按钮 → 触发 logout 函数 → 调用登出接口 → 清除本地 Cookie → 设置 isLoggedIn 为 false → 清空 userInfo → 弹窗提示 → 界面自动更新

整个流程是同步的(虽然有 await,但用户感知上是瞬间完成的)。状态变化后,React 会自动重新渲染相关组件,用户看到的界面就从"已登录"变成了"未登录"。

这就是 React 的魅力:你只需要关心状态,界面会自动跟着变。


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

Logo

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

更多推荐