概述

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")

对于 postpatch(或 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.createserver.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 的每个主要概念。