写在前面
最近在使用Mockjs 作为项目里面mock 数据的工具,发现mockjs 做的拦截部分是自己实现摸拟了一个XMLHttpRequest 的方法做的拦截,使用Mockjs 拦截请求后,在chrome 的network 上无法看到请求(具体mockjs使用方法可以查看他的api,mockjs-api这里我不多做阐述),但为了更加真实的像后台返回数据,我自己使用Node 作为中间代理去实现了一个mock-plugin .
express中间件介绍
因为插件相当于是实现了一个express的中间件的方式,所以这里简单对express中间件的使用做一个说明:
express 中间件通过app.use (也有app.get,app.post 等方法)的方式注册到express 的实例某个属性上,将执行函数存放在栈内部,然后在回调执行的时候调用next() 方法将执行下一个存在栈内的方法。
这里列举一个示例:
const express = require('express');
const app = express();
app.use(function (req, res, next) {
console.log('first all use');
next()
});
app.use(function (req, res, next){
setTimeout(() => {
console.log(`two all use`)
next()
}, 1000)
});
app.use(function (req, res, next) {
console.log('end all use')
next()
});
app.use('/', function (req, res, next) {
res.end('hello use')
});
app.listen(4000, function () {
console.log(`起动服务成功!`)
});
通过node 执行以上代码后,在浏览器上通过访问http://locahost:4000 可以看到控制台打印:

可以发现在执行的时候先执行了use 注册的中间件,然后再执行到get 路由的时候,又执行了app.use 注册的中间件。
详细的express中间件可以在express官网查看
实现dev-server
devServer 可以使用webpack-dev-server 然后通过before 的回调去做一层拦截,这样也能够实现在响应之前对后台的数据做一些处理。
我这儿选择自己实现一个devServer ,在之前使用webpack-dev-server 的服务大概需要配置,port , proxy ,以及跨域https 等。当然自己实现devServer 就没必要实现那么多功能了,正常在开发场景下很多也不一定用得上,这里我主要使用了webpack-dev-middleware和webpack-hot-middleware达到自动编译和热更新的目的,以及可以自己在中间添加express中间件.
贴上代码:
const path = require('path');
const express = require('express');
const webpack = require('webpack');
const webpackConfig = require('./webpack.dev');
const devMiddleware = require('webpack-dev-middleware');
const hotMiddleware = require('webpack-hot-middleware');
const app = express();
const compiler = webpack(webpackConfig); // webpack开发环境配置
const mockPlugin = require('./mock-plugin');
const config = {
prd: 8800
};
// 注册webpack-dev-middleware中间件
app.use(
devMiddleware(compiler, {
publicPath: webpackConfig.output.publicPath
})
);
// 注册webpack-hot-middleware中间件
app.use(
hotMiddleware(compiler)
);
// 注册mockPlugin插件
app.use(
mockPlugin({
routes: {
'/app': 'http://locahost:3002', // 测试代理到服务器的地址
'/api': 'http://localhost:3003' // 测试代理到服务器的地址
},
root: path.resolve(__dirname) // 项目根目录
})
);
app.listen(config.prd, function () {
console.log('访问地址:', `http://localhost:${config.prd}`);
});
具体的一些演示操作,这里也不多讲了(这不是实现mock-plugin的重点),网上也有很多如果通过webpack-dev-middleware 和webpack-hot-middleware 的教程,唯一的区别是代理部分,网上可能用的是http-proxy 之类已现有的工具,因为我们这儿需要在请求代理中间还需要处理一层,所以这儿我们自己实现mockPlugin 注册进去。
摸拟mock文件
因为mock 工具包含了一个请求后台的结果自动写入到Mock目录下。所以这里将目录层级设置为与请求路径保持一致:
api接口:/app/home/baseInfo => 目录:mock\app\home\baseInfo.js
对应 baseInfo.js 模板:
// mock 开关
exports.check = function () {
return true;
}
// mock 数据
exports.mockData = function () {
return {
"success": true,
"errorMsg": "",
"data": {
name: 'test'
}
}
}
当check 为true 时对就请求将会取mockData 的数据。
主逻辑实现
mock-plugin 主要暴露一个高阶函数,第一层为请求代理配置,返回的函数的参数与app.get('/') 的回调参数一致,不描述细节,大概输出主要的逻辑。
// 获取mock文件的mock数据
const setMockData = (moduleName) => {
const {mockData} = require(moduleName);
return mockData();
};
// 中间件暴露方法
module.exports = function (options) {
const {routes, root} = options;
return async (req, res, next) => {
let {isReq, host} = await valid.isRequestPath(routes, req);
// 不是请求地址直接return掉
if (!isReq) {
next();
return;
}
// 如果存在Mock对应的文件
let filePath = await valid.isMockFileName(root, req.path);
if (filePath) {
// 检验本地mock文件开关是否开启
let check = await valid.inspectMockCheck(filePath);
if (check) {
// 发送本地mock数据
return res.send(setMockData(filePath))
} else {
// 请求结果
let body = await request(host, req, res).catch(proxyRes => {
res.status(proxyRes.statusCode);
});
// 发送请求的结果信息
return res.send(body);
}
} else {
// 请求返回主体
let body = await request(host, req, res).catch(proxyRes => {
res.status(proxyRes.statusCode);
next();
});
if (body) {
// 定义需要写入文件路径
const filePath = path.resolve(root, `mock${req.path}.js`);
// 写入mock文件
writeMockFile(filePath, body);
// 响应返回主体
return res.send(body);
}
}
};
};
以下是一些校验方法,详细代码就不贴了,具体源码可查看:https://github.com/moxaIce/lo...
-
isRequestPath 校验是否为api 接口请求, 返回 Promise 包含isReq 布尔值,host 请求域名, route 请求路由的对象。
-
isMockFileName 是否存在对应的mock 文件,返回Promise 返回匹配路径或者空字符串
-
inspectMockCheck 校验模拟文件请求,开关是否开起, 返回布尔值
至于request方法和writeMockFile方法看下面的小结。
以下是我自己画的一个逻辑图,有点丑见谅:

