尝试在Apollo上进行优雅的API实现(GraphQL)
首先,
这次的样本已经准备好了,包括Apollo Server和Apollo Client的示例。
我以前写过这样一篇文章。
使用React(+Redux)和gRPC实现的Clean Architecture + 微服务架构
就BFF前端API部分而言,我认为通过阅读下一篇文章,可以用GraphQL来实现客户端通信,这样会更灵活、更强大,所以我尝试了一下。
我想向前端工程师们宣传Apollo Client。
请参考下面的内容,了解REST API和GraphQL之间的区别。
“GraphQL是一种改变应用程序开发流程的技术,与REST有所不同。”
引入GraphQL的好处大致如下。
-
- APIのインタフェース定義(送信パラメータのデータ型定義)ができる(それにともない、データの型定義によるAPIパラメータのデータ型バリデーションチェックができる)
-
- データ取得条件に合わせてGETエンドポイントを大量に作成しなくて済む(GraphQLスキーマ単位になる)
-
- APIのバージョン管理ができる
- APIレスポンスのキャッシュが容易
以下是反面的缺点。
-
- 日本語の記事が少ない
- GraphQLインタフェース定義を学習するコストがかかる
Apollo Client有一个名为Link的功能来管理通信后的数据,因此被期望作为Redux的替代品。
Apollo引入GraphQL
Apollo是一个用于GraphQL前端和后端的库。
需要在后端使用Apollo Server,在前端使用Apollo Client。
此外,还附带了一个名为GraphiQL的可视化编辑器工具,可以轻松进行API的操作验证。
后端(Apollo Server)
支持各种NodeJS框架。
-
- express
-
- koa
-
- hapi
-
- restify
-
- lambda
-
- micro
-
- azure-functions
- adonis
由于本次实现采用 Express,因此我们需要使用 npm 安装支持 Express 的 Apollo。
npm install --save apollo-server-express graphql-tools graphql express body-parser
以下是最小的样本实现。
const express = require('express')
const bodyParser = require('body-parser')
const { graphqlExpress, graphiqlExpress } = require('apollo-server-express')
const { makeExecutableSchema } = require('graphql-tools')
const app = express()
process.on('uncaughtException', (err) => console.error(err))
process.on('unhandledRejection', (err) => console.error(err))
app.use(bodyParser.urlencoded({extended: true}))
app.use(bodyParser.json())
// GraphQLスキーマ定義
const typeDefs = `
"""
type Query (必須)
"""
type Query { books: [Book] }
"""
返却するデータ構造
"""
type Book { title: String, author: String }
`
// ダミーデータ
const books = [
{
title: 'Harry Potter and the Sorcerer\'s stone',
author: 'J.K. Rowling',
},
{
title: 'Jurassic Park',
author: 'Michael Crichton',
},
]
// resolvers
const resolvers = {
Query: { books: () => books },
}
// スキーマ生成
const schema = makeExecutableSchema({
typeDefs,
resolvers,
})
// GraphQLエンドポイント
app.use('/graphql', bodyParser.json(), graphqlExpress({ schema }))
// GraphiQL:GraphQLクエリのvisual editor
// TODO: 本番デプロイ時はアクセス出来ないようにする
app.use('/graphiql', graphiqlExpress({ endpointURL: '/graphql' }))
app.listen(5000, () => {
console.log('Access to http://localhost:5000')
})
请使用以下命令执行。
$ node server.js
当您访问http://localhost:5000/graphiql时,将显示GraphiQL。您可以编写查询并通过点击”▶”按钮来执行,这样您就可以获取虚拟数据。
query {
books {
title,
author,
}
}

