AI 编程时代手工匠人代码打造 React 项目实战(四):使用路由参数 & mock 接口数据

前置工作

使用 pnpm i msw @mswjs/data axios 安装 msw 和 axios

编码流程

1.添加路由参数

现在我们的表单只支持新增,如果要修改原来已有的数据,我们就得把目标数据代入,对此我们给路由的路径上添加 id 参数,来匹配对应的数据项

tsx 复制代码
// router/index.tsx
const router = createBrowserRouter([
  {
    path: "/",
    children: [
      {
        path: "user",
        children: [
          //...
          // 添加可选参数 userId, 当它存在时我们认为是修改存在的项
          {
            path: 'form/:userId?',
            Component: UserForm
          }
        ]
      }
    ]
  }
])

使用 useParams hook 来获取路径上的参数,当 id 参数存在时,我们认为是修改存在的项

tsx 复制代码
// pages/user/UserForm.tsx
import { UserContext, UserDispatchContext } from "./UserContext";
import { useNavigate, useParams } from "react-router";
​
function UserForm() {
  const dispatch = useContext(UserDispatchContext)
  const users = useContext(UserContext)
  const params = useParams()
  const { userId } = params
  
  //...
  useEffect(() => {
    if (userId) {
      const target = users.find(item => item.userId === userId) || defaultFormData
      form.setFieldsValue({...target})
    }
  }, [userId, users, form])
  //...
  const handleSubmit = (values: UserFormData) => {
    // 新增修改的 payload
    if (userId) {
      dispatch({
        type: 'update',
        payload: {
          id: userId,
          patch: {
            ...values,
            age: Number(values.age),
          }
        }
      })
    } else {
      dispatch({
        type: 'add',
        payload: {
          ...values,
          age: Number(values.age),
          userId: Math.ceil(Math.random() * 100) + ""
        }
      })
    }
    
    // 添加之后 reset
    handleReset();
    navigate('../list')
  };
}
2.使用 msw 配置 mock

我们使用 msw@mswjs/data 来模拟接口数据, msw 是一个基于 Service Worker 的 mock 工具,可以拦截我们注册了的请求, @mswjs/data 可以提供一个运行时的虚拟数据库,正好符合我们练习的需求。

参考mswjs 让前端 mock 不只是在本地的步骤,使用 npx msw init ./public --save 生成一个脚本,然后在 src 中创建我们的 mock 文件夹

接着创建我们的数据库文件

ts 复制代码
// src/mock/db.ts
import { factory, primaryKey } from '@mswjs/data';
​
export const db = factory({
  user: {
    id: primaryKey(String),
    name: String,
    age: Number
    city: String,
    role: String
  },
});
​
export default db

创建 handler 文件,这里我们先测试一下获取列表功能

ts 复制代码
// src/mock/handlers/user.ts
import { http } from 'msw'
import { v4 as uuidv4 } from 'uuid'
import { db } from '../database'
import { getApiUrl, sendJson } from '../utils'
import type { User } from '@/types/user'
import { RESPONSE_CODE_DICT } from '@/types/http'
​
export const userHandlers = [
  // 获取用户列表
  http.post('https://api.react-bootstrap.com/user/getList', () => {
    const data = db.user.getAll()
    return sendJson(RESPONSE_CODE_DICT.SUCCESS, data)
  }),
]
​
export default userHandlers
​
// src/mock/handler.ts
import type { HttpHandler } from "msw";
​
const handlers = import.meta.glob("./handlers/*.ts", { eager: true }) as Record<
  string,
  {
    default: HttpHandler[];
  }
>;
​
const allHandlers = Object.keys(handlers).reduce((pre, currKey) => {
  return pre.concat(handlers[currKey].default);
}, [] as HttpHandler[]);
console.log(allHandlers);
​
export default allHandlers;

创建 mock 入口文件,由于 Service Worker的注册是需要时间的,所以 这里我们给 start 添加 await

ts 复制代码
// src/mock/index.ts
import { setupWorker } from 'msw/browser'
import allHandlers from './handler'
​
export const worker = setupWorker(...allHandlers)
​
export async function initMswWorker() {
   await worker.start({
    onUnhandledRequest: 'bypass',
    serviceWorker: {
      url: `/mockServiceWorker.js`,
    },
  })
}

接着我们简单封装一下 axios

ts 复制代码
// src/http/request.ts
import axios from "axios";
​
const request = axios.create({
  baseURL: "https://api.react-bootstrap.com",
  timeout: 10000,
  headers: {
    'Content-Type': 'application/json'
  }
})
​
export default request

修改 main.ts 在应用启动前初始化我们的 mock 服务

ts 复制代码
// src/main.tsx
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { RouterProvider } from 'react-router'
import router from './router/index.tsx'
import { initMswWorker } from './mock/index.ts'
​
initMswWorker().then(() =>{
  createRoot(document.getElementById('root')!).render(
    <StrictMode>
      <RouterProvider router={router} />
    </StrictMode>,
  )
})

