使用Typescript安全地开发Apollo服务器
在编写Apollo服务器时,由于大部分文档都是用JavaScript和TypeScript编写的,所以有点困扰。
由GraphQL Codegen自动生成代码
GraphQL的魅力主要体现在其可靠的模式(Schema)上。GraphQL Codegen的目的就是借鉴这种可靠性,并将其应用在TypeScript的类型定义中。
请先从npm上获得所需的模块。
npm install @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-operations @graphql-codegen/typescript-resolvers graphql-toolkit --save-dev
如果你创建了一个用SDL编写的schema.graphql文件,那么请在根文件夹附近编写codegen.yml文件。
overwrite: true
documents: null
schema: "**/*.graphql"
generates:
src/generated/graphql.ts:
plugins:
- "typescript"
- "typescript-operations"
- "typescript-resolvers"
config:
useIndexSignature: true
avoidOptionals: true
在`src/generated/graphql.ts`中生成了许多类型。其中包括一些特定于Apollo的类型,同时还有一些普通类型和输入类型,可以直接用作类型,并进行有效利用。
创建入口点
几乎和教程一样,创建一个index.ts文件。
import { ApolloServer, gql } from 'apollo-server';
import fs from 'fs';
import path from 'path';
import resolvers from './resolvers'; //後で定義
import { dataSources, context } from './datasources'; //後で定義
const typeDefs = gql(fs.readFileSync(path.resolve(__dirname, '<スキーマファイルへのパス>'), 'utf8'));
const server = new ApolloServer({
typeDefs,
resolvers,
dataSources,
context
});
server.listen(4002).then(({ url }) => {
console.log(`? Server ready at ${url}`);
});
对于 typeDefs 的定义,我感觉不够优雅…正在寻找更好的方法。
数据源,上下文的定义。
首先,让我们定义一下背景情况。
export const context = async () => ({
/* Contextの定義 */
ctx1: "foo"
ctx2: "bar"
});
type PartialContext = ReturnType<typeof context> extends Promise<infer T> ? T : never;
请参考我的以往文章来了解有关Typescript的infer的信息❤️
接下来定义DataSources。
export const dataSources = () => ({
/* Datasourceの定義 */
ds1: new MyDataSource1<PartialContext>(),
ds2: new MyDataSource2<PartialContext>()
});
为什么DataSource使用泛型将在后面进行说明。实际使用的上下文是将数据源添加到PartialContext中的内容。
export type TContext = PartialContext & {
dataSources: ReturnType<typeof dataSources>
};
添加自己的数据源
我們這樣做吧。 zuò ba.)
import { DataSource, DataSourceConfig } from 'apollo-datasource';
import { InMemoryLRUCache, KeyValueCache } from 'apollo-server-caching';
export interface Context {
ctx1: string
}
export default class MyDataSource1<T extends Context> extends DataSource<T> {
context?: Context
cache?: KeyValueCache
constructor() {
super();
}
initialize({ context, cache }: DataSourceConfig<Context>) {
this.context = context;
// this.context.ctx1にアクセスできる
this.cache = cache || new InMemoryLRUCache();
}
}
使用泛型的目的是为了确保DataSource所需的上下文与PartialContext保持一致。
使用泛型是为了确保DataSource所需的上下文与PartialContext保持一致。
解析程序的定义
import { TContext } from './datasources';
import { Resolvers } from './generated/graphql';
export const resolvers: Resolvers<TContext> = {
// resolverのなかみ
};
终于看到了graphql-codegen的优点。通过使用Resolvers和TContext,可以推断出解析器函数的参数类型。还可以将解析器分解并使文件更加紧凑。
import { TContext } from './datasources';
import { Resolvers, QueryResolvers, UserResovlers } from './generated/graphql';
const queryResolvers: QueryResolvers<TContext> = {...}
const userResolvers: UserResolvers<TContext> = {...}
export const resolvers: Resolvers<TContext> = {
Query: queryResolvers,
User: userResolvers
};
标量的定义。
这个scalar的规格太难懂了,感觉有点糟糕。
这个样子
import {
GraphQLScalarType,
Kind,
GraphQLScalarTypeConfig,
ValueNode
} from 'graphql';
import moment from 'moment';
const config: GraphQLScalarTypeConfig<moment.Moment, string> = {
name: 'DateTime',
description: 'DateTime custom scalar type, in the form of YYYY-MM-DDTHH:mm:ss',
parseValue(value: string) {
return moment(value);
},
serialize(value: moment.Moment) {
return value.format();
},
parseLiteral(ast: ValueNode) {
switch (ast.kind) {
case Kind.STRING:
return moment(ast.value);
default:
return null;
}
}
};
export const datetimeScalar = () => new GraphQLScalarType(config);
GraphQLScalarTypeConfig<moment.Moment, string>表示的是一个Scalar类型,它在SDL中具有string类型,并且在解析器的第二个参数中具有Moment类型。parseValue用于将SDL转换为解析器,而serialize用于将解析器转换为SDL。
对于parseLiteral,您打算用于什么目的呢?我有点搞不懂。
Directive的定义是什么?
因为我还没有使用过,所以不知道。
使用联邦
使用Apollo的原因之一可能是基于联邦机制,容易定义微服务。
首先,在codegen.yml中添加以下内容。
generates:
src/generated/graphql.ts:
plugins:
...
config:
avoidOptionals: true
useIndexSignature: true
federation: true #追加
只需要按照教程的步骤设置模式即可。