前言:刚刚看了下的后台,发现我技术文章中,阅读留言最多的是关于移动端的文章,甚至还有人付费赞赏或咨询。关于 PC 端的技术文章就显得比较冷清了,唉,废了好大劲写的,没人看。和我想的一样,移动端才是王道,下次找工作我也搞移动端。
背景: 去年我写了一篇 学习使用 json-server 和 mockjs 的文章,当时没有仔细研究,文章只提到了 MockJs 其中一个 Random 的用法,关于 MockJs 拦截器和另一个很常用的 Mock.mock 函数都没有提及。这次来搞一下。
唉!以前我也是经常会听到前后端分离这个名词,只模糊的知道它最重要的一个作用就是,大大的提升了前端的地位。但是平时在开发的时候,我也会想奶奶的,没有接口这前端不就写了一个破页面,后期还的和后端对接口,对接口的时候花费的时间,肯定是不比前端开发的时候短,工期上最起码一半一半吧,这也就前后端分离,我这个小脑袋就不是太明白了。
不过我现在是终于搞明白这个问题了,对于前端来讲,真正的前后端分离,标志是不依赖后端的前端工作开发完成,项目基本宣告结束。后端开发完接口,只需要提换一个 URL 就行了,这也意味着前端需要去写一些接口。
除了带来开发任务稍微重点外,我至少看到了两个最大的优点:
- 前端可以深入理解项目,毕竟也像后端写接口了,对项目的理解比之前只写页面深入多了。
- 编程能力大大的提高,基本前后端都干了,算是入门全栈工程师了。
我体会最深就是这两点。
插曲:对了,今天中午我捡到一个手机,我必须要用一张动图来描述下:
这个经典的 GIF ,来自郑伊健的恐怖电影「第一诫」,清凉一冬,绝对值得一看,虽然剧情有很大的 bug ,但是港片就这点好,它有很抓人的地方,让你觉得特别好看。
正文由此开始:
待续~~~
一、在哪拦截真正的请求
- 使用 Axios 响应拦截器
- 在项目入口文件(一般是
index,js
)使用 Mock.mock 来拦截 - 在
webpack-dev-server
的before
处拦截
上面三种方案都可以,但你要知道接口很多,需要支持批量引入,所以使用 Axios 响应拦截器就不太可取,只能在这简单的造些假数据。
著名开源项目 vue-element-admin 开发环境下模拟假接口使用的是 在 webpack-dev-server
的 before
处拦截。生产环境下是在项目入口文件(index,js
)使用 Mock.mock 模拟的。
二、如何拦截请求
拦截请求的步骤如下,根据 devserverbefore 配置的栗子:
module.exports = {
//...
devServer: {
before: function (app, server, compiler) {
app.get('/some/path', function (req, res) {
res.json({ custom: 'response' });
});
}
}
};
可知道 before
接收一个函数,函数的第一个参数一般叫 app ,因为它的作用和 express 的 app 是等效的。也就是说这个 app 自带路由,正好解决接口批量引入的问题。
在项目中,一般都是这么写,把逻辑提出去:
{
devServer: {
port: port,
open: true,
before: require('./mock/mock-server.js')
}
}
./mock/mock-server.js
文件的内容为:
const chokidar = require('chokidar')
const bodyParser = require('body-parser')
const chalk = require('chalk')
const path = require('path')
const Mock = require('mockjs')
const mockDir = path.join(process.cwd(), 'mock')
function registerRoutes(app) {
let mockLastIndex
const { mocks } = require('./index.js')
const mocksForServer = mocks.map(route => {
return responseFake(route.url, route.type, route.response)
})
for (const mock of mocksForServer) {
app[mock.type](mock.url, mock.response)
mockLastIndex = app._router.stack.length
}
const mockRoutesLength = Object.keys(mocksForServer).length
return {
mockRoutesLength: mockRoutesLength,
mockStartIndex: mockLastIndex - mockRoutesLength
}
}
function unregisterRoutes() {
Object.keys(require.cache).forEach(i => {
if (i.includes(mockDir)) {
delete require.cache[require.resolve(i)]
}
})
}
// for mock server
const responseFake = (url, type, respond) => {
return {
url: new RegExp(`${process.env.VUE_APP_BASE_API}${url}`),
type: type || 'get',
response(req, res) {
console.log('request invoke:' + req.path)
res.json(Mock.mock(respond instanceof Function ? respond(req, res) : respond))
}
}
}
// 1. 由此开始
module.exports = app => {
// parse app.body
// https://expressjs.com/en/4x/api.html#req.body
// 2. post 请求
app.use(bodyParser.json())
app.use(bodyParser.urlencoded({
extended: true
}));
// 3. 注册路由
const mockRoutes = registerRoutes(app)
// 4. 获取路由的长度和缓存路由开始的位置
var mockRoutesLength = mockRoutes.mockRoutesLength
var mockStartIndex = mockRoutes.mockStartIndex
// watch files, hot reload mock server
// 5. 监控文件/文件夹的变换 => 使用 chokidar 避免平台的兼容
chokidar.watch(mockDir, {
ignored: /mock-server/,
ignoreInitial: true
}).on('all', (event, path) => {
// 6. 新建文件/文件被修改
if (event === 'change' || event === 'add') {
try {
// remove mock routes stack
// 7. 已经家在的路由要被一移除
app._router.stack.splice(mockStartIndex, mockRoutesLength)
// clear routes cache
// 8. 因为文件被修改,需要清除已被加载文件的缓存
unregisterRoutes()
// 9. 重新注册路由
const mockRoutes = registerRoutes(app)
mockRoutesLength = mockRoutes.mockRoutesLength
mockStartIndex = mockRoutes.mockStartIndex
console.log(chalk.magentaBright(`\n > Mock Server hot reload success! changed ${path}`))
} catch (error) {
console.log(chalk.redBright(error))
}
}
})
}
./mock/index.js
文件的内容为:
const Mock = require('mockjs')
const { param2Obj } = require('./utils')
const user = require('./user')
const role = require('./role')
const article = require('./article')
const search = require('./remote-search')
const mocks = [
...user,
...role,
...article,
...search
]
// for front mock
// please use it cautiously, it will redefine XMLHttpRequest,
// which will cause many of your third-party libraries to be invalidated(like progress event).
function mockXHR() {
// mock patch
// https://github.com/nuysoft/Mock/issues/300
Mock.XHR.prototype.proxy_send = Mock.XHR.prototype.send
Mock.XHR.prototype.send = function () {
if (this.custom.xhr) {
this.custom.xhr.withCredentials = this.withCredentials || false
if (this.responseType) {
this.custom.xhr.responseType = this.responseType
}
}
this.proxy_send(...arguments)
}
function XHR2ExpressReqWrap(respond) {
return function (options) {
let result = null
if (respond instanceof Function) {
const { body, type, url } = options
// https://expressjs.com/en/4x/api.html#req
result = respond({
method: type,
body: JSON.parse(body),
query: param2Obj(url)
})
} else {
result = respond
}
return Mock.mock(result)
}
}
for (const i of mocks) {
Mock.mock(new RegExp(i.url), i.type || 'get', XHR2ExpressReqWrap(i.response))
}
}
module.exports = {
mocks,
mockXHR
}
mockXHR 不用看,因为这是给线上环境用的,所以可以简单的改写为:
const user = require('./user')
const role = require('./role')
const article = require('./article')
const search = require('./remote-search')
const mocks = [
...user,
...role,
...article,
...search
]
module.exports = {
mocks
}
随便找一个,例如 user 看下接口怎么写的:
module.exports = [
// user login
{
url: '/vue-element-admin/user/login',
type: 'post',
response: config => {
const { username } = config.body
const token = tokens[username]
// mock error
if (!token) {
return {
code: 60204,
message: 'Account and password are incorrect.'
}
}
return {
code: 20000,
data: token
}
}
}
]
完美,到此结束。
三、疑问️
我想你一定对更改文件的时候,为什么要清路由和清缓存感兴趣。
- 路由缓存
如果熟悉 express 框架,看到 app._router.stack 你就知道了。不知道也没关,我演示给你看,新建一个JS 文件,文件内容为:
const express = require("express");
const { writeFileSync } = require("fs");
const app = express();
app.get("/index", function (req, res) {
res.send(require.cache);
});
console.log(require.resolve("express"))
setTimeout(() => {
app.get("/index", function (req, res) {
});
writeFileSync("./test.js", JSON.stringify( (app._router.stack) ,null, 4));
}, 9000)
app.listen(3000, () => {
console.log("启动成功!");
});
执行结束,看在 test.js 文件的内容:
[
{
"name": "query",
"keys": [],
"regexp": {
"fast_star": false,
"fast_slash": true
}
},
{
"name": "expressInit",
"keys": [],
"regexp": {
"fast_star": false,
"fast_slash": true
}
},
{
"name": "bound dispatch",
"keys": [],
"regexp": {
"fast_star": false,
"fast_slash": false
},
"route": {
"path": "/index",
"stack": [
{
"name": "",
"keys": [],
"regexp": {
"fast_star": false,
"fast_slash": false
},
"method": "get"
}
],
"methods": {
"get": true
}
}
},
{
"name": "bound dispatch",
"keys": [],
"regexp": {
"fast_star": false,
"fast_slash": false
},
"route": {
"path": "/index",
"stack": [
{
"name": "",
"keys": [],
"regexp": {
"fast_star": false,
"fast_slash": false
},
"method": "get"
}
],
"methods": {
"get": true
}
}
}
]
发现没,重复被添加的路由,不是覆盖而是扩展。
- 模块缓存
这个涉及到 CJS 模块的运行机制, 记住 require 的文件会被加到 require.cache 里面,当文件改变读的是缓存,而不是最新更改的文件。
四、优化
项目结构过大,如果只在 mock 文件夹里面管理有点麻烦,我就想在页面所在目录直接写接口,怎么办?没错使用 require.context 来批量引入。但是 NodeJs 是没有批量引入的 API 的。找遍了 npm 也没发现一个 package 和 require.context 长得像的。
难道没办法了吗?当然不是,自己动手丰衣足食。 依照 vue-cli 插件的命名规范,我给写的 package 取名 node-plugin-require-context ,简单讲下实现原理:
- 依赖
__dirname
来获取需要读取的文件目录。 - 通过 readdirSync 来批量读取文件夹名和文件名
- 通过 statSync 的 isDirectory 方法判断读取的文件是否是文件夹
- 如果是,恰好 useSubdirectories 也等于 true ,递归查询。
- 最好返回 requireContext 函数
其实还有一个缺点,如果你看过 Antd-Pro 项目,你就会发现它模拟数据,模块化采用的是 ESModule ,而不是 CJS。保持编码模块化风格一致确实也是需要优化的一个地方,不管了,反正我不干。
五、重写 MockJs 教程
官网本来就是中文的,我在使用中发现写的贼好,我就不用画蛇添足了。建议每次使用前:
- 搂一眼文档,就搂一眼就行了,不要多。
- 打开示例 对着找就行了。