在现代电子商务发展迅速的今天,构建一个高效、易用的购物平台是开发者的一项关键技能。

该系列是全栈实践新坑,使用 React 和 NestJS 的技术栈、从零开始开发一个完整的购物平台(其实是先前开的几个全栈实践坑都让我意识到自己基础实力不足)。

1. 初始化 React + TypeScript 项目

  1. 使用以下命令创建 React 项目:

    1
    yarn create react-app shopping-nest --template typescript
  2. 导航至 shopping-nest 目录:

    1
    cd shopping-nest
  3. 使用 Yarn 安装依赖:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // 安装 ESLint 和 Prettier 相关依赖
    yarn add @typescript-eslint/eslint-plugin @typescript-eslint/parser
    yarn add -D eslint prettier eslint-config-prettier eslint-plugin-prettier
    yarn add -D eslint-config-react-app

    // 安装 react-router-dom
    yarn add react-router-dom @types/react-router-dom

    // 安装 axios
    yarn add axios

    // 安装 TailwindCSS 相关依赖
    yarn add tailwindcss postcss autoprefixer

    // 安装 UI 组件和图标库
    yarn add @headlessui/react
    yarn add lucide-react
  4. 运行 yarn run start 检查一下是否会出问题。

2. 配置 ESLint 和 Prettier

  • 我使用的是 Jetbrains WebStorm,记得要更新到 2024 的版本喔。
  • ESLint 的版本为 9.13.0
  • Prettier 的版本为 3.3.3

2.1. 配置 ESLint

  1. 运行以下命令:

    1
    npx eslint --init
  2. 根据自己的习惯选择。

  3. 生成的 mjs 配置文件差不多如下,我自己修改了 files 值为 src 目录下的文件。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    import globals from "globals";
    import pluginJs from "@eslint/js";
    import tseslint from "typescript-eslint";
    import pluginReact from "eslint-plugin-react";


    export default [
    {files: ["src/**/*.{js,mjs,cjs,ts,jsx,tsx}"]},
    {languageOptions: { globals: globals.browser }},
    pluginJs.configs.recommended,
    ...tseslint.configs.recommended,
    pluginReact.configs.flat.recommended,
    ];

    可以查看 ESLint 官方文档或者 TypeScript-ESLint 文档自行修改。我自己就保留默认的了。

2.2. 配置 Prettier

  1. 在项目目录处创建 .prettierrc 文件一个(项目目录这里默认为 package.json 所在的目录)。

  2. 除了查看 Prettier 官方文档自己填写外,还可以使用一些工具生成 Prettier 配置内容。

    我这里用了 Prettier Config Generator 生成:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    {
    "printWidth": 120,
    "tabWidth": 2,
    "useTabs": false,
    "semi": true,
    "singleQuote": true,
    "trailingComma": "none",
    "bracketSpacing":true,
    "bracketSameLine": true,
    "arrowParens": "avoid"
    }

3. 配置 Tailwind CSS

3.1. 安装并初始化 TailWind CSS 配置

在项目目录下使用以下命令来生成 tailwind.config.jspostcss.config.js

1
npx tailwindcss init -p

然后修改 tailwind.config.js 的内容,将 content 配置为监控 src 文件夹下的所有文件,以便在这些文件中应用 TailWind 的样式:

1
2
3
4
5
6
7
8
/** @type {import('tailwindcss').Config} */
export default {
content: [],
theme: {
extend: {},
},
plugins: []
};

src/index.css 文件的顶部添加以下内容,导入 TailWind 的核心样式、组件和工具:

1
2
3
@tailwind base;
@tailwind components;
@tailwind utilities;

3.2. 安装并配置 daisyUI 组件库

为了方便开发,安装 daisyUI 组件库,它提供了丰富的组件和自定义主题功能:

1
yarn add daisyui

然后在 tailwind.config.js 文件中引入 daisyUI 插件:

1
2
3
4
5
6
7
8
9
10
11
import daisyui from "daisyui";

// ...

export default {
// ...
plugins: [
daisyui,
],
daisyui: {}
}

对于 daisyUI 的配置,可以根据其文档进行修改。

我安装 daisyUI 还有一个目的,那就是其自定义主题的功能。

daisyUI主题生成器里,你可以选择自己设计一套颜色方案,或者说随机出一套颜色方案。该页面中还有预览页面可供参考。

