使用graphql和apollo client构建react web应用

graphql是一种用于 API 的查询语言(摘自官网)。

我们为什么要用graphql

相信大家在开发web应用的时候常常会遇到以下这些问题:后端更新了接口却没有通知前端,从而导致各种报错;后端修改接口字段名或者数据类型,前端也要跟着改,同时还要重新测试;项目涉及的接口数量繁多,如果是使用typescript的话还要手动的一个接口一个接口的去写interface。如果项目中使用了graphql的话,以上这些问题都会改善很多。利用插件graphql能够自动化的生成接口的相应typescript interface,需要的字段以及数据结构都由前端编写的graphql代码决定,不用实际请求就可以知道服务器会返回什么数据。

举一个简单的例子:向部署了graphql的服务器发送以下graphql代码:

query:query DroidById($id: ID!) {  // 调用名为DroidById的Query
  droid(id: $id) {       // 调用DroidById这个query的查询字段droid(相当于方法),传入id
    name           // 要求服务器返回实体中的name字段
  }
}
variables:{
  "id": 1
}

如果id1的相应数据存在,就会获得这样的响应:

{
  "data": {
  “droid”:{
      “name”:”his name”
  }
  }
}

这个是查询操作,修改操作也很简单:

query:mutation NHLMutation($id:Int!){ // 调用名为NHLMutation的Mutation,变量id类型为int,必传
    deletePlayer(id:$id) // 调用NHLMutation下的deletePlayer方法,传入id
}
variables:{
  id:1
}

如果成功的话,服务器则返回:

{
  "data": {
    "deletePlayer": true // 具体的返回数据类型可用内省(introspection)功能查到
  }
}

更多graphql的相关功能和语法,见官方教程:http://graphql.cn/learn

