MirageJS Part 1 - Overview

最近在项目中使用了 Mirage ,看着官方文档,做了一些笔记。文中的内容都来自于官方文档,包括例子,并没有完整翻译官方文档,结合自己的使用体会做了增删,就是翻译了又没完全翻译。

简介

Mirage 是一个让前端开发人员模拟后端 API 的 JavaScript 库。用人话说就是:Mirage 是一个 Mock 服务器。

Mirage 在浏览器中运行。它能拦截 Javascript 应用程序发出的任何 XMLHttpRequestfetch 请求,并允许模拟这些请求所对应的响应结果。

Mirage 除了拦截 HTTP 请求外,还提供了一个模拟数据库和一系列辅助函数,可以轻松模拟后端服务。

Mirage 借鉴了典型的 API 服务端框架的概念,拥有例如:

  • 处理 HTTP 请求的路由(routes)
  • 数据库(database)和数据模型(models),用于存储数据和定义数据之间的关系
  • factories(模型工厂) and fixtures for stubbing data(数据填充)
  • 用于格式化 HTTP 响应的序列化函数(serializers)

安装

Mirage 很容易可以集成到现有项目中。

1
2
3
npm install --save-dev miragejs
# or
yarn add --dev miragejs

概览

Mirage 可以通过 路由处理器(route handlers)轻易的模拟一个 API 请求的响应结果。

静态路由(Static Route Handlers)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { createServer } from "miragejs"

createServer({
routes() {
this.namespace = 'api'

// in app makes a GET request to `/api/movies`, Mirage will respond with this data.
this.get('/movies', () => {
return {
movies: [
{ id: 1, name: 'Inception', year: 2010},
{ id: 1, name: 'Interstellar', year: 2014},
{ id: 1, name: 'Dunkirk', year: 2017},
],
}
})
}
})

可以像上面这个示例中一样,使用静态路由来完成对 API 返回的模拟。路由处理器支持所有的 HTTP 动词(get/post/delete/put …)。

也可以在路由的第三个参数中使用 timing 选项来模拟服务器延迟。

1
2
3
4
5
6
7
8
9
10
11
12
...
// Using the `timing` option to slow down the response
this.get(
'/movies',
() => {
return {
movies: [],
}
},
{ timing: 4000 }
)
...

也可以在路由中定义如何从 API 中获取需要的请求参数。

1
2
3
4
5
6
7
8
9
...
// 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 }
})
...

也可以返回一个 HTTP Error。

1
2
3
4
5
6
7
8
9
...
// 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)
})
...

动态路由(Dynamic Route Handlers)

静态路由已经可以很好的工作了。但是静态路由不能总是满足我们花样请求数据的需求,比如按条件筛选查询结果时,静态路由就不能很好的满足需求。我们需要一个能模拟真实查询数据库时那种方式,让我们在路由之前先引入数据模型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { createServer, Model } from "miragejs"

createServer({
// 引入并定义数据模型
models: {
movie: Model,
},
routes() {
this.namespace = 'api'
// schema 参数可以访问到 model 中的数据,
// 这里的 `movies` 是复数形式,表示的是模型(Model)的集合(Collection)
this.get('/movies', (schema, request) => {
return schema.movies.all()
})
}
})

如果现在请求 GET /api/movies ,将返回一个空数组,因为我们还没给定义好的模型填充数据。

1
2
3
4
// GET /api/movies
{
"movies": []
}

现在让我们来给模型填充数据。

1
2
3
4
5
6
7
8
9
createServer({
models: {},
routes() {},
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() 方法将向数据库中插入一条数据(Mirage 会在内存中运行一个数据库),第一个参数是模型的名称,第二个参数是要插入的数据。在实际开发中,可以模型工厂里,配合 Marak/faker.js 等库插入大量的假数据用于测试。

现在再请求 GET /api/movies ,将会返回一个结果:

1
2
3
4
5
6
7
8
9
// GET /api/movies

{
"movies": [
{ "id": 1, "name": "Inception", "year": 2010 },
{ "id": 2, "name": "Interstellar", "year": 2014 },
{ "id": 3, "name": "Dunkirk", "year": 2017 }
]
}

既然是动态路由,那应该做一些动态路由应该作的事情。下面的例子使用标准的 RESTful 路由模式来返回 Movie 资源:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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()
})

路由缩写(Shorthands)

使用缩写可以减少代码的冗余,也可以让路由定义更简洁。在默认的情况下,动态路由一节里, RESTful 请求返回的示例中的所有路由定义,都可以使用缩写的方式来完成,路由缩写与上面例子中定义的同名路由完成一样的工作:

1
2
3
4
5
this.get("/movies")
this.get("/movies/:id")
this.post("/movies")
this.patch("/movies/:id")
this.del("/movies/:id")