请求代理
代理的作用不用多说,都知道是解决了前端起的服务和直接请求后台的跨域问题。我这儿主要是在中间件内部通过http.request 方法发起一个http请求,对于http.request 方法的使用可以看这里, 里面也有比较详细的示例,我这儿贴上我写的代码:
/**
* @description 请求方法
*/
const url = require('url');
const http = require('http');
module.exports = function (host, req, res) {
let body = '';
return new Promise((resolve, reject) => {
const parse = url.parse(host);
let proxy = http.request(
{
host: host.hostname,
port: parse.port,
method: req.method,
path: req.path,
headers: req.headers
},
(proxyRes) => {
// 非200字段内直接响应错误 , 在主逻辑里处理
if (proxyRes.statusCode < 200 || proxyRes.statusCode > 300) {
reject(proxyRes)
}
proxyRes.on('data', (chunk) => {
body += chunk.toString();
}).on('end', () => {
try {
resolve(JSON.parse(body));
} catch (e) {
// 将响应结果返回,在主文件做异常回调
reject(proxyRes)
}
}).on('error', (err) => {
console.log(`error is`, err);
})
});
proxy.on('error', (e) => {
console.error(`请求报错:${e.message}`)
});
proxy.end()
})
};
代理的实现比较简单,主要通过外层传入host 和requset , response 在内部用url 解析得到ip 然后配置request 的options , 通过监听data 与end 事件将得到的主体报文resolve 出去,以及中间对非200 段内的响应处理。
文件写入
通过中间传options 传入的root , 可以得到完整的mock 路径path.resolve(__dirname, mock${req.path}.js) 。传入到写入mock 文件方法里
module.exports = async function (filePath, body) {
await dirExists(path.dirname(filePath));
fs.writeFile(filePath, echoTpl(JSON.stringify(body)), function (err) {
if (err) {
console.log(`写入文件失败`)
}
});
}
定义mockjs 模板
const echoTpl = (data) => {
return `exports.check = function () {
return false
}
exports.mockData = function () {
return ${data}
}
`
};
dirExists 通过递归的方式写入文件目录
// 获取文件信息,报错则文件不存在
const getStat = (path) => {
return new Promise((resolve) => {
fs.stat(path, (err, stats) => {
if (err) {
resolve(false);
} else {
resolve(stats);
}
})
})
};
// 创建目录
const mkdir = (dir) => {
return new Promise((resolve) => {
fs.mkdir(dir, err => {
if (err) {
resolve(false);
} else {
resolve(true);
}
})
})
};
// 写入文件
const dirExists = async (dir) => {
let isExists = await getStat(dir);
//如果该路径且不是文件,返回true
if (isExists && isExists.isDirectory()) {
return true;
} else if (isExists) {//如果该路径存在但是文件,返回false
return false;
}
//如果该路径不存在
let tempDir = path.parse(dir).dir;
//递归判断,如果上级目录也不存在,则会代码会在此处继续循环执行,直到目录存在
let status = await dirExists(tempDir);
let mkdirStatus;
if (status) {
mkdirStatus = await mkdir(dir);
}
return mkdirStatus;
};
效果演示
使用koa起一个node服务并且暴露如下路由和其中的数据,具体代码可以看这儿,我这儿只贴上了关键代码
router.get('/app/home/baseInfo', user_controller.baseInfo)
router.post('/app/login', user_controller.login)
const login = async (ctx, next) => {
ctx.body = {
success: true,
message: '',
code: 0,
data: {
a: 1,
b: '2'
}
}
};
const baseInfo = async (ctx, next) => {
ctx.body = {
success: true,
errorMsg: '',
data: {
avatar: 'http:///themes/taurus/html/img/example/user/dmitry_b.jpg',
total: 333,
completed: 30,
money: '500'
}
};
};
mounted() {
axios.get('/app/home/baseInfo', function (res) {
console.log(`res 23`, res)
});
axios({
url: '/app/login',
method: 'post',
headers: {
// 'Content-Type': 'application/json;charset=UTF-8',
'a': 'b'
}
})
}
具体效果可以看下图:
 前端在访问的时候会将后台响应的数据自动写入到Mock目录下。
结语
感觉在写文章的过程中发现写入mock 文件的时候可能使用stream 会更好一点,年底了业务需求不太多,避免上班划水,随便想了写一写~
|