序列化器

序列化器是一个对象,负责将从您的路由处理程序返回的模型或集合转换为前端应用程序预期格式的 JSON 负载。

例如,此路由处理程序返回一个 Movie 模型

this.get("movies/:id", (schema, request) => {
  return schema.movies.find(request.params.id)
})

序列化器将该 JavaScript 对象转换为此 JSON 负载

// GET /movies/1

{
  "data": {
    "id": "1",
    "type": "movies",
    "attributes": {
      "title": "Interstellar"
    }
  }
}

序列化器是 Mirage 架构中与数据层交互的最后一个主要部分,因为生成格式良好的 JSON 响应通常涉及遍历模型的关系图。

让我们看看它们是如何工作的。

选择要使用的序列化器

使用 Mirage 序列化器的第一步是选择要开始使用的包含的序列化器,这反过来取决于您的后端使用什么 JSON 格式将数据提供给您的 JavaScript 应用程序。

上面的 JSON 负载是遵循 JSON:API 规范 的 API 的示例。它具有非常具体的结构,区分属性和关系,支持命名和多态关系、链接、查询参数包含等等。它还解决了其他定义不太严格的格式中存在的大量问题。

如果您的现有后端 API 使用 JSON:API,Mirage 附带一个 JSONAPISerializer,它将为您完成繁重的工作。如果您要启动一个新的应用程序,请考虑使用 JSON:API,因为它可以解决您在构建 API 时遇到的许多问题,并可以帮助您的团队避免无谓的争论。

当然,还有很多 JavaScript 应用程序不使用 JSON:API 作为其 API 序列化格式。

Mirage 附带了另外两个命名的序列化器,它们与流行的后端格式相匹配。 ActiveModelSerializer 旨在模仿类似于使用 active_model_serializer gem 构建的 Rails API 的 API,并且 RestSerializer 是许多其他常见 API 的良好起点。

根据您自己的后端 API 的格式,您需要选择最接近的序列化器作为起点,并对其进行自定义以匹配您的生产格式。我们将在本指南的后面部分详细讨论。

定义序列化器

选择好要使用的序列化器后,像这样将其定义为应用程序范围内的默认序列化器

import { createServer, RestSerializer } from "miragejs"

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

应用程序序列化器是系统中每个模型和集合使用的默认序列化器。

如果您需要为特定模型类型自定义序列化器,则可以定义模型特定的序列化器,这些序列化器优先于您的应用程序序列化器。

import { createServer, RestSerializer } from "miragejs"

createServer({
  serializers: {
    application: RestSerializer,
    movie: RestSerializer.extend({
      include: ["castMembers"],
    }),
  },
})

您将在本指南的后面部分看到可能需要的模型特定自定义类型的示例。

通常,您可能需要进行应用程序范围内的自定义,除了模型特定的自定义之外。因此,最佳实践是使用从应用程序序列化器继承的模型特定序列化器。您可以像这样实现:

import { createServer, RestSerializer } from "miragejs"

let ApplicationSerializer = RestSerializer.extend({
  root: false,
})

createServer({
  serializers: {
    application: ApplicationSerializer,
    movie: ApplicationSerializer.extend({
      include: ["castMembers"],
    }),
  },
})

现在,当您创建模型特定的序列化器时,将维护应用程序范围内的自定义。

自定义序列化器

在自定义应用程序的序列化器时,您主要会调整 Mirage 的默认值。

例如,如果您的应用程序希望属性名称为 PascalCase

// GET /movies/1

{
  Id: '1',
  ReleaseDate: 'Interstellar'
}

您可以覆盖序列化器的 keyForAttribute 方法

import { RestSerializer } from "miragejs"
import { camelCase, upperFirst } from "lodash"

let ApplicationSerializer = RestSerializer.extend({
  keyForAttribute(attr) {
    return upperFirst(camelCase(attr))
  },
})

请查看每个序列化器的 API 文档,以了解有关所有可用的自定义钩子的更多信息。

关系

关系是序列化器另一个重要的方面,因为后端有许多不同的方法来处理关系。

例如,默认情况下,JSONAPISerializer 仅在通过查询参数包含请求时才包含关系信息。

/* GET /movies/1?include=cast-members */