了解完了graphql,接下来介绍一个基于graphql的框架:apollo(官网:https://www.apollographql.com/docs/react/)。它集成了状态管理、错误处理、Loading效果等功能,在react中如果数据由apollo来管理的话,基本上就没redux什么事了。apollo会尽可能地帮你解决技术上的问题,让你专心于业务。

官网的文档和教程已经写的很详细了,但如果有具体的案例的话应该会理解得更深刻。以下就是一个针对于Player类的一个增删改查小应用。

 

应用的后台是使用.net core编写的,地址:https://github.com/axel10/graphql-demo-backend 安装完.net core sdkcdNHLStats.Api目录下运行dotnet run即可在localhost:5000端口上启动服务器。localhost:5000/graphqlgraphql endpoint,可调试graphql

在开始编写业务代码之前,先用graphql-code-generator来生成graphql服务器提供的接口(types.d.ts),这一步由于按照官网上提供的教程来就行,过程十分简单,这里就直接略过。详见https://graphql-code-generator.com/docs/getting-started/

首先是查询:

先编写graphql语句:(query/player.ts

export const CREATE_PLAYER = gql`
  mutation ($player: PlayerInput!) {
    createPlayer(player: $player) {
      id name birthDate
    }
  }
`

然后是具体逻辑(index.tsx

import React from 'react'
import { ApolloProvider, Query } from 'react-apollo'
import ReactDOM from 'react-dom'
import { Create } from 'src/components/createPlayerForm'
import {  GET_PLAYER } from 'src/querys/player'
import { NhlMutation, NhlQuery, PlayerType } from 'src/types'
import { client } from 'src/utils/apolloClient'
import './base.less'

class PlayerList extends React.Component {

  public render () {
    return (
      <div>
        <Query query={GET_PLAYER}>
          {
            ({ loading, error, data }) => {
              if (loading) return <p>Loading...</p>
              if (error) return <p>Error :(</p>
              const players: PlayerType[] = data.players
              return players.map((o, i) => (
                <div key={i}}>
                  {o.name} {o.birthDate} 
                </div>
              ))
            }
          }
        </Query>
      </div>
    )
  }
}

接着渲染组件:

import ApolloClient from 'apollo-boost'

const client = new ApolloClient({ 
  uri: 'http://localhost:5000/graphql'  //graphql服务器的endpoint
})

 

 

ReactDOM.render(
  <div>
    <ApolloProvider client={client}>
      <PlayerList/>
    </ApolloProvider>
  </div>, document.getElementById('root'))

这样我们就完成了取出数据并渲染这一步。接下来我们来试着创建player

先编写graphql:(querys/player.ts

export const CREATE_PLAYER = gql`
  mutation ($player: PlayerInput!) {
    createPlayer(player: $player) {
      id name birthDate
    }
  }
`

新建components/createPlayerForm/index.tsx

import React from 'react'
import { Mutation, MutationFunc } from 'react-apollo'
import { CREATE_PLAYER, GET_PLAYER } from 'src/querys/player'
import { NhlMutation, NhlQuery, PlayerInput } from 'src/types'
import { FormUtils } from 'src/utils/formUtils'
import styles from './style.less'

interface IState {
  form: PlayerInput
}

const initState: IState = {
  form: { name: '' }
}

const formUtils = new FormUtils<IState>({
  initState
})

export class Create extends React.Component {

  public handleCreateSubmit = (createPlayer: MutationFunc, data) => (e: React.FormEvent) => {
    e.preventDefault()
    const form = e.target as HTMLFormElement
    createPlayer({ variables: { player: formUtils.state[form.getAttribute('name')] } }) // 取出表单数据并提交
  }

  public handleUpdate = (cache, { data }: { data: NhlMutation }) => { // 服务器相应成功后更新本地数据
    const createdPlayer = data.createPlayer
    const { players } = cache.readQuery({ query: GET_PLAYER }) as NhlQuery // 先读取本地数据
    cache.writeQuery({ query: GET_PLAYER, data: { players: players.concat(createdPlayer) } }) // 写入处理后的数据
  }

  public render () {
    return (
      <div className={styles.CreatePlayer}>
        新增player
        <Mutation mutation={CREATE_PLAYER}
                  update={this.handleUpdate}
        >
          {
            (createPlayer, { data }) => (
              <form name='form' onSubmit={this.handleCreateSubmit(createPlayer, data)}>
                <div>
                  <label>
                    姓名
                    <input type='text' name='name' onChange={formUtils.bindField}/>
                  </label>
                </div>
                <div>
                  <label>
                    身高
                    <input type='number' name='height' onChange={formUtils.bindField}/>
                  </label>
                </div>
                <div>
                  <label>
                    出生日期
                    <input type='date' name='birthDate' onChange={formUtils.bindField}/>
                  </label>
                </div>
                <div>
                  <label>
                    体重
                    <input type='number' name='weightLbs' onChange={formUtils.bindField}/>
                  </label>
                </div>
                <button type='submit'>提交</button>
              </form>
            )
          }
        </Mutation>
      </div>
    )
  }
}

完成后渲染:

ReactDOM.render(
  <div>
    <ApolloProvider client={client}>
      <PlayerList/>
      <Create/>
    </ApolloProvider>
  </div>, document.getElementById('root'))

这样我们就可以看到新增player的表单了。

接下来是修改模态框:(components/editPlayerModal/index.tsx)

import * as React from 'react'
import { Mutation, MutationFunc } from 'react-apollo'
import { EDIT_PLAYER, GET_PLAYER } from 'src/querys/player'
import { NhlQuery, PlayerInput, PlayerType } from 'src/types'
import { removeTypename } from 'src/utils/utils'
import { FormUtils } from '../../utils/formUtils'
import styles from './style.less'

interface IState {
  form: PlayerInput
}

const initState: IState = {
  form: { name: '' }
}

const formUtils = new FormUtils<IState>({
  initState
})

export default class EditPlayerModal extends React.Component<{ player: PlayerType, onCancel: () => void }> {

  public formName = 'edit'

  constructor (props) {
    super(props)
    formUtils.state[this.formName] = this.props.player
  }

  public handleEditSubmit = (editPlayer: MutationFunc, data) => (e: React.FormEvent) => {
    const player = removeTypename(formUtils.state[this.formName]) // 删除apollo为了进行状态管理而添加的__typename字段,否则报错
    editPlayer({
      variables: { player },
      update (cache, { data }) {
        const { players } = cache.readQuery({ query: GET_PLAYER }) as NhlQuery
        Object.assign(players.find(o => o.id === player.id), player) // 提交修改
        cache.writeQuery({ query: GET_PLAYER, data: { players } }) // 写入
      }
    }) // 提交
    this.props.onCancel()
  }

  public render () {
    const { player, onCancel } = this.props
    console.log(player)

    return (
      <div className={styles.wrap}>
        <div className='form-content'>
          <Mutation mutation={EDIT_PLAYER}
          >
            {
              (editPlayer, { data }) => {
                return (
                  <div>
                    <span className={styles.cancel} onClick={onCancel}>取消</span>
                    <form name={this.formName} onReset={formUtils.resetForm}
                          onSubmit={this.handleEditSubmit(editPlayer, data)}>
                      <div>
                        <label>
                          姓名
                          <input defaultValue={player.name} type='text' name='name' onChange={formUtils.bindField}/>
                        </label>
                      </div>
                      <div>
                        <label>
                          身高
                          <input defaultValue={player.height} type='text' name='height'
                                 onChange={formUtils.bindField}/>
                        </label>
                      </div>
                      <div>
                        <label>
                          出生日期
                          <input defaultValue={player.birthDate} type='text' name='birthDate'
                                 onChange={formUtils.bindField}/>
                        </label>
                      </div>
                      <div>
                        <label>
                          体重
                          <input defaultValue={player.weightLbs ? player.weightLbs.toString() : ''} type='number'
                                 name='weightLbs'
                                 onChange={formUtils.bindField}/>
                        </label>
                      </div>
                      <button type='submit'>提交</button>
                    </form>
                  </div>
                )
              }
            }
          </Mutation>
        </div>
      </div>
    )
  }
}

然后利用showEditPlayerModal方法显示模态框(utils/utils.ts

import gql from 'graphql-tag'
import React from 'react'
import { ApolloProvider } from 'react-apollo'
import ReactDOM from 'react-dom'
import EditPlayerModal from 'src/components/editPlayerModal'
import { PlayerType } from 'src/types'
import { client } from 'src/utils/apolloClient'
import { PlayerFragement } from 'src/utils/graphql/fragements'

export function showEditPlayerModal (player: PlayerType) {
  client.query<{ player: PlayerType }>({
    query: gql`
      query ($id:Int!){
        player(id:$id){
          ...PlayerFragment
        }
      }
      ${PlayerFragement}
    `,
    variables: {
      id: player.id
    }
  }).then(o => {
    console.log(o)
    document.body.appendChild(container)
    ReactDOM.render(
      <ApolloProvider client={client}>
        <EditPlayerModal player={o.data.player} onCancel={onCancel}/>
      </ApolloProvider>,
      container)
  })
  const container = document.createElement('div')
  container.className = 'g-mask'
  container.id = 'g-mask'

  function onCancel () {
    ReactDOM.unmountComponentAtNode(container)
    document.body.removeChild(container)
  }
}

function omitTypename (key, val) {
  return key === '__typename' ? undefined : val
}

export function removeTypename (obj) {
  return JSON.parse(JSON.stringify(obj), omitTypename)
}

其中的代码片段PlayerFragement:(utils/graphql/fragements.ts

import gql from 'graphql-tag'

export const PlayerFragement = gql`
  fragment PlayerFragment on PlayerType{
    id
    birthDate
    name
    birthPlace
    weightLbs
    height
  }
`

完成后修改PlayListrender方法,使每一次点击条目都会弹出修改模态框:

import { showEditPlayerModal } from 'src/utils/utils'

 

...

class PlayerList extends React.Component {

  public showEditModal = (player: PlayerType) => () => {
    showEditPlayerModal(player)
  }

  public render () {
    return (
      <div>
        <Query query={GET_PLAYERS}>
          {
            ({ loading, error, data }) => {
              if (loading) return <p>Loading...</p>
              if (error) return <p>Error :(</p>
              const players: PlayerType[] = data.players
              return players.map((o, i) => (
                <div key={i} onClick={this.showEditModal(o)}>
                  {o.name} {o.birthDate}
                </div>
              ))
            }
          }
        </Query>
      </div>
    )
  }
}

 

这样修改功能也完成了。最后是删除:

修改PlayerListrender方法:

public render () {
  return (
    <div>
      <Query query={GET_PLAYERS}>
        {
          ({ loading, error, data }) => {
            if (loading) return <p>Loading...</p>
            if (error) return <p>Error :(</p>
            const players: PlayerType[] = data.players
            return players.map((o, i) => (
              <div key={i} onClick={this.showEditModal(o)}>
                {o.name} {o.birthDate} <span style={{ color: 'red' }} onClick={this.deletePlayer(o.id)}>删除</span>
              </div>
            ))
          }
        }
      </Query>
    </div>
  )
}

添加删除方法:

public deletePlayer = (id) => (e: React.MouseEvent) => {
  e.stopPropagation()
  client.mutate({
    mutation: DELETE_PLAYER,
    variables: {
      id
    },
    update (cache, { data }: { data: NhlMutation }) {
      console.log(data)
      const { players } = cache.readQuery({ query: GET_PLAYERS }) as NhlQuery
      cache.writeQuery({ query: GET_PLAYERS, data: { players: players.filter(item => item.id !== id) } })
    }
  })
}

删除Playergraphql语句:

export const DELETE_PLAYER = gql`
  mutation NHLMutation($id:Int!){
    deletePlayer(id:$id)
  }
`

这样增删改查就全部完成了。

graphql是一个比较新的概念,学习曲线可能略显陡峭,不过总体来说不会太难。

项目地址:https://github.com/axel10/graphql-demo-frontend

参考:

https://fullstackmark.com/post/17/building-a-graphql-api-with-aspnet-core-2-and-entity-framework-core

原文地址:https://www.cnblogs.com/axel10/p/10287609.html