Skip to content

Latest commit

 

History

History
728 lines (592 loc) · 27.1 KB

ch05_02.md

File metadata and controls

728 lines (592 loc) · 27.1 KB

解析器

到目前为止,在我们对 GraphQL 的讨论中,我们非常关注查询。模式定义了允许客户端进行的查询操作以及不同类型之间的关联方式。模式描述了数据需求,但不执行获取该数据的工作。该工作由解析器处理。

解析器是返回特定字段数据的函数。解析器函数以模式指定的类型和形状返回数据。解析器可以是异步的,并且可以从 REST API、数据库或任何其他服务获取或更新数据。

让我们看一下解析器对于我们的根查询可能是什么样子。在项目根目录的 index.js 文件中,将 totalPhotos 字段添加到查询中:

const typeDefs = `
  type Query {
    totalPhotos: Int!
  }
`;
const resolvers = {
  Query: {
    totalPhotos: () => 42,
  },
};

typeDefs 变量是我们定义模式的地方。它只是一个字符串。每当我们创建像 totalPhotos 这样的查询时,它都应该由同名的解析器函数支持。类型定义描述字段应返回的类型。解析器函数从某处返回该类型的数据——在本例中,只是一个静态值 42。

同样重要的是要注意,解析器必须定义在一个与模式中的对象具有相同类型名称的对象下。totalPhotos 字段是查询对象的一部分。该字段的解析器也必须是 Query 对象的一部分。

我们已经为根查询创建了初始类型定义。我们还创建了第一个支持 totalPhotos 查询字段的解析器。要创建模式并启用对模式的查询执行,我们将使用 Apollo Server:

// 1. Require 'apollo-server'
const { ApolloServer } = require("apollo-server");
const typeDefs = `
  type Query {
    totalPhotos: Int!
  }
`;
const resolvers = {
  Query: {
    totalPhotos: () => 42,
  },
};
// 2. Create a new instance of the server.
// 3. Send it an object with typeDefs (the schema) and resolvers
const server = new ApolloServer({
  typeDefs,
  resolvers,
});
// 4. Call listen on the server to launch the web server
server
  .listen()
  .then(({ url }) => console.log(`GraphQL Service running on ${url}`));

在请求 ApolloServer 之后,我们将创建一个新的服务器实例,向它发送一个具有两个值的对象:typeDefs 和 resolvers。这是一个快速且最小的服务器设置,它仍然允许我们建立一个强大的 GraphQL API。在本章后面,我们将讨论如何使用 Express 扩展服务器的功能。

此时,我们已准备好执行对 totalPhotos 的查询。一旦我们运行 npm start,我们应该看到 GraphQL Playground 在 http 上运行:

{
  totalPhotos
}

totalPhotos 的返回数据如预期的那样为 42:

{
  "data": {
    "totalPhotos": 42
  }
}

解析器是 GraphQL 实现的关键。每个字段都必须有一个对应的解析器函数。解析器必须遵循模式的规则。它必须与模式中定义的字段同名,并且必须返回模式定义的数据类型。

根解析器

正如第 4 章中所讨论的,GraphQL API 具有用于查询、变更和订阅的根类型。这些类型位于顶层,代表 API 的所有可能入口点。到目前为止,我们已经将 totalPhotos 字段添加到 Query 类型中,这意味着我们的 API 可以查询该字段。

让我们通过为 Mutation 创建一个根类型来添加它。突变字段称为 postPhoto 并将名称和描述作为 String 类型的参数。发送突变时,它必须返回一个布尔值:

const typeDefs = `
  type Query {
    totalPhotos: Int!
  }
  type Mutation {
    postPhoto(name: String! description: String): Boolean!
  }
`;

创建 postPhoto 突变后,我们需要在解析器对象中添加相应的解析器:

// 1. A data type to store our photos in memory
var photos = [];
const resolvers = {
  Query: {
    // 2. Return the length of the photos array
    totalPhotos: () => photos.length,
  },
  // 3. Mutation and postPhoto resolver
  Mutation: {
    postPhoto(parent, args) {
      photos.push(args);
      return true;
    },
  },
};

首先,我们需要创建一个名为 photos 的变量来将照片的详细信息存储在一个数组中。在本章后面,我们会将照片存储在数据库中。

接下来,我们增强 totalPhotos 解析器以返回照片数组的长度。每当查询此字段时,它将返回当前存储在数组中的照片数量。从这里,我们添加 postPhoto 解析器。

