使用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 (
      
{ ({ loading, error, data }) => { if (loading) return

Loading...

if (error) return

Error :(

const players: PlayerType[] = data.players return players.map((o, i) => (
{o.name} {o.birthDate}
)) } }
) } }

接着渲染组件:

import ApolloClient from 'apollo-boost'

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

 

 

ReactDOM.render(
  
, 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({
  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 (
      
新增player {CREATE_PLAYER} update={this.handleUpdate} > { (createPlayer, { data }) => (
this.handleCreateSubmit(createPlayer, data)}>
) }
) } }

完成后渲染:

ReactDOM.render(
  
, 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({ 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 (
{EDIT_PLAYER} > { (editPlayer, { data }) => { return (
取消
this.formName} onReset={formUtils.resetForm} onSubmit={this.handleEditSubmit(editPlayer, data)}>
) } }
) } }

然后利用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(
      
        
      ,
      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 (
      
{ ({ loading, error, data }) => { if (loading) return

Loading...

if (error) return

Error :(

const players: PlayerType[] = data.players return players.map((o, i) => (
this.showEditModal(o)}> {o.name} {o.birthDate}
)) } }
) } }

 

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

修改PlayerListrender方法:

public render () {
  return (
    
{ ({ loading, error, data }) => { if (loading) return

Loading...

if (error) return

Error :(

const players: PlayerType[] = data.players return players.map((o, i) => (
this.showEditModal(o)}> {o.name} {o.birthDate} this.deletePlayer(o.id)}>删除
)) } }
) }

添加删除方法:

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