我对颜色不敏感,设计能力也很遭殃。这是我随机出的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
export default {
// ...
daisyui: {
themes: [
{
mytheme: {
"primary": "#60a5fa",
"primary-content": "#030a15",
"secondary": "#00b7ac",
"secondary-content": "#000c0b",
"accent": "#d68900",
"accent-content": "#100700",
"neutral": "#182f19",
"neutral-content": "#ccd1cc",
"base-100": "#32253a",
"base-200": "#2a1f31",
"base-300": "#221928",
"base-content": "#d2cfd4",
"info": "#00a7c9",
"info-content": "#000a0f",
"success": "#67c400",
"success-content": "#040e00",
"warning": "#f97316",
"warning-content": "#150500",
"error": "#dc2626",
"error-content": "#ffd9d4",
}
}
]
}
}

3.3. 配置 PostCSS

根据 PostCSS 官方说的:

PostCSS 是一种利用 JS 插件转换样式的工具。

这些插件可以检查 CSS、支持变量和混合体、转译未来的 CSS 语法、内联图片等。

postcss.config.js 的初始配置如下:

1
2
3
4
5
6
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

目前这是基本的配置。如果需要更多 PostCSS 功能,可以根据需求进一步配置。

4. 设置路由基础结构

路由系统是管理不同 URL 对应显示不同页面内容的机制。

4.1. 创建统一布局

作为开发者,在构建 Web 应用时,创建一个统一的布局非常重要。因为它能够为用户提供一致的界面的导航体验。

在大多数应用中,我们会有一些固定的部分,比方说导航栏、页脚,以及一个用于动态展示内容的区域。

1
2
3
// src/layouts/MainLayout.tsx
import React from 'react';
import { Outlet } from 'react-router-dom';

首先引入 Outlet,它是 react-router-dom 提供的一个工具,允许在布局中插入不同的内容。

通过 Outlet,我们可以渲染由路由定义的组件,也就是首页啦、关于页这些,也不需要每次都重写导航和布局。

1
2
3
4
5
6
7
8
const MainLayout = () => {
return (
<div>
<header className="bg-primary shadow">
<nav className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
{/* TODO: 导航内容 */}
</nav>
</header>

<header> 标签来定义页面的头部,这里我之后会引入导航栏组件,先放个 TODO 马克一下。

1
2
3
4
5
6
7
8
      <main>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<Outlet />
</div>
</main>
</div>
);
};

接下来是 <main> 标签,它是页面的核心内容区域。

<Outlet /> 会根据当前路由,动态渲染不同的组件。在开发中,这个设计的好处是我们可以轻松地切换页面,并保持一致的布局框架。

4.2. 路由配置

在单页面应用,也就是 SPA 中,路由是关键。它决定了用户访问某个路径时应该显示哪个组件。

我们现在需要一个机制,让用户能够在不同页面之间切换,比如从首页切换到用户账户信息页。路由能够帮助我们将 URL 和组件相互关联,确保用户在访问特定路径时,看到对应的页面。

1
2
3
// src/router.tsx
import { createBrowserRouter } from 'react-router-dom';
import MainLayout from './layouts/MainLayout';

首先导入 createBrowserRouter,用它可以创建一个支持浏览器历史记录的路由系统。接着我们将先前定义好的 MainLayout 引入作为根布局。

1
2
3
4
5
6
const router = createBrowserRouter([
{
path: '/',
element: <MainLayout />
}
]);

这意味着,无论用户访问的子页面是什么,MainLayout 的结构都会保持一致,而页面主体部分会根据路由变化而动态加载。

4.3. 设置应用入口

App 组件是整个应用的入口。它负责将路由系统注入到 React 的组件树中,这样其他组件才能知道根据不同的路径应该显示什么内容。

为了加载整个路由配置,我们需要一个统一的入口,因此需要用到 RouterProvider。它将之前配置的路由传递给应用,让各个子组件能够根据 URL 做出相应的渲染。

1
2
3
// src/App.tsx
import { RouterProvider } from 'react-router-dom';
import router from './router';

引入 RouterProvider 和先前定义好的 router 配置。

1
2
3
const App = () => {
return <RouterProvider router={router} />;
};

RouterProvider 包裹住应用的根组件,并把 router 传递给它。通过这种方式,整个应用的路由系统就生效了。

