我自己编写了一个graphql-codegen插件,副标题为:“我想自动生成一个能够将GraphQL查询与ApolloClient结合并自动生成Typescript代码的插件”

总结

简介

概述

概括

当我写了GraphQL查询时,graphql-codegen会生成类型,但是我希望能减少手动编写类型的工作量,所以我自己编写了graphql-codegen的插件。

做得到的人

1. 概念图的例子

如果有这样的模式存在,

type Tweet {
  id: ID!
  body: String
  date: String
  Author: User
  Stats: Stat
}

type User {
  id: ID!
  username: String
  first_name: String
  last_name: String
  full_name: String
  name: String @deprecated
  avatar_url: String
}

type Stat {
  views: Int
  likes: Int
  retweets: Int
  responses: Int
}

type Notification {
  id: ID
  date: String
  type: String
}

type Meta {
  count: Int
}

type Comment {
  id: String
  content: String
}

type Query {
  Tweet(id: ID!): Tweet
  Tweets(limit: Int, skip: Int, sort_field: String, sort_order: String): [Tweet]
  TweetsMeta: Meta
  User(id: ID!): User
  Notifications(limit: Int): [Notification]
  NotificationsMeta: Meta
}

type Mutation {
  createTweet(body: String): Tweet
  deleteTweet(id: ID!): Tweet
  markTweetRead(id: ID!): Boolean
}

type Subscription {
  commentAdded(repoFullName: String!): Comment
}

请参考此链接:https://github.com/marmelab/GraphQL-example/blob/master/schema.graphql

2. 查询示例

假设我们要创建一个编写了这样一个查询的GraphQL文件。

query TweetMeta {
  TweetsMeta {
    count
  }
}

query Tweet($id: ID!) {
  Tweet(id: $id) {
    body
    date
    Author {
      full_name
    }
  }
}

mutation CreateTweet($body: String) {
  createTweet(body: $body) {
    id
  }
}

subscription SubscComment($repoFullName: String!) {
  commentAdded(repoFullName: $repoFullName) {
    id
    content
  }
}

3. 创建示例 lì)

然后它会以这样的方式生成TS代码(为每个被监视的.graphql文件生成一个类)。

import * as Type from "./types";
import * as Node from "./nodes";
import * as ApolloType from "apollo-client";
import ApolloClient from "apollo-client";
export interface ClientClass {
  readonly client: ApolloClient<any>;
}

export class TweetClient implements ClientClass {
  constructor(readonly client: ApolloClient<any>) {}

  tweetMeta = (
    options?: Omit<
      ApolloType.QueryOptions<Type.TweetMetaQueryVariables>,
      "query"
    >
  ) =>
    this.client.query<Type.TweetMetaQuery, Type.TweetMetaQueryVariables>({
      ...options,
      ...{ query: Node.TweetMeta }
    });

  tweet = (
    options?: Omit<ApolloType.QueryOptions<Type.TweetQueryVariables>, "query">
  ) =>
    this.client.query<Type.TweetQuery, Type.TweetQueryVariables>({
      ...options,
      ...{ query: Node.Tweet }
    });

  createTweet = (
    options?: Omit<
      ApolloType.MutationOptions<
        Type.CreateTweetMutation,
        Type.CreateTweetMutationVariables
      >,
      "mutation"
    >
  ) =>
    this.client.mutate<
      Type.CreateTweetMutation,
      Type.CreateTweetMutationVariables
    >({ ...options, ...{ mutation: Node.CreateTweet } });

  subscComment = (
    options?: Omit<
      ApolloType.SubscriptionOptions<Type.SubscCommentSubscriptionVariables>,
      "query"
    >
  ) =>
    this.client.subscribe<
      Type.SubscCommentSubscription,
      Type.SubscCommentSubscriptionVariables
    >({ ...options, ...{ query: Node.SubscComment } });
}

4. 用途

不需要写gql标签,只需使用自动补全功能,舒适地编写。真是太开心了。

import { TweetClient } from "./generated/class";
import ApolloClient from "apollo-boost";
import "isomorphic-fetch";

const client = new TweetClient(
  new ApolloClient({ uri: "http://localhost:4000/" })
);

async function main() {
  const hoge = await client.tweetMeta();
  console.log(JSON.stringify(hoge.data.TweetsMeta));

  const huga = await client.createTweet({
    variables: {
      body: "aaa"
    }
  });
  //dataはnullチェックしないと怒られる
  console.log(JSON.stringify(huga.data && huga.data.createTweet));

  const piyo = await client.tweet({ variables: { id: "hoga" } });
  console.log(JSON.stringify(piyo.data));
}

main();

做法

环境

可以使用 yarn 或 npm 进行安装。

{
  "scripts": {
    "codegen": "graphql-codegen --config codegen.yml --watch",
    "server": "node server.js"
  },
  "devDependencies": {
    "@graphql-codegen/cli": "^1.3.1",
    "@graphql-codegen/typescript": "^1.3.1",
    "@graphql-codegen/typescript-document-nodes": "^1.3.1-alpha-21fe4751.62",
    "@graphql-codegen/typescript-operations": "1.3.1",
    "@types/graphql": "^14.2.2",
    "apollo-client": "^2.6.3",
    "change-case": "^3.1.0",
    "graphql": "^14.4.2",
    "prettier": "^1.18.2",
    "typescript": "^3.5.2"
  },
  "dependencies": {
    "apollo-boost": "^0.4.3",
    "apollo-server": "^2.6.7",
    "isomorphic-fetch": "^2.2.1"
  }
}

随便搭建一个模拟服务器。

如果将ApolloServer的mock设置为true,那么服务器将随机建立起来。

server.js
const { ApolloServer, gql } = require(“apollo-server”);const typeDefs = `
type Tweet {
id: ID!
body: String
date: String
Author: User
Stats: Stat
}

type User {
id: ID!
username: String
first_name: String
last_name: String
full_name: String
name: String @deprecated
avatar_url: String
}

type Stat {
views: Int
likes: Int
retweets: Int
responses: Int
}

type Notification {
id: ID
date: String
type: String
}

type Meta {
count: Int
}

type Comment {
id: String
content: String
}

type Query {
Tweet(id: ID!): Tweet
Tweets(limit: Int, skip: Int, sort_field: String, sort_order: String): [Tweet]
TweetsMeta: Meta
User(id: ID!): User
Notifications(limit: Int): [Notification]
NotificationsMeta: Meta
}

type Mutation {
createTweet(body: String): Tweet
deleteTweet(id: ID!): Tweet
markTweetRead(id: ID!): Boolean
}

type Subscription {
commentAdded(repoFullName: String!): Comment
}

`;

const server = new ApolloServer({
typeDefs,
mocks: true
});

server.listen().then(({ url }) => {
console.log(`服务器已在 ${url} 上准备就绪`);
});

启动

终端
yarn server

运行Graphql-codegen

编写graphql-codegen的配置文件。

overwrite: true
schema: "http://localhost:4000" # サーバのアドレス。勝手にスキーマを呼んでくれる
documents: "queries/*.graphql" # 監視対象 ここのファイルに更新があれば自動的に型を作成
generates:
  ./generated/types.ts:
    plugins:
      - typescript
      - typescript-operations
  ./generated/nodes.ts:
    plugins:
      - typescript-document-nodes

请在目录queries/中以前面提到的例子tweet.graphql作为监视对象保存。

然后运行 graphql-codegen。

yarn codegen

然后,它会在./generated/types.ts中生成类型,并在./generated/nodes.ts中生成带有gql标签的查询常量。此外,这些常量将根据queries文件的变化进行逐步更新。

如果将”codegen”配置中的watch选项移除,自动更新功能将被禁用。

生成的节点.ts(自動生成)
从”graphql”导入{ DocumentNode }
从”graphql-tag”导入gql导出常量TweetMeta: DocumentNode = gql`
query TweetMeta {
TweetsMeta {
count
}
}
`;

导出常量Tweet: DocumentNode = gql`
query Tweet($id: ID!) {
Tweet(id: $id) {
body
date
Author {
full_name
}
}
}
`;

导出常量CreateTweet: DocumentNode = gql`
mutation CreateTweet($body: String) {
createTweet(body: $body) {
id
}
}
`;

导出常量SubscComment: DocumentNode = gql`
subscription SubscComment($repoFullName: String!) {
commentAdded(repoFullName: $repoFullName) {
id
content
}
}
`;