修改 UserLayou.tsx 来测试 mock 是否可用

ts 复制代码
import request from "@/http/request";
​
function UserLayout() {
  useEffect(() => {
    let ignore = false
    request.post('user/getList').then(res => {
      if (ignore) return
      console.log(res.data)
    })
    return () => {
      ignore = true
    }
  }, [])
  //...
}

打开控制台发现成功打印出了数据,至此我们完成了模拟从服务端获取数据的流程,接着我们调整我们的代码从接口获取数据。之前我们做的 Context 用来给 form 和 list 提供数据,但是真实场景下这两个页面的数据不是关联的,一般都是从服务端根据参数来获取,所以我们可以移除掉 UserLayout 这个文件,在 form 和 list 里面写业务逻辑

ts 复制代码
// src/mock/handlers/user.ts
import { http } from "msw";
import { v4 as uuidv4 } from "uuid";
import { db } from "../database";
import { getApiUrl, sendJson } from "../utils";
import type { User } from "@/types/user";
import { RESPONSE_CODE_DICT } from "@/types/http";
​
export const userHandlers = [
  // 获取用户列表
  http.post(getApiUrl("/user/getList"), () => {
    const data = db.user.getAll();
    return sendJson(RESPONSE_CODE_DICT.SUCCESS, data);
  }),
​
  // 获取用户
  http.get(getApiUrl("/user/:id"), ({ params }) => {
    const { id } = params
    const target = db.user.findFirst({
      where: { 
        id: {
          equals: id as string
        }
       }
    })
    if (target) {
      return sendJson(RESPONSE_CODE_DICT.SUCCESS, target)
    } else {
      return sendJson(RESPONSE_CODE_DICT.NOT_FOUND, null, '用户不存在')
    }
  }),
​
  http.post<never, Omit<User, "id">>(
    getApiUrl("/user/create"),
    async ({ request }) => {
      const newUser = await request.json();
​
      // 向 user 表中添加一个用户数据
      const user = db.user.create({
        id: uuidv4(),
        ...newUser,
      });
​
      return sendJson(RESPONSE_CODE_DICT.SUCCESS, user);
    }
  ),
​
  // 删除用户
  http.delete(getApiUrl("/user/:id"), ({ params }) => {
    const { id } = params;
​
    const targetUser = db.user.findFirst({
      where: {
        id: { equals: id as string },
      },
    });
​
    if (targetUser) {
      db.user.delete({
        where: {
          id: { equals: id as string },
        },
      });
​
      return sendJson(
        RESPONSE_CODE_DICT.SUCCESS,
        null,
        `删除用户【${targetUser.name}】成功`
      );
    } else {
      return sendJson(RESPONSE_CODE_DICT.NOT_FOUND, null, "用户不存在");
    }
  }),
​
  // 更新用户
  http.post<never, User>(getApiUrl("/user/update"), async ({ request }) => {
    const newUser = await request.json();
    const targetId = newUser.id;
​
    const targetUser = db.user.findFirst({
      where: {
        id: { equals: targetId },
      },
    });
​
    if (targetUser) {
      const updated = db.user.update({
        where: {
          id: { equals: targetId },
        },
        data: newUser,
      });
​
      return sendJson(RESPONSE_CODE_DICT.SUCCESS, updated, `更新成功`);
    } else {
      return sendJson(RESPONSE_CODE_DICT.NOT_FOUND, null, "用户不存在");
    }
  }),
];
​
export default userHandlers;

修改 list

tsx 复制代码
// pages/user/UserList.tsx
import type { User } from "@/types/user";
import { type TableProps, Table } from "antd";
import { useState, type ChangeEvent, useEffect } from "react";
import { useNavigate } from "react-router";
import request from "@/http/request";
​
const defaultCondition = {
  keyword: ''
}
​
function UserList() {
  const navigate = useNavigate();
  const [data, setData] = useState<User[]>([]);
  const [condition, setCondition] = useState({ ...defaultCondition });
​
  const updateCondition = (key: keyof typeof condition, value: typeof condition[keyof typeof condition]) => {
    setCondition({
      ...condition,
      [key]: value
    })
  }
​
  // 从接口获取数据
  const fetchData = async () => {
    const res = await request.post("user/getList", { ...condition });
    return res.data.data || [];
  }
​
  const loadData = async() => {
    const res = await fetchData()
    setData(res)
  }
​
  // 页面初始化时获取数据
  useEffect(() => {
    let ignore = false;
    const loadData = async () => {
      const result = await fetchData();
      if (!ignore) {
        setData(result);
      }
    };
    loadData();
    return () => {
      ignore = true;
    };
  }, []);
​
  const handleDelete = async (targetId: string) => {
    await request.delete(`/user/${targetId}`);
    //重新获取数据
    fetchData();
  };
​
  const columns: TableProps<User>["columns"] = [
    {
      title: "用户ID",
      dataIndex: "id",
      key: "id",
    },
    {
      title: "姓名",
      dataIndex: "name",
      key: "name",
    },
    {
      title: "年龄",
      dataIndex: "age",
      key: "age",
    },
    {
      title: "所在地",
      dataIndex: "city",
      key: "city",
    },
    {
      title: "角色名称",
      dataIndex: "role",
      key: "role",
    },
    {
      title: "操作",
      dataIndex: "operation",
      key: "operation",
      render: (_, record) => {
        return (
          <>
            <button onClick={() => navigate(`../form/${record.id}`)}>修改</button>
            <button onClick={() => handleDelete(record.id)}>删除</button>
          </>
        );
      },
    },
  ];
​
  return (
    <div>
      <input
        value={condition.keyword}
        onChange={(e: ChangeEvent<HTMLInputElement>) => {
          updateCondition('keyword', e.target.value)
        }}
      />
      <button onClick={loadData}>获取</button>
      <button onClick={() => navigate("../form")}>创建</button>
      <Table dataSource={data} columns={columns} />
    </div>
  );
}
​
export default UserList;