这一次,我们在 postPhoto 函数中使用函数参数。第一个参数是对父对象的引用。有时您会在文档中看到它表示为 _、root 或 obj。在这种情况下,postPhoto 解析器的父级是一个 Mutation。父级当前不包含我们需要使用的任何数据,但它始终是发送给解析器的第一个参数。因此,我们需要添加一个占位符父参数,以便我们可以访问发送到解析器的第二个参数:变异参数。

发送到 postPhoto 解析器的第二个参数是发送到此操作的 GraphQL 参数:名称和可选的描述。args 变量是包含以下两个字段的对象:{name,description}。现在,参数代表一个照片对象,所以我们将它们直接推入照片数组。

现在是时候在 GraphQL Playground 中测试 postPhoto 突变了,为 name 参数发送一个字符串:

mutation newPhoto {
  postPhoto(name: "sample photo")
}

此突变将照片详细信息添加到数组并返回 true。让我们修改此突变以使用查询变量:

mutation newPhoto($name: String!, $description: String) {
  postPhoto(name: $name, description: $description)
}

将变量添加到突变后,必须传递数据以提供字符串变量。在 Playground 的左下角,让我们将名称和描述的值添加到查询变量窗口:

{
  "name": "sample photo A",
  "description": "A sample photo for our dataset"
}

类型解析器

当执行 GraphQL 查询、变更或订阅时,它会返回与查询形状相同的结果。我们已经看到解析器如何返回标量类型值,如整数、字符串和布尔值,但解析器也可以返回对象。

对于我们的照片应用程序,让我们创建一个照片类型和一个将返回照片对象列表的 allPhotos 查询字段:

const typeDefs = `
  # 1. Add Photo type definition
  type Photo {
    id: ID!
    url: String!
    name: String!
    description: String
  }
  # 2. Return Photo from allPhotos
  type Query {
    totalPhotos: Int!
    allPhotos: [Photo!]!
  }
  # 3. Return the newly posted photo from the mutation
  type Mutation {
    postPhoto(name: String! description: String): Photo!
  }
`

因为我们已经将 Photo 对象和 allPhotos 查询添加到我们的类型定义中,所以我们需要在解析器中反映这些调整。postPhoto 突变需要以 Photo 类型的形状返回数据。查询 allPhotos 需要返回与 Photo 类型具有相同形状的对象列表:

// 1. A variable that we will increment for unique ids
var _id = 0;
var photos = [];
const resolvers = {
  Query: {
    totalPhotos: () => photos.length,
    allPhotos: () => photos,
  },
  Mutation: {
    postPhoto(parent, args) {
      // 2. Create a new photo, and generate an id
      var newPhoto = {
        id: _id++,
        ...args,
      };
      photos.push(newPhoto);
      // 3. Return the new photo
      return newPhoto;
    },
  },
};

因为 Photo 类型需要 ID,所以我们创建了一个变量来存储 ID。在 postPhoto 解析器中,我们将通过递增此值来生成 ID。args 变量提供照片的名称和描述字段,但我们还需要一个 ID。通常由服务器创建标识符和时间戳等变量。因此,当我们在 postPhoto 解析器中创建一个新的照片对象时,我们添加 ID 字段并将名称和描述字段从 args 扩展到我们的新照片对象中。

突变不返回布尔值,而是返回一个与 Photo 类型的形状相匹配的对象。该对象是使用生成的 ID 以及随数据传入的名称和描述字段构建的。此外,postPhoto 突变将照片对象添加到照片数组。这些对象与我们在模式中定义的照片类型的形状相匹配,因此我们可以从 allPhotos 查询中返回整个照片数组。

WARNING
使用递增变量生成唯一 ID 显然是一种非常不可扩展的创建 ID 的方法,但将在此处用作演示目的。在真实的应用程序中,您的 ID 可能由数据库生成。

为了验证 postPhoto 是否正常工作,我们可以调整突变。因为 Photo 是一种类型,所以我们需要为我们的变异添加一个选择集:

mutation newPhoto($name: String!, $description: String) {
  postPhoto(name: $name, description: $description) {
    id
    name
    description
  }
}

通过突变添加几张照片后,以下 allPhotos 查询应返回所有添加的照片对象的数组:

query listPhotos {
  allPhotos {
    id
    name
    description
  }
}

我们还在照片架构中添加了一个不可为 null 的 url 字段。当我们将 url 添加到我们的选择集中时会发生什么?