虽然话是这么说,但因为内部什么组件都没写好,运行时还是什么都看不到的……

5. 配置状态管理工具

在开发中,状态管理是前端应用的核心部分之一,尤其是在涉及到用户登录、登出、数据持久化等功能时。

Zustand 是一个轻量级的状态管理库,它相比于 Redux 等传统工具更加简洁易用。因为是个练习项目,我便选择了这个更小巧的状态管理库。

5.1. 简单的例子

5.1.1. 初始化 Zustand 状态管理器

src 目录下创建一个 stores 目录,用于存放状态管理相关的文件。

在本例子中,代码结构分为三部分:状态定义和处理(UserStateactions.ts),Zustand 状态创建和持久化(index.ts),以及一些辅助函数(api.tslog.tsselector.ts)。

使用以下命令安装 Zustand:

1
yarn add zustand

stores 目录下创建 index.ts,用作状态管理的入口,这样在应用的其他部分就可以方便地引入状态管理逻辑。

1
import useUserStore from './user';

作为一个大致的参考,我选择去写一个用户状态。这里先引入一下这个还未开始写的自定义钩子,理想情况下,它应当允许我们访问和操作与用户相关的状态。

在整个应用中,我们将通过这个钩子获取当前用户信息或调用登录操作。

5.1.2. 定义用户状态类型和接口

stores 目录下创建 user 目录,接着又在 user 目录下创建 types.ts

1
2
3
4
5
6
7
8
9
10
11
export interface User {
id: string;
name: string;
email: string;
}

export interface UserState {
user: User | null;
isLoading: boolean;
error: string | null;
}
  • TypeScript 中的 interface 用于定义用户状态的结构。使用 interface 能更直观地展示用户状态中的各项属性,同时在项目扩展时易于维护

这里定义了 UserUserState,这两个接口分别描述了用户对象的结构和与用户相关的状态。

当然,作为一个参考,这些值后续一定会进行修改或者扩展。

  • User 接口不用多说。UserState 接口包括了:

    • user:当前登录的用户信息;没有用户登录的话就是 null
    • isLoading:是否正在进行异步操作,比如登录请求
    • error:当然是错误信息啦
  • setUser 是一个函数属性,接收一个 User 或者 null 类型的参数

  • login 函数属性接收 LoginCredentials 参数,并返回一个 Promise<User>(使用 TypeScript 的时候这样写有助于检查函数的参数和返回值类型,减少类型错误)

5.1.3. 创建 Zustand 状态管理器

user 目录下创建 index.ts

我们通过 Zustand 来创建一个用户状态管理器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';
import { UserState } from './types';
import createUserSlice from './actions';

const useUserStore = create<UserState>()(
devtools(
persist(
createUserSlice,
{
name: 'user-storage'
}
)
)
);

export default useUserStore;

这里我们引入了 Zustand 的两个中间件:devtoolspersist

  • devtools 允许我们在开发时使用 Redux DevTools 进行状态调试,方便查看状态的变化
  • persist 实现状态的持久化,将用户状态保存在 localStorage 中(先前使用 Redux 的时候,都是要手动使用 localStorage 进行持久性保存。Zustand 则可以直接使用 persist 中间件实现状态的持久化)。这样即使用户刷新页面,用户信息依然保留
    • name: 'user-storage' 指定了持久化状态的存储键名

5.1.4. 定义用户状态的操作与异步行为

user 目录下创建 actions.ts,定义状态和异步操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { StateCreator } from 'zustand';
import { User, UserState } from './types';

const createUserSlice: StateCreator<UserState> = (set) => ({
user: null,
isLoading: false,
error: null,

setUser: (user: User | null) => set({ user }),

login: async (credentials: { email: string; password: string }) => {
set({ isLoading: true, error: null });
try {
// TODO: 调用API
set({ isLoading: false, user: response.data });
} catch (error) {
set({ isLoading: false, error: error.message });
}
}
});

export default createUserSlice;

在这个文件中,我们定义了用户状态的操作逻辑和异步操作。

