学以致用之一个项目快速上手TypeScript + React

  • 2022-10-06
  • 769
  • 0

最好的学习方法是完成一个完整的项目,在快速入门了TypeScript 和 React之后尝试用相关知识搭建一个完整的前端网站完善总结相关知识点。

耗时: 7 days

1.需求背景

实现一个活动分享的社交平台。访客可以根据一些过滤条件来浏览活动、点赞或加入活动,以及查看其他人的资料与发起的活动。

  • 访客需要登陆来查看活动
  • 登陆后,访客可以浏览活动,可以通过频道(标签)以及时间范围来搜索活动。
    你需要为活动列表实现无限滚动行为。
  • 访客可以查看活动详情,包括活动名称,描述,活动照片,活动时间和地点, 参加或点赞的访客,以及活动的评论。 访客也可以评论、点赞或者参加活动。
  • 展示地址:http://199.255.98.226:3000/
    推荐F12 切换移动端模式查看

2.项目目录

.
├── public
└── src
    ├── api                 // 定义了请求相关的接口
    ├── assets              // 存放相关资源
    │   ├── icon
    │   ├── images
    │   ├── scss
    │   └── svg
    ├── components          // 定义了一些可复用简单组件 或 抽离逻辑复杂部分作为组件
    │   ├── Comment
    │   ├── EventCard
    │   ├── HeadNav
    │   ├── ResultPanel
    │   ├── SearchPanel
    │   ├── TimePicker
    │   └── VirtualList
    ├── pages                   // 各个模块的页面入口
    │   ├── Detail
    │   ├── Error
    │   ├── Home
    │   ├── Login
    │   └── User
    ├── router              // 路由相关管理 及 权限校验组件
    │   └── Auth
    ├── store                   // redux 存储相关定义
    │   └── reducers
    ├── types                   // 各个模块引用到的类型定义
    └── utils                   // 一些工具方法,例如格式化时间,根据传入时间类型获得对应时段开始和结束时间戳

3.关键部分设计与实现

3.1 请求拦截器

因为后端接口需要权限校验,项目使用了axios,并编写对应的请求拦截器对请求的发起和响应做响应的处理。

//请求拦截器
service.interceptors.request.use(
  (request) => {
    if (request.url === '/auth/token') return request;
    const userInfo = localStorage.getItem('userInfo');
    if (!userInfo) throw new Error();
    request.headers &&
      (request.headers = {
        'Content-Type': 'application/json',
        'X-BLACKCAT-TOKEN': `${JSON.parse(userInfo).token}`
      });
    return request;
  },
  (error) => {
    console.log(error);
    return Promise.reject(error);
   }
);

 

请求拦截器对于非登陆接口请求,会在请求头中会加入”X-BLACKCAT-TOKEN” 支持进行后端权限校验

service.interceptors.response.use(
  (response) => {
    if (response.status === 200) {
      return response.data;
    } else {
      return response.data;
    }
  },
  (error) => {
    console.log(error);
    if (error.response?.status === 403) {
      localStorage.removeItem('userInfo');
    }
    return Promise.reject(error);
  }
);

响应拦截器对应响应状态为403(权限校验未通过)的情况做特殊处理,清除登陆的缓存。

3.2 登陆状态保存与路由守卫

3.2.1 登陆状态保存

在登陆页完成登陆请求后,会将登陆返回的用户信息及token储存在localStorage中

localStorage.setItem('userInfo', JSON.stringify(action.payload))

3.2.2 路由守卫

项目中使用’react-router-dom 6’做路由管理,创建了路由表做集中式的路由管理。另外做了较为简单的路由鉴权,编写了Auth组件,对需要登陆权限的路由页面进行保护。需要进入的路由页面组件作为参数传递给Auth组件,Auth组件在进入时获取缓存中的登陆信息,如果获取不到登陆信息则重定向到登陆页面。

// 鉴权组件
const Auth: FC<AuthProps> = ({ children }) => {
  const userInfo = localStorage.getItem('userInfo');
  if (!userInfo) {
    return <Navigate to="/login"></Navigate>;
  } else {
    return children;
  }
};
// 路由表中使用如下
  {
    path: '/',
    element: (
      <Auth>
        <HomePage />
      </Auth>
    )
  },

3.3 Store设计

项目中,使用redux 来进行集中式的状态管理。方便进行一些复杂的跨组件状态管理。主要用来管理三个方面的数据。包括用户登陆信息相关、搜索参数与结果相关、活动信息相关。