query listPhotos {
  allPhotos {
    id
    name
    description
    url
  }
}

当 url 添加到我们查询的选择集中时,会显示错误:无法为不可为空的字段 Photo.url 返回 null。我们不在数据集中添加 url 字段。我们不需要存储 URL,因为它们可以自动生成。我们模式中的每个字段都可以映射到解析器。我们需要做的就是将一个 Photo 对象添加到我们的解析器列表中,并定义我们想要映射到函数的字段。在这种情况下,我们想使用一个函数来帮助我们解析 URL:

const resolvers = {
  Query: { ... },
  Mutation: { ... },
  Photo: {
  url: parent => `http://yoursite.com/img/${parent.id}.jpg`
  }
}

因为我们要为照片 URL 使用解析器,所以我们在解析器中添加了一个 Photo 对象。这个添加到根的照片解析器称为普通解析器。普通解析器被添加到解析器对象的顶层,但它们不是必需的。我们可以选择使用简单的解析器为照片对象创建自定义解析器。如果您不指定普通解析器,GraphQL 将回退到默认解析器,该解析器返回与字段同名的属性。

当我们在查询中选择照片的 url 时,会调用相应的解析器函数。发送给解析器的第一个参数始终是父对象。在这种情况下,父级表示正在解析的当前 Photo 对象。我们在这里假设我们的服务只处理 JPEG 图像。这些图像以其带照片的 ID 命名,可以在 http://yoursite.com/img/route 中找到。因为 parent 是照片,我们可以通过这个参数获取照片的 ID,并用它自动生成当前照片的 URL。

当我们定义 GraphQL 模式时,我们描述了应用程序的数据要求。借助解析器,我们可以强大而灵活地满足这些要求。函数给了我们这种力量和灵活性。函数可以是异步的,可以返回标量类型和返回对象,还可以从各种来源返回数据。解析器只是函数,我们 GraphQL 模式中的每个字段都可以映射到解析器。

使用输入和枚举

是时候向我们的 typeDef 引入枚举类型 PhotoCategory 和输入类型 PostPhotoInput 了:

enum PhotoCategory {
  SELFIE
  PORTRAIT
  ACTION
  LANDSCAPE
  GRAPHIC
}

type Photo {
  ...
  category: PhotoCategory!
}

input PostPhotoInput {
  name: String!
  category: PhotoCategory=PORTRAIT
  description: String
}

type Mutation {
  postPhoto(input: PostPhotoInput!): Photo!
}

第 4 章中,我们在为 PhotoShare 应用程序设计模式时创建了这些类型。我们还添加了 PhotoCategory 枚举类型,并为我们的照片添加了类别字段。在解析照片时,我们需要确保照片类别(一个与枚举类型中定义的值相匹配的字符串)可用。当用户发布新照片时,我们还需要收集一个类别。

我们添加了一个 PostPhotoInput 类型来组织单个对象下的 postPhoto 突变的参数。此输入类型有一个类别字段。即使用户没有提供类别字段作为参数,也会使用默认值 PORTRAIT。

对于 postPhoto 解析器,我们也需要进行一些调整。照片的详细信息、名称、描述和类别现在嵌套在输入字段中。我们需要确保我们在 args.input 而不是 args 访问这些值:

postPhoto(parent, args) {
  var newPhoto = {
    id: _id++,
    ...args.input
  }
  photos.push(newPhoto)
    return newPhoto
}

现在,我们使用新的输入类型运行突变:

mutation newPhoto($input: PostPhotoInput!) {
  postPhoto(input: $input) {
    id
    name
    url
    description
    category
  }
}

我们还需要在查询变量面板中发送相应的 JSON:

{
  "input": {
    "name": "sample photo A",
    "description": "A sample photo for our dataset"
  }
}

如果未提供类别,它将默认为 PORTRAIT。或者,如果为类别提供了一个值,则在将操作发送到服务器之前,将根据我们的枚举类型对其进行验证。如果它是一个有效的类别,它将作为参数传递给解析器。

使用输入类型,我们可以使传递给突变的参数更可重用且更不容易出错。组合输入类型和枚举时,我们可以更具体地说明可以提供给特定字段的输入类型。输入和枚举非常有价值,当你一起使用它们时会变得更好。

边和连接