对于大多数应用来说,登录是一个异步过程,我们需要在发起请求时更新 isLoading 状态,同时在请求失败时记录错误信息。

  1. 初始状态,也就是用户未登录时,user 设为 nullisLoadingfalseerror 也为空。

  2. setUser 是一个简单的同步方法,用于手动设置用户信息。

  3. login 是一个异步函数,用于处理登录逻辑。

    开发中,典型的流程是:

    1. 设置 isLoadingtrue,以便显示加载状态
    2. 发起登录请求(因为还没写,就用 TODO 标记了。注意哈,现在这个时候跑指定报错)
    3. 请求成功后,将返回的用户信息存储到状态中,并重置 isLoadingfalse
    4. 如果请求失败,捕获错误,并更新 error 状态,用户可看到错误提示(现在当然不行)

5.2. 进阶配置

我们已经配置了 Zustand 的基本用户状态管理。接下来,我们将借助 TypeScript,进一步优化和扩展状态管理的功能,包括状态持久化、自定义中间件和选择器等。

5.2.1. 状态持久化与部分存储

在生产环境中,为了提升用户体验,状态持久化是一个常见需求。Zustand 提供了 persist 中间件,帮助我们将部分状态保存在 localStorage 或其他存储中,以确保页面刷新后状态不会丢失。

stores/user/index.ts 中,我们定义了 persistOptions,并在其中使用了 partialize 功能,将状态中关键的部分(如用户信息和更新时间)持久化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { create } from 'zustand';
import { devtools, persist, subscribeWithSelector } from 'zustand/middleware';
import type { PersistOptions } from 'zustand/middleware';
import { UserState } from './types';
import createUserSlice from './actions';
import { log } from '../common/log';
import { createSelectors } from '../common/selector';

type UserPersist = Pick<UserState, 'user' | 'lastUpdated'>;

const persistOptions: PersistOptions<UserState, UserPersist> = {
name: 'user-storage',
partialize: (state) => ({
user: state.user,
lastUpdated: state.lastUpdated,
}),
};
  • Pick<UserState, 'user' | 'lastUpdated'>:使用 Pick 类型将 UserState 中的 userlastUpdated 属性挑选出来,简化了持久化的内容
  • PersistOptions 类型:类型声明让我们清楚地知道哪些状态会被持久化,避免错误持久化不必要的数据
    • partialize 是一个用于选择性地存储状态对象中部分属性的函数。在我们的 persistOptions 里,它的作用是从 UserState 状态中挑出 userlastUpdated 这两个属性,并将其存储到持久化的存储中

5.2.2. 订阅特定的状态

subscribeWithSelector 允许我们订阅特定的状态属性变化。与直接订阅整个状态的变化不同,它可以细化到仅在某些具体属性更新时触发回调,从而减少不必要的订阅响应。

继续写 stores/user/index.ts

1
2
3
4
5
6
7
8
9
10
const useUserStoreBase = create<UserState>()(
devtools(
persist(
subscribeWithSelector(
log(createUserSlice),
),
persistOptions,
)
)
);

通过组合其他的 Zustand 插件,我们创建了一个订阅机制。这样做的好处是提高性能、避免不必要的渲染。

接下来这段代码订阅了 useUserStoreBase 中的 user 属性:

1
2
3
4
5
6
7
8
9
10
11
12
useUserStoreBase.subscribe(
(state) => state.user,
(user) => {
if (user) {
console.log('User logged in: ', user.name);
} else {
console.log('User logged out');
}
}
)

export const useUserStore = createSelectors(useUserStoreBase);
  • (state) => state.user 是一个选择器函数,只返回 state 中的 user 属性,从而使订阅仅响应 user 的变化
  • user 属性变化时,回调触发。回调会根据 user 是否存在(如 usernull,或者用户登陆了新的信息)来输出不同的登录状态信息

5.2.3. 自定义日志中间件

为了方便调试,我们可以创建一个日志中间件。这个中间件会在每次状态更新时,记录状态变化信息。

stores/common/log.ts 中定义 log 函数,扩展 Zustand 的 set 方法,使其在应用状态变化时输出变更详情。

先写一个泛型类型,用于定义 set 函数所接收的各种更新方式:

1
2
3
import { StateCreator } from 'zustand';

type SetStateAction<T> = T | Partial<T> | ((state: T) => T | Partial<T>);
  • SetStateAction<T> 的作用是确保状态的更新类型符合期望,允许直接提供新的状态值、部分更新或基于当前状态的更新函数
    • T:泛型参数,表示整个状态对象的类型,例如 UserState
    • 类型定义:
      • T:可以直接传入整个状态对象,用于完全替换现有状态
      • Partial<T>:可以传入部分状态对象,即只更新部分属性。Partial<T> 将状态对象的所有属性变为可选
      • (state: T) => T | Partial<T>:可以传入一个函数,这个函数接收当前状态作为参数,并返回新的状态或部分状态。这种方式允许在回调中基于现有状态动态生成更新值