{
  "data": {
    "id": "1",
    "type": "movies",
    "attributes": {
      "title": "Interstellar"
    },
    "relationships": {
      "cast-members": {
        "data": [
          { "type": "people", "id": "1" },
          { "type": "people", "id": "2" },
          { "type": "people", "id": "3" }
        ]
      }
    }
  },
  "included": [
    { "id": "1", "type": "people", "attributes": { "name": "Susan" } },
    { "id": "2", "type": "people", "attributes": { "name": "Bob" } },
    { "id": "3", "type": "people", "attributes": { "name": "Jane" } }
  ]
}

否则,只包含主要资源。

/* GET /movies/1 */

{
  "data": {
    "id": "1",
    "type": "movies",
    "attributes": {
      "title": "Interstellar"
    }
  }
}

但一些 API 会包含所有资源的关系 ID,无论请求中是否使用查询参数包含。

JSONAPISerializer 上有一个选项可以启用此功能。

JSONAPISerializer.extend({
  alwaysIncludeLinkageData: true,
})

现在,对/movies/1 的 GET 请求将返回以下有效负载。

/* GET /movies/1 */

{
  "data": {
    "id": "1",
    "type": "movies",
    "attributes": {
      "title": "Interstellar"
    },
    "relationships": {
      "cast-members": {
        "data": [
          { "type": "people", "id": "1" },
          { "type": "people", "id": "2" },
          { "type": "people", "id": "3" }
        ]
      }
    }
  }
}

现在,您的 JavaScript 应用程序可以使用这些 ID 来随后获取相关的演员成员。

其他 API 协议通过响应链接来获取相关数据来工作。 JSONAPISerializer 也为此提供了一个钩子。

JSONAPISerializer.extend({
  links(movie) {
    return {
      "cast-members": {
        related: `/api/movies/${movie.id}/cast-members`,
      },
    }
  },
})

现在对/movies/1 的 GET 请求将返回以下有效负载。

/* GET /movies/1 */

{
  "data": {
    "id": "1",
    "type": "movies",
    "attributes": {
      "title": "Interstellar"
    },
    "relationships": {
      "cast-members": {
        "links": {
          "related": "/api/movies/1/cast-members"
        }
      }
    }
  }
}

其他序列化器也具有控制如何加载相关数据的机制。 请务必查看 API 文档以了解所有详细信息。

使用序列化 JSON

虽然大多数路由处理程序应该返回一个模型或集合实例,并将序列化逻辑留给序列化器,但有时在您的路由处理程序中执行一些最终的序列化逻辑可能很方便。

您可以在路由处理程序中使用this.serialize 辅助方法来执行此操作 - 确保使用 function 而不是胖箭头,以便您可以访问正确的 this

createServer({
  routes() {
    this.get("/movies", function (schema, request) {
      let movies = schema.movies.all()
      let json = this.serialize(movies)

      json.meta.size = movies.length

      return json
    })
  },
})

serialize 辅助程序将使用典型的查找逻辑,首先检查模型特定的序列化器,然后回退到默认的应用程序序列化器。

如果您有特殊情况,还可以通过将序列化器名称作为第二个参数传入来使用特定序列化器。

import { createServer, RestSerializer } from "miragejs"

let ApplicationSerializer = RestSerializer.extend()

createServer({
  serializers: {
    application: ApplicationSerializer,
    movieWithRelationships: ApplicationSerializer.extend({
      include: ["castMembers", "reviews"],
    }),
  },

  routes() {
    this.get("/movies/:id", function (schema, request) {
      let movie = schema.movies.find(request.params.id)
      let json = this.serialize(movie, "movie-with-relationships")

      return json
    })
  },
})

请注意,字符串名称是您用来定义序列化器 (movieWithRelationships) 的键的连字符分隔版本 (movie-with-relationships)。


一般来说,您应该努力使用提供的钩子来实现适用于大多数模型的应用程序序列化器。

一致性是 API 的关键。您的生产 API 越一致,在 Mirage 中实现它就越容易。使用 Mirage 服务器在您的前端和后端团队之间就 API 协议中的不一致进行沟通。传统的 API 协议契约将帮助您编写更少的代码 - 不仅在 Mirage 中,而且在您的其他 JavaScript 应用程序中!

请务必查看 API 文档以了解所有可用的钩子,以便自定义您的序列化器层。


现在我们已经介绍了 Mirage 的所有主要概念,我们准备看看如何使用 Mirage 来有效地测试我们的 JavaScript 应用程序。