正如我们之前所讨论的,GraphQL 的力量来自边:数据点之间的连接。在建立 GraphQL 服务器时,类型通常映射到模型。将这些类型视为保存在类似数据的表中。从那里,我们将类型与连接联系起来。让我们探索可用于定义类型之间相互关联关系的连接类型。

一对多

用户需要访问他们之前发布的照片列表。我们将在名为 postedPhotos 的字段上访问此数据,该字段将解析为用户发布的经过过滤的照片列表。因为一个用户可以发布多张照片,所以我们称之为一对多关系。让我们将 User 添加到我们的 typeDef 中:

type User {
  githubLogin: ID!
  name: String
  avatar: String
  postedPhotos: [Photo!]!
}

至此,我们已经创建了一个有向图。我们可以从User类型遍历到Photo类型。要获得无向图,我们需要提供一种从照片类型返回到用户类型的方法。让我们向 Photo 类型添加一个 postedBy 字段:

type Photo {
  id: ID!
  url: String!
  name: String!
  description: String
  category: PhotoCategory!
  postedBy: User!
}

通过添加 postedBy 字段,我们创建了一个指向发布照片的用户的链接,从而创建了一个无向图。这是一对一的连接,因为一张照片只能由一个用户发布。

样本用户
为了测试我们的服务器,让我们将一些示例数据添加到我们的 index.js 文件中。请务必删除设置为空数组的当前照片变量:

var users = [
  { githubLogin: "mHattrup", name: "Mike Hattrup" },
  { githubLogin: "gPlake", name: "Glen Plake" },
  { githubLogin: "sSchmidt", name: "Scot Schmidt" },
];
var photos = [
  {
    id: "1",
    name: "Dropping the Heart Chute",
    description: "The heart chute is one of my favorite chutes",
    category: "ACTION",
    githubUser: "gPlake",
  },
  {
    id: "2",
    name: "Enjoying the sunshine",
    category: "SELFIE",
    githubUser: "sSchmidt",
  },
  {
    id: "3",
    name: "Gunbarrel 25",
    description: "25 laps on gunbarrel today",
    category: "LANDSCAPE",
    githubUser: "sSchmidt",
  },
];

因为连接是使用对象类型的字段创建的,所以它们可以映射到解析器函数。在这些函数内部,我们可以使用有关父级的详细信息来帮助我们定位并返回连接的数据。

让我们将 postedPhotos 和 postedBy 解析器添加到我们的服务中:

const resolvers = {
  ...
  Photo: {
    url: parent => `http://yoursite.com/img/${parent.id}.jpg`,
    postedBy: parent => {
      return users.find(u => u.githubLogin === parent.githubUser)
    }
  },
  User: {
    postedPhotos: parent => {
      return photos.filter(p => p.githubUser === parent.githubLogin)
    }
  }
}

在照片解析器中,我们需要为 postedBy 添加一个字段。在这个解析器中,我们需要弄清楚如何找到连接的数据。使用 .find() 数组方法,我们可以获得 githubLogin 与每张照片保存的 githubUser 值匹配的用户。.find() 方法返回单个用户对象。

在用户解析器中,我们使用数组的 .filter() 方法检索该用户发布的照片列表。此方法仅返回包含与父用户的 githubLogin 值匹配的 githubUser 值的那些照片的数组。filter 方法返回一组照片。

现在让我们尝试发送 allPhotos 查询:

query photos {
  allPhotos {
    name
    url
    postedBy {
      name
    }
  }
}

当我们查询每张照片时,我们可以查询发布该照片的用户。解析器正在定位并返回用户对象。在此示例中,我们仅选择发布照片的用户的姓名。鉴于我们的示例数据,结果应返回以下 JSON:

{
  "data": {
    "allPhotos": [
      {
        "name": "Dropping the Heart Chute",
        "url": "http://yoursite.com/img/1.jpg",
        "postedBy": {
          "name": "Glen Plake"
        }
      },
      {
        "name": "Enjoying the sunshine",
        "url": "http://yoursite.com/img/2.jpg",
        "postedBy": {
          "name": "Scot Schmidt"
        }
      },
      {
        "name": "Gunbarrel 25",
        "url": "http://yoursite.com/img/3.jpg",
        "postedBy": {
          "name": "Scot Schmidt"
        }
      }
    ]
  }
}

我们负责将数据与解析器连接起来,但一旦我们能够返回连接的数据,我们的客户就可以开始编写强大的查询。在下一节中,我们将向您展示一些创建多对多连接的技术。

多对多

