GraphQL

GraphQL

一般资源:

GitLab 上的 GraphQL:

Libraries

当使用 GraphQL 进行前端开发时,我们使用Apollo (特别是 )和Vue Apollo .

如果在 Vue 应用程序中使用 GraphQL,则” Vue 中的部分可以帮助您学习如何集成 Vue Apollo.

对于其他用例,请查看Vue 外部的部分.

Tooling

Apollo GraphQL VS Code extension

如果使用 VS Code,则 Apollo GraphQL 扩展名支持.graphql文件中的自动完成. 若要设置 GraphQL 扩展,请按照下列步骤操作:

  1. apollo.config.js文件添加到gitlab本地目录的根目录中.
  2. 用以下内容填充文件:

  3. 重新启动 VS Code.

Exploring the GraphQL API

我们 GraphQL API 可以通过 GraphiQL 在您的实例的探索/-/graphql-explorer或 . 如有需要,请查阅《 GitLab GraphQL API 参考》文档 .

您可以在 GraphiQL 的文档浏览器的右侧检查所有现有的查询和变异. 也可以直接在左选项卡上编写查询和变异,然后单击左上角的执行查询按钮来检查其执行情况:

Apollo Client

为了保存在不同的应用程序中创建的重复客户端,我们使用 . 这将使用正确的 URL 设置 Apollo 客户端,并设置 CSRF 标头.