GraphQL 的架构定义
以下的文章提供了詳細信息:
《GraphQL入門- 讓您想要使用GraphQL》的文章將細節整理得很好。
在中文中,获取数据使用Query,更新数据使用Mutation。
在模式定义中,定义type Query或type Mutation是必需的。
模式定义如下所示。
const typeDefs = `
type Query {
フィールド名(引数): 返却データ型
}
type Mutation {
フィールド名(引数): 返却データ型
}
}
`
// resolvers
const resolvers = {
Query: {
フィールド名: (引数) => 返却データ,
},
Mutation: {
フィールド名: (引数) => 返却データ,
},
}
数据的基本类型如下所示。
-
- Int: 32bit整数型
-
- Float: 浮動小数型
-
- String: UTF-8 文字列型
-
- Boolean: true もしくは false
- ID: ユニークなスカラー値、キャッシュに使われる。Stringをシリアライズ(直列化)したデータで保存されている
此外,您可以为任意对象单元定义数据类型。以下是定义Book类型的示例。如果要将参数设置为必需,请在数据类型末尾添加!。
type Query {
books: [Book]!,
}
type Book { title: String, author: String }
请参考GraphQL的官方文档:Schemas and Types,以获取更详细的信息。
當定義多個方法時,將方法添加到type Query或type Mutation的區塊內。另外,用”””括起來則成為註解。
const typeDefs = `
type Query {
books: [Book],
items: [Item],
}
"""
返却するデータ構造
"""
type Book { title: String, author: String }
type Item { title: String }
`
// resolvers
const resolvers = {
Query: {
books: () => books,
items: () => items,
},
}
如果需要传递参数,请按照以下方式书写。
解析函数签名
参数将传递给args。
// GraphQLスキーマ定義
const typeDefs = `
type Query {
books(author: String): [Book],
}
type Book { title: String, author: String }
`
// resolvers
const resolvers = {
Query: {
books: (obj, args, context, info) => {
const author = args.author
return books.filter(book => book.author === author)
},
},
}
在GraphiQL中创建以下查询。
query ($author: String!){
books(author: $author) {
title,
author,
}
}
在查询变量中,指定以下搜索条件参数。
JSON的键将存储在$author中。
{
"author": "Michael Crichton"
}

前端(Apollo Client)
首先,我們將從React + Apollo Client嘗試通信至Apollo Server。然後,我們將下載Apollo Client。
npm install --save apollo-boost react-apollo graphql-tag graphql
您可以使用ApolloClient来生成ApolloProvider并通过它来包裹应用程序组件,以便在内部可以使用GraphQL。
/*globals module: false */
import React from 'react'
import ReactDOM from 'react-dom'
import { hot } from 'react-hot-loader'
import { ApolloProvider } from 'react-apollo'
import ApolloClient from 'apollo-boost'
import App from './App'
const client = new ApolloClient({
uri: 'http://localhost:5050/graphql',
})
const render = () => {
ReactDOM.render(
<ApolloProvider client={client}>
<App />
</ApolloProvider>
,
document.getElementById('root'),
)
}
// Webpack Hot Module Replacement API
hot(module)(render)
render()
这是App.js。在qql中编写查询。
通过Query组件来实际调用API。
API的结果将会返回{loading, error, data}。
为了方便使用,我创建了一个名为graphQL的HOC来进行包装。
import React from 'react'
import gql from 'graphql-tag'
import { Query } from 'react-apollo'
// GraphQLクエリ
const query = gql`
query ($author: String!){
books(author: $author) {
title,
author,
}
}
`
// HOC
const graphQL = (param) => (WrappedComponent) => (props) => (
<Query
query={query}
variables={param}>
{({loading, error, data}) => <WrappedComponent {...props} loading={loading} error={error} data={data} />}
</Query>
)
class App extends React.Component {
render () {
const { loading, error, data } = this.props
if (loading) return <p>Loading...</p>
if (error) return <p>Error...</p>
return (<div>
{data.books.map(book =>
<div key={book.title}>
<h4>{book.title}</h4>
<span>{book.author}</span>
</div>
)}
</div>)
}
}
export default graphQL({
author: 'Michael Crichton',
})(App)

我們可以像替代 Redux 的 reducer 和 react-redux 的 connect 這樣做。
以下是一个学习Apollo Client + React的参考。
参考资料:Apollo Client + React入门指南。
Apollo Client能否替代Redux?
在参考文章中,有一篇专栏说GraphQL是否替代了Redux,
Apollo Client交互与GraphQL通信的结果状态管理,包括服务器端渲染等与通信相关的部分(比如redux-thunk和redux-saga)可以替代Redux。(主要涉及到react-redux的connect和reducer)
相反,对于通信以外的应用程序数据(例如保持画面转换状态的react-router-redux等),我觉得管理机制较为薄弱(或者说不存在?)。(apollo-link-state似乎只处理通信相关)
基于上述情况,目前似乎无法摒弃Redux,需要两个本地存储,这可能是一种反模式。(相反,如果这个问题解决了,我很想积极使用)
从路由来看,貌似可以将history传递给props?
基于GraphQL的设计思想本身非常好,我认为与Rest API相比,它使通信更加智能。