如今,Web 应用程序的前端和后端之间存在着松散的耦合。它们通常由不同的团队开发,而保持这些团队和技术同步并非易事。为了解决此问题的一部分,我们可以“伪造”后端技术通常会创建的 API 服务器,并像 API 或端点已经存在一样进行开发。
创建模拟或“伪造”组件的最常用术语是 *模拟*。模拟允许您模拟 API,而(理想情况下)无需更改前端。模拟的实现方式有很多,至少在我看来,这就是让大多数人感到害怕的原因。
让我们来了解一下一个好的 API 模拟应该是什么样的,以及如何将模拟的 API 实现到新的或现有的应用程序中。
请注意,我即将展示的实现是与框架无关的——因此它可以与任何框架或原生 JavaScript 应用程序一起使用。
Mirage:模拟框架
我们将使用的模拟方法称为 Mirage,它是一个相对较新的方法。我已经测试过很多模拟框架,最近才发现了这个框架,它彻底改变了我的开发方式。
Mirage 被宣传为一个面向前端的框架,它配备了现代界面。它在您的浏览器中(客户端)运行,通过拦截 XMLHttpRequest
和 Fetch 请求来实现。
我们将逐步创建使用模拟 API 的简单应用程序,并在此过程中介绍一些常见问题。
Mirage 设置
让我们创建一个标准的待办事项应用程序来演示模拟。我将使用 Vue 作为我的首选框架,但当然,您也可以使用其他框架,因为我们使用的是与框架无关的方法。
因此,请在您的项目中安装 Mirage
# Using npm
npm i miragejs -D
# Using Yarn
yarn add miragejs -D
要开始使用 Mirage,我们需要设置一个“服务器”(带引号,因为它是一个伪造的服务器)。在我们进入设置之前,我将介绍我发现最有效的文件夹结构。
/
├── public
├── src
│ ├── api
│ │ └── mock
│ │ ├── fixtures
│ │ │ └── get-tasks.js
│ │ └── index.js
│ └── main.js
├── package.json
└── package-lock.json
在 mock
目录中,打开一个新的 index.js
文件并定义您的模拟服务器
// api/mock/index.js
import { Server } from 'miragejs';
export default function ({ environment = 'development' } = {}) {
return new Server({
environment,
routes() {
// We will add our routes here
},
});
}
我们添加到函数签名中的环境参数只是一个约定。我们可以根据需要传递不同的环境。
现在,打开您的应用程序引导文件。在本例中,它是 src/main.js
文件,因为我们使用的是 Vue。导入您的 createServer
函数,并在 *开发环境* 中调用它。
// main.js
import createServer from './mock'
if (process.env.NODE_ENV === 'development') {
createServer();
}
我们在这里使用的是 process.env.NODE_ENV
环境变量,它是一个常见的全局变量。条件语句允许 Mirage 在生产环境中进行树摇,因此它不会影响您的生产包。
这就是我们设置 Mirage 所需的所有操作!正是这种易用性使得 Mirage 的 DX 非常出色。
为了使本文简单易懂,我们的 createServer
函数默认设置为 development
环境。在大多数情况下,它将默认设置为 test
,因为在大多数应用程序中,您将在开发模式下调用 createServer
一次,但在测试文件中调用多次。
工作原理
在我们发出第一个请求之前,让我们快速了解一下 Mirage 的工作原理。
Mirage 是一个**客户端**模拟框架,这意味着所有模拟都将在浏览器中进行,Mirage 使用 Pretender 库来实现。Pretender 将暂时替换本地的 XMLHttpRequest
和 Fetch 配置,拦截所有请求,并将它们定向到 Mirage 挂钩到的一个小型模拟服务。
如果您打开开发者工具并进入网络选项卡,您将看不到任何 Mirage 请求。这是因为请求被拦截并由 Mirage(通过后端的 Pretender)处理。Mirage 会记录所有请求,我们稍后会讨论。
让我们发出请求!
让我们创建一个对 /api/tasks
端点的请求,该端点将返回一个待办事项列表,我们将在待办事项应用程序中显示这些列表。请注意,我使用的是 axios 来获取数据。这只是我的个人喜好。同样,Mirage 与本地的 XMLHttpRequest
、Fetch 和任何其他库都兼容。
// components/tasks.vue
export default {
async created() {
try {
const { data } = await axios.get('/api/tasks'); // Fetch the data
this.tasks = data.tasks;
} catch(e) {
console.error(e);
}
}
};
打开您的 JavaScript 控制台——您应该会在那里看到来自 Mirage 的错误
Mirage: Your app tried to GET '/api/tasks', but there was no route defined to handle this request.
这意味着 Mirage 正在运行,但路由器还没有被模拟。让我们通过添加该路由来解决这个问题。
模拟请求
在我们的 mock/index.js
文件中,有一个 routes()
钩子。路由处理程序允许我们定义哪些 URL 应该由 Mirage 服务器处理。
要定义路由处理程序,我们需要将其添加到 routes()
函数中。
// mock/index.js
export default function ({ environment = 'development' } = {}) {
// ...
routes() {
this.get('/api/tasks', () => ({
tasks: [
{ id: 1, text: "Feed the cat" },
{ id: 2, text: "Wash the dishes" },
//...
],
}))
},
});
}
routes()
钩子是我们定义路由处理程序的方式。使用 this.get()
方法可以模拟 GET
请求。所有请求函数的第一个参数是我们正在处理的 URL,第二个参数是一个返回一些数据的函数。
需要注意的是,Mirage 接受任何 HTTP 请求类型,每种类型都有相同的签名
this.get('/tasks', (schema, request) => { ... });
this.post('/tasks', (schema, request) => { ... });
this.patch('/tasks/:id', (schema, request) => { ... });
this.put('/tasks/:id', (schema, request) => { ... });
this.del('/tasks/:id', (schema, request) => { ... });
this.options('/tasks', (schema, request) => { ... });
我们将在稍后讨论回调函数的 schema
和 request
参数。
有了这些,我们就成功地模拟了我们的路由,我们应该在控制台中看到来自 Mirage 的成功响应。

