最近在项目中使用了 Mirage ,看着官方文档 ,做了一些笔记。文中的内容都来自于官方文档,包括例子,并没有完整翻译官方文档,结合自己的使用体会做了增删,就是翻译了又没完全翻译。
简介 Mirage 是一个让前端开发人员模拟后端 API 的 JavaScript 库。用人话说就是:Mirage 是一个 Mock 服务器。
Mirage 在浏览器中运行。它能拦截 Javascript 应用程序发出的任何 XMLHttpRequest
或 fetch
请求,并允许模拟这些请求所对应的响应结果。
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 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' 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 ... this .get( '/movies' , () => { return { movies : [], } }, { timing : 4000 } ) ...
也可以在路由中定义如何从 API 中获取需要的请求参数。
1 2 3 4 5 6 7 8 9 ... 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 ... 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' this .get('/movies' , (schema, request ) => { return schema.movies.all() }) } })
如果现在请求 GET /api/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 { "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} ` }, 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()
使用 server.createList
API 来批量生成数据,可以在 seeds()
方法中同时使用 server.create
和 server.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 import React from "react" import { render, waitForElement } from "@testing-library/react" import App from "./App" import startMirage from "./start-mirage" let serverbeforeEach(() => { 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" }), ], }) }, })
我们定义了两个模型 movie
和 castMember
,并且定义了两个模型之间的关系。现在就可以在路由定义中查询出关联的数据了:
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 { "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 { "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
,实际还有 ActiveModelSerializer
和 RestSerializer
,将在后面的章节作详细的说明。
如果 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 { "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 ( ) { this .passthrough() }, })
当需要为现有的应用开发新的功能,不需要等待后端的 API 更新,使用 Mirage 就可以模拟新的 API 并且放行已经在生产环境中使用的 API :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 createServer({ routes ( ) { this .get("/movies" ) this .passthrough() this .passthrough("http://api.acme.com/**" ) }, })
通过这种方法,可以专注的开发和测试新功能而不用担心因为旧接口的存在而不能 Mock。
本章节的内容已经足够在你的项目中使用 Mirage 了。下一章节来详细说说 Mirage 中的一些主要概念。
评论