3.3.1 用户登陆信息相关: userInfoSlice

主要用户保存登陆后接口返回的用户信息,提供顶部导航栏以及个人主页需要的信息。其结构如下:

export interface UserInfoState {
  token: string;
  user: {
    avatar: string;
    email: string;
    id: number;
    username: string;
  };
}
// 提供一个action 用于更新用户信息
updateUserInfo: (state, action: PayloadAction<UserInfo>)

3.3.2 搜索参数相关:searchSlice

主要用于同步搜索面板选中的参数,包括选中时间,选中的channels,还有存储搜索结果相关信息,包括搜索结果面板是否显示、搜索条数、搜索内容信息。

const initialState: SearchInfoState = {
  // 搜索结果
  searchResultInfo: {
    showResult: false,
    resultNum: 0,
    resultInfo: ''
  },
  // 维持全局搜索参数,用户下拉自动根据上一次参数获取新活动
  searchParams: {
    selectedDate: '',
    channelId: [],
    startDate: 0,
    endDate: 0
  }
};
// 提供action
updateSearchParamsChannelId: (state, action: PayloadAction<number[]>) //更新搜索channel参数
appendSearchParamsChannelId: (state, action: PayloadAction<number>) //增加搜索channel参数
updateSearchParamsSelectedDate: (state, action: PayloadAction<DateList>) //根据选中日期更新搜索的时间戳
resetSearchInfo: (state) //重置搜索结果

集中管理搜索参数可以在每次活动列表下拉时,获得准确的当前选中的搜索参数来获取更多活动,也可以在搜索结果面板中快捷重置搜索结果和搜索参数。

3.3.3 活动信息相关:eventsSlice

主要用户储存当前可用与展示的活动列表,是否有可以查询更多活动

export interface EventsListInfoState {
  eventsList: EventInfo[];
  hasMore: boolean;
}
// 提供action
updateEventsList: (state, action: PayloadAction<EventsResult>) // 更新整个活动列表
appendEventsList: (state, action: PayloadAction<EventsResult>) // 增加活动
resetEventsList: (state) // 重置活动列表

在搜索组件中会获得初次搜索结果更新完整活动列表中,在虚拟列表组件中会获得更多活动信息增加在活动列表中。

3.4 无限滚动设计

3.4.1 虚拟列表思路分析

  • outBox总高度 = 当前所有数据的高度和
  • cacheContentHeight数组用于储存累计到当前index的高度
  • 根据当前scrollTop,遍历cacheContentHeight,找到cacheContentHeight[index] > scrollTop,则对应index就是应该要展示的数据范围的startIndex
  • [startIndex, startIndex + 10]要渲染的数据范围,让内层inbox渲染对应数据,然后通过向下偏移cacheContentHeight[index] 即可出现在可视区域里面

3.4.2 具体实现

  • 初始化的时候,给outBox预估一个高度,大概为 活动列表长度*300,starIndex设置为0,使开始的数据有足够空间渲染
  • 渲染完成后,遍历inBox里面的元素,更新 cacheContentHeight 数组,且更新outBox总高度
  • 通过监听滚动事件,遍历cacheContentHeight和当前scrollTop进行比较,更新startIndex,同时inBox偏移更新,使得新的数据出现在新的可视区域
  • 监听startIndex的变化,当startIndex变化时,新的数据渲染出现,需要更新cacheContentHeight数组
  • 另外为了保证滚动数据展示的平滑,在startIndex=n(n>0)的时候,实际inBox的向下偏移值为cacheContentHeight[n-1],为不可视区域留下缓冲区

3.4.3 列表数据更新

列表数据何时更新呢?我们需要在用户快滚到底部的时候提前加载下一页内容,所以在监听startIndex变化后还需要做判断,判断startIndex 和 总数据列表长度相差小于5 就应该请求获得更多数据。

同时滚动过快startIndex变化会很快,有可能会导致重复请求获得更多活动信息。所以做了节流防抖,触发请求后一秒内无法再触发新的请求。

if (eventList.length - startIndex <= 5 && !loading) {
  setLoading(true);
// 同时滚动过快startIndex变化会很快,有可能会导致重复请求获得更多活动信息。所以做了节流,触发请求后一秒内无法再触发新的请求。
  setTimeout(() => setLoading(false), 1000);
  moreEvents();
}

评论

还没有任何评论,你来说两句吧

苟活时长: Copyright © 2019-2020 OJO