使用动态数据
尝试在我们的应用程序中添加新的待办事项是不可能的,因为我们 GET
响应中的数据是硬编码的。Mirage 的解决方案是提供一个轻量级的数据层,充当数据库。让我们修复一下我们目前遇到的问题。
与 routes()
钩子一样,Mirage 定义了一个 seeds()
钩子。它允许我们为服务器创建初始数据。我将把 GET
数据移动到 seeds()
钩子中,在那里我将把它推送到 Mirage 数据库中。
seeds(server) {
server.db.loadData({
tasks: [
{ id: 1, text: "Feed the cat" },
{ id: 2, text: "Wash the dishes" },
],
})
},
我把我们的静态数据从 GET
方法移动到 seeds()
钩子中,在那里该数据被加载到一个模拟数据库中。现在,我们需要重构我们的 GET
方法以返回来自该数据库的数据。这实际上非常简单——任何 route()
方法回调函数的第一个参数都是模式。
this.get('/api/tasks', (schema) => {
return schema.db.tasks;
})
现在,我们可以通过发出 POST
请求来向我们的应用程序添加新的待办事项
async addTask() {
const { data } = await axios.post('/api/tasks', { data: this.newTask });
this.tasks.push(data);
this.newTask = {};
},
我们通过创建 POST /api/tasks
路由处理程序来模拟 Mirage 中的此路由
this.post('/tasks', (schema, request) => {})
使用回调函数的第二个参数,我们可以看到已发送的请求。

