我自己编写了一个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,那么服务器将随机建立起来。
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选项移除,自动更新功能将被禁用。
从”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
}
}
`;
导出类型也许 = 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(文字列)を書くと楽しい。