从零开始开发聊天APP (前端篇一)

523 阅读4分钟

创建项目

注意:node 18+ 以上

react-native简称rn,rn是一个非常强大的跨平台开发方案,缺点中文文档常常更新不及时,且版本迭代很快版本差异也有影响,开发环境配置比较复杂对一个纯前端开发者来说也是不小的阻碍。 不同的node版本与jdk版本、rn版本甚至是依赖包版本都需对应有一点差错均会app崩溃。。。

这里我采用expo方式会避免以上问题,且效率也较高,这种方案比较推荐。

注意:有些安卓设备打不开expo GO 应用检查一下电池优化这个应用耗电量会很大某些情况会被这些优化掉导致打不开app

npx create-expo-app@latest

npm run start 启动项目

项目启动后会显示一个二维码和两个访问地址,移动端如果手机连接者电脑且开发者模式开启会自动下载一个app "Expo Go",如果没有自动下载也可以扫码二维码下载,下载过程可能会比较漫长跟网络有关,ios、android都能够下载到,然后打开app可以手动输入访问地址也可以扫码快捷打开就能正常访问了,没有用手机也可以在浏览器直接访问地址也可以打开,expo是支持PC、android、ios一同构建。建议!!还是用对应的设备预览开发因为样式上可能会有细小差距。废话不多说app端开启!

页面代码就不在这里写了有兴趣可以访问仓库

储存token
安装依赖
npx expo install @react-native-async-storage/async-storage
新增 utils/storage.ts 添加以下代码
import AsyncStorage from '@react-native-async-storage/async-storage';

const KEY = "CHAT-TOKEN"

export const setToken = async (value: string) => {
  try {
    return await AsyncStorage.setItem(KEY, value);

  } catch (e) {
    // saving error
  }
};


export const getToken = async () => {
  try {
    return await AsyncStorage.getItem(KEY);
  } catch (e) {
    // error reading value
  }
};

export const removeToken = async () => {
  try {
    return await AsyncStorage.removeItem(KEY);
  } catch (e) {
    // error reading value
  }
};

app/_layout.tsx 判断登录状态

很简单因为默认打开页面时首页只需要在加载完成时判断是否有token就可以了
...
useEffect(() => {
        if (loaded) {
            SplashScreen.hideAsync()
            + getToken().then((token) => {
            +        if (!token) {
            +                router.replace('/login')
            +        }
            })
        }
}, [loaded])
...

接口请求

axios 配置每个人习惯不同这里我就简单配置一下

npm intsall axios -S
npm install ahooks -S // 个人习惯了这个hooks库选择性安装


import axios from 'axios';
import type { AxiosRequestConfig } from 'axios';
import { getToken, removeToken } from './storage';

// 接口定义返回的数据 返回的数据 多一项或少一项都将报错
export declare interface responseData<T> {
  code: number;
  msg: string;
  data: T;
}

export declare interface ErrorData<T = any> {
  code: number;
  msg: string;
  data: T;
}

// 生产环境用
const BASE_URL = '/api';

const instance = axios.create({
  baseURL: BASE_URL,
  withCredentials: true,
  timeout: 60 * 1000,
});

// 请求拦截器
instance.interceptors.request.use(
  async (config:AxiosRequestConfig) => {
    const token = await getToken();
    if (token) {
      config.headers = {
        ...config.headers,
      };
      config.headers['Authorization'] = `Bearer ${token}`
    }
    return config;
  },
  (error) => {
    console.log('请求拦截器报错', error);
    return Promise.resolve(error);
  }
);

// 响应拦截器
instance.interceptors.response.use(
  (response) => {
    const res = response.data as responseData<any>;
    // 返回200正常返回
    if (
      res.code === 200
    ) {
      return res || true;
    }
    // 登录失效
    if (res.code === 401) {
      removeToken();
      return;
    }
    if (res.code === 500) {
      return;
    }
  },
  (error) => {
    console.log('error',error)
  }
);

function requset<T = any>(
  config: AxiosRequestConfig,
  options?: AxiosRequestConfig
): Promise<responseData<T>> {
  if (!options) {
    return instance.request({
      ...config,
    });
  }

  return instance.request({
    ...config,
    cancelToken: options.cancelToken,
  });
}

export default requset;

export interface requestType<T> {
  (config: AxiosRequestConfig, options?: AxiosRequestConfig): Promise<
    responseData<T>
  >;
}

登录