我们要添加到服务中的下一个功能是能够在照片中标记用户。这意味着可以在许多不同的照片中标记用户,而照片中可以标记许多不同的用户。照片标签将在用户和照片之间创建的关系可以称为多对多——许多用户对许多照片。

为了完成多对多关系,我们将 taggedUsers 字段添加到 Photo 并将 inPhotos 字段添加到 User。让我们修改 typeDef:

type User {
  ...
  inPhotos: [Photo!]!
}
type Photo {
  ...
  taggedUsers: [User!]!
}

taggedUsers 字段返回用户列表,inPhotos 字段返回用户出现的照片列表。为了完成这种多对多连接,我们需要添加一个标签数组。要测试标记功能,您需要为标记填充一些样本数据:

var tags = [
  { photoID: "1", userID: "gPlake" },
  { photoID: "2", userID: "sSchmidt" },
  { photoID: "2", userID: "mHattrup" },
  { photoID: "2", userID: "gPlake" },
];

当我们有一张照片时,我们必须搜索我们的数据集以找到在照片中被标记的用户。当我们有一个用户时,我们需要找到该用户出现的照片列表。因为我们的数据当前存储在 JavaScript 数组中,所以我们将使用解析器中的数组方法来查找数据:

Photo: {
  ...
  taggedUsers: parent => tags
    // Returns an array of tags that only contain the current photo
    .filter(tag => tag.photoID === parent.id)
    // Converts the array of tags into an array of userIDs
    .map(tag => tag.userID)
    // Converts array of userIDs into an array of user objects
    .map(userID => users.find(u => u.githubLogin === userID))
},
User: {
  ...
  inPhotos: parent => tags
    // Returns an array of tags that only contain the current user
    .filter(tag => tag.userID === parent.id)
    // Converts the array of tags into an array of photoIDs
    .map(tag => tag.photoID)
    // Converts array of photoIDs into an array of photo objects
    .map(photoID => photos.find(p => p.id === photoID))
}

taggedUsers 字段解析器过滤掉所有不是当前照片的照片,并将过滤后的列表映射到实际用户对象的数组。inPhotos 字段解析器按用户过滤标签并将用户标签映射到实际照片对象数组。

我们现在可以通过发送 GraphQL 查询来查看每张照片中标记了哪些用户:

query listPhotos {
  allPhotos {
    url
    taggedUsers {
      name
    }
  }
}

您可能已经注意到我们有一个标签数组,但我们没有名为 Tag 的 GraphQL 类型。GraphQL 不要求我们的数据模型与我们的模式中的类型完全匹配。我们的客户可以通过查询用户类型或照片类型来找到每张照片中被标记的用户以及任何用户被标记的照片。他们不需要查询标签类型:那只会使事情复杂化。我们已经完成了在我们的解析器中查找标记用户或照片的繁重工作,专门让客户可以轻松查询此数据。

自定义标量

第 4 章所述,GraphQL 有一组默认标量类型,您可以将其用于任何字段。Int、Float、String、Boolean 和 ID 等标量适用于大多数情况,但在某些情况下您可能需要创建自定义标量类型以满足您的数据要求。

当我们实现自定义标量时,我们需要围绕如何序列化和验证类型创建规则。例如,如果我们创建一个 DateTime 类型,我们将需要定义什么应该被视为有效的 DateTime。

让我们将这个自定义 DateTime 标量添加到我们的 typeDef 中,并在创建的字段的照片类型中使用它。创建的字段用于存储发布特定照片的日期和时间:

const typeDefs = `
  scalar DateTime
  type Photo {
    ...
    created: DateTime!
  }
  ...
`

我们模式中的每个字段都需要映射到解析器。创建的字段需要映射到 DateTime 类型的解析器。我们为 DateTime 创建了一个自定义标量类型,因为我们想要解析和验证将此标量用作 JavaScript 日期类型的任何字段。考虑将日期和时间表示为字符串的各种方式。所有这些字符串都代表有效日期:

  • "4/18/2018"
  • "4/18/2018 1:30:00 PM"
  • "Sun Apr 15 2018 12:10:17 GMT-0700 (PDT)"
  • "2018-04-15T19:09:57.308Z"

我们可以使用这些字符串中的任何一个通过 JavaScript 创建日期时间对象:

var d = new Date("4/18/2018");
console.log(d.toISOString());
// "2018-04-18T07:00:00.000Z"

