使用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 }
如果id为1的相应数据存在,就会获得这样的响应:
{ "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 sdk后cd到NHLStats.Api目录下运行dotnet run即可在localhost:5000端口上启动服务器。localhost:5000/graphql为graphql 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 } `
完成后修改PlayList的render方法,使每一次点击条目都会弹出修改模态框:
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> ) } }
这样修改功能也完成了。最后是删除:
修改PlayerList的render方法:
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) } }) } }) }
删除Player的graphql语句:
export const DELETE_PLAYER = gql` mutation NHLMutation($id:Int!){ deletePlayer(id:$id) } `
这样增删改查就全部完成了。
graphql是一个比较新的概念,学习曲线可能略显陡峭,不过总体来说不会太难。
项目地址:https://github.com/axel10/graphql-demo-frontend
参考: