我对GraphQL的片段协作和变量感到疑惑

最近,我利用一些空闲时间小心翼翼地摸索着Apollo,但有一些事情始终让我感到困惑,所以我决定写下备忘录。

我事先声明,这并没有特定的结论。

GraphQL的Fragment colocation是什么意思

让我们整理一下关于客户端GraphQL中碎片集合的相关内容,以便描述我所面临的问题感受。

“colocate”这个词本身在平常不太常听到(我也只在GraphQL的语境中使用过它)。它的意思好像是“一起放置”。

在中国,我们要将什么与什么放在一起呢?那就是GraphQL查询和组件(这里的组件指的是React或Angular中的视图部件单位)。

GraphQL有一个重要特点是“客户端决定包含在查询响应中的数据”。考虑到REST API中,URL的确定意味着响应格式是唯一的,这一点对于客户端来说是一个重大的区别。

客户端本质上是指屏幕。因此,前面提到的特点是指“查询的响应由渲染屏幕的组件决定”。

前面提到的”将查询和组件放在一起”的colocation概念与GraphQL的理念非常契合。

从这里开始,我们将以GitHub API的GraphQL查询作为主题,思考有关Fragment的问题。

query AppQuery {
  viewer {
    repositories(first: 10) {
      nodes {
        name,
        url,
        description,
        languages(first: 10, orderBy: { field: SIZE, direction: DESC }) {
          nodes {
            id,
            name,
          },
        },
      },
      pageInfo {
        hasPreviousPage,
        hasNextPage,
        endCursor,
        startCursor,
      },
      totalCount,
    },
  },
}

这是一个查询自己GitHub仓库前10个的查询。

嗯,考虑将其放置于屏幕组件中,仍然存在一些问题。

    • レポジトリリストやレポジトリの詳細部分の再利用性が低い

 

    そもそも長い

因此,我们将查询分割如下。

fragment RepoItem on Repository {
  name,
  url,
  description,
  languages(first: 10, orderBy: { field: SIZE, direction: DESC }) {
    nodes {
      id,
      name,
    },
  },
}

fragment RepoList on User {
  repositories(first: 10) {
    nodes {
      ...RepoItem,
    },
    pageInfo {
      hasPreviousPage,
      hasNextPage,
      endCursor,
      startCursor,
    },
    totalCount,
  },
}

query AppQuery {
  viewer {
    ...RepoList,
  },
}

通过将GraphQL的数据层次结构拆分为部分片段,例如对于查询”我收藏的仓库列表”,可以重复使用RepoList。

回想起「查询的响应确定了要绘制的屏幕组件」的特点时,我们可以得出结论,提取的片段应该由相应的组件决定所需的字段。

换句话说,组件层次结构与查询片段层次结构相匹配似乎是一个好主意。

composition.png

图中虚线表示的是词语组合。

在Facebook Relay和Apollo Client等主要的GraphQL客户端库中,也提到了组件和片段的相互配合34。

组件实现示例

实际上,关于之前的查询,我会在这篇帖子的末尾附上一个使用Angular + Apollo实现的例子。

为了更容易传达colocation的状态,我们将HTML模板和GraphQL编写在同一个.ts文件中。但是,colocation的本质是“一起放置”,所以可以准备一个与组件对应的目录,并将.html和.graphql文件分开放置。

代码片段和变量

到目前为止,没有什么特别的问题,但是从我想在片段中使用变量的时候开始,情况变得不太妙了。

我們嘗試將分頁參數作為變數附加到先前的查詢中。

fragment RepoItem on Repository {
  # 略
}

fragment RepoList on User {
  repositories(first: $first, after: $after) {
    nodes {
      ...RepoItem,
    },
    pageInfo {
      hasPreviousPage,
      hasNextPage,
      endCursor,
      startCursor,
    },
    totalCount,
  },
}

query AppQuery($first: Int!, $after: String) {
  viewer {
    ...RepoList,
  },
}

在GraphQL的语法中,只有Query(或Mutation)才能定义变量。
不存在一种语法,可以在fragment中定义封闭的变量。

因此,上述的 repositories(first: $first, after: $after)部分会变成状态,即”知道了RepoList片段在父查询中定义了哪些变量”。

孩子必须意识到自己需要了解亲层知识,这实在让人不舒服。

各个客户端库的现状

这个问题实际上是关于 GraphQL 的语法,如果在 fragment 的范围内能够定义变量,似乎就能解决。事实上,根据实际调查,有一个名为 https://github.com/facebook/graphql/issues/204 的问题讨论了 fragment scoped variable。

这个问题在讨论Relay Modern时提出来了,但最终只是将experimentalFragmentVariables选项合并到graphql-js中而已,目前的状况。

这个选项实际上是一个相当微妙的东西,原本是在Facebook内部使用的GraphQL实现中存在的一个功能。这个功能似乎是“片段能够定义全局变量”的一个谁需要的特性。
以前面给出的例子为例,即使构建了如下查询,解析器也不会报错。

fragment RepoList on User {
  # クエリに変数定義が無くてもエラーにはならないが、 $firstはglobalなscope
  repositories(first: $first, after: $after) {
    # 略
  },
}

query AppQuery {
  viewer {
    ...RepoList,
  },
}

最后,Relay Modern使用了独特的指令@arguments / @argumentsDefinitions,使得可以在片段范围内定义变量。

fragment RepoList on User @argumentsDefinitions(
  first: { type: "Int" },
  after: { type: "String" },
) {
  repositories(first: $first, after: $after) {
    # 略
  },
}

query AppQuery($first: Int!, $after: String) {
  viewer {
    ...RepoList @arguments(first: $first, after: $after),
  },
}

受到Relay Modern的影响,Apollo也提出了类似功能的需求,但截至2019年1月目前似乎没有特别的进展。

    • https://github.com/apollographql/apollo-client/issues/2723

 

    https://github.com/apollographql/apollo-feature-requests/issues/18

最后

关于这次写的内容,我并不是说无论如何都无法解决,就不能创建应用程序之类的话。说实话,即使变量是在查询全局的情况下,只要适当地引入命名规范等,我认为在fragment之间变量冲突几乎不会发生。

只是觉得无法单单靠Apollo解决而感到不舒服而已。如果有人对这个问题有任何解决方法,可以在评论中告诉我。

附录:使用Angular + Apollo在片段可置放地点上。

完整版请参见 https://github.com/Quramy/apollo-angular-example。

import gql from 'graphql-tag';
import { Component, Input } from '@angular/core';
import { RepoItem } from './__generated__/RepoItem'

const fragment = gql`
  fragment RepoItem on Repository {
    id,
    name,
    url,
    description,
    languages(first: 10, orderBy: { field: SIZE, direction: DESC }) {
      nodes { id, name },
    },
  }
`;

@Component({
  selector: 'app-repo-item',
  styleUrls: ['./repo-item.component.css'],
  template: `
    <section>
      <header class="title">
        <a [href]="repoItem.url" targe="_blank">
          {{repoItem.name}}
        </a>
      </header>
      <p class="desc" *ngIf="repoItem.description">{{repoItem.description}}</p>
      <p class="desc" *ngIf="!repoItem.description">(no description)</p>
      <ul class="langs" *ngIf="repoItem.languages.nodes">
        <li class="lang-label" *ngFor="let lang of repoItem.languages.nodes">
          {{lang.name}}
        </li>
      </ul>
    </section>
  `,
})
export class RepoItemComponent {
  static fragment = fragment;
  @Input() repoItem: RepoItem;
}
import gql from 'graphql-tag';
import { Component, Input } from '@angular/core';
import { RepoItemComponent } from '../repo-item/repo-item.component';
import { RepoList } from './__generated__/RepoList';
import { Apollo } from 'apollo-angular';

const fragment = gql`
  ${RepoItemComponent.fragment}
  fragment RepoList on User {
    repositories(first: 10) {
      nodes {
        ...RepoItem,
      },
      pageInfo {
        hasPreviousPage,
        hasNextPage,
        endCursor,
        startCursor,
      },
      totalCount,
    }
  }
`;

@Component({
  selector: 'app-repo-list',
  styleUrls: ['./repo-list.component.css'],
  template: `
    <div *ngIf="repoList.repositories && repoList.repositories.nodes as repositories">
      <app-repo-item
        class="item"
        *ngFor="let node of repositories"
        [repoItem]="node"
      ></app-repo-item>

      <app-simple-pager
        [hasPrev]="repoList.repositories.pageInfo.hasPreviousPage"
        [hasNext]="repoList.repositories.pageInfo.hasNextPage"
        (prev)="prev()"
        (next)="next()"
      ></app-simple-pager>
    </div>
  `,
})
export class RepoListComponent {
  static fragment = fragment;
  @Input() repoList: RepoList;

  constructor(private apollo: Apollo) { }

  prev() { /* 略 */ }

  next() { /* 略 */ }
}
import gql from 'graphql-tag';
import { Component, OnInit } from '@angular/core';
import { Apollo } from 'apollo-angular';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { AppQuery } from './__generated__/AppQuery';
import { RepoListComponent } from './repo-list/repo-list.component';

const appQuery = gql`
  ${RepoListComponent.fragment}
  query AppQuery {
    viewer {
      ...RepoList,
    },
  }
`;

@Component({
  selector: 'app-root',
  styleUrls: ['./app.component.css'],
  template: `
  <div class="container">
    <h1>
      Your GitHub repositories
    </h1>
    <div *ngIf="data$ | async as data">
      <app-repo-list [repoList]="data.viewer"></app-repo-list>
    </div>
  </div>
  `,
})
export class AppComponent implements OnInit {
  data$: Observable<AppQuery>;

  constructor(private apollo: Apollo) { }

  ngOnInit() {
    this.data$ = this.apollo.watchQuery<AppQuery>({
      query: cursorsQuery,
    }).valueChanges.pipe(map(({ data }) => data));
  }
}
需求驱动架构(Demand Driven Architecture)被称为是一种将组件进行重构,并根据层级进行切割fragment的方法,这样的说法可能更为准确。
广告
将在 10 秒后关闭
bannerAds