生成的类型.ts(自动生成)
导出类型也许 = T | null;
/** 所有内置和自定义的标量,映射到它们的实际值 */
导出类型标量 = {
ID: 字符串;
String: 字符串;
Boolean: 布尔值;
Int: 数字;
Float: 数字;
Upload: 任何;
};导出枚举CacheControlScope {
公共 = “PUBLIC”,
私人 = “PRIVATE”
}

导出类型评论 = {
__typename?: “评论”;
id?:也许<标量[“字符串”]>;
内容?:也许<标量[“字符串”]>;
};

导出类型元数据 = {
__typename?: “元数据”;
计数?:也许<标量[“数字”]>;
};

导出类型变异 = {
__typename?: “变异”;
createTweet?:也许<推特>;
deleteTweet?:也许<推特>;
markTweetRead?:也许<标量[“布尔”]>;
};

导出类型变异CreateTweetArgs = {
body?:也许<标量[“字符串”]>;
};

导出类型变异DeleteTweetArgs = {
id:标量[“ID”];
};

导出类型变异MarkTweetReadArgs = {
id:标量[“ID”];
};

导出类型通知 = {
__typename?: “通知”;
id?:也许<标量[“ID”]>;
日期?:也许<标量[“字符串”]>;
类型?:也许<标量[“字符串”]>;
};

导出类型查询 = {
__typename?: “查询”;
推特?:也许<推特>;
推特?:也许<Array<也许<推特>>>>;
推特们?:也许<元数据>;
用户?:也许<用户>;
通知?:也许<Array<也许<通知>>>>;
通知们?:也许<元数据>;
};

导出类型查询推特Args = {
id:标量[“ID”];
};

导出类型查询推特们Args = {
limit?:也许<标量[“数字”]>;
跳过吗?:也许<标量[“数字”]>;
sort_field?:也许<标量[“字符串”]>;
sort_order?:也许<标量[“字符串”]>;
};

导出类型查询用户Args = {
id:标量[“ID”];
};

导出类型查询通知Args = {
limit?:也许<标量[“数字”]>;
};

导出类型(stat) = {
__typename?: “统计”;
观点?:也许<标量[“数字”]>;
喜欢吗?:也许<标量[“数字”]>;
转推吗?:也许<标量[“数字”]>;
响应?:也许<标量[“数字”]>;
};

导出类型订阅 = {
__typename?: “订阅”;
评论添加了吗?:也许<评论>;
};

导出类型订阅评论添加Args = {
repoFullName:标量[“字符串”];
};

导出类型推特 = {
__typename?: “推特”;
id:标量[“ID”];
身体?:也许<标量[“字符串”]>;
日期?:也许<标量[“字符串”]>;
作者?:也许<用户>;
统计?:也许<统计>;
};

导出类型用户 = {
__typename?: “用户”;
id:标量[“ID”];
用户名?:也许<标量[“字符串”]>;
姓?:也许<标量[“字符串”]>;
名?:也许<标量[“字符串”]>;
全名?:也许<标量[“字符串”]>;
名字?:也许<标量[“字符串”]>;
头像_Url?:也许<标量[“字符串”]>;
};
export类型推特元查询变量 = {};

export类型推特元查询 = { __typename?: “查询” } & {
推特元吗?:也许<{ __typename?: “元数据” } & Pick<元数据, “计数”>>;
};

export类型推特查询变量 = {
id:标量[“ID”];
};

export类型推特查询 = { __typename?: “查询” } & {
推特:也许<
{ __typename?: “推特” } & Pick<推特, “身体” | “日期”> & {
作者:也许<{ __typename?: “用户” } & Pick<用户, “全名”>>;
}
>;
};

export类型CreateTweetMutation变量 = {
body?:也许<标量[“字符串”]>;
};

export类型CreateTweetMutation = { __typename?: “变异” } & {
createTweet:也许<{ __typename?: “推特” } & Pick<推特, “id”>>;
};

export类型SubscCommentSubscription变量 = {
repoFullName:标量[“字符串”];
};

export类型SubscCommentSubscription = { __typename?: “订阅” } & {
commentAdded:也许<
{ __typename?: “评论” } & Pick<评论, “id” | “内容”>
>;
};

写一个我自己的插件。

