使用 create-react-app 创建项目
npx create-react-app react-mock
执行 eject 命令
npm run eject
删除 package.json 文件中的 eslintConfig 选项
npm i path-to-regexp fast-glob chokidar axios
在 config 文件夹中创建 WebpackMiddlewareMock.js 文件
让webpack-dev-server加载中间件,把mock配置文件的请求地址和响应数据映射到dev server的路由上
- const { pathToRegexp } = require("path-to-regexp");
- const fg = require("fast-glob");
- const path = require("path");
- const chokidar = require("chokidar");
-
- const VALID_METHODS = [
- "GET",
- "POST",
- "PUT",
- "DELETE",
- "PATCH",
- "HEAD",
- "OPTIONS",
- ];
- const DEFAULT_METHOD = "GET";
- const MOCK_FILE_PATTERN = "mock/**/*.{js,ts}";
- const DEFAULT_OPTIONS = {
- rootDir: "",
- exclude: [],
- };
-
- class WebpackMiddlewareMock {
- constructor(options = {}) {
- this.options = { ...DEFAULT_OPTIONS, ...options };
- const { rootDir, exclude } = this.options;
- this.mockConfigs = this.getConfigs(rootDir, exclude);
- }
-
- parseMockKey(key) {
- const keyItems = key.split(/\s+/);
- if (keyItems.length === 1) {
- return {
- method: DEFAULT_METHOD,
- path: keyItems[0],
- };
- } else {
- const [method, path] = keyItems;
- const upperCaseMethod = method.toLocaleUpperCase();
- if (!VALID_METHODS.includes(upperCaseMethod)) {
- console.error(`method ${method} is not supported`);
- }
- if (!path) {
- console.error(`${key} path is not defined`);
- }
- return {
- method,
- path,
- };
- }
- }
-
- getConfigs(rootDir, exclude) {
- const ignore = exclude.map(
- (ele) => `mock${ele.startsWith("/") ? "" : "/"}${ele}`
- );
- const mockFiles = fg
- .sync(MOCK_FILE_PATTERN, {
- cwd: rootDir,
- ignore,
- })
- .map((file) => path.join(rootDir, file));
-
- const mockConfigs = [];
- mockFiles.forEach((mockFile) => {
- // disable require cache
- delete require.cache[mockFile];
- let mockModule;
- try {
- mockModule = require(mockFile);
- } catch (error) {
- console.error(`Failed to parse mock file ${mockFile}`);
- console.error(error);
- return;
- }
- const config = mockModule.default || mockModule || {};
- for (const key of Object.keys(config)) {
- const { method, path } = this.parseMockKey(key);
- const handler = config[key];
- if (
- !(
- typeof handler === "function" ||
- typeof handler === "object" ||
- typeof handler === "string"
- )
- ) {
- console.error(
- `mock value of ${key} should be function or object or string, but got ${typeof handler}`
- );
- }
- mockConfigs.push({
- method,
- path,
- handler,
- });
- }
- });
-
- return mockConfigs;
- }
-
- matchPath(req, mockConfigs) {
- for (const mockConfig of mockConfigs) {
- const keys = [];
- if (req.method.toLocaleUpperCase() === mockConfig.method) {
- const re = pathToRegexp(mockConfig.path, keys);
- const match = re.exec(req.path);
- if (re.exec(req.path)) {
- return {
- keys,
- match,
- mockConfig,
- };
- }
- }
- }
- }
-
- decodeParam(val) {
- if (typeof val !== "string" || val.length === 0) {
- return val;
- }
- try {
- return decodeURIComponent(val);
- } catch (error) {
- if (error instanceof URIError) {
- error.message = `Failed to decode param ' ${val} '`;
- error.status = 400;
- error.statusCode = 400;
- }
- throw error;
- }
- }
-
- createWatch() {
- const watchDir = this.options.rootDir;
- const watcher = chokidar
- .watch(watchDir, {
- ignoreInitial: true,
- ignored: [/node_modules/],
- })
- .on("all", () => {
- const { rootDir, exclude } = this.options;
- this.mockConfigs = this.getConfigs(rootDir, exclude);
- });
- return watcher;
- }
-
- createMiddleware() {
- const middleware = (req, res, next) => {
- const matchResult = this.matchPath(req, this.mockConfigs);
- if (matchResult) {
- const { match, mockConfig, keys } = matchResult;
- const { handler } = mockConfig;
- if (typeof handler === "function") {
- const params = {};
- for (let i = 1; i < match.length; i += 1) {
- const key = keys[i - 1];
- const prop = key.name;
- const val = this.decodeParam(match[i]);
- if (val !== undefined) {
- params[prop] = val;
- }
- }
- req.params = params;
- handler(req, res, next);
- return;
- } else {
- return res.status(200).json(handler);
- }
- } else {
- next();
- }
- };
- this.createWatch();
- return {
- name: "mock",
- middleware: middleware,
- };
- }
-
- static use(options) {
- const instance = new WebpackMiddlewareMock(options);
- const middleware = instance.createMiddleware();
- return middleware;
- }
- }
-
- module.exports = WebpackMiddlewareMock;
修改 config/webpackDevServer.config.js 文件
引入 WebpackMiddlewareMock 中间件
const WebpackMiddlewareMock = require("./WebpackMiddlewareMock");
删除 onBeforeSetupMiddleware 和 onAfterSetupMiddleware 选项,替换 setupMiddlewares 选项
- setupMiddlewares: (middlewares, devServer) => {
- const mockMiddleware = WebpackMiddlewareMock.use({
- rootDir: paths.appPath,
- });
- middlewares.unshift(mockMiddleware);
- return middlewares;
- },
在项目根目录创建 mock 文件夹,并创建 user.js 文件
- module.exports = {
- // 返回值是 String 类型
- "GET /api/name": "tom",
- // 返回值 Array 类型
- "POST /api/users": [
- { name: "foo", id: 0 },
- { name: "bar", id: 1 },
- ],
- "GET /api/users/:id": (req, res) => {
- res.send({
- params: req.params,
- });
- },
- // 返回值是 Object 类型
- "DELETE /api/users/1": { name: "bar", id: 1 },
- };
修改 App.js 文件
- import { useState } from "react";
- import axios from "axios";
-
- function App() {
- const [resultGet, setResultGet] = useState("");
- const [resultPost, setResultPost] = useState("");
- const [resultParams, setResultParams] = useState("");
- const [resultDelete, setResultDelete] = useState("");
-
- const handleGet = async () => {
- const res = await axios.get("/api/name");
- setResultGet(res.data);
- };
- const handlePost = async () => {
- const res = await axios.post("/api/users");
- setResultPost(JSON.stringify(res.data));
- };
- const handleParams = async () => {
- const res = await axios.get("/api/users/100");
- setResultParams(JSON.stringify(res.data));
- };
- const handleDelete = async () => {
- const res = await axios.delete("/api/users/1");
- setResultDelete(JSON.stringify(res.data));
- };
-
- return (
- <div className="App">
- <button onClick={handleGet}>"GET /api/name"button>
- <h2>{resultGet}h2>
- <hr />
-
- <button onClick={handlePost}>"POST /api/users"button>
- <h2>{resultPost}h2>
- <hr />
-
- <button onClick={handleParams}>"GET /api/users/:id"button>
- <h2>{resultParams}h2>
- <hr />
-
- <button onClick={handleDelete}>"DELETE /api/users/1"button>
- <h2>{resultDelete}h2>
- <hr />
- div>
- );
- }
-
- export default App;
启动项目测试