这样写的目的是为了更灵活的状态更新方式,不仅可以直接替换状态,也可以部分更新或在回调函数中动态更新。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
export const log = <T extends object>(
config: StateCreator<T, [], [], T>
): StateCreator<T, [], [], T> =>
(set, get, api) => config(
(partial: SetStateAction<T>, replace?: boolean) => {
console.log('Applying', { partial, replace });
if (replace) {
set(partial as T | ((state: T) => T), true);
} else {
set(partial as SetStateAction<T>);
}
console.log('New state: ', get());
},
get,
api
);

log 是一个高阶函数(也就是 HOC),接受一个 Zustand 的 StateCreator 配置函数,并返回一个经过增强的 StateCreator,用于记录状态的变化。

log 的作用是对传入的 config 配置函数进行包装,以便在状态更新时打印更新的内容和更新后的状态,用于调试。

  • log 的内部逻辑:

    1. 参数:

      • config:一个 Zustand 的 StateCreator 函数,负责创建状态。此函数会调用 set 函数来更新状态
    2. 返回值:一个增强的 StateCreator 函数,用于替代原始 config 函数

    3. 内部逻辑:

      • 包装 set 函数:调用 config 时,将自定义的 set 函数传入

        • 自定义的 set 函数接收 partialreplace 两个参数:

          • partial:可以是新的状态值、部分状态值,也可以是一个返回状态的函数
          • replace:布尔值,表示是否完全替换现有状态
        • 日志输出:

          • console.log('Applying', { partial, replace }) 在更新前输出即将应用的部分状态或新状态
          • console.log('New state: ', get()) 在更新后输出新的
      • 更新逻辑:

        1. replacetrue,则完全替换当前状态;否则只应用部分更新
        2. 调用 get 获取新的状态并打印日志

5.2.4. 状态选择器

在状态管理中,我们通常需要对状态进行选择,以便在不同组件中访问特定的状态字段。

createSelectors 帮助我们自动生成访问器,减少在不同组件中冗余的状态逻辑。

stores/common/selector.ts 中定义 createSelectors,它会为状态中的每个字段创建一个 getter 函数,便于状态的解耦:

1
2
3
4
5
import { StoreApi, UseBoundStore } from 'zustand';

type WithSelectors<S> = S extends { getState: () => infer T }
? S & { use: { [K in keyof T]: () => T[K] } }
: never;
  • WithSelectors<S> 定义了一个条件类型,用于增强传入的 store 类型 S
    • S extends { getState: () => infer T } 检查 S 是否包含 getState 方法,并从中推断出 T 类型(状态对象的类型)
    • 返回:
      • S 满足条件,则返回 S 并附加一个 use 属性
        • use 是一个对象,包含状态对象中每个键对应的 getter 方法,这些方法返回 T[K],即每个状态属性的值
      • S 不满足条件,则返回 never
1
2
3
4
5
6
7
8
9
10
11
12
13
14
export const createSelectors = <
S extends UseBoundStore<StoreApi<T>>,
T extends object
>(_store: S) => {
const store = _store as WithSelectors<S>;
store.use = {} as { [K in keyof T]: () => T[K] };
const state = store.getState();

for (const k of Object.keys(state) as Array<keyof T>) {
store.use[k] = () => state[k];
}

return store;
}

createSelectors 函数:

  • 参数:接收一个 Zustand store 实例 _store
  • 类型约束:
    • S extends UseBoundStore<StoreApi<T>>:约束 S 必须是一个 UseBoundStore 类型的 store
    • T extends object:状态对象 T 必须是一个对象
  • 逻辑:
    1. 将传入的 store _store 进行类型转换,以便使用 WithSelectors 增强后的类型
    2. store 增加 use 属性(一个空对象),作为存放每个状态属性 getter 方法的容器
    3. 获取当前 store 的 state 对象
    4. for 循环遍历 state 对象的键(即状态对象的属性)
      • 对每个键 k,在 store.use 中创建一个对应的 getter 方法 store.use[k],返回 state[k] 的值
    5. 返回增强后的 store 实例 store,其中包含 use 对象和对应的 getter 方法

