使用graphql和apollo client構建react web應用

 

graphql是一種用於 API 的查詢語言(摘自官網)。前端

咱們爲何要用graphqlreact

相信你們在開發web應用的時候經常會遇到如下這些問題:後端更新了接口卻沒有通知前端,從而致使各類報錯;後端修改接口字段名或者數據類型,前端也要跟着改,同時還要從新測試;項目涉及的接口數量繁多,若是是使用typescript的話還要手動的一個接口一個接口的去寫interface。若是項目中使用了graphql的話,以上這些問題都會改善不少。利用插件graphql可以自動化的生成接口的相應typescript interface,須要的字段以及數據結構都由前端編寫的graphql代碼決定,不用實際請求就能夠知道服務器會返回什麼數據。git

舉一個簡單的例子:向部署了graphql的服務器發送如下graphql代碼:github

query:query DroidById($id: ID!) {  // 調用名爲DroidById的Query
  droid(id: $id) {       // 調用DroidById這個query的查詢字段droid(至關於方法),傳入id
    name           // 要求服務器返回實體中的name字段
  }
}
variables:{
  "id": 1
}

 

若是id1的相應數據存在,就會得到這樣的響應:web

{
  "data": {
  「droid」:{
      「name」:」his name」
  }
  }
}

 

這個是查詢操做,修改操做也很簡單:typescript

query:mutation NHLMutation($id:Int!){ // 調用名爲NHLMutation的Mutation,變量id類型爲int,必傳
    deletePlayer(id:$id) // 調用NHLMutation下的deletePlayer方法,傳入id
}
variables:{
  id:1
}

 

若是成功的話,服務器則返回:redux

{
  "data": {
    "deletePlayer": true // 具體的返回數據類型可用內省(introspection)功能查到
  }
}

更多graphql的相關功能和語法,見官方教程:http://graphql.cn/learn後端

瞭解完了graphql,接下來介紹一個基於graphql的框架:apollo(官網:https://www.apollographql.com/docs/react/)。它集成了狀態管理、錯誤處理、Loading效果等功能,在react中若是數據由apollo來管理的話,基本上就沒redux什麼事了。apollo會盡量地幫你解決技術上的問題,讓你專心於業務。api

官網的文檔和教程已經寫的很詳細了,但若是有具體的案例的話應該會理解得更深入。如下就是一個針對於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

相關文章
相關標籤/搜索