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 小时前
一个没测暗色模式的 Bug,吃掉了我一半 谷歌扩展用户
前端·javascript·程序员
liangshanbo12151 小时前
React useState 函数式更新面试题整理
前端·react.js·前端框架
涛涛ing1 小时前
为什么你的页面在 Safari 上总出问题?Interop 2027 正在解决这个 20 年老毛病
前端
不一样的少年_2 小时前
WebP 压缩到底在干嘛?小白也能看懂的原理拆解
前端·后端·图片资源
问心无愧05132 小时前
ctf show web 177
前端·笔记
不一样的少年_2 小时前
PNG/JPG 如何变成 WebP?真相不是改后缀!
前端·后端·图片资源
杉氧2 小时前
RN 性能调优指南:重渲染(Re-renders)控制与长列表(FlatList)优化
android·前端·react native
一_个前端2 小时前
[JS] 一站式搞定 PDF、图片、Dom弹窗、表格的浏览器打印功能
前端
JavaGuide2 小时前
阿里 Qoder 又开源了一个专门给 Claude Code、Codex 做“体检”的项目
前端·后端
不一样的少年_2 小时前
JPEG 压缩到底在干嘛?小白也能看懂的 8 步拆解
前端·后端·图片资源