假设 store 的状态对象如下:

1
2
3
4
5
6
const useStore = createSelectors(
create((set) => ({
user: { name: "Alice", age: 30 },
loggedIn: true
}))
);

那么调用 useStore.use.user() 就会返回:

1
{ name: "Alice", age: 30 }

5.2.5. API 请求的配置和错误处理

在前端状态管理中,一般会包含 API 请求的逻辑。

我们在 stores/user/actions 中,定义一个 createUserSlice 函数,它是 Zustand 中 UserState 的部分实现,用于管理用户相关的状态和操作。

首先导入依赖:

1
2
3
4
import { StateCreator } from 'zustand';
import { AxiosResponse } from 'axios';
import { User, UserState, LoginCredentials } from './types';
import api from '../common/api';

定义状态和操作:

1
2
3
4
5
6
7
const createUserSlice: StateCreator<UserState> = (set) => ({
user: null,
isLoading: false,
error: null,
lastUpdated: null,

// ...
  • user:存储当前用户信息
  • isLoading:指示登录操作是否正在进行中
  • error:保存登录过程中发生的错误信息
  • lastUpdated:记录上次用户数据更新的时间戳
1
2
3
// ...
setUser: (user: User | null) => set({ user }),
// ...
  • setUser:一个同步方法,用于直接设置 user 状态。接收一个 User 对象或者 null,并调用 set 更新状态
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// ...
login: async (credentials: LoginCredentials) => {
set({ isLoading: true, error: null });
try {
const response: AxiosResponse<User> = await api.post<User>('/auth/login', credentials);
const user = response.data;

set({
user,
isLoading: false,
lastUpdated: Date.now(),
error: null
});

return user;
} catch (error) {
const errorMessage = error instanceof Error
? error.message
: 'An unexpected error occurred during login';

set({
isLoading: false,
error: errorMessage,
user: null
});

throw error;
}
},
// ...

login 方法:

  • 启动加载状态:调用 set({ isLoading: true, error: null })isLoading 设置为 true,并清除之前的错误
  • API 请求:await api.post<User>('/auth/login', credentials) 向服务器发送登录请求。返回的 response.data 包含了用户信息
  • 成功处理:
    1. 若请求成功,set 更新状态,存储用户数据、停止加载、设置 lastUpdated 时间戳,并清除错误
    2. 返回 user,便于在调用 login 的地方使用
  • 错误处理:
    1. 如果请求失败,捕获 error 并生成错误消息
    2. 更新 setisLoading 设置为 false,保存 error 信息,并将 user 设置为 null
    3. 抛出错误,以便调用 login 的组件也能捕获并处理该错误
1
2
3
4
5
6
7
8
9
10
11
  // ...
logout: () => {
set({
user: null,
error: null,
lastUpdated: null
});
}
});

export default createUserSlice;

logout 方法是登出功能,说白了就是将所有的状态设置为 null,从而达到清除当前用户信息和错误的效果。

6. 设置 API 请求封装

至于 API 嘛,写在了 stores/common/api.ts 中:

1
2
3
4
5
6
import axios from 'axios';

const api = axios.create({
baseURL: process.env.REACT_APP_API_URL,
timeout: 10000
});

axios 配置了一个 API 实例 api,设置了基本的请求和响应拦截器。

  • baseURL 为环境变量 REACT_APP_API_URL,还设置了 10 秒的超时时间
1
2
3
4
5
6
7
api.interceptors.request.use(
(config) => {
// TODO: 添加认证信息
return config;
},
(error) => Promise.reject(error)
);
  1. 请求拦截器 api.interceptors.request.use 提供了请求发送前的自定义逻辑处理。可以在 config 中添加认证信息(也就是老生常谈的 JWT Authorization 头)。

  2. 如果请求在发送前就失败了,那么拦截器将直接拒绝该错误

1
2
3
4
5
6
7
8
9
api.interceptors.response.use(
(response) => response,
(error) => {
// TODO: 统一错误信息
return Promise.reject(error);
}
);

export default api;

响应拦截器 api.interceptors.response.use 允许在接收到响应时进行自定义处理。

  1. 响应成功会直接返回数据
  2. 请求出错,error 就会被统一处理,然后传递给调用方处理

有很多功能先放 TODO 了,能差不多 GET 到意思就好。