1. 创建react项目
使用 create-react-app 创建项目
npx create-react-app react-mock
执行 eject 命令
npm run eject
删除 package.json 文件中的 eslintConfig 选项
2. 安装依赖包
npm i path-to-regexp fast-glob chokidar axios
3. 创建中间件
在 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;
4. 修改webpackDevServer
修改 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 },
};
5. 测试mock请求
修改 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;
启动项目测试