让我们自己编写graphql_codegen插件吧。只要能输出ts字符串作为返回值就好。

参考:编写你的第一个插件·GraphQL代码生成器

module.exports = {
  plugin: (schema, documents, config) => {
    //graphql_codegenがかき集めた、おおよそ人が読むようにできていない、documentsを読み込みながら、jsでts(文字列)を書く。
    //ここでtsをreturnする。
    //出力は勝手にgraphql_codegenがprettierが走らせて整形してくれるので、改行とかタブとか気にせず書く。
  }
};

加载自己编写的插件

overwrite: true
schema: "schema.graphql" # サーバのアドレス。勝手にスキーマを呼んでくれる
documents: "queries/*.graphql" # 監視対象 ここのファイルに更新があれば自動的に型を作成
generates: # 生成先
  ./generated/types.ts:
    plugins:
      - typescript
      - typescript-operations
  ./generated/nodes.ts: # 生成先
    plugins:
      - typescript-document-nodes
  ./generated/class.ts: # 生成先
      - プラグイン名.js # プラグイン読み込み

写插件时,非常感激change-case包能够帮助将驼峰命名转换为其他形式。

最後寫了什麼?

最后,我写了这样的代码。

var path = require("path");
var changeCase = require("change-case");

const imp = `
    import * as Type from "./types";
    import * as Node from "./nodes";
    import * as ApolloType from "apollo-client";
    import ApolloClient from "apollo-client";
    export interface ClientClass {readonly client: ApolloClient<any>;}        
`;

function makeClassHeader(basename) {
  return `
    export class ${changeCase.pascalCase(
      basename
    )}Client implements ClientClass{
        constructor(readonly client: ApolloClient<any>) {}          
    `;
}

function makeClassMethods(operations) {
  return operations.map(e => {
    const camelName = changeCase.camelCase(e.name);
    const pascalName = changeCase.pascalCase(e.name);
    const pascalOperation = changeCase.pascalCase(e.operation);

    const queryType = `Type.${pascalName + pascalOperation}`;
    const variableType = `Type.${pascalName + pascalOperation + "Variables"}`;
    const optionType = getOptionName(e.operation, queryType, variableType);

    return `
    ${camelName} = (options?:Omit<${optionType},"${operationQueryName[e.operation]}">) => 
        this.client.${operationName[e.operation]}<${queryType},${variableType}>
        ({...options,...{${operationQueryName[e.operation]}:Node.${pascalName}}})    
    `;
  });
}

const operationName = {
  query: "query",
  mutation: "mutate",
  subscription: "subscribe"
};

function getOptionName(operation, query, variable) {
  switch (operation) {
    case "query":
      return `ApolloType.QueryOptions<${variable}>`;
    case "mutation":
      return `ApolloType.MutationOptions<${query},${variable}>`;
    case "subscription":
      return `ApolloType.SubscriptionOptions<${variable}>`;
  }
}

const operationQueryName = {
    query: "query",
    mutation: "mutation",
    subscription: "query"
};

module.exports = {
  plugin: (schema, documents, config) => {
    const classes = documents
      .map(doc => {
        const filePath = doc.filePath;
        const baseName = path.basename(filePath, path.extname(filePath));

        const classHeader = makeClassHeader(baseName);

        const definitions = doc.content.definitions;
        const operations = definitions.map(e => ({
          operation: e.operation,
          name: e.name.value
        }));
        const methods = makeClassMethods(operations);

        return [classHeader, methods.join("\n"), `}`].join("\n");
      })
      .join("\n");

    return [imp, classes].join("\n");
  }
};

然后将配置文件写成这样

overwrite: true
schema: "schema.graphql" # サーバのアドレス。勝手にスキーマを呼んでくれる
documents: "queries/*.graphql" # 監視対象 ここのファイルに更新があれば自動的に型を作成
generates:
  ./generated/types.ts:
    plugins:
      - typescript
      - typescript-operations
  ./generated/nodes.ts:
    plugins:
      - typescript-document-nodes
  ./generated/class.ts:
      - type-apollo-class.js

虽然有些杂乱,但我感到满足。

可以做到的人

总结

    • GraphQL楽しい

 

    JSでTS(文字列)を書くと楽しい。
广告
将在 10 秒后关闭
bannerAds