概觀

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 有一個資料層來協助您編寫更強大的伺服器實作。讓我們看看它是如何運作的,方法是替換上面的基本 Stub 資料。

首先,我們會告訴 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,可以一次產生許多記錄。

您可以使用 server.createserver.createList 在您的 seeds 函數中調用您的工廠

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 的每個主要概念。