非常建议始终使用路由缩写来定义路由,除非你需要在路由定义里完成额外的工作。

模型工厂(Factories)

模型工厂是一个可以为指定模型生成逼真种子数据的对象,最终将用生成的种子数据填充到模型中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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来为 movie 模型创建数据,Mirage 将会使用定义好的模型工厂来生成默认的数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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 API。

1
2
3
4
5
6
7
import { createServer, Factory } from "miragejs"

createServer({
seeds(server) {
server.createList("movie", 10)
},
})

在编写单元测试时(测试环境),Mirage 会载入路由定义,但会忽略为路由生成的种子数据,但是勿需担心,你可以在开始测试之前为 Mirage 路由生成数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 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)
})

数据关系(Relationships)

Mirage 提供了 ORM 来处理数据关系。继续上面的例子,一部影片可以有多个演员,所以来定义一个一对多的关系:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { createServer, hasMany, belongsTo } from "miragejs"

createServer({
models: {
movie: Model.extend({
castMembers: hasMany(),
}),
castMember: Model.extend({
movie: belongsTo(),
}),
},
seeds(server) {
server.create('movie', {
name: "Interstellar",
year: 2014,
castMembers: [
server.create("cast-member", { name: "Matthew McConaughey" }),
server.create("cast-member", { name: "Anne Hathaway" }),
server.create("cast-member", { name: "Jessica Chastain" }),
],
})
},
})

我们定义了两个模型 moviecastMember ,并且定义了两个模型之间的关系。现在就可以在路由定义中查询出关联的数据了:

1
2
3
4
5
this.get('/movie/:id/cast-menbers', (schema, request) => {
let movie = schema.movies.find(request.params.id)

return movie.castMembers
})

Mirage 会自动使用外键来维护两个模型间的关系,所以不需要担心数据会混乱。

响应数据序列化(Serializers)

Mirage 默认返回的响应数据是这样子的:

1
2
3
4
5
6
7
8
9
// GET /api/movies

{
"movies": [
{ "id": 1, "name": "Inception", "year": 2010 },
{ "id": 2, "name": "Interstellar", "year": 2014 },
{ "id": 3, "name": "Dunkirk", "year": 2017 }
]
}

也许你的产品使用的是 JSON:API spec ,所以响应的数据看起来像这样子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 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 的设计初衷是为了完整重现你的产品的 API 服务。借助 Mirage 提供的数据序列化(Serializers)能力,可以自定义格式化响应数据。

Mirage 提供了几个与流行的后端数据响应格式匹配的命名序列化方法:

1
2
3
4
5
6
7
import { createServer, JSONAPISerializer } from "miragejs"

createServer({
serializers: {
application: JSONAPISerializer,
}
})

例子中只演示了 JSONAPISerializer ,实际还有 ActiveModelSerializerRestSerializer ,将在后面的章节作详细的说明。

如果 Mirage 提供的序列化不能满足需求,可以基于 Serializer 基类扩展自己的数据格式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { createServer, Serializer } from "miragejs"

createServer({
serializers: {
application: Serializer.extend({
keyForAttribute(attr) {
return dasherize(attr)
},
keyForRelationship(attr) {
return dasherize(attr)
},
}),
},
})

Mirage 的序列化层知道模型间的关系,这有利于模拟数据侧载或者嵌套关系数据:

1
2
3
4
5
6
7
8
9
10
11
createServer({
serializers: {
movie: Serializer.extend({
include: ["crewMembers"],
}),
},

routes() {
this.get("/movies/:id")
},
})

响应数据中将自动包含关系数据 crew-members

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 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 自定义项。

放行(Passthrough)

默认情况下,如果你的应用发出的请求未定义相应的 Mock 路由请求,Mirage 将会抛出一个错误。

假如你现在的应用正在使用 Mirage 或者不想使用 Mirage 模拟所有的 API,为避免因未定义的路由而抛出错误,使用 passthrough() 方法放行未定义的请求:

1
2
3
4
5
6
createServer({
routes() {
// Allow unhandled requests on the current domain to pass through
this.passthrough()
},
})

当需要为现有的应用开发新的功能,不需要等待后端的 API 更新,使用 Mirage 就可以模拟新的 API 并且放行已经在生产环境中使用的 API :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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/**")
},
})

通过这种方法,可以专注的开发和测试新功能而不用担心因为旧接口的存在而不能 Mock。


本章节的内容已经足够在你的项目中使用 Mirage 了。下一章节来详细说说 Mirage 中的一些主要概念。

MirageJS Part 2 - Main Concepts - Route handlers Guide for changing dev environment from Mac to Windows

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×