最近这段时间,我一直在参与一个前端项目。每当我从庸碌的生活中赢得片刻喘息的时候,我不由得感慨,在程序员朴实无华且枯燥的职业生涯里,写自己喜欢的代码的机会少之又少,写别人喜欢的代码的机会俯拾皆是,更多的时候像是“为他人作嫁衣裳”。古人云,“遍身罗绮者,不是养蚕人”,当每天面对着被改得面目全非的代码的时候,内心固然早已波澜不惊、宠辱偕忘,可还是会期待美好的事情发生,因为从工程化的角度而言,每天都在思考的事情,其实就是怎么样做会更好一点。过去这些年里,微服务、前后端分离的呐喊声不绝于耳,实际应用过程中则是会遇到各种各样的问题。在今天这篇文章里,我想和大家聊聊 Vue.js 结合 Mock.js 实现接口模拟这个话题,为什么选择这个话题呢?我个人认为,它实际上触及了前后端分离的“灵魂”,并且由此可以引出像文档管理、流程控制等等一系列研发协同的问题。你或许会忍不住问道,前后端分离的“灵魂”是什么呢?各位看官们稍坐,且听我一一道来!
问题现状
在谈到前后端分离这个话题的时候,在公司层面上对应地往往是组织架构的分离,典型的做法就是让前端和后端成为两个不同的团队,其中,前端团队负责表示层的实现,不限于页面布局、样式风格、交互逻辑等等;后端团队负责数据接口的实现,不限于数据库设计、接口设计、编写 API 等等。对应到 Vue.js 里,前端团队负责写各种各样的页面/组件、数据绑定,后端团队负责提供各种各样的数据接口,这听起来非常地合理,对不对?的确,主流的前后端分离实践都是这样讲的,所以,我们只要套用这个模型,就可以达到预期的效果,对不对?可惜,人类习惯于为这个世界寻找某种颠扑不破的真理,可恰恰人类本身才是这个世界里最不稳定的存在?疫情常态化的当下,每次都被病毒一通嘲讽,抄作业都不会抄啊!
首先,第一个问题,前、后端团队没有形成“契约”,前端团队拿到原型以后就开始设计页面,ViewModel 中的字段命名、定义完全是由前端团队凭“感觉”写出来的,人类离谱就离谱在,可以靠“感觉”这种玄之又玄的东西决定很多事情。这样做的后果就是,后面真正对接后端接口的时候,发现大量的字段没法对应上,不得不再折腾一遍数据绑定,如果是中途由别人来接手,那么面对的可能就是不同的数据结构间的映射转换。试想,后端程序员尚有 AutoMapper 和 Mapster 可以用,前端程序员可就没有那么幸运啦!更不必说,前端天生比后端面临更频繁的改动,只要涉及到页面布局、交互逻辑的变化,ViewModel 的修改基本无可避免,这样就导致同一个页面多次返工,我相信这个结果大家都不想看到。
其次,当前、后端团队约定好接口文档以后,双方都按照这份接口文档去完成各自的开发工作,这样听起来简直不能更合理对不对?实际上,在后端团队完成接口开发以前,前端团队会有一段时间的“真空期”或者“黑写期”,因为前端并不知道这段代码能否在真实的环境下工作。此时,前端团队可能会造一点假数据来进行接口模拟,得益于 JavaScript 这门语言的高度灵活、自由,前端团队可能会直接调用一个本地函数来返回假数据,这意味着它并不会触发真实地 HTTP 请求。那么,当有一天后端团队完成了接口开发,你将会把这些本地函数替换为 Axios 的方法,甚至在更极端的情况下,前端团队不能访问后端团队的接口,此时,双方会就本地函数还是 Axios 方法产生一场拉锯战,你告诉我,还有什么比这更折磨一个人的吗?
所以,综合下来,其实是两个非常普遍的问题:
第一,前、后端团队如何制定一份对协同有利的接口文档,这份文档是通过工具生成还是人工编写。我个人是特别讨厌用 IM 或者邮件来发送接口文档的,因为没办法做到版本控制或者说让所有手中都有一份最新的接口的文档。
第二,如何管理项目中用到的各种假数据,以及如何让项目在假数据和真实接口中“无痛”切换。前端项目的特点是所见即所得,这让它比看不见、摸不着的后端项目更受用户青睐,毕竟还有什么比能让用户亲眼看到更亲切的东西呢?
在“小步快跑、快速迭代”的敏捷思想的驱使下,我们经常需要给用户演示各种功能。也许,在某个时刻,页面上的数据亦真亦假,你还会觉得,管理这些假数据没什么意义吗?而这正是驱使我了解 Mock.js 的动力所在,世上的很多事情,你未必能如愿以偿、做到最好,可你依然要了解什么是最好,“山不厌高,海不厌深”,向不那么完美的世界妥协是现实,永远值得去追寻更完美的世界是理想,这两者在我心目中并不冲突,你觉得呢?
改进思路
OK,既然找到了问题的症结所在,我们逐一对症下药即可,就像“三过家门而不入”的大禹,选择用疏导的方式治水,让洪水通过疏通的河道流到大海中去,而不是靠一味地“堵”,程序中 90% 的代码都是在给用户“打补丁”,防止对方做出什么骚操作来,那么,是不是可以用某种方式去引导对方呢?我最讨厌听到的话就是,用户想要怎么怎么样,这是没有办法的事情,如果只需要一个传话筒,我们为什么不直接用传呼机呢?作为一个老古董,恐怕现在的 00 后都不知道什么是传呼机。你生命中当下流行或者推崇的东西,总有一天会过期。可即便如此,你还是要全力以赴。显然,这是个哀伤的故事。
Swagger
对于接口文档的管理问题,我自始至终都推荐 Swagger 这个神器,因为我和这个世界上的绝大多数的程序员一样,都认同一种相对朴素的价值观,即 “懒惰是一种美德”。因为我不喜欢靠人工来维护接口文档,所以,只要有机会用上 Swagger,我一定会用 Swagger 来管理接口文档。不管是过去写 API 和 MVC,还是现在写 gRPC。对我来说,选择 Swagger 是一件自然而然的事情,因为我懒,因为我不理解为什么有人需要导出 Word 或者 Pdf 格式的接口文档。也许,Swagger 那千篇一律的页面风格会让人感到无所适从,喜欢的人非常喜欢,讨厌的人非常讨厌。在前、后端分离的项目中,有一份白纸黑字的接口文档,显然要比“口口相传”靠谱得多。当然,如果你有足以媲美 Swagger 的接口文档管理工具/平台,欢迎大家在评论区留言分享。下面是我曾经写过的关于 Swagger 的文章:
- 通过 ApiExplorer 为 Swagger 提供 MVC 扩展
- gRPC 搭配 Swagger 实现微服务文档化
- .NET Core POCOController 在动态 Web API 中的应用
- ASP.NET Core gRPC 打通前端世界的尝试
Mock.js
好了,下面我们来介绍今天这篇博客的主角:Mock.js。如果你有养成写单元测试的好习惯,那么,你一定对 Mock 的概念了如指掌。在 .NET 的生态中,有一个大名鼎鼎的模拟库:Moq,它可以让我们更加方便地模拟各种对象的行为。什么情况下需要模拟呢?我想,是目标对象不可用的时候。如果把这个认知迁移到前端开发中,我们就会发现前端依赖最多的其实是后端的接口。那么,有没有一种方案,可以让前端在后端接口不可用的情况下,模拟出调用后端接口的效果呢?而这,就是 Mock.js 存在的意义。事实上,我一开始就讲到,在后端团队完成接口开发以前,前端团队会有一段时间的“真空期”或者“黑写期”,而这段时间显然是最需要进行模拟的一个环节。综上所述,不管是技术层面还是流程层面,这个模拟的阶段都是真实存在着的,并非是人为捏造或者臆想出来的东西。
在考虑引入 Mock.js 以前,项目中充斥着类似于下面这样的代码。你不得不承认,JavaScript 是一门灵活而且强大的编程语言,考虑到它返回的是一个 Promise,所以,它和调用后端接口相比没有任何区别,你可以直接在组件中调用这个方法:
export async function getMessages() {
return new Promise(resolve => {
const data = [{
'id': '065CB06C-E082-0E55-24A5-54917C4BD182',
'eventTime': '2017-07-07 08:50:16',
'content': '人生若只如初见,何事秋风悲画扇'
},{
'id': 'C0C3298D-6A91-DE5B-EBD2-30F60E59E4EE',
'eventTime': '2017-08-07 18:50:16',
'content': '醉后不知天在水 满船清梦压星河'
}
//...
]
return resolve({
code: 200,
data: data
})
})
}
这个方案最大的问题是,当后端接口准备就绪以后,你还需要用实际的请求过程比如 Axios 替换掉这个方法,虽然这个工作量并不算特别大,可我总觉得这不是一个正确的思路,虽然代码频繁地改动对程序员来说是家常便饭,我是个不大愿意在同一件事情上来回折腾的人,我更喜欢古龙武侠小说里那种一招制敌的感觉,我们来一起看看 Mock.js 是如何解决这个问题的:
// 通过 npm 安装
npm install mockjs
// 通过 yarn 安装
yarn add mockjs
首先,我们通过 npm
或者 yarn
安装 Mock.js,对于上面的这个例子,我们可以像下面这样改造。一开始我们有提到,如果去管理这些假数据,我这里的建议是按照模块建立相应的文件夹,并最终通过一个统一的入口 /mock/index.js
来导入这些假数据。
// ./mock/message/index.js
const Mock = require('mockjs');
const Random = Mock.Random;
// 生成 10 条消息并赋值给 data 字段
const messageList = Mock.mock({
'data|10': [{
eventTime: () => Random.datetime(),
content: () => Random.csentence(5, 10),
id: () => Random.guid()
}]
})
export function getMessageList() {
return {
code: 200,
data: messageList.data
}
}
接下来,我们在 /mock/index.js
这个文件中注册相应的路由即可:
const Mock = require('mockjs');
import { getMessageList, addMessage } from './message'
Mock.setup({ timeout: 500 })
Mock.mock(/\/api\/messages/, 'get', getMessageList)
需要注意的是,这里一定要使用 /\/api\/messages/
这种带转义符的方式来定义路由,它表示这个请求会被拦截,实际调用的是 getMessageList
这个方法,如果我们直接写 /api/messages/
会返回 404,这一点非常重要。现在,我们就可以直接在组件或者页面内访问这个路由:
this.$http.get("/api/messages").then((res) => {
let resData = res.data;
if (resData.code == 200) {
this.messageList = resData.data;
}
});
其中,$http
是挂载到 Vue
原型上的 Axios 对象,显而易见,只要前、后端都严格按照文档来定义这个路由,那么,这个代码是不需要再调整的。等后端接口开发完成以后,我们只需要切换到正式地址联调即可。当然,前端和后端可能会部署在不同的服务器上,而这个对我们的影响,无外乎是修改一下 Axios 的 baseURL
。那么,Vue 怎么知道什么时候调用 Mock,什么时候调用真实接口呢?答案就藏在入口文件 main.js
中,只要我们导入了 /mock/index.js
这个文件,Mock 就会生效:
import './mock/index'
如下图所示,这里的的消息列表完全由 Mock.js 驱动,其中的消息内容、时间等都是随机生成的:
在平时的工作中,我常常听到一个词叫做“造数据”,本质上这依然属于 Mock 的范畴。现实生活中使用到的数据,毫无疑问会比这篇文章中的例子更加复杂和多样化,典型的有地址、邮箱、身份证号、IP 等等。对于这些,Mock.js 同样可以模拟,下面列举了常见的 API,更详细的可以参考官方文档中的 示例:
// 随机生成省份、直辖市或者自治区
Random.province()
Mock.mock('@province')
Mock.mock('@province()')
// 随机生成 URL
Random.url()
Mock.mock('@url')
Mock.mock('@url()')
// 随机生成中文段落
Random.cparagraph()
Mock.mock('@cparagraph')
Mock.mock('@cparagraph()')
// 随机生成日期
Random.datetime()
Mock.mock('@datetime')
Mock.mock('@datetime()'
如果用使用过 .NET 里的 Bogus 这个库,相信我,你会对这一切感到相当亲切。
登高望远
OK,写到这里一切,从点题的角度来看,这篇文章已经可以画上完美的句号,因为 Mock.js 在 Vue 中的基本用法,其实已经完全讲完啦!当然,我们在这个基础上尝试更多的东西。比如,YAPI,这同样是一个功能强大的 API 管理平台,它可以导入 Swagger 格式的接口文档,并为每一个 API 接口创建对应的 Mock 接口,在这种情况下,前端可以使用对应的 Mock 接口完成前期开发。当然,我认为,一个逻辑上自洽的流程应该是,定义接口、生成文档、生成 Mock 接口、使用 Mock 接口、使用真实接口,大家觉得呢?
个人心目中,只有国产软件 Apifox 能勉强达到这一点,如图所示,每当我们填入一个 API 接口定义的时候,它会为我们创建对应的用例,每个用例对应一个 Mock 的地址,某种程度上讲,它的确做到它对外宣称的集 API 文档、API 调试、API Mock、API 自动化测试 等多种特性于一身,同样地,它可以导出各种格式的 API 文档,我个人觉得这个软件挺好用的,YAPI 每次都需要打开网页去增加新接口,这个从流程上讲更潜移默化一点。
虽然但是,有那么多的工具帮助你管理文档、Mock 接口,可你还是做不好一个项目,你说,这是为什么呢?我猛然间想起去年写过的一篇博客,《使用 HttpMessageHandler 实现 HttpClient 请求管道自定义》,里面就在写,如何借助 HttpMessageHandler 做 API 接口的 Mock,人啊,兜兜转转,大抵又回到了原点罢!