- 1. Mock Data 基础
1.1. 是什么?
Mock 数据是前端开发过程中必不可少的一环,是分离前后端开发的关键链路。通过预先跟服务器端约定好的接口,模拟请求数据甚至逻辑,能够让前端开发更加独立自主,不会被服务端的开发所阻塞。
1.2. 常见 Mock 方式?
2. Mock Data 实现
2.1. 技术要点?
- 如何利用 webpack-dev-server 的 before 特性,进行请求拦截;
- 如何利用 body-parser 解析请求体(request body)
- 如何利用 pathToRegexp 匹配 URL 路径
- 如何利用 chokidar 实现 Mock Data 热更新。
- 如何利用 clear-module 清理 Mock Data 模块缓存。
- 如何利用 Mock.js 生成随机数据。
2.2. 效果预览?
2.3. Mock 文件编码方式
Mock 文件的编码方式,参考自 Umijs:
- 支持静态值
- 支持动态函数
2.4. devServer.before
- webpack-dev-server 底层基于 express。
- 核心是其 middleware 机制。
- devServer.before,会在其内部所有 middleware 执行之前触发。
- 这给了我们拦截、分析请求,并返回自定义 Mock Data 的机会。
2.5. body-parser
- Node.js body parsing middleware.
- Parse incoming request bodies in a middleware before your handlers, available under the req.body property.
- body-parser provides the following parsers:
- JSON body parser
- Raw body parser
- Text body parser
- URL-encoded form body parser
a. 搭建一个 Demo(此时没使用 body-parser):
代码语言:javascript复制
const express = require('express');
const app = express();
app.post('/login', (req, res) => {
console.log('********************')
console.log(req.body);
res.send("Hello WEBJ2EE.");
});
app.listen(3000, () => {
console.log('http://127.0.0.1:3000')
})
b. 使用 Postman 发送 POST 请求:
c. 不使用 body-parser 的情况下,直接获取 req.body,结果将是 undefined。
d. 配置 JSON 解析器。
代码语言:javascript复制
const express = require('express');
const bodyParser = require('body-parser');
const app = express();
app.post('/login', bodyParser.json(), (req, res) => {
console.log('********************')
console.log(req.body);
res.send("Hello WEBJ2EE.");
});
app.listen(3000, () => {
console.log('http://127.0.0.1:3000')
});
e. 使用 Postman 再次发送 JSON 数据,将得到执行结果。
备注:如果在模拟器上以非JSON格式发送,则会获得一个空的JSON对象
2.6. chokidar
A neat wrapper around node.js fs.watch / fs.watchFile / fsevents.
代码语言:javascript复制a. 搭建一个 Demo:
const path = require("path");
const chokidar = require("chokidar");
chokidar.watch(path.resolve(__dirname, "ftp"))
.on("all", (event, path)=>{
console.log(event, path);
});
b. 看看其监听能力:
- 增加文件时,显示的事件名是add,并且显示对应的文件名;
- 修改文件内容时,显示的事件名是change,并且显示对应的文件名;
- 增加目录时,显示的事件名是addDir,并且显示对应的目录名;
- 删除文件时,显示的事件名是unlink,并且显示对应的文件名;
- 删除目录时,显示的事件名是unlinkDir,并且显示对应的目录名;
2.7. clear-module
Useful for testing purposes when you need to freshly import a module.
示例:
代码语言:javascript复制
// foo.js
let i = 0;
module.exports = () => i;
const clearModule = require('clear-module');
require('./foo')();
//=> 1
require('./foo')();
//=> 2
clearModule('./foo');
require('./foo')();
//=> 1
2.8. 综合示例:Mocker.ts
代码语言:javascript复制
import URL from 'url';
import PATH from 'path';
import { Request, Response, NextFunction, Application } from 'express';
import bodyParser from 'body-parser';
import * as toRegexp from 'path-to-regexp';
import { TokensToRegexpOptions, ParseOptions, Key } from 'path-to-regexp';
import clearModule from 'clear-module';
import chokidar from 'chokidar';
import color from 'colors-cli/safe';
export type MockerResultFunction = ((req: Request, res: Response, next?: NextFunction) => void);
export type MockerResult = string | number| Array<any> | Record<string, any> | MockerResultFunction;
export type MockerProxyRoute = Record<string, MockerResult> & {}
export type WatchFiles = Array<string>;
const pathToRegexp = toRegexp.pathToRegexp;
let mocker: MockerProxyRoute = {};
function pathMatch(options: TokensToRegexpOptions & ParseOptions) {
options = options || {};
return function (path: string) {
var keys: (Key & TokensToRegexpOptions & ParseOptions & { repeat: boolean })[] = [];
var re = pathToRegexp(path, keys, options);
return function (pathname: string, params?: any) {
var m = re.exec(pathname);
if (!m) return false;
params = params || {};
var key, param;
for (var i = 0; i < keys.length; i ) {
key = keys[i];
param = m[i 1];
if (!param) continue;
params[key.name] = decodeURIComponent(param);
if (key.repeat) params[key.name] = params[key.name].split(key.delimiter)
}
return params;
}
}
}
// Merge multiple Mockers
function getConfig(watchFiles: WatchFiles) {
return watchFiles.reduce((mocker, file) => {
const mockerItem = require(file);
return Object.assign(mocker, mockerItem.default ? mockerItem.default : mockerItem);
}, {})
}
function watchMockFiles(watchFiles: WatchFiles){
const watcher = chokidar.watch(watchFiles.map(watchFile => PATH.dirname(require.resolve(watchFile))));
watcher.on('all', (event, path) => {
if (event === 'change' || event === 'add') {
try {
watchFiles.forEach(file => cleanCache(file));
mocker = getConfig(watchFiles);
console.log(`${color.green_b.black(' Done: ')} Hot Mocker ${color.green(path.replace(process.cwd(), ''))} file replacement success!`);
} catch (ex) {
console.error(`${color.red_b.black(' Failed: ')} Hot Mocker ${color.red(path.replace(process.cwd(), ''))} file replacement failed!!`);
}
}
});
}
function cleanCache(modulePath: string) {
try {
modulePath = require.resolve(modulePath);
} catch (e) {}
var module = require.cache[modulePath];
if (!module) return;
clearModule(modulePath);
}
export default function (app: Application, watchFile: string | string[]) {
const watchFiles = Array.isArray(watchFile) ? watchFile : [watchFile];
if (watchFiles.some(file => !file)) {
throw new Error('Mocker file does not exist!.');
}
mocker = getConfig(watchFiles);
if (!mocker) {
return (req: Request, res: Response, next: NextFunction) => {
next();
}
}
watchMockFiles(watchFiles);
app.all('/*', (req: Request, res: Response, next: NextFunction) => {
const mockerKey: string = Object.keys(mocker).find((kname) => {
return !!pathToRegexp(kname.replace((new RegExp('^' req.method ' ')), '')).exec(req.path);
});
if (mocker[mockerKey]) {
let bodyParserMethd = bodyParser.json(); // 默认使用json解析
bodyParserMethd(req, res, function () {
const result = mocker[mockerKey];
if (typeof result === 'function') {
const rgxStr = ~mockerKey.indexOf(' ') ? ' ' : '';
req.params = pathMatch({ sensitive: false, strict: false, end: false })(mockerKey.split(new RegExp(rgxStr))[1])(URL.parse(req.url).pathname);
result(req, res, next);
} else {
res.json(result);
}
});
} else {
next();
}
});
return (req: Request, res: Response, next: NextFunction) => {
next();
}
}
核心逻辑截取自:mocker-api
- 利用 chokidar 监听 mock 文件变更,实现热更新特性。
- 监听到文件更新后,使用 clear-module 清理模块缓存,在下次请求拦截时,才能使更新后的 mock 文件生效。