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

参考:

https://fullstackmark.com/post/17/building-a-graphql-api-with-aspnet-core-2-and-entity-framework-core

使用graphql和apollo client构建react web应用的更多相关文章

  1. 使用 Spring 3 MVC HttpMessageConverter 功能构建 RESTful web 服务

    原文地址:http://www.ibm.com/developerworks/cn/web/wa-restful/ 简介: Spring,构建 Java™ 平台和 Enterprise Edition ...

  2. 使用Nginx+CppCMS构建高效Web应用服务器

    使用Nginx+CppCMS构建高效Web应用服务器 1:Why当前,越来越多的网站使用了各种框架,大部分框架使用了脚本语言.半编译语言等.比如Java.Python.Php.C#.NET等.这些框架 ...

  3. 用webpack4从零开始构建react脚手架

    用webpack4从零开始构建react脚手架 使用脚手架 git clone git@github.com:xiehaitao0229/react-wepack4-xht.git` `cd reac ...

  4. [react001] 使用webpack自动构建react 项目

    1.react 简介 React 是一个Facebook出品的前端UI开发框架.react官方的tutorials 为了让人容易上手,并没有给在平常工作使用react的详细配置,随意学习的深入,你为了 ...

  5. SpringBoot实战(十)之使用Spring Boot Actuator构建RESTful Web服务

    一.导入依赖 <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http ...

  6. 基于jersey和Apache Tomcat构建Restful Web服务(一)

    基于jersey和Apache Tomcat构建Restful Web服务(一) 现如今,RESTful架构已然成为了最流行的一种互联网软件架构,它结构清晰.符合标准.易于理解.扩展方便,所以得到越来 ...

  7. 使用 Jersey 和 Apache Tomcat 构建 RESTful Web 服务

    作者: Yi Ming Huang, 软件工程师, IBM Dong Fei Wu, 软件工程师, IBM Qing Guo, 软件工程师, IBM 出处: http://www.ibm.com/de ...

  8. 使用 Spring Boot Actuator 构建 RESTful Web 应用

    Spring Boot Actuator 是 Spring Boot 的一个子项目.通过它,可以很轻易地为应用提供多种生产级服务.本教程中,你将通过构建一个应用来学习如何添加这些服务. 1. 你需要构 ...

  9. 【读书笔记】2016.12.10 《构建高性能Web站点》

    本文地址 分享提纲: 1. 概述 2. 知识点 3. 待整理点 4. 参考文档 1. 概述 1.1)[该书信息] <构建高性能Web站点>: -- 百度百科 -- 本书目录: 第1章 绪论 ...

随机推荐

  1. LevelDB速记

    LevelDb的基本结构如下: 由六大部分组成: 一.MemTable,用户写入和读取的直接对象, 二.Immutable MemTable,用户状态写入的对象写满的MemTable之后会转为Immu ...

  2. C#中转义符

    C#转义字符: 一种特殊的字符常量:以反斜线"\"开头,后跟一个或几个字符.具有特定的含义,不同于字符原有的意义,故称“转义”字符.主要用来表示那些用一般字符不便于表示的控制代码. ...

  3. 微信小程序,设置所有标签样式

    page, view, scroll-view, swiper, movable-area, cover-view, text, icon, rich-text, progress, button, ...

  4. Win7命令mklink的使用

    C盘空间越来越小,在Win7里还标红了,心里看得不舒服,得想一些方法腾出一些空间.看了AppData,Chrome占了1G多的空间. 当时安装Chrome浏览器时因为不能指定安装目录,所以Chrome ...

  5. swift xcode设置 ,代码折叠,全局折叠 快捷键

    在preference text editing 里面打开 function 折叠的项, 折叠方法快捷键: option+command +left/right 全局折叠快捷键: shift+opti ...

  6. hdu 3374 String Problem (kmp+最大最小表示法)

    题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=3374 题目大意:输出最大和最小的是从哪一位开始的,同时输出最小循环节的个数. 这里简单介绍对字符串最小 ...

  7. CSS3动画(重要)

    CSS3 动画 CSS3,我们可以创建动画,它可以取代许多网页动画图像,Flash动画,和JAVAScripts. CSS3 @keyframes 规则 要创建CSS3动画,你将不得不了解@keyfr ...

  8. MySQL 之 foreign key

    前段回顾 create table 表名( 字段名1 类型[(宽度) 约束条件], 字段名2 类型[(宽度) 约束条件], 字段名3 类型[(宽度) 约束条件] ); #解释: 类型:使用限制字段必须 ...

  9. 在生成的Debug中test.exe的同级目录下创建一个文件,如TestLog.log

    在上次编写一个日志类库时,想在.exe的同级目录下创建.log文件,对于这个路径的获得很简单,调用GetModuleFileName()函数即可.但是要去掉.exe而换成.log时,由于对字符串处理不 ...

  10. [Leetcode Week9]Minimum Path Sum

    Minimum Path Sum 题解 原创文章,拒绝转载 题目来源:https://leetcode.com/problems/minimum-path-sum/description/ Descr ...