基于 Redux + TypeScript 实现强类型检查和对 Json 的数据清理

突然像是打通了任督二脉一样就用了 generics 搞定了之前一直用 any 实现的类型……

关于 Redux 的部分,这里不多赘述,基本的实现都在这里:Redux Toolkit 调用 API 的四种方式async thunk 解决 API 调用的依赖问题

之前的实现方法是这个:TS 使用自动提示生成对象中的键,不过实现了一下还是稍微麻烦了一些,而且 CRUD 中的 Create 操作比较难(因为缺乏默认值),之后还是换了一种写法。

虽然这里是用的是 react redux,不过因为不涉及到渲染部分,以及 redux 也可以在非 react 项目中使用,所以还是放到 TS 分类中了(x

下面就根据自己的经验和目前写的项目讲一下我自己琢磨出来的实现。

对象的类型定义

这种主要是通过 type or interface 去实现,具体没什么差别啦,不过对于我们来说,数据类型是固定的,没必要修改对应的数据类型,使用 type 就好了。具体用 type 还是 interface,还是具体需求具体分析。

另外 type 的优势就是少打一些字……?

具体实现如下:

export type IPost = {
  body: string;
  // 因为网上数据和项目数据格式不一致,所以这里暂时注释掉
  //   id: number;
  title: string;
  userId: number;
};

// 这个跟 redux 存储状态相关
// export interface IPostModel extends ISimpleDataFormat<IPost> {}
export type IRdmMarketModel = ISimpleDataFormat<IRdmMarket>;

随后定义一个默认值:

export const defaultPost: IPost = {
  body: '',
  id: 0,
  title: '',
  userId: 0,
};

这样就完成了最初的设定。

Redux 的类型配置

Redux 的配置就比较复杂了,这一个也是比较项目相关的部分,下面也会一步步地进行拆解进行实现。

API 传来的数据类型

initialState 还是挺重要的,因为我们的 API 有十几二十多个,每一个的存储类型都是一致的,因此就不可能说将同样的东西 cv 十几二十遍。最终我设计的 Redux 存储格式为:

我们的业务是,后端绝对会返回一个 data 的数组,其中的数据类型大致为:

{
  "data": [
    {
      "type": "",
      "id": 0,
      "attributes": {},
      // 并不一定存在
      "relationships": {}
    }
  ],
  // include 也是外链关系,为relationship所包含的entity,数据类型与data一致
  "includes": {}
}

其中 type 为当前 entity 的名称,attributes 为当前 entity 所包含的属性,relationship 为可能存在的外链,includes 通过 query parameters 调用,为 relationship 中,外链对象的具体数据。

在具体存储的时候,我想把 id 放到 attributes 中,这样转化为数组给 UI 组件时会方便一些。于是,我定义了 ISimpleDataFormat 的类型:

export type IRelationship = {
  data: {
    type: string;
    id: number;
  };
};

// 其实这个用 { [key: string]: IRelationship } 的效果应该也是一样的……?
export type IRelationships = Record<string, IRelationship>;

export type ISimpleDataFormat<T> = {
  attributes: T & { id: string };
  relationships?: IRelationships;
};

relationship 我保存了后端传来的数据格式,一来没有特别的复杂,二来对于 create/update 操作,有的时候需要添加 relationship 进行双重验证和建立外链。

Redux 状态的设定

同样,我也对 Redux 存储的状态进行了标准化。首先它需要有一个 loading 状态,这样可以方便 UI 进行状态的更改,同时提醒用户正在拉去数据。其次需要错误的状态,同样也是为了显示在 UI 上,最后需要分别存储 data 和 included。经过讨论,最终决定以 K-V 对的方法存储数据。

最后的状态定义如下:

// 也可以使用 Record,我之后应该会以 Record 为主,毕竟写起来方便些
export type ReduxDataType<T> = { [key: string]: T };

export type ISliceStateType<T, U> = {
  isLoading: boolean;
  error: null | SerializedError;
  data: ReduxDataType<T>;
  included: ReduxDataType<U>;
};

export const initialSliceState = {
  isLoading: false,
  error: null,
  data: {},
  included: {},
};

其中 included 可能会存在多个不同的外链,不过目前我们只用到了一个,所以用 <T, U>,如果之后 included 出现两个以上的外链,那么……再改叭……

slice 的实现

这里主要讲一个 fetch,其他的操作基本一致,没什么特别大的区别。API 方面的话,就用网上的 dummy API 了:

import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
import { initialSliceState } from '../sliceType';
import { ISimpleDataFormat, ISliceStateType } from '../../../types';
import { pick } from 'lodash';

export type IPost = {
  body: string;
  title: string;
  userId: number;
};

export interface IPostModel extends ISimpleDataFormat<IPost> {}

export const defaultPost: IPost = {
  body: '',
  title: '',
  userId: 0,
};

export const fetchPost = createAsyncThunk('posts/fetch', async () => {
  return fetch('https://jsonplaceholder.typicode.com/posts')
    .then((response) => response.json())
    .then((json) => json);
});

const postSlice = createSlice({
  name: 'rdmMarket',
  initialState: initialSliceState as ISliceStateType<IPostModel, unknown>,
  reducers: {},
  extraReducers(builder) {
    builder.addCase(fetchPost.fulfilled, (state, action) => {
      state.isLoading = false;
      action.payload.forEach((data: IPost & { id: string }) => {
        const id = String(data.id);
        const pickedAttributes = pick(data, Object.keys(defaultPost)) as IPost;

        const model: IPostModel = {
          attributes: {
            id,
            ...pickedAttributes,
          },
        };

        state.data[id] = model;
      });
    });
  },
});

export const postReducer = postSlice.reducer;

效果:

基于 Redux + TypeScript 实现强类型检查和对 Json 的数据清理-LMLPHP

基于 Redux + TypeScript 实现强类型检查和对 Json 的数据清理-LMLPHP

这里的数据相对而言比较简单,因此说使用 pick 对 model 的操作好像有些多余。不过对我们项目来说:

  1. 后端会自动生成一堆前端用不到的 key,比如说创建时间、更新时间、创建对象、版本等,这些东西前段用不到,存储在状态里就是占用无谓的空间。我们项目数据量还比较大,有的时候会有十几二十来万的数据(2b 项目,非 2c),所以能做一点优化就做一点优化。
  2. 多余的数据传到后端会被 rejected 掉,所以对数据的过滤是必须的

基于 Redux + TypeScript 实现强类型检查和对 Json 的数据清理-LMLPHP

如果说一些属性是不需要的,直接在 interface 中删掉,TS 就会自动报错了。


类型检查主要还是防止 typo 以及写代码更方便一些,比如说:

基于 Redux + TypeScript 实现强类型检查和对 Json 的数据清理-LMLPHP

基于 Redux + TypeScript 实现强类型检查和对 Json 的数据清理-LMLPHP

我们项目的数据都是强定形的,所以对我们项目来说使用 TS 的优势绝对比使用 JS 多……尤其是有些 entity 会有一百多个 attributes,这时候如果没有一些智能提示或者是类型检查,报错都得掉一堆头发

是的,真的发生过,后来发现就因为是 typo 所以拿不到值……还有就是后端改了一些数据类型,然后前端没有在所有的组件中全部更新,导致有些页面出现问题有些事正常的。也就是那时候我们决定要落实 TS 的实现(虽然到最后只有我一个人在认真想怎么重构,猪队友都在用 any……痛苦……

08-05 05:06