在这里,我们使用一种格式创建了一个新的日期对象,然后将该日期时间字符串转换为 ISO 格式的日期字符串。

JavaScript Date 对象不理解的任何内容都是无效的。您可以尝试解析以下数据:

var d = new Date("Tuesday March")
console.log( d.toString() )
// "Invalid Date"

当我们查询照片的 created 字段时,我们要确保此字段返回的值包含 ISO 日期时间格式的字符串。每当字段返回日期值时,我们将该值序列化为 ISO 格式的字符串:

const serialize = value => new Date(value).toISOString()

serialize 函数从我们的对象中获取字段值,只要该字段包含格式化为 JavaScript 对象的日期或任何有效的日期时间字符串,GraphQL 将始终以 ISO 日期时间格式返回它。

当您的模式实现自定义标量时,它可以用作查询中的参数。假设我们为 allPhotos 查询创建了一个过滤器。此查询将返回在特定日期之后拍摄的照片列表:

type Query {
  ...
  allPhotos(after: DateTime): [Photo!]!
}

如果我们有这个字段,客户可以向我们发送一个包含 DateTime 值的查询:

query recentPhotos(after:DateTime) {
  allPhotos(after: $after) {
    name
    url
  }
}

他们会使用查询变量发送 $after 参数:

{
  "after": "4/18/2018"
}

我们要确保在将 after 参数发送到解析器之前将其解析为 JavaScript Date 对象:

const parseValue = value => new Date(value)

我们可以使用 parseValue 函数来解析随查询一起发送的传入字符串的值。无论 parseValue 返回什么,都会传递给解析器参数:

const resolvers = {
  Query: {
    allPhotos: (parent, args) => {
      args.after // JavaScript Date Object
      ...
    }
  }
}

自定义标量需要能够序列化和解析日期值。我们还需要在一处处理日期字符串。这是客户端将日期字符串直接添加到查询本身的时候:

query {
  allPhotos(after: "4/18/2018") {
    name
    url
  }
}

after 参数未作为查询变量传递。相反,它已直接添加到查询文档中。在我们解析这个值之前,我们需要在它被解析成抽象语法树 (AST) 之后从查询中获取它。我们使用 parseLiteral 函数在解析之前从查询文档中获取这些值:

const parseLiteral = ast => ast.value

parseLiteral 函数用于获取直接添加到查询文档的日期值。在这种情况下,我们需要做的就是返回该值,但如果需要,我们可以在此函数内采取额外的解析步骤。

在创建自定义标量时,我们需要设计用于处理 DateTime 值的所有这三个函数。让我们将自定义 DateTime 标量的解析器添加到我们的代码中:

const { GraphQLScalarType } = require('graphql')
...
const resolvers = {
  Query: { ... },
  Mutation: { ... },
  Photo: { ... },
  User: { ... },
  DateTime: new GraphQLScalarType({
    name: 'DateTime',
    description: 'A valid date time value.',
    parseValue: value => new Date(value),
    serialize: value => new Date(value).toISOString(),
    parseLiteral: ast => ast.value
  })
}

我们使用 GraphQLScalarType 对象为自定义标量创建解析器。DateTime 解析器位于我们的解析器列表中。创建新的标量类型时,我们需要添加三个函数:serialize、parseValue 和 parseLiteral,它们将处理实现 DateType 标量的任何字段或参数。

样本日期
在数据中,我们还要确保为两张现有照片添加创建键和日期值。任何有效的日期字符串或日期对象都可以使用,因为创建的字段在返回之前将被序列化:

var photos = [
  {
    ...
    "created": "3-28-1977"
  },
  {
    ...
    "created": "1-2-1985"
  },
  {
    ...
    "created": "2018-04-15T19:09:57.308Z"
  }
]

现在,当我们将 DateTime 字段添加到我们的选择集中时,我们可以看到这些日期和类型被格式化为 ISO 日期字符串:

query listPhotos {
  allPhotos {
    name
    created
  }
}

唯一剩下要做的就是确保我们在发布每张照片时为每张照片添加时间戳。我们通过为每张照片添加一个 created 字段并使用 JavaScript Date 对象为其添加当前 DateTime 时间戳来实现这一点:

postPhoto(parent, args) {
  var newPhoto = {
    id: _id++,
    ...args.input,
    created: new Date()
  }
  photos.push(newPhoto)
  return newPhoto
}

现在,当发布新照片时,它们将带有创建日期和时间的时间戳。

👈 上一节 下一节 👉