graphql是一種用於 API 的查詢語言(摘自官網)。前端
咱們爲何要用graphql?react
相信你們在開發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 }
若是id爲1的相應數據存在,就會得到這樣的響應: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 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
參考: