使用 MockJs — 实现真正的前后端分离

前言:刚刚看了下的后台,发现我技术文章中,阅读留言最多的是关于移动端的文章,甚至还有人付费赞赏或咨询。关于 PC 端的技术文章就显得比较冷清了,唉,废了好大劲写的,没人看。和我想的一样,移动端才是王道,下次找工作我也搞移动端

背景: 去年我写了一篇 学习使用 json-server 和 mockjs 的文章,当时没有仔细研究,文章只提到了 MockJs 其中一个 Random 的用法,关于 MockJs 拦截器和另一个很常用的 Mock.mock 函数都没有提及。这次来搞一下。

唉!以前我也是经常会听到前后端分离这个名词,只模糊的知道它最重要的一个作用就是,大大的提升了前端的地位。但是平时在开发的时候,我也会想奶奶的,没有接口这前端不就写了一个破页面,后期还的和后端对接口,对接口的时候花费的时间,肯定是不比前端开发的时候短,工期上最起码一半一半吧,这也就前后端分离,我这个小脑袋就不是太明白了。

不过我现在是终于搞明白这个问题了,对于前端来讲,真正的前后端分离,标志是不依赖后端的前端工作开发完成,项目基本宣告结束。后端开发完接口,只需要提换一个 URL 就行了,这也意味着前端需要去写一些接口。

除了带来开发任务稍微重点外,我至少看到了两个最大的优点:

  • 前端可以深入理解项目,毕竟也像后端写接口了,对项目的理解比之前只写页面深入多了。
  • 编程能力大大的提高,基本前后端都干了,算是入门全栈工程师了。

我体会最深就是这两点。

插曲:对了,今天中午我捡到一个手机,我必须要用一张动图来描述下:

郑伊健第一诫(www.52doutu.cn:m:103)(2020-12-08)

这个经典的 GIF ,来自郑伊健的恐怖电影「第一诫」,清凉一冬,绝对值得一看,虽然剧情有很大的 bug ,但是港片就这点好,它有很抓人的地方,让你觉得特别好看。

正文由此开始:


待续~~~

一、在哪拦截真正的请求

  • 使用 Axios 响应拦截器
  • 在项目入口文件(一般是 index,js)使用 Mock.mock 来拦截
  • webpack-dev-serverbefore 处拦截

上面三种方案都可以,但你要知道接口很多,需要支持批量引入,所以使用 Axios 响应拦截器就不太可取,只能在这简单的造些假数据。

著名开源项目 vue-element-admin 开发环境下模拟假接口使用的是 在 webpack-dev-serverbefore 处拦截。生产环境下是在项目入口文件(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 教程

官网本来就是中文的,我在使用中发现写的贼好,我就不用画蛇添足了。建议每次使用前:

  • 搂一眼文档,就搂一眼就行了,不要多。
  • 打开示例 对着找就行了。

你可能感兴趣的:(使用 MockJs — 实现真正的前后端分离)