一旦導入GraphQL的模式驅動開發,開發效率將大大提升!
我现在参与的项目使用GraphQL引入了基于模式的开发,效率相当高,并且有利于提高质量,所以我打算写一篇文章总结一下我的实践经验。
希望能对正在考虑引入模式驱动开发的人提供支持。
我基于之前开发的项目创建了一个Boilerplate。实际的代码可以通过查看这里更容易理解:
https://github.com/Takumi0901/graphql-koa-apollo-react-boilerplate
如果开发过程没有使用Schema驱动,会遇到哪些困难?
我认为有一个问题是前端开发往往会开始得较慢。
基本上流程是设计 > API实现 > 前端实现。而且在开发过程中如果有不顺利的部分,可能需要对API进行实现和修正。这样一来,前端的实现就会进一步延迟。
根据这个API的实现情况,前端应该会像这样实现。然后,之后可能会有回头修复的情况,这对开发效率来说不太好。
使用基于模式的开发有什么好处?
-
- API開発とフロント開発を同時に進めることができる
-
- スキーマがドキュメントとして存在するので出戻りがない(少ない)
- スキーマを元に型定義するので品質もアップ
在中国,只需要一个选项,将以下内容进行本地化:
除此之外,直接使用模式驱动开发的好处是,通过使用下面提到的graphql-codegen工具,我们可以通过一个命令从模式中生成类型定义文件,从而提高开发效率。
复习开发流程
首先,基于模式的开发过程大致可以按照以下的流程进行。
-
- 定义模式
-
- 创建模拟对象
-
- 前后端都实现
- 测试或发布
在进行文章撰写时,将与开发流程进行对照。
项目的结构图
基本方针确定如下。
-
- GraphQLを使う
-
- サーバサイド(BFF)はnodeで
-
- フロントはReact
- すでにマイクロサービスはある
模式
首先,我们进行模式的定义。
例如,可以这样写。本次我们将简单明了地定义 User 列表、单个 User 获取和登录的模式。
scalar Date
scalar DateTime
scalar Error
scalar EmailAddress
scalar URL
type Query {
user(id: ID!): User
users: [User]
}
type Mutation {
login(email: String!, password: String!): AuthResponse!
}
type AuthResponse {
success: Boolean
error: Error
token: String
}
type User {
id: ID!
name: String
email: EmailAddress
registerDate: DateTime
profileImageUrl: URL
}
如果上述的模式是可以的,那么我们将继续创建查询和变更。
query user($id: ID!) {
user(id: $id) {
id
name
profileImageUrl
registerDate
}
}
query users {
users {
id
name
registerDate
profileImageUrl
}
}
mutation loginStaff($email: String!, $password: String!) {
login(email: $email, password: $password) {
token
}
}
如果你使用TypeScript,你需要对类型进行定义,例如解析器的类型定义等等。这个过程非常繁琐。但是通过使用graphql-codegen,你可以完全避免这些繁琐的工作。
只要有模式(schema),我们可以通过一条命令来生成TS的类型定义、useQuery(下文中会提到)、useMutation(下文中会提到)等。我认为这与模式驱动开发很搭配。一旦定义了模式,前端的视图就可以使用查询和变更操作。
请您查看官方说明以了解详细的步骤。
我已经设置了codegen的配置文件。将输出目录分别设为server/和client/。
overwrite: true
schema: './graphql/schema.graphql' // スキーマの場所
documents: './graphql/**/*.graphql' // queryとかmutationの定義の場所
generates:
../server/src/gen/types.ts: // 出力先 こっちはserver側
plugins:
- 'typescript'
- 'typescript-resolvers'
../client/src/gen/actions.tsx: // 出力先 こっちはclient側
plugins:
- 'typescript'
- 'typescript-operations'
- 'typescript-react-apollo'
config:
withComponent: false
withHooks: true
withHOC: false
服务器/源码/生成/类型.ts
虽然有些省略,但基本上大致是这样生成的。
export type AuthResponse = {
success?: Maybe<Scalars['Boolean']>
error?: Maybe<Scalars['Error']>
token?: Maybe<Scalars['String']>
}
export type Mutation = {
login: AuthResponse
}
export type MutationLoginArgs = {
email: Scalars['String']
password: Scalars['String']
}
export type Query = {
user?: Maybe<User>
users?: Maybe<Array<Maybe<User>>>
}
export type QueryUserArgs = {
id: Scalars['ID']
}
export type User = {
id: Scalars['ID']
name?: Maybe<Scalars['String']>
email?: Maybe<Scalars['EmailAddress']>
registerDate?: Maybe<Scalars['DateTime']>
profileImageUrl?: Maybe<Scalars['URL']>
}
export type MutationResolvers<Context = any, ParentType = Mutation> = {
login?: Resolver<AuthResponse, ParentType, Context, MutationLoginArgs>
}
export type QueryResolvers<Context = any, ParentType = Query> = {
user?: Resolver<Maybe<User>, ParentType, Context, QueryUserArgs>
users?: Resolver<Maybe<Array<Maybe<User>>>, ParentType, Context>
}
export type UserResolvers<Context = any, ParentType = User> = {
id?: Resolver<Scalars['ID'], ParentType, Context>
name?: Resolver<Maybe<Scalars['String']>, ParentType, Context>
email?: Resolver<Maybe<Scalars['EmailAddress']>, ParentType, Context>
registerDate?: Resolver<Maybe<Scalars['DateTime']>, ParentType, Context>
profileImageUrl?: Resolver<Maybe<Scalars['URL']>, ParentType, Context>
}
export type Resolvers<Context = any> = {
AuthResponse?: AuthResponseResolvers<Context>
Mutation?: MutationResolvers<Context>
Query?: QueryResolvers<Context>
User?: UserResolvers<Context>
}
export type IResolvers<Context = any> = Resolvers<Context>
客户/源代码/生成/动作/(客户端/源代码/生成/操作)。
这次打算只使用hooks来进行实现,所以会使用react-apollo-hooks。通过将codegen.yml中的withHooks设置为true,就可以生成它。还可以选择Apollo的组件类型等,以便根据前端实现进行生成。
type Maybe<T> = T | null
/** All built-in and custom scalars, mapped to their actual values */
export type Scalars = {
ID: string
String: string
Boolean: boolean
Int: number
Float: number
EmailAddress: any
DateTime: any
URL: any
Error: any
Date: any
}
export type AuthResponse = {
success?: Maybe<Scalars['Boolean']>
error?: Maybe<Scalars['Error']>
token?: Maybe<Scalars['String']>
}
export type Mutation = {
login: AuthResponse
}
export type MutationLoginArgs = {
email: Scalars['String']
password: Scalars['String']
}
export type Query = {
user?: Maybe<User>
users?: Maybe<Array<Maybe<User>>>
}
export type QueryUserArgs = {
id: Scalars['ID']
}
export type User = {
id: Scalars['ID']
name?: Maybe<Scalars['String']>
email?: Maybe<Scalars['EmailAddress']>
registerDate?: Maybe<Scalars['DateTime']>
profileImageUrl?: Maybe<Scalars['URL']>
}
export type LoginMutationVariables = {
email: Scalars['String']
password: Scalars['String']
}
export type LoginMutation = { __typename?: 'Mutation' } & {
login: { __typename?: 'AuthResponse' } & Pick<AuthResponse, 'token'>
}
export type UserQueryVariables = {
id: Scalars['ID']
}
export type UserQuery = { __typename?: 'Query' } & {
user: Maybe<{ __typename?: 'User' } & Pick<User, 'id' | 'name' | 'profileImageUrl' | 'registerDate'>>
}
export type UsersQueryVariables = {}
export type UsersQuery = { __typename?: 'Query' } & {
users: Maybe<Array<Maybe<{ __typename?: 'User' } & Pick<User, 'id' | 'name' | 'registerDate' | 'profileImageUrl'>>>>
}
import gql from 'graphql-tag'
import * as ReactApolloHooks from 'react-apollo-hooks'
export const LoginDocument = gql`
mutation login($email: String!, $password: String!) {
login(email: $email, password: $password) {
token
}
}
`
export function useLoginMutation(
baseOptions?: ReactApolloHooks.MutationHookOptions<LoginMutation, LoginMutationVariables>
) {
return ReactApolloHooks.useMutation<LoginMutation, LoginMutationVariables>(LoginDocument, baseOptions)
}
export const UserDocument = gql`
query user($id: ID!) {
user(id: $id) {
id
name
profileImageUrl
registerDate
}
}
`
export function useUserQuery(baseOptions?: ReactApolloHooks.QueryHookOptions<UserQueryVariables>) {
return ReactApolloHooks.useQuery<UserQuery, UserQueryVariables>(UserDocument, baseOptions)
}
export const UsersDocument = gql`
query users {
users {
id
name
registerDate
profileImageUrl
}
}
`
export function useUsersQuery(baseOptions?: ReactApolloHooks.QueryHookOptions<UsersQueryVariables>) {
return ReactApolloHooks.useQuery<UsersQuery, UsersQueryVariables>(UsersDocument, baseOptions)
}
模拟服务器
你可以很容易地使用Apollo Server创建它。实际上,只需要这样。
此外,你也可以轻松地自定义响应。如果你想了解更详细的方法,请查看模拟-Mocking – Apollo Docs。
const { ApolloServer, gql } = require('apollo-server');
const typeDefs = gql`
type Query {
hello: String
}
`;
const server = new ApolloServer({
typeDefs,
mocks: true,
});
server.listen().then(({ url }) => {
console.log(`? Server ready at ${url}`)
});
微服务
用户、管理员等都以微服务化的方式进行了划分。
最好的朋友(后端为前端)
简单介绍一下技术栈的话…
使用Node + koa + Apollo + TypeScript。而且还能在BFF中使用GraphQL,这得益于现有的微服务架构。GraphQL在这个项目中非常匹配。
我使用Apollo-Server搭建GraphQL服务器,在http://localhost:4000/graphql等任意URL上启动GraphiQL IDE,这样可以在浏览器上进行实时调试和开发。
const Query: QueryResolvers = {
talent(obj, args, context, info) {
// マイクロサービスからかき集めてデータを返す
},
users(obj, args, context, info) {
// マイクロサービスからかき集めてデータを返す
}
}
const Mutation: MutationResolvers = {
async login(_obj, arg: { email: string; password: string }) {
const { password, email } = arg
// 何かしらの処理
}
}
const resolvers: Resolvers = {
Query,
Mutation,
}
export default resolvers
前端
我正在使用React + Apollo + TypeScript进行实现。
首先,根据前面提到的模拟服务器进行实现,一旦解析器完成,就在开发服务器上进行开发和确认,然后继续进行。
此外,这次我们还使用了 withHooks 来进行获取和更新。graphql-codegen 会为我们生成 useUsersQuery 和 useLoginMutation,所以在前端中只需要使用它们即可。这个 withHooks 真的很棒。它可以按照组件和功能的方式创建,并且可以将其封装在其中。个人而言,我认为与 Atomic Design 结合使用会更好。
const Users: React.FunctionComponent<{}> = () => {
const { data, loading } = useUsersQuery()
if (loading) return <div>Loading</div>
if (Object.keys(data).length < 1) return null
return (
<React.Fragment>
{data.talents.map((e, key) => {
return (
<div>
// 何かしら表示
</div>
)
})}
</React.Fragment>
)
}
const LoginContent: React.FunctionComponent<{}> = () => {
const onSubmitSignIn = useLoginMutation({
update: (_, { data }) => {
// mutationでsuccess後の処理
}
})
return (
// form
)
}
只是顺便说一句,Redux不必要。
使用 GraphQL,Redux变得越来越不再必要了。从根本上说,它就像是在服务器端和Redux中管理相同的数据一样。而且,对于GraphQL,或者说在这种情况下是Apollo,它还提供了错误处理,可以返回 UNAUTHENTICATED 或 INTERNAL_SERVER_ERROR 这样的代码,这样就可以很容易地结合使用 Toast 实现而不需要太多的成本。
关于考试
基本上它是基於架構生成的,因此類型和數據的一致性是保證的。此外,它還提供GraphQL錯誤驗證,所以品質比傳統開發方式要好很多。
然而,当需要作为变量传递电子邮件和密码的登入Muation时,模式的变更并不及时传达给前端。
例如,假设在模式中进行了更改,比如将”email”更改为”emailAddress”,则会重新生成类型定义和withHooks等内容。但是在视图方面,
useLoginMutation({variables: {email: '', password: ''}})
所指的地方不會自動變更。嗯,當然是理所當然的。雖然不太可能經常發生這種情況,但似乎需要防止在這一區域進行的更改在測試中被錯誤地發布出去。
可以拍摄快照等方式,检查视图差异是可行的,但最好在发布后慢慢引入。
我尝试了很多不同的方式,但 easygraphql-tester 是最简单和易理解的。接下来,只需要使用 Circle Ci 等进行测试就可以了,对吧?
describe('A user', function() {
let tester
beforeAll(() => {
tester = new EasyGraphQLTester(schemaCode)
})
test('UsersDocument', () => {
tester.test(true, UsersDocument)
})
test('UserDocument', () => {
tester.test(true, UserDocument, { id: 1 })
})
test('LoginDocument', () => {
tester.test(true, LoginStaffDocument, { email: 'example@gmail.com', password: 'hgoehoge' })
})
})
尽管如此,还存在问题。
在项目发布后进行扩展或有变更时,团队提出了关于未来发布的问题,包括模式管理(包括版本)。我希望以后能在文章中涉及这方面的内容。
虽然第一次引入GraphQL到项目中,但通过基于模式的开发,我认为工作效率得到了提高,质量也有所提升,这是一件好事。
我能感受到与BFF很合得来,但如果不是这样的情况,我很好奇会怎样,所以如果有机会的话,想试试看。
希望未来能有更多的GraphQL案例。