默认客户端接受两个参数: resolversconfig .

  • 创建resolvers参数以接受用于本地状态管理查询和突变的 resolvers 对象
  • config参数采用配置设置的对象:
    • cacheConfig字段接受设置的可选对象以
    • baseUrl允许我们传递与主端点不同的 GraphQL 端点的 URL(即${gon.relative_url_root}/api/graphql
    • assumeImmutableResults (默认设置为false )-此设置为true ,将假定更新 Apollo Cache 时的每个操作都是不可变的. 它还将freezeResults设置为true ,因此任何尝试freezeResults Apollo Cache 的尝试都会在开发环境中引发控制台警告. 在将此选项设置为true之前,请确保在缓存更新操作中遵循不变性模式.

为了在运行时保存查询编译,webpack 可以直接导入.graphql文件. 这使 webpack 可以在编译时对查询进行预处理,而不是由客户端进行查询的编译.

为了将查询与突变和片段区分开来,建议使用以下命名约定:

  • all_users.query.graphql用于查询;
  • add_user.mutation.graphql进行突变;
  • 片段的basic_user.fragment.graphql .

片段是使复杂的 GraphQL 查询更具可读性和可重用性的一种方式. 这是 GraphQL 片段的示例:

  1. fragment DesignListItem on Design {
  2. id
  3. image
  4. event
  5. filename
  6. notesCount
  7. }

片段可以存储在单独的文件中,可以导入并用于查询,突变或其他片段.

  1. #import "./design_list.fragment.graphql"
  2. #import "./diff_refs.fragment.graphql"
  3. fragment DesignItem on Design {
  4. ...DesignListItem
  5. fullPath
  6. diffRefs {
  7. ...DesignDiffRefs
  8. }
  9. }

有关片段的更多信息:

Usage in Vue

要使用 Vue Apollo,请导入插件以及默认客户端. 这应该在安装 Vue 应用程序的同一时间创建.

  1. import Vue from 'vue';
  2. import VueApollo from 'vue-apollo';
  3. import createDefaultClient from '~/lib/graphql';
  4. Vue.use(VueApollo);
  5. const apolloProvider = new VueApollo({
  6. defaultClient: createDefaultClient(),
  7. });
  8. new Vue({
  9. ...,
  10. apolloProvider,
  11. ...
  12. });

Vue Apollo 文档中阅读有关更多信息.

Local state with Apollo

  1. import Vue from 'vue';
  2. import VueApollo from 'vue-apollo';
  3. import createDefaultClient from '~/lib/graphql';
  4. Vue.use(VueApollo);
  5. const defaultClient = createDefaultClient({
  6. resolvers: {}
  7. });
  8. defaultClient.cache.writeData({
  9. data: {
  10. user: {
  11. name: 'John',
  12. surname: 'Doe',
  13. age: 30
  14. },
  15. },
  16. });
  17. const apolloProvider = new VueApollo({
  18. defaultClient,
  19. });

我们可以使用@client Apollo 指令查询本地数据:

  1. // user.query.graphql
  2. query User {
  3. user @client {
  4. name
  5. surname
  6. age
  7. }
  8. }

除了创建本地数据,我们还可以使用@client字段扩展现有的 GraphQL 类型. 当我们需要为尚未添加到 GraphQL API 中的字段模拟 API 响应时,这非常有用.

Mocking API response with local Apollo cache

当我们需要在本地模拟某些 GraphQL API 响应,查询或变异时(例如,当它们仍未添加到我们的实际 API 中时),使用本地 Apollo 缓存非常方便.

例如,我们在查询中使用了有关DesignVersion片段

  1. fragment VersionListItem on DesignVersion {
  2. id
  3. sha
  4. }

我们还需要获取版本作者和’created at’属性,以在版本下拉列表中显示它们,但这些更改仍未在我们的 API 中实现. 我们可以更改现有片段,以针对这些新字段获得模拟的响应:

  1. fragment VersionListItem on DesignVersion {
  2. id
  3. sha
  4. author @client {
  5. avatarUrl
  6. name
  7. }
  8. createdAt @client
  9. }

现在,Apollo 将尝试为每个标有@client指令的字段查找解析器 . 让我们为DesignVersion类型创建一个解析器(为什么要使用DesignVersion ?,因为我们的片段是在这种类型上创建的).

  1. // resolvers.js
  2. const resolvers = {
  3. DesignVersion: {
  4. author: () => ({
  5. avatarUrl:
  6. 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
  7. name: 'Administrator',
  8. __typename: 'User',
  9. createdAt: () => '2019-11-13T16:08:11Z',
  10. },
  11. };
  12. export default resolvers;

我们需要将解析器对象传递给我们现有的 Apollo Client:

现在,每次尝试获取版本时,我们的客户端都会从远程 API 端点获取idsha ,并将我们的硬编码值分配给authorcreatedAt版本属性. 有了这些数据,前端开发人员就可以在 UI 部件上工作,而不会被后端阻塞. 将实际响应添加到 API 后,可以快速删除自定义本地解析器,并且对查询/片段的唯一更改是@client指令删除.

在阅读有关使用 Apollo 进行本地状态管理的更多信息.

Using with Vuex

When Apollo Client is used within Vuex and fetched data is stored in the Vuex store, there is no need in keeping Apollo Client cache enabled. Otherwise we would have data from the API stored in two places - Vuex store and Apollo Client cache. More to say, with Apollo’s default settings, a subsequent fetch from the GraphQL API could result in fetching data from Apollo cache (in the case where we have the same query and variables). To prevent this behavior, we need to disable Apollo Client cache passing a valid fetchPolicy option to its constructor:

  1. import fetchPolicies from '~/graphql_shared/fetch_policy_constants';
  2. export const gqClient = createGqClient(
  3. {},
  4. {
  5. fetchPolicy: fetchPolicies.NO_CACHE,
  6. },
  7. );

Feature flags in queries

有时在 GraphQL 查询中的功能标志后面放置一个实体可能会很有用. 例如,当处理后端已经合并但前端没有合并的功能时,您可能希望将 GraphQL 实体放在功能标记后面,以允许创建和合并较小的合并请求.

为此, if语句通过,我们可以使用@include指令排除实体.

  1. query getAuthorData($authorNameEnabled: Boolean = false) { username name @include(if: $authorNameEnabled) }

然后,在对查询的 Vue(或 JavaScript)调用中,我们可以传递功能标记. 此功能标志将需要已经正确设置. 有关正确方法,请参阅功能部件标志文档 .

  1. export default {
  2. apollo: {
  3. user: {
  4. query: QUERY_IMPORT,
  5. variables() {
  6. return {
  7. authorNameEnabled: gon?.features?.authorNameEnabled,
  8. };
  9. },
  10. }
  11. },
  12. };

创建组件时,将自动对组件的apollo属性进行查询. 某些组件反而希望按需发出网络请求,例如,带有延迟加载项的下拉列表.

有两种方法可以做到这一点:

  1. Use the skip property
  1. export default {
  2. apollo: {
  3. user: {
  4. query: QUERY_IMPORT,
  5. skip() {
  6. // only make the query when dropdown is open
  7. return !this.isOpen;
  8. },
  9. }
  10. },
  11. };
  1. Using addSmartQuery

您可以在您的方法中手动创建智能查询.

  1. handleClick() {
  2. this.$apollo.addSmartQuery('user', {
  3. // this takes the same values as you'd have in the `apollo` section
  4. query: QUERY_IMPORT,
  5. }),
  6. };

Working with pagination

GitLab 的 GraphQL API 对连接类型使用中继样式的游标分页 . 这意味着使用”游标”来跟踪应从中提取下一项的数据集中的位置. 是对连接的良好概述和介绍.

每个连接类型(例如DesignConnectionDiscussionConnection )都有一个字段pageInfo ,其中包含分页所需的信息:

  1. pageInfo {
  2. endCursor
  3. hasNextPage
  4. hasPreviousPage
  5. startCursor
  6. }

Here:

  • startCursorendCursor显示第一项和最后一项的光标.
  • hasPreviousPagehasNextPage允许我们检查当前页面之前或之后是否还有更多页面可用.

当我们以连接类型获取数据时,我们可以before参数的afterbefore传递游标,以指示分页的起点或终点. 应该分别在它们的后跟firstlast参数,以指示我们要在给定端点之后或之前获取多少个项目.

  1. query {
  2. project(fullPath: "root/my-project") {
  3. id
  4. issue(iid: "42") {
  5. designCollection {
  6. designs(atVersion: null, after: "Ihwffmde0i", first: 10) {
  7. edges {
  8. node {
  9. id
  10. }
  11. }
  12. }
  13. }
  14. }
  15. }
  16. }

Using fetchMore method in components

进行初始抓取时,我们通常希望从头开始进行分页. 在这种情况下,我们可以:

  • 跳过传递光标.
  • null明确传递给after .

提取数据后,我们应该保存一个pageInfo对象. 假设我们将其存储到 Vue 组件data

  1. data() {
  2. return {
  3. }
  4. },
  5. apollo: {
  6. designs: {
  7. query: projectQuery,
  8. variables() {
  9. return {
  10. // rest of design variables
  11. ...
  12. first: 10,
  13. };
  14. },
  15. result(res) {
  16. this.pageInfo = res.data?.project?.issue?.designCollection?.designs?.pageInfo;
  17. },
  18. },
  19. },

当我们想移至下一页时,我们使用 Apollo 方法,在该方法中传递一个新的游标(以及可选的新变量). 在updateQuery挂钩中,我们必须在获取下一页之后返回要在 Apollo 缓存中看到的结果.

请注意,我们不必再保存pageInfo了; fetchMore触发查询result挂钩.

Testing

Mocking response as component data

使用 ,可以轻松快速地测试获取 GraphQL 查询的组件. 最简单的方法是使用shallowMount ,然后在组件上设置数据

  1. it('tests apollo component', () => {
  2. const vm = shallowMount(App);
  3. vm.setData({
  4. ...mock data
  5. });
  6. });

Testing loading state

如果需要测试当 GraphQL API 的结果仍在加载时组件的呈现方式,我们可以将加载状态模拟到相应的 Apollo 查询/突变中:

  1. function createComponent({
  2. loading = false,
  3. } = {}) {
  4. const $apollo = {
  5. queries: {
  6. designs: {
  7. loading,
  8. },
  9. };
  10. wrapper = shallowMount(Index, {
  11. sync: false,
  12. mocks: { $apollo }
  13. });
  14. }
  15. it('renders loading icon', () => {
  16. createComponent({ loading: true });
  17. expect(wrapper.element).toMatchSnapshot();
  18. })

Testing Apollo components

如果我们在组件中使用ApolloQueryApolloMutation ,为了测试其功能,我们需要先添加一个存根:

  1. import { ApolloMutation } from 'vue-apollo';
  2. function createComponent(props = {}) {
  3. wrapper = shallowMount(MyComponent, {
  4. sync: false,
  5. propsData: {
  6. ...props,
  7. },
  8. stubs: {
  9. ApolloMutation,
  10. },
  11. });
  12. }

ApolloMutation组件通过作用域插槽公开了mutate方法. 如果要测试此方法,则需要将其添加到模拟中:

  1. const mutate = jest.fn().mockResolvedValue();
  2. const $apollo = {
  3. mutate,
  4. };
  5. function createComponent(props = {}) {
  6. wrapper = shallowMount(MyComponent, {
  7. sync: false,
  8. propsData: {
  9. ...props,
  10. },
  11. stubs: {
  12. ApolloMutation,
  13. },
  14. mocks: {
  15. $apollo:
  16. }
  17. });
  18. }

然后我们可以检查是否使用正确的变量调用了mutate

  1. const mutationVariables = {
  2. mutation: createNoteMutation,
  3. update: expect.anything(),
  4. variables: {
  5. input: {
  6. noteableId: 'noteable-id',
  7. body: 'test',
  8. discussionId: '0',
  9. },
  10. },
  11. };
  12. it('calls mutation on submitting form ', () => {
  13. createComponent()
  14. findReplyForm().vm.$emit('submitForm');
  15. expect(mutate).toHaveBeenCalledWith(mutationVariables);
  16. });

目前,GitLab 的 GraphQL 突变具有两种不同的错误模式: 顶级和 .

利用 GraphQL 突变时,我们必须考虑处理这两种错误模式,以确保用户在发生错误时能够收到适当的反馈.

Top-level errors

这些错误位于 GraphQL 响应的”顶级”. 这些是不可恢复的错误,包括参数错误和语法错误,因此不应直接呈现给用户.

Handling top-level errors

Apollo 意识到顶级错误,因此我们能够利用 Apollo 的各种错误处理机制来处理这些错误(例如,在调用mutate方法之后处理 Promise 拒绝,或处理从组件发出的error事件).

由于这些错误不是针对用户的,因此应在客户端定义顶级错误的错误消息.

这些错误嵌套在 GraphQL 响应的data对象中. 这些是可恢复的错误,理想情况下,可以直接向用户显示.

Handling errors-as-data

首先,我们必须向我们的变异对象添加errors

  1. mutation createNoteMutation($input: String!) {
  2. createNoteMutation(input: $input) {
  3. note {
  4. id
  5. + errors
  6. }
  7. }

现在,当我们提交此突变并发生错误时,响应中将包含errors供我们处理:

  1. {
  2. data: {
  3. mutationName: {
  4. errors: ["Sorry, we were not able to update the note."]
  5. }
  6. }
  7. }

处理数据错误时,请根据您的最佳判断来确定是将错误消息显示在响应中,还是将另一条客户端定义的消息显示给用户.

Usage outside of Vue

通过直接导入默认客户端并将其与查询一起使用,还可以在 Vue 之外使用 GraphQL.

  1. import createDefaultClient from '~/lib/graphql';
  2. import query from './query.graphql';
  3. const defaultClient = createDefaultClient();
  4. .then(result => console.log(result));

使用 Vuex 时 ,在以下情况下禁用缓存:

  • 数据正在其他地方缓存
  • 如果数据正在其他地方缓存,或者对于给定的用例完全不需要,则用例不需要缓存.