概述
Mirage 允许你通过编写 路由处理程序来模拟 API 响应。
路由处理程序的最简单示例是一个返回对象的函数
import { createServer } from "miragejs"
createServer({
routes() {
this.namespace = "api"
this.get("/movies", () => {
return {
movies: [
{ id: 1, name: "Inception", year: 2010 },
{ id: 2, name: "Interstellar", year: 2014 },
{ id: 3, name: "Dunkirk", year: 2017 },
],
}
})
},
})
现在,每当你的应用向 /api/movies
发送 GET 请求时,Mirage 都会使用此数据进行响应。
如果你的 API 所在的主机或端口与你的应用不同,请设置 urlPrefix
routes() {
this.urlPrefix = 'https://127.0.0.1:3000';
使用这种静态路由处理程序,你可以走得很远,它们是让你熟悉 Mirage 工作原理的好方法。所有 HTTP 动词都可用,有一个 timing
选项,你可以使用它来模拟速度较慢的服务器,你甚至可以返回自定义的 Response
来查看当你的应用从 API 收到错误时它的行为方式。
import { createServer, Response } from "miragejs"
createServer({
routes() {
this.namespace = "api"
// Responding to a POST request
this.post("/movies", (schema, request) => {
let attrs = JSON.parse(request.requestBody)
attrs.id = Math.floor(Math.random() * 100)
return { movie: attrs }
})
// Using the `timing` option to slow down the response
this.get(
"/movies",
() => {
return {
movies: [
{ id: 1, name: "Inception", year: 2010 },
{ id: 2, name: "Interstellar", year: 2014 },
{ id: 3, name: "Dunkirk", year: 2017 },
],
}
},
{ timing: 4000 }
)
// Using the `Response` class to return a 500
this.delete("/movies/1", () => {
let headers = {}
let data = { errors: ["Server did not respond"] }
return new Response(500, headers, data)
})
},
})
动态路由处理程序
静态路由处理程序确实有效,它们是模拟 HTTP 响应的常用方法,但上面这些硬编码响应存在一些问题
它们缺乏灵活性。如果你想更改某个路由响应的数据以用于单个测试,该怎么办?现在你必须从头开始重写整个处理程序。
它们包含格式逻辑。与 JSON 负载形状相关的逻辑(例如,我们上面负载中的
movies: []
根键)现在在所有路由处理程序中都重复出现。它们过于基本。不可避免地,当你的 Mirage 服务器需要处理更复杂的事情(如关系)时,这些简单的临时响应就开始失效了。
Mirage 有一个 数据层来帮助你编写更强大的服务器实现。让我们看看它是如何工作的,方法是替换上面的基本存根数据。
首先,我们将告诉 Mirage 我们有一个动态的 Movie
模型
import { createServer, Model } from "miragejs"
createServer({
models: {
movie: Model,
},
routes() {
this.namespace = "api"
this.get("/movies", () => {
return {
movies: [
{ id: 1, name: "Inception", year: 2010 },
{ id: 2, name: "Interstellar", year: 2014 },
{ id: 3, name: "Dunkirk", year: 2017 },
],
}
})
},
})
模型使我们的路由处理程序能够利用 Mirage 的 内存数据库。数据库使我们的路由处理程序变得动态,因此我们可以更改返回的数据,而无需重写处理程序。
让我们更新我们的路由处理程序,使其成为动态的
this.get("/movies", (schema, request) => {
return schema.movies.all()
})
schema
参数是访问我们新的 Movie
模型的方式。此路由现在将响应 Mirage 数据库中所有作者,这些作者在请求时存在。因此,我们只需更改 Mirage 数据库中的记录,就可以更改此路由响应的数据。
最后一步是填充数据库。目前,如果我们向上面的新处理程序发送请求,响应将类似于以下内容
// GET /api/movies
{
"movies": []
}
这是因为 Mirage 的数据库是空的。我们可以使用 种子来在数据库中开始一些初始数据
createServer({
models: {
movie: Model,
},
routes() {
this.namespace = "api"
this.get("/movies", (schema, request) => {
return schema.movies.all()
})
},
seeds(server) {
server.create("movie", { name: "Inception", year: 2010 })
server.create("movie", { name: "Interstellar", year: 2014 })
server.create("movie", { name: "Dunkirk", year: 2017 })
},
})
server.create
获取模型名称和属性对象,并将新数据插入数据库。
现在,当我们的 JavaScript 应用向 /api/movies
发送请求时,我们的服务器会返回以下内容。
// GET /api/movies
{
"movies": [
{ "id": 1, "name": "Inception", "year": 2010 },
{ "id": 2, "name": "Interstellar", "year": 2014 },
{ "id": 3, "name": "Dunkirk", "year": 2017 }
]
}
请注意,Mirage 的数据库会自动为每个记录分配一个自增 ID。
我们也从响应中消除了所有硬编码数据,这意味着如果我们的应用随着时间的推移修改了 Mirage 数据库中的数据,对这个端点的响应也会相应地改变。
希望你能看到数据库、模型和 Schema
API 如何极大地简化了我们的服务器定义。以下是针对 Movie
资源的一组五个标准 RESTful 路由。
this.get("/movies", (schema, request) => {
return schema.movies.all()
})
this.get("/movies/:id", (schema, request) => {
let id = request.params.id
return schema.movies.find(id)
})
this.post("/movies", (schema, request) => {
let attrs = JSON.parse(request.requestBody)
return schema.movies.create(attrs)
})
this.patch("/movies/:id", (schema, request) => {
let newAttrs = JSON.parse(request.requestBody)
let id = request.params.id
let movie = schema.movies.find(id)
return movie.update(newAttrs)
})
this.delete("/movies/:id", (schema, request) => {
let id = request.params.id
return schema.movies.find(id).destroy()
})
有了这个 Mirage 定义,你就可以完全构建和测试你的前端应用,完成所有动态功能,并考虑到服务器可能存在的每种状态。一旦你对代码感到满意,你就可以将其部署到一个与你的 Mirage 定义具有相同 API 契约的生产服务器上。
简写
Mirage 有一个 简写 概念,用于减少编写传统 API 端点所需的代码。
例如,路由处理程序
this.get("/movies", (schema, request) => {
return schema.movies.all()
})
可以写成
this.get("/movies")
对于 post
、patch
(或 put
)和 del
方法也存在简写。以下是上面定义的 Movie
资源的完整资源路由集,使用简写编写。
this.get("/movies")
this.get("/movies/:id")
this.post("/movies")
this.patch("/movies/:id")
this.del("/movies/:id")
简写使编写服务器定义简洁明了,所以尽可能地使用它们。在模拟新路由时,你应该始终从简写开始,然后在需要更多控制时降级到扩展的函数路由处理程序。
工厂
在上面的示例中,我们使用 server.create
API 播种了 Mirage 的数据库。
seeds(server) {
server.create("movie", { name: "Inception", year: 2010 })
server.create("movie", { name: "Interstellar", year: 2014 })
server.create("movie", { name: "Dunkirk", year: 2017 })
}
虽然能够为每个记录传入所有属性很好,但有时我们只是想要一种更快的方法来创建新的数据库记录。这就是工厂发挥作用的地方。
工厂 是可以轻松为你的 Mirage 服务器生成逼真数据的对象。可以将它们视为模型的蓝图。
我们可以为 Movie
模型创建一个工厂,如下所示。
import { createServer, Model, Factory } from "miragejs"
createServer({
models: {
movie: Model,
},
factories: {
movie: Factory.extend({}),
},
})
然后,我们可以在工厂上定义一些属性。它们可以是简单的类型,如布尔值、字符串或数字,也可以是返回动态数据的函数。
import { createServer, Model, Factory } from "miragejs"
createServer({
models: {
movie: Model,
},
factories: {
movie: Factory.extend({
title(i) {
return `Movie ${i}` // Movie 1, Movie 2, etc.
},
year() {
let min = 1950
let max = 2019
return Math.floor(Math.random() * (max - min + 1)) + min
},
rating: "PG-13",
}),
},
})
现在,当我们使用 server.create
API 时,Mirage 会使用我们的工厂来帮助我们生成新数据。(它仍然会尊重我们传入的属性覆盖。)
server.create("movie")
server.create("movie")
server.create("movie", { rating: "R" })
server.db.dump()
/*
Mirage's database now contains
{
movies: [
{
id: 1,
title: "Movie 1",
year: 1992,
rating: "PG-13",
},
{
id: 2,
title: "Movie 2",
year: 2008,
rating: "PG-13",
},
{
id: 3,
title: "Movie 3",
year: 1947,
rating: "R",
}
]
}
*/
还有一个 server.createList
API 可用于一次生成多个记录。
你可以在 seeds
函数中使用 server.create
和 server.createList
来调用你的工厂
import { createServer, Factory } from "miragejs"
createServer({
seeds(server) {
server.createList("movie", 10)
},
})
以及在你的测试环境中。在测试环境中,Mirage 会加载它的路由,但会忽略它的种子,这让你有机会将数据库设置为测试所需的精确状态。
// app-test.js
import React from "react"
import { render, waitForElement } from "@testing-library/react"
import App from "./App"
import startMirage from "./start-mirage"
let server
beforeEach(() => {
server = startMirage({ environment: "test" })
})
afterEach(() => {
server.shutdown()
})
it("shows the list of movies", async () => {
server.createList("movie", 5)
const { getByTestId } = render(<App />)
await waitForElement(() => getByTestId("movie-list"))
expect(getByTestId("movie-item")).toHaveLength(5)
})
工厂为你提供了一种简单的方法来设置 Mirage 服务器的初始数据,无论是在开发过程中还是在每个测试的基础上。
关系
处理关系始终很棘手,模拟处理关系的端点也不例外。幸运的是,Mirage 附带了一个 ORM 来帮助你保持路由处理程序的整洁。
假设你的 Movie
有多个 CastMembers
。你可以在模型中声明这种关系。
import { createServer, hasMany, belongsTo } from "miragejs"
createServer({
models: {
movie: Model.extend({
castMembers: hasMany(),
}),
castMember: Model.extend({
movie: belongsTo(),
}),
},
})
现在 Mirage 知道这两个模型之间的关系,这在编写路由处理程序时非常有用
this.get("/movies/:id/cast-members", (schema, request) => {
let movie = schema.movies.find(request.params.id)
return movie.castMembers
})
以及在创建相关数据的图表时。
it("shows the cast members for a movie", async () => {
const movie = server.create("movie", {
title: "Interstellar",
castMembers: [
server.create("cast-member", { name: "Matthew McConaughey" }),
server.create("cast-member", { name: "Anne Hathaway" }),
server.create("cast-member", { name: "Jessica Chastain" }),
],
})
const { getByTestId } = render(<App path={`/movies/${movie.id}`} />)
await waitForElement(() => getByTestId("cast-member-list"))
expect(getByTestId("cast-member")).toHaveLength(3)
})
Mirage 使用外键为你跟踪这些相关模型,因此你不必担心任何混乱的簿记细节,因为你的 JavaScript 应用会读取并写入数据库中的新关系。
序列化器
Mirage 的设计使你可以完全复制你的生产 API 服务器。
到目前为止,我们已经看到 Mirage 的默认有效负载格式如下。
// GET /api/movies
{
"movies": [
{ "id": 1, "name": "Inception", "year": 2010 },
{ "id": 2, "name": "Interstellar", "year": 2014 },
{ "id": 3, "name": "Dunkirk", "year": 2017 }
]
}
但当然,并非所有后端 API 都符合这种格式。
例如,你的 API 可能会使用 JSON:API 规范,看起来更像这样。
// GET /api/movies
{
"data": [
{
"id": 1,
"type": "movies",
"attributes": { "name": "Inception", "year": 2010 }
},
{
"id": 2,
"type": "movies",
"attributes": { "name": "Interstellar", "year": 2014 }
},
{
"id": 3,
"type": "movies",
"attributes": { "name": "Dunkirk", "year": 2017 }
}
]
}
这就是 Mirage 序列化器存在的原因。 序列化器 允许你自定义响应的格式化逻辑,而无需更改路由处理程序、模型、关系或 Mirage 设置的任何其他部分。
Mirage 附带了一些与流行的后端格式匹配的命名序列化器。
import { createServer, JSONAPISerializer } from "miragejs"
createServer({
serializers: {
application: JSONAPISerializer,
},
})
你也可以从基类扩展,并使用它的格式化钩子来编写你自己的序列化器。
import { createServer, Serializer } from "miragejs"
createServer({
serializers: {
application: Serializer.extend({
keyForAttribute(attr) {
return dasherize(attr)
},
keyForRelationship(attr) {
return dasherize(attr)
},
}),
},
})
Mirage 的序列化器层了解你的关系,这在模拟预期要侧加载或嵌入相关数据的端点时很有帮助。
例如,使用以下配置。
createServer({
serializers: {
movie: Serializer.extend({
include: ["crewMembers"],
}),
},
routes() {
this.get("/movies/:id")
},
})
对 /movies/1
的 GET 请求会自动包含相关的剧组成员。
// GET /movies/1
{
"movie": {
"id": 1,
"title": "Interstellar"
},
"crew-members": [
{
"id": 1,
"movie-id": 1,
"name": "Matthew McConaughey"
},
{
"id": 2,
"movie-id": 1,
"name": "Anne Hathaway"
},
{
"id": 3,
"movie-id": 1,
"name": "Jessica Chastain"
}
]
}
Mirage 的命名序列化器为你完成了许多这种工作,因此你应该将它们用作起点,只有在你需要时才添加针对你的 API 的特定自定义项。
直通
即使你在处理现有应用,或者不想模拟整个 API,Mirage 也是一个很好的工具。默认情况下,如果你的 JavaScript 应用发出了没有定义相应路由处理程序的请求,Mirage 会抛出错误。
为了避免这种情况,告诉 Mirage 允许未处理的请求通过。
createServer({
routes() {
// Allow unhandled requests on the current domain to pass through
this.passthrough()
},
})
现在你可以像往常一样进行开发,例如针对现有 API 进行开发。
当需要构建新功能时,你不必等待 API 更新。只需定义你需要的新的路由。
createServer({
routes() {
// Mock this route and Mirage will intercept it
this.get("/movies")
// All other API requests on the current domain will still pass through
// e.g. GET /api/directors
this.passthrough()
// If your API requests go to an external domain, pass those through by
// specifying the fully qualified domain name
this.passthrough("http://api.acme.com/**")
},
})
你就可以完全开发和测试该功能。这样,你就可以逐步构建你的服务器定义,随着你不断前进,为服务器的每个状态添加一些可靠的验收测试。
这应该足以让你开始使用!
文档的下一部分将详细介绍 Mirage 的每个主要概念。