一旦導入GraphQL的模式驅動開發,開發效率將大大提升!

我现在参与的项目使用GraphQL引入了基于模式的开发,效率相当高,并且有利于提高质量,所以我打算写一篇文章总结一下我的实践经验。

希望能对正在考虑引入模式驱动开发的人提供支持。

我基于之前开发的项目创建了一个Boilerplate。实际的代码可以通过查看这里更容易理解:
https://github.com/Takumi0901/graphql-koa-apollo-react-boilerplate

如果开发过程没有使用Schema驱动,会遇到哪些困难?

我认为有一个问题是前端开发往往会开始得较慢。
基本上流程是设计 > API实现 > 前端实现。而且在开发过程中如果有不顺利的部分,可能需要对API进行实现和修正。这样一来,前端的实现就会进一步延迟。

根据这个API的实现情况,前端应该会像这样实现。然后,之后可能会有回头修复的情况,这对开发效率来说不太好。

使用基于模式的开发有什么好处?

    • API開発とフロント開発を同時に進めることができる

 

    • スキーマがドキュメントとして存在するので出戻りがない(少ない)

 

    スキーマを元に型定義するので品質もアップ

在中国,只需要一个选项,将以下内容进行本地化:
除此之外,直接使用模式驱动开发的好处是,通过使用下面提到的graphql-codegen工具,我们可以通过一个命令从模式中生成类型定义文件,从而提高开发效率。

复习开发流程

首先,基于模式的开发过程大致可以按照以下的流程进行。

    1. 定义模式

 

    1. 创建模拟对象

 

    1. 前后端都实现

 

    测试或发布

在进行文章撰写时,将与开发流程进行对照。

项目的结构图

基本方针确定如下。

    • GraphQLを使う

 

    • サーバサイド(BFF)はnodeで

 

    • フロントはReact

 

    すでにマイクロサービスはある
image.png

模式

首先,我们进行模式的定义。
例如,可以这样写。本次我们将简单明了地定义 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案例。

广告
将在 10 秒后关闭
bannerAds