新增 useAccount.ts hooks 请求地址可以单独抽离做个配置,我这里为了看起来方便就不处理了。

import { useRequest } from 'ahooks';
import request from "@/utils/request"
import { showToast } from '@/utils/utils';

type AccountType = { username: string, password: string }

export type UserType = {
  _id: string;
  conversationIds: string[];
  createdAt: string;
  name: string;
  remark: string;
  updatedAt: string;
  username: string;
  avatar?: string;
  image?: string;
}

const login = async (account: AccountType) => {
  return request({
    url: '/auth/login',
    method: 'POST',
    data: account,
  })
}

export const useLogin = () => {
  const { data, loading, refresh, run, params, runAsync } = useRequest(login, {
    manual: true, // 是否手动触发 
    debounceWait: 500,
    refreshOnWindowFocus: false,
    loadingDelay: 100,
    onError: (error) => {
      console.log('error', error)
    },
    onSuccess: (res) => {
      console.log('success', res)
      showToast(res.msg)
    },
  });

  return {
    data,
    loading,
    params,
    refresh,
    run,
    runAsync
  }
}

const fetchUserinfo = async () => {
  return request({
    url: '/user/userinfo',
    method: 'get',
  })
}

export const useUserInfo = () => {
  const { data, loading, refresh, run, params, runAsync } = useRequest(fetchUserinfo, {
    manual: true, // 是否手动触发 
    debounceWait: 500,
    refreshOnWindowFocus: false,
    loadingDelay: 100,
    cacheKey: 'user-info',
    staleTime: 1000 * 60 * 60,
    cacheTime: 1000 * 60 * 60,
    onError: (error) => {
      console.log('error', error)
    },
    onSuccess: (res) => {
      console.log('success', res)
    },
  });

  return {
    data,
    loading,
    params,
    refresh,
    run,
    runAsync
  }
}

登陆页面 login.tsx

    简单验证一下
    const handleLogin = async () => {
            if (!username || !password) return showToast('请输入用户名和密码')
            const res = await runAsync({ username, password })
            setToken(res.data.token)
            router.replace('/(tabs)')
    }

zustand 储存全局用户数据

store/useUser.tsx

import { UserType } from '@/hooks/useAccount'
import { create } from 'zustand'
export interface UserStore {
	user: Partial<UserType>
	setUser: (loading: UserStore['user']) => void
}

const useUserStore = create<UserStore>((set) => ({
	user: {},
	setUser: (user) => set({ user }),
}))

export default useUserStore

app/(tabs)/_layout.tsx

登录有缓存中储存的有token,每次重新进入时如果有token则重新获取用户数据,useUserInfo中添加了请求缓存避免请求次数过多,每小时内真正发起只一次请求。

...
import useUserStore from '../store/useUser'
import UserScreen from './user'
...
const { runAsync } = useUserInfo()
const { user, setUser } = useUserStore()
const getUserinfo = async () => {
        const res = await runAsync()
        setUser(res.data)
}

useEffect(() => {
    getUserinfo()
}, [])

useConversation.ts 查询当前会用的会话列表

import { useRequest } from 'ahooks';
import request from "@/utils/request"
import { showToast } from '@/utils/utils';
import { UserType } from './useAccount';


export type ConersationType = {
  _id: string;
  name: string;
  isGroup: boolean;
  lastMessageAt: string;
  lastMessage: string;
  conversationIds: MessageType[];
  image: string;
  participants: UserType[];
  createdAt: number | Date;
  updatedAt: number | Date;
}

export type MessageType = {
  _id: string;
  body: string;
  image: string;
  senderId: UserType[];
  receiverId: string;
  conversationId: string;
  isReads: string[];
  createdAt: number | Date;
  updatedAt: number | Date;
}

const fetch = async () => {
  return request<ConersationType[]>({
    url: '/user/conversation',
    method: 'GET',
  })
}

export const useConversation = () => {
  const { data, loading, refresh, run, params, runAsync, mutate } = useRequest(fetch, {
    manual: false, // 是否手动触发  直接执行
    debounceWait: 500,
    refreshOnWindowFocus: false,
    loadingDelay: 100,
    onError: (error) => {
      console.log('error', error)
    },
    onSuccess: (res) => {
      console.log('success', res)
      // showToast(res.msg)
    },
  });

  return {
    data,
    loading,
    params,
    refresh,
    run,
    runAsync,
    mutate
  }
}

基础的模块就已经完成了接下来是具体业务代码