修改 form

tsx 复制代码
// pages/user/UserForm.tsx
import request from "@/http/request";
import type { UserFormData } from "@/types/user";
import { Form, Input, Select } from "antd";
import {  useEffect } from "react";
import { useNavigate, useParams } from "react-router";
​
const defaultFormData: Partial<UserFormData> = {
  name: "",
  age: undefined,
  city: "广州",
  role: "销售",
};
​
​
function UserForm() {
  const navigate = useNavigate()
​
  const params = useParams()
  const { userId } = params
​
  const [form] = Form.useForm();
​
​
  useEffect(() => {
    if (!userId) return
​
    let ignore = false
    const fetchInfo = async () => {
      const info =  await request.get(`user/${userId}`)
      if (!ignore) {
        form.setFieldsValue(info.data.data)
      }
    }
    fetchInfo()
​
    return () => {
      ignore = true
    }
  }, [])
​
​
  const handleReset = () => {
    form.resetFields();
  };
​
  const handleSubmit = async(values: UserFormData) => {
    if (userId) {
      await request.post('user/update', {
        id: userId,
        ...values
      })
    } else {
      await request.post('user/create', values)
    }
    
    // 添加之后 reset
    handleReset();
    navigate('../list')
  };
​
  return (
    <Form
      form={form}
      labelCol={{ span: 8 }}
      wrapperCol={{ span: 16 }}
      style={{ maxWidth: 600 }}
      initialValues={defaultFormData}
      autoComplete="off"
      onFinish={handleSubmit}
    >
      <Form.Item<UserFormData>
        label="姓名"
        name="name"
        rules={[{ required: true, message: "请输入姓名" }]}
      >
        <Input />
      </Form.Item>
      <Form.Item<UserFormData>
        label="年龄"
        name="age"
        rules={[{ required: true, message: "请输入年龄" }]}
      >
        <Input />
      </Form.Item>
      <Form.Item<UserFormData>
        label="所在地"
        name="city"
        rules={[{ required: true }]}
      >
        <Select>
          <Select.Option value={"深圳"}>深圳</Select.Option>
          <Select.Option value={"广州"}>广州</Select.Option>
        </Select>
      </Form.Item>
      <Form.Item<UserFormData>
        label="角色"
        name="role"
        rules={[{ required: true }]}
      >
        <Select>
          <Select.Option value={"销售"}>销售</Select.Option>
          <Select.Option value={"销售经理"}>销售经理</Select.Option>
        </Select>
      </Form.Item>
​
      <Form.Item label={null}>
        <>
          <button type="submit">{userId ? '修改' : '添加' }</button>
          <button type="button" onClick={handleReset}>
            重置
          </button>
        </>
      </Form.Item>
    </Form>
  );
}
​
export default UserForm

小结

我们实现了根据路由参数来获取初始数据,同时实现了一套 mock server 来模拟发送请求,这个 mock server 还可以结合 faker.js 来初始化更多数据。下一步我们完善这个模块的所有功能,包括分页,筛选,交互优化

相关推荐
追梦人物1 小时前
Uniswap 手续费和协议费机制剖析
前端·后端·区块链
拾光拾趣录2 小时前
基础 | 🔥6种声明方式全解⚠️
前端·面试
PineappleCoder4 小时前
深入浅出React状态提升:告别组件间的"鸡同鸭讲"!
前端·react.js
wycode4 小时前
Vue2源码笔记(1)编译时-模板代码如何生效之生成AST树
前端·vue.js
程序员嘉逸4 小时前
LESS 预处理器
前端
橡皮擦1994 小时前
PanJiaChen /vue-element-admin 多标签页TagsView方案总结
前端
程序员嘉逸4 小时前
SASS/SCSS 预处理器
前端
咕噜分发企业签名APP加固彭于晏4 小时前
腾讯云eo激活码领取
前端·面试
子林super4 小时前
MySQL 复制延迟的排查思路
前端