zoukankan      html  css  js  c++  java
  • 使用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

  • 相关阅读:
    Python基础之入门
    Python基础之语句语法
    Python基础之变量
    Nginx实现反向代理和负载均衡
    Nginx配置文件简述
    Nginx简介及环境搭建
    C#利用RabbitMQ实现消息订阅与发布
    C#利用RabbitMQ实现点对点消息传输
    RabbitMQ交换机类型简述
    Android
  • 原文地址:https://www.cnblogs.com/axel10/p/10287609.html
Copyright © 2011-2022 走看看