requestBody
属性中包含我们发送的数据。这意味着现在我们可以使用它来创建一个新的待办事项。
this.post('/api/tasks', (schema, request) => {
// Take the send data from axios.
const task = JSON.parse(request.requestBody).data
return schema.db.tasks.insert(task)
})
待办事项的 id
将由 Mirage 的数据库默认设置。因此,无需跟踪 id 并将其与您的请求一起发送——就像真实的服务器一样。
动态路由?当然!
最后要介绍的是动态路由。它们允许我们在 URL 中使用动态段,这对于删除或更新应用程序中的单个待办事项非常有用。
我们的删除请求应该发送到 /api/tasks/1
、/api/tasks/2
等等。Mirage 提供了一种方法,我们可以使用它来定义 URL 中的动态段,如下所示
this.delete('/api/tasks/:id', (schema, request) => {
// Return the ID from URL.
const id = request.params.id;
return schema.db.tasks.remove(id);
})
在 URL 中使用冒号 (:
) 是我们定义 URL 中动态段的方法。冒号后面,我们指定段的名称,在本例中,名为 id
,它映射到特定待办事项的 ID。我们可以通过 request.params
对象访问段的值,其中属性名称对应于段名称 - request.params.id
。然后,我们使用模式从 Mirage 数据库中删除具有相同 ID 的项。
如果你已经注意到,到目前为止我所有的路由都以 api/
为前缀。一遍又一遍地写这个可能会很麻烦,你可能想要让它更容易。Mirage 提供了 namespace
属性,可以提供帮助。在路由钩子内部,我们可以定义 namespace
属性,这样我们就不必每次都写出来。
routes() {
// Prefix for all routes.
this.namespace = '/api';
this.get('/tasks', () => { ... })
this.delete('/tasks/:id', () => { ... })
this.post('/tasks', () => { ... })
}
好的,让我们把它集成到一个现有的应用中。
到目前为止,我们所看到的一切都是将 Mirage 集成到一个新的应用程序中。但是,如何将 Mirage 添加到现有的应用程序中呢?Mirage 已经为你准备好了,所以你不必模拟整个 API。
首先要注意的是,如果网站发出了 Mirage 没有处理的请求,那么将 Mirage 添加到现有的应用程序中会导致错误。为了避免这种情况,我们可以告诉 Mirage 通过所有未处理的请求。
routes() {
this.get('/tasks', () => { ... })
// Pass through all unhandled requests.
this.passthrough()
}
现在,我们可以基于现有 API 进行开发,Mirage 只处理我们 API 中缺失的部分。
Mirage 甚至可以更改它捕获请求的基 URL。这很有用,因为通常服务器不会运行在 localhost:3000
上,而是运行在自定义域名上。
routes() {
// Set the base route.
this.urlPrefix = 'https://devenv.ourapp.example';
this.get('/tasks', () => { ... })
}
现在,我们所有的请求都将指向真正的 API 服务器,但 Mirage 会像我们在设置新应用程序时一样拦截它们。这意味着从 Mirage 到真正的 API 的过渡非常无缝 - 从模拟服务器中删除路由,一切都将正常进行。
总结
在过去的五年里,我使用过很多模拟框架,但我从来没有真正喜欢过任何现有的解决方案。直到最近,当我的团队面临着对模拟解决方案的需求时,我才发现了 Mirage。
其他解决方案,例如常用的 JSON-Server,是需要与前端一起运行的外部进程。此外,它们通常只是一台 Express 服务器,上面有一些实用功能。结果是,像我们这样的前端开发人员需要了解中间件、NodeJS 和服务器的工作原理……这些东西我们可能都不想处理。其他尝试,例如 Mockoon,具有复杂的界面,同时缺乏急需的功能。还有一组框架只用于测试,例如流行的 SinonJS。不幸的是,这些框架无法用于模拟常规行为。
我的团队设法创建了一个功能服务器,使我们能够像使用真正的后端一样编写前端代码。我们通过编写前端代码库来做到这一点,而无需运行任何外部进程或服务器。这就是我喜欢 Mirage 的原因。它非常易于设置,但功能强大,足以处理任何抛给它的东西。你可以用它来编写返回静态数组的基本应用程序,也可以用它来编写完整的后端应用程序 - 无论是新的应用程序还是现有应用程序。
除了我们在这里介绍的实现之外,Mirage 还有更多功能。我们所介绍内容的工作示例可以在 GitHub 上找到。(有趣的事实:Mirage 也适用于 GraphQL!)Mirage 有完善的 文档,其中包括大量分步教程,所以一定要查看。
太好了,谢谢!
Mock Service Worker 是另一个 API 模拟库。我以前没听说过 Mirage,但它看起来非常像 MSW。
https://mswjs.io/
这也是一个很好的建议。我还没有尝试过,因为当我寻找解决方案时,Mirage 最先出现。我发现 MSW 缺少 ORM,如果你有不同意见请纠正我。一个简单的功能,你可以用
Map()
实现,但在你需要更新状态时非常有用。感谢你的回复,我一定会尝试一下 MSW。
我希望,"前端友好"是指它有一个 GUI,可以让你快速地创建 API 原型,而无需学习或使用 express 或任何其他后端框架。
事实证明,它的工作流程与设置 express 模拟 API 完全相同,只是没有明显的优势。
感谢你分享 MSW!如果有人使用 Cypress 进行测试,你是否仍然认为 API 模拟库(如 Mirage 或 MSW)提供的价值很大?
@Mike
你可以用 Cypress 使用它,我经常这样做。基本上设置是一样的。你用
environment
设置为 'test' 创建一个服务器,然后就可以开始了。现在在你的测试中,你可以使用
server.create('user', {something})
并在访问页面时检查该数据是否存在。@Marko: 谢谢你的回复。我不明白为什么你使用 Mirage(或 MSW)与 Cypress,而不是直接使用 fixtures…
@Mike 好吧,确实存在区别。在 Cypress(或任何其他测试框架)中使用 fixtures 意味着你需要 stub 你的请求。这意味着拦截
cy.request()
响应并用 fixture 替换它。使用和模拟服务器(任何类型的服务器)可以让你在没有任何更改的情况下正常测试你的应用程序,从而保持测试的简洁。此外,我试图遵循 DRY 和 KISS 原则,我只设置一次模拟服务器,并在测试 "编码" 环境的两个地方使用它。我认为模拟服务器就像我私下控制的开发服务器,在那里我可以控制数据,而不会影响任何用户。
本质上,就像生活中的所有事情一样,它取决于个人喜好。你可以 stub 你的请求,你可以创建一个模拟 API,或者根本不进行测试。
Msw 看起来比 mirage 更具前景,因为它可以帮助你通过 Network 选项卡查看请求。这使得调试变得更加容易。
说实话,我不介意,甚至有点喜欢它。我认为我保留了
Network
选项卡中的真实请求,我可以快速地扫描它们。所有模拟的、虚拟的请求都像你通常对开发内容所做的那样,在控制台中 nicely 记录下来。关于 MSW,正如我在之前的评论中所说,它看起来非常不错,但乍一看,它缺少 ORM。我将在一些更小的项目中尝试一下。
我去年发现了 https://mockaroo.com,从那以后一直在大量使用它。它非常易于使用,我实际上非常享受能够创建一个新的 API 端点并玩弄它生成虚拟或自定义数据的所有选项时。
Mockaroo 很棒,我已经使用了很长时间,用来生成各种随机数据。但我从来没有用它来模拟 API,有一些原因让我远离这种解决方案。
像 Mockaroo 或 Mocky 这样的服务是外部服务,存在于你的应用程序之外。当你有团队成员需要创建私人帐户来编辑、更新或创建端点时,这可能会成为问题。此外,这对公司来说是又一层复杂性。例如,所有开发人员都需要付费帐户吗?当开发人员离开公司时会发生什么,你如何撤销访问权限。
出于这些相同的原因,我喜欢 Mirage 和上面提到的 MSW 这样的工具。它们将完整的 API 模拟功能直接带入你的代码,你无需离开编辑器来更新任何内容。一个额外的优势是版本控制和 PR 检查。
对于想要在不编写任何代码的情况下获得模拟功能的开发人员,我强烈推荐 tweak 浏览器扩展:https://tweak-extension.com/
这是一个很好的概述。作为一名工程师,有时在 Laravel 中进行模拟对我来说毫无意义。你的教程是一个很好的概述。我希望将来在进行这种前端测试时能重新查看它。谢谢!
感谢你可爱的评论。发现 Mirage 对我来说是一个突破,它改变了我的开发方式。它非常容易让整个团队上手。
感觉当你完成模拟框架的设置后,后端就已经完成了,哈哈。