前置工作
使用 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 来初始化更多数据。下一步我们完善这个模块的所有功能,包括分页,筛选,交互优化