! # @zetavg

fb.me/pokaichang72
串串起前後端資料的橋樑

GraphQL 

& Relay
Q: 這個 app 需要多少 API
Endpoints?
Ans: 4
GET /api/todos.json
POST /api/todos.json
PATCH /api/todos/id.json
DELETE /api/todos/id.json
Q: 那這個 app 需要多少 API
Endpoints 呢?
/api/v1/posts.json
/api/v2/posts.json
/api/v3/posts.json
/api/v4/posts.json
/api/v65535/posts.json
⋯⋯
/api/posts.json
/api/posts.json?include=author
/api/posts.json?include=author,comments
/api/posts.json?cover=true&include=author,comments
/api/posts.json?cover=true&include=author,comments_with
API 應該是這樣
不是這樣
Q: 那這個 app 需要多少 API
Endpoints 呢?
Ans: 1
Ans: 1
GraphQL
$
{
"name": "Neson",
"bio":"Yet another geek.",
"followers": [◌, ◌, ◌],
"repos": [◌, ◌, ◌]

}
$
{
"name": "Lucy",
"bio":"...",
"followers": [◌],
"repos": [◌, ◌]

}
$
{
"name": "Ja
"bio":"..."
"followers"
"repos": [◌
}
$
{
"name": "Pusheen",
"bio":"Nyan nyan nyan~",
"followers": [◌],
"repos": [◌]

}
!
{
"name": "Thing Compiler",
"description": "...",
"stargazers": [◌, ◌, ◌]

}
!
{
"name": "Hello World",
"description": "...",
"stargazers": [◌, ◌]

}
!
{
"name": "Handy Ut
"description": ".
"stargazers": [◌]
}
!
{
"name": "Awes
"description"
"stargazers":
}
!
{
"name": "Todo",
"description": "...",
"stargazers": [◌]

}
!
{ }
{
"viewer": ◌

}
GraphQL
$
{
"name": "Neson",
"bio":"Yet another geek.",
"followers": [◌, ◌, ◌],
"repos": [◌, ◌, ◌]

}
$
{
"name": "Lucy",
"bio":"...",
"followers": [◌],
"repos": [◌, ◌]

}
$
{
"name": "Ja
"bio":"..."
"followers"
"repos": [◌
}
$
{
"name": "Pusheen",
"bio":"Nyan nyan nyan~",
"followers": [◌],
"repos": [◌]

}
!
{
"name": "Thing Compiler",
"description": "...",
"stargazers": [◌, ◌, ◌]

}
!
{
"name": "Hello World",
"description": "...",
"stargazers": [◌, ◌]

}
!
{
"name": "Handy Ut
"description": ".
"stargazers": [◌]
}
!
{
"name": "Awes
"description"
"stargazers":
}
!
{
"name": "Todo",
"description": "...",
"stargazers": [◌]

}
!
{ }
{
"viewer": ◌

}
$
{
"name": "Pusheen",
"bio":"Nyan nyan nyan~",
"followers": [◌],
"repos": [◌]

}
$
{
"name"
"bio":
"follo
"repos
}
!
!
{
"name": "Todo",
"description": "...",
"stargazers": [◌]

}
!
{
"name": "Thing Compiler",
"description": "...",
"stargazers": [◌, ◌, ◌]

}
!
{
"name": "Thi
"description
"stargazers"
}
!
{ }
{
"viewer": ◌

}
$
{
"name": "Neson",
"bio":"Yet another geek.",
"followers": [◌, ◌, ◌],
"repos": [◌, ◌, ◌]

}
$ dd
dd
dd
dd
!
{
"name": "Thing Compiler",
"description": "...",
"stargazers": [◌, ◌, ◌]

}
!
{
"name": "Thi
"description
"stargazers"
}
!
!
!
{
"name": "Todo",
"description": "...",
"stargazers": [◌]

}
{ }
{
"viewer": ◌

}
$
{
"name": "Neson",
"bio":"Yet another geek.",
"followers": [◌, ◌, ◌],
"repos": [◌, ◌, ◌]

}
$
{
"name"
"bio":
"follo
"repos
}
$
{
"name": "Pusheen",
"bio":"Nyan nyan nyan~",
"followers": [◌],
"repos": [◌]

}
$
$
$
$
$
$
!
!
{
"name": "Thi
"description
"stargazers"
}
$
{
"name": "Pusheen",
"bio":"Nyan nyan nyan~",
"followers": [◌],
"repos": [◌]

}
!
!
{
"name": "Todo",
"description": "...",
"stargazers": [◌]

}
!
{
"name": "Thing Compiler",
"description": "...",
"stargazers": [◌, ◌, ◌]

}
{ }
{
"viewer": ◌

}
$ $ $
$ $ $
$ $ $
$ $
/
$
{
"name"
"bio":
"follo
"repos
}
$
{
"name": "Neson",
"bio":"Yet another geek.",
"followers": [◌, ◌, ◌],
"repos": [◌, ◌, ◌]

}
⬡ GraphQL 簡介
⬡ Relay ⼿手感評測
⬡ Relay 基本使⽤用
⬡ 全端案例例:GraphQL & Relay on Rails

https://github.com/zetavg/RailsRelayTodoMVC
不會講到跟 Apollo 的比較
Outline
⬡ GraphQL 簡介
⬡ Relay ⼿手感評測
⬡ Relay 基本使⽤用
⬡ 全端案例例:GraphQL & Relay on Rails

https://github.com/zetavg/RailsRelayTodoMVC
不會講到跟 Apollo 的比較
Outline
基本查詢
⬢ 最外層⼀一定是 query
⬢ 問什什麼得什什麼
{
"data": {
"viewer": {
"name": "Pokai Chang"
}
}
}
query {
viewer {
name
}
}
查詢更更多
⬢ 巢狀狀選取欄欄位 (field)
{
"data": {
"viewer": {
"name": "Pokai Chang",
"birthday": {
"month": 7,
"day": 2
}
}
}
}
query {
viewer {
name
birthday {
month
day
}
}
}
巢狀狀查更更多
⬢ 底下的資料 (node) 可能是同⼀一種類型 (type)
{
"data": {
"viewer": {
"name": "Pokai Chang",
"following": [
{ "name": "..." },
{ "name": "..." },
{ "name": "..." }
]
}
}
}
query {
viewer {
name
following {
name
}
}
}
巢狀狀查更更多多多多
⬢ 可以幹奇怪的事⋯⋯
{
"data": {
"viewer": {
"name": "Pokai Chang",
"following": [
{
"name": "…",
"followers": [
{
"name": "Pokai Chang",
"following": [
{
"name": "…",
"followers": [
{
"name": "Pokai C
query {
viewer {
name
following {
name
followers {
following {
name
followers {
name
}
}
}
}
}
}
型別定義即⽂文件
query {
viewer {
name
birthday {
month
day
}
following {
name
}
}
}
type Query {
viewer: User
}
type User {
name: String
birthday: Date
followers: [User]
following: [User]
}
type Date {
year: Integer
month: Integer
day: Integer
}
$
{
"name": "Neson",
"bio":"Yet another geek.",
"followers": [◌, ◌, ◌],
"repos": [◌, ◌, ◌]

}
$
{
"name": "Lucy",
"bio":"...",
"followers": [◌],
"repos": [◌, ◌]

}
$
{
"name": "Ja
"bio":"..."
"followers"
"repos": [◌
}
$
{
"name": "Pusheen",
"bio":"Nyan nyan nyan~",
"followers": [◌],
"repos": [◌]

}
!
{
"name": "Thing Compiler",
"description": "...",
"stargazers": [◌, ◌, ◌]

}
!
{
"name": "Hello World",
"description": "...",
"stargazers": [◌, ◌]

}
!
{
"name": "Handy Ut
"description": ".
"stargazers": [◌]
}
!
{
"name": "Awes
"description"
"stargazers":
}
!
{
"name": "Todo",
"description": "...",
"stargazers": [◌]

}
!
{ }
{
"viewer": ◌

}
$
{
"name"
"bio":
"follo
"repos
}
$
{
"name": "Pusheen",
"bio":"Nyan nyan nyan~",
"followers": [◌],
"repos": [◌]

}
!
{
"name": "Thing Compiler",
"description": "...",
"stargazers": [◌, ◌, ◌]

}
!
{
"name": "Thi
"description
"stargazers"
}
!
!
!
{
"name": "Todo",
"description": "...",
"stargazers": [◌]

}
{ }
{
"viewer": ◌

}
$
{
"name": "Neson",
"bio":"Yet another geek.",
"followers": [◌, ◌, ◌],
"repos": [◌, ◌, ◌]

}
query {
viewer {
name
bio
}
}
!
{
"name": "Thing Compiler",
"description": "...",
"stargazers": [◌, ◌, ◌]

}
!
{
"name": "Thi
"description
"stargazers"
}
!
!
!
{
"name": "Todo",
"description": "...",
"stargazers": [◌]

}
{ }
{
"viewer": ◌

}
$
{
"name": "Neson",
"bio":"Yet another geek.",
"followers": [◌, ◌, ◌],
"repos": [◌, ◌, ◌]

}
$
{
"name"
"bio":
"follo
"repos
}
$
{
"name": "Pusheen",
"bio":"Nyan nyan nyan~",
"followers": [◌],
"repos": [◌]

}
query {
viewer {
name
bio
followers {
name
}
}
}
!
!
{
"name": "Todo",
"description": "...",
"stargazers": [◌]

}
$
{
"name"
"bio":
"follo
"repos
}
$
{
"name": "Pusheen",
"bio":"Nyan nyan nyan~",
"followers": [◌],
"repos": [◌]

}
!
{
"name": "Thing Compiler",
"description": "...",
"stargazers": [◌, ◌, ◌]

}
!
{
"name": "Thi
"description
"stargazers"
}
!
{ }
{
"viewer": ◌

}
$
{
"name": "Neson",
"bio":"Yet another geek.",
"followers": [◌, ◌, ◌],
"repos": [◌, ◌, ◌]

}
query {
viewer {
name
bio
followers {
name
}
repos {
name
stargazers {
name
}
}
}
}
{ }
{
"viewer": ◌

}
!
!
{
"name": "Todo",
"description": "...",
"stargazers": [◌]

}
$
{
"name": "Neson",
"bio":"Yet another geek.",
"followers": [◌, ◌, ◌],
"repos": [◌, ◌, ◌]

}
$
{
"name"
"bio":
"follo
"repos
}
$
{
"name": "Pusheen",
"bio":"Nyan nyan nyan~",
"followers": [◌],
"repos": [◌]

}
!
{
"name": "Thing Compiler",
"description": "...",
"stargazers": [◌, ◌, ◌]

}
!
{
"name": "Thi
"description
"stargazers"
}
!
query {
viewer {
name
bio
followers {
name
}
repos {
name
stargazers {
name
}
}
girlfriend {
name
}
}
}
'
query {
viewer {
name
bio
followers {
name
}
repos {
name
stargazers {
name
}
}
girlfriend {
name
}
}
}
{
"errors": [
{
"message": "Field 'girlfriend' doesn't

exist on type ‘User’",
...
}
]
}
'
Arguments 參參數
⬢ 每個 field 會定義可⽤用的參參數
query {
user(id: 1) {
name
}
}
Arguments 參參數
⬢ 也可以做巢狀狀查詢
query {
user(id: 1) {
name
repo(name: "awesome-graphql") {
name
description
}
}
}
Fragment 片段
fragment profileFields on User {
name
bio
avatarUrl
}
query {
viewer {
...profileFields
}
user(id: 1) {
...profileFields
}
}
先把固定會⽤用到的欄欄位
存成有意義的片段
改資料 ? Mutation
⬢ query 改成 mutation,data 放在 arguments
⬢ 其實就像 HTTP GET/POST,只是慣例例,沒有硬性限制
mutation {
addComment(input: { subjectId: 1, body: "Hi." }) {
subject {
comments {
body
}
}
}
}
graphql.org
⬡ GraphQL 簡介
⬡ Relay ⼿手感評測
⬡ Relay 基本使⽤用
⬡ 全端案例例:GraphQL & Relay on Rails

https://github.com/zetavg/RailsRelayTodoMVC
不會講到跟 Apollo 的比較
Outline
無限捲軸
Caching
Prefetch Caching
Server Data Updating
Optimistic Update
Realtime Update
facebook.github.io/relay
Relay



Relay Modern
(Relay 1.0)
const View = (data) => UI
Redux 資料流
View
State
subscribe
Redux Store
Redux 資料流
View
State
Reducer
Action
subscribe
prevState
Redux 資料流
View
State
Reducer
Action
subscribe
prevState
Backend ?
Redux 資料流 ?
View
State
Reducer
Action
subscribe
prevState
Backend
Action
Action
Action
Redux 資料流 ?
View
Action
Action
Action
Action
Backend
State
State
State
Reducer
Reducer Reducer
Reducer
???
???
$
{
"name": "Neson",
"bio":"Yet another geek.",
"followers": [◌, ◌, ◌],
"repos": [◌, ◌, ◌]

}
$
{
"name": "Lucy",
"bio":"...",
"followers": [◌],
"repos": [◌, ◌]

}
$
{
"name": "Ja
"bio":"..."
"followers"
"repos": [◌
}
$
{
"name": "Pusheen",
"bio":"Nyan nyan nyan~",
"followers": [◌],
"repos": [◌]

}
!
{
"name": "Thing Compiler",
"description": "...",
"stargazers": [◌, ◌, ◌]

}
!
{
"name": "Hello World",
"description": "...",
"stargazers": [◌, ◌]

}
!
{
"name": "Handy Ut
"description": ".
"stargazers": [◌]
}
!
{
"name": "Awes
"description"
"stargazers":
}
!
{
"name": "Todo",
"description": "...",
"stargazers": [◌]

}
!
{ }
{
"viewer": ◌

}
GraphQL
Relay 的狀狀況
View
$
$
$ $
$
dd
$ $ $ $
dd
dd
dd
Relay Store
Relay 的狀狀況
View
$
$ $
$
dd
$ $ $ $
dd
dd
dd
Relay Store
viewer {
name
bio
}
$
Backend
query {
viewer {
name
bio
}
}
{
"data": {
"viewer": {
"name": "…",
"Bio": "…"
}
}
}
Relay 的狀狀況
View
ㄎㄎㄎㄎ$
$ $
$
dd
$ $ $ $
dd
dd
dd
Relay Store
viewer {
name
bio
}
$
Relay 的狀狀況
View
ㄎㄎㄎㄎ
dd
dd
dd
dd
$ $
$ $ $ $ $
viewer {
name
bio
}
$
$
viewer {
followers {
name
}
}
Backend
query {
viewer {
followers {
name
}
}
}
{
"data": {
"viewer": {
"followers": […]
}
}
}
Relay Store
Request only the diff!
Relay 的狀狀況
View
ㄎㄎㄎㄎ
dd
dd
dd
dd
$ $
$ $ $ $ $
viewer {
name
bio
}
$
$
viewer {
followers {
name
}
}
Relay 的狀狀況
viewer {
name
bio
}
View
$
ㄎㄎㄎㄎ$
$ $
$ $ $ $ $
viewer {
followers {
name
}
}
dd
dd
dd
dd
viewer {
repos {
name
description
}
}
Backend
query {
viewer {
repos {
name
description
}
}
}
{
"data": {
"viewer": {
"repos": […]
}
}
}
Relay 的狀狀況
viewer {
name
bio
}
View
$
ㄎㄎㄎㄎ$
$ $
$ $ $ $ $
viewer {
followers {
name
}
}
dd
dd
dd
dd
viewer {
repos {
name
description
}
}
Relay 的狀狀況
View
$
ㄎㄎㄎㄎ$
$ $
$ $ $ $ $
dd
dd
dd
dd
RenameRepoMutation(
repoID: "…",
name: ""
)
Relay 的狀狀況
View
$
ㄎㄎㄎㄎ$
$ $
$ $ $ $ $
dd
dd
dd
dd
RenameRepoMutation(
repoID: "…",
name: ""
)
Optimistic Updater
Relay 的狀狀況
View
$
ㄎㄎㄎㄎ$
$ $
$ $ $ $ $
dd
dd
dd
dd
RenameRepoMutation(
repoID: "…",
name: ""
)
Optimistic UpdaterBackend
Updater
Relay 的狀狀況
View
$
ㄎㄎㄎㄎ$
$ $
$ $ $ $ $
dd
dd
dd
dd
RenameRepoMutation(
repoID: "…",
name: ""
)
Optimistic UpdaterBackend
Updater
想分⾴頁 ? Connections
⬢ 游標分⾴頁法:Relay Cursor Connections
query {
viewer {
friends(first: 10, after: "someCursor") {
edges {
cursor
node {
id
name
}
}
pageInfo {
hasNextPage
}
}
}
}
Edge (UserEdgeType)
Node (UserType)
{ … }
Cursor
游標 Connection
Edges
Edge (UserEdgeType)
Node (UserType)
{ … }
Cursor
Edge (UserEdgeTy
Node (UserType
{ … }
Cursor
Page Info
起始游標
⬢ Offset based pagination
⬢ Cursor based pagination
Why Cursor?
page 1 page 2
page 1 page 2
page 3
page 3
' 壞ㄌ
next 5next 5
⬡ Relay 可信的 Optimistic Updater 機制可以做
「連線問題時重試」甚⾄至離線更更動上線後同步
⬡ Relay 的 backend 不⼀一定要是 server,也許可以
是裝置上的 database
⬡ 再狂⼀一點,也許我們還能透過⾃自⼰己實作 Relay
RecordSource 組出這樣的架構:
Fun Things to Try
RelayRelay ViewServer
Client DB Memory
Optimistic Update
Management
{歡樂 Demo 時間}
適⽤用情況
⬢ Data driven,data 越多越雜,投資越划算
⬢ React app
⬢ 略略懂後端
⬢ 踩雷的勇氣
Relay Environment
Handler Provider
Network Layer
Store
Record Source
GraphQL Endpoint
Query Renderer
Container
Component
Fragment
Query
跟後端不熟?
⬡ GraphQL 簡介
⬡ Relay ⼿手感評測
⬡ Relay 基本使⽤用
⬡ 全端案例例:GraphQL & Relay on Rails

https://github.com/zetavg/RailsRelayTodoMVC
不會講到跟 Apollo 的比較
Outline
在 React 專案中安裝 Relay
$ yarn add react-relay
安裝 GraphQL 語法識別外掛
$ yarn add --dev babel-plugin-relay
記得⼿手動加 Babel Plugin
⬢ 編輯 .eslintrc 或 package.json
"plugins": [
"relay",
]
安裝 Relay GraphQL 編譯器
$ yarn add --dev relay-compiler
使⽤用 relay-compiler
⬢ 在 JavaScript 裡寫的 GraphQL 需要被事先編譯⋯⋯



⬢ 改了了 graphql`…` tag 裡⾯面的內容後都要跑⼀一次
⬢ 或是加上 --watch 參參數監看變化
$ relay-compiler --src ./src --schema ./schema.graphql
源碼⽬目錄 GraphQL Schema 檔位置
import { Environment } from ‘relay-runtime’
const environment = new Environment({
network,
store,
})
export default environment
環境準備
Relay Environment
Handler Provider
Network Layer
Store
Record Source
GraphQL Endpoint
Network Layer
Store
⬢ 先看整個環境⋯⋯
import { Network } from ‘relay-runtime'
const API_ENDPOINT = 'https://api.github.com/graphql'
const fetchQuery = (operation, variables) => {
return fetch(API_ENDPOINT, {
method: 'POST',
body: JSON.stringify({
query: operation.text,
variables,
}),
}).then(response => response.json())
}
const network = Network.create(fetchQuery)
exports default network
Relay 網路路層
⬢ 給⼀一個溝通介⾯面,⼀一般就是包裝 fetch
Relay Environment
Handler Provider
Network Layer
Store
Record Source
GraphQL Endpoint
import {
RecordSource,
Store,
} from 'relay-runtime'
const source = new RecordSource()
const store = new Store(source)
export default store
Relay 倉儲
Relay Environment
Handler Provider
Network Layer
Store
Record Source
GraphQL Endpoint
⬢ Relay 內建具備 garbage collection 的 in-memory
record source
以上,前置準備完成 🎉
const MyProfile = () => (
<QueryRenderer
environment={environment}
query={graphql`
query MyProfileQuery {
viewer {
name
}
}
`}
render={({ error, props }) => {
if (error) {
return <Text>{error.message}</Text>
} else if (props) {
return <Text>Hello, {props.viewer.name}!</Text>
}
return <Text>Loading...</Text>
}}
/>
)
基本招 QueryRenderer
可以設計載入中假畫⾯面
注意名字有規定⽤用 "[檔案名稱]Query"
Relay Environment
Handler Provider
Network Layer
Store
Record Source
GraphQL Endpoint
Query Renderer
Component
Query
const UserProfileComponent = ({ user }) => (
<Text>Hello, {user.name}!</Text>
)
const UserProfile = createFragmentContainer(
UserProfileComponent,
graphql`
fragment UserProfile_user on User {
name
login
avatarUrl
}
`,
)
export default UserProfile
拆元件 FragmentContainer
注意名字也有規定⽤用 "[檔案名稱]_[prop 名稱]"
Relay Environment
Store
Query Renderer
Container
Component
Fragment
Query
拆元件 FragmentContainer
直接引⽤用定義在 Container 裡的 Fragment
Relay Environment
Store
Query Renderer
Container
Component
Fragment
Query
import MyProfile from '...'
const MyProfile = () => (
<QueryRenderer
environment={environment}
query={graphql`
query MyProfileQuery {
viewer {
...UserProfile_user
}
}
`}
render={({ error, props }) => {
if (error) {
return <Text>{error.message}</Text>
} else if (props) {
return <MyProfile user={props.viewer} />
}
return <Text>Loading...</Text>
}}
/>
)
Component Tree 層層疊疊
Query Renderer
Container Fragment
Query
ContainerContainer
Component Component
FragmentFragment
const UserProfileComponent = ({ user }) => (
<View>
<UserName user={user} />
<UserBio user={user} />
</View>
)
const UserProfile = createFragmentContainer(
UserProfileComponent,
graphql`
fragment UserProfile_user on User {
...UserName_user
...UserBio_user
}
`,
)
const UserNameComponent = ({ user }) => (
<Text>Hello, {user.name}!</Text>
)
const UserName = createFragmentContainer(
UserNameComponent,
graphql`
fragment UserName_user on User {
name
avatarUrl
}
`,
)
const UserBioComponent = ({ user }) => (
<Text>{user.bio}</Text>
)
const UserBio = createFragmentContainer(
UserBioComponent,
graphql`
fragment UserBio_user on User {
bio
}
`,
)
特種 Container
⬢ RefetchContainer
⬡ 提供 instance method 可以改⽤用不同 arguments 索取資料
⬢ PaginationContainer
⬡ RefetchContainer 進化版
⬡ ⽤用 Relay Connection 做分⾴頁專⽤用
⬡ 可以做出 loadMore()、refresh() 等 method 給無限捲軸和
pull to refresh 使⽤用
Mutation
const mutation = graphql`
mutation AddCommentMutation(
$input: AddCommentInput!
) {
addComment(input: $input) {
clientMutationId
commentEdge {
cursor
node {
id
body
}
}
subject {
id
commentsCount
}
}
}
`
let variables = {
input: {
subjectId: "aGVsbG8=",
body: "Hello Relay",
}
}
commitMutation(
environment,
{
mutation,
variables,
onCompleted: (response) => {
console.log('Success!')
},
onError: (err) => {
console.error(err)
},
},
)
1
2
3
1
2
3
⼀一樣有命名規定
宣告 $input 變數以及型別,內容可以在使⽤用時再指定
與 query ⼀一樣列列出欄欄位,這裡需要列列出所有 mutation 執⾏行行後會變更更或新增的資料
2
⬢ 還有需要做的事⋯⋯
Mutation Updater
const mutation = graphql`
mutation AddCommentMutation(
$input: AddCommentInput!
) {
addComment(input: $input) {
clientMutationId
commentEdge {
cursor
node {
id
body
}
}
subject {
id
commentsCount
}
}
}
`
Mutation 執⾏行行後,我們需要把新的 edge 安插到

某個 subject 底下的 comments connection
若若在 payload root 發現具有 id 的 node,

Relay 慣例例會幫我們更更新好 store 中相應的 node
Mutation Updater
commitMutation(
environment,
{
mutation,
variables: { ... },
updater: (store) => {
const payload = store.getRootField('addComment')
const newEdge = payload.getLinkedRecord('commentEdge')
const subject = payload.getLinkedRecord('subject')
const conn =
ConnectionHandler.getConnection(subject, ‘commentsConnection')
ConnectionHandler.insertEdgeAfter(conn, newEdge)
},
optimisticUpdater: (store) => {
…
},
},
)
可⾃自訂 store 的 update ⽅方式
Mutation Updater
ConnectionHandler.insertEdgeAfter(conn, newEdge)
},
optimisticUpdater: (store) => {
const subject = store.get(subjectID)
const newCommentID = `client:newComment:${tempID++}`
const newNode = store.create(newCommentID, 'Comment')
newNode.setValue(newCommentID, 'id')
newNode.setValue(variables.input.body, 'body')
const newEdge = store.create(
`client:newCommentEdge:${tempID++}`,
'TodoItemEdge',
)
newEdge.setLinkedRecord(newNode, 'node')
const conn = ConnectionHandler.getConnection(subject, 'commentsConnection')
ConnectionHandler.insertEdgeAfter(conn, newEdge)
subject.setValue(
subject.getValue('commentsCount') + 1,
'commentsCount',
)
},
},
)
製作出暫時的 node
製作出暫時的 edge
把暫時的 edge 安插進 connection
⼿手動更更新計數器
⬡ GraphQL 簡介
⬡ Relay ⼿手感評測
⬡ Relay 基本使⽤用
⬡ 全端案例例:GraphQL & Relay on Rails

https://github.com/zetavg/RailsRelayTodoMVC
不會講到跟 Apollo 的比較
Outline
(GraphQL + Relay) on Rails
(GraphQL + Relay) on Rails
全端⼀一天可以寫完,拿來來做 RN 更更顯神⼒力力
(GraphQL + Relay) on Rails
Debug 省下的時間可以拿來來調各種 1px
Image source: https://blog.codinghorror.com/learn-to-read-the-source-luke/
請⾒見見 https://github.com/zetavg/RailsRelayTodoMVC
Thanks + Q&A

GraphQL & Relay - 串起前後端世界的橋樑

  • 1.
  • 2.
    Q: 這個 app需要多少 API Endpoints?
  • 3.
    Ans: 4 GET /api/todos.json POST/api/todos.json PATCH /api/todos/id.json DELETE /api/todos/id.json
  • 4.
    Q: 那這個 app需要多少 API Endpoints 呢?
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
    Q: 那這個 app需要多少 API Endpoints 呢?
  • 10.
  • 11.
  • 12.
    $ { "name": "Neson", "bio":"Yet anothergeek.", "followers": [◌, ◌, ◌], "repos": [◌, ◌, ◌]
 } $ { "name": "Lucy", "bio":"...", "followers": [◌], "repos": [◌, ◌]
 } $ { "name": "Ja "bio":"..." "followers" "repos": [◌ } $ { "name": "Pusheen", "bio":"Nyan nyan nyan~", "followers": [◌], "repos": [◌]
 } ! { "name": "Thing Compiler", "description": "...", "stargazers": [◌, ◌, ◌]
 } ! { "name": "Hello World", "description": "...", "stargazers": [◌, ◌]
 } ! { "name": "Handy Ut "description": ". "stargazers": [◌] } ! { "name": "Awes "description" "stargazers": } ! { "name": "Todo", "description": "...", "stargazers": [◌]
 } ! { } { "viewer": ◌
 } GraphQL
  • 13.
    $ { "name": "Neson", "bio":"Yet anothergeek.", "followers": [◌, ◌, ◌], "repos": [◌, ◌, ◌]
 } $ { "name": "Lucy", "bio":"...", "followers": [◌], "repos": [◌, ◌]
 } $ { "name": "Ja "bio":"..." "followers" "repos": [◌ } $ { "name": "Pusheen", "bio":"Nyan nyan nyan~", "followers": [◌], "repos": [◌]
 } ! { "name": "Thing Compiler", "description": "...", "stargazers": [◌, ◌, ◌]
 } ! { "name": "Hello World", "description": "...", "stargazers": [◌, ◌]
 } ! { "name": "Handy Ut "description": ". "stargazers": [◌] } ! { "name": "Awes "description" "stargazers": } ! { "name": "Todo", "description": "...", "stargazers": [◌]
 } ! { } { "viewer": ◌
 }
  • 14.
    $ { "name": "Pusheen", "bio":"Nyan nyannyan~", "followers": [◌], "repos": [◌]
 } $ { "name" "bio": "follo "repos } ! ! { "name": "Todo", "description": "...", "stargazers": [◌]
 } ! { "name": "Thing Compiler", "description": "...", "stargazers": [◌, ◌, ◌]
 } ! { "name": "Thi "description "stargazers" } ! { } { "viewer": ◌
 } $ { "name": "Neson", "bio":"Yet another geek.", "followers": [◌, ◌, ◌], "repos": [◌, ◌, ◌]
 } $ dd dd dd dd
  • 15.
    ! { "name": "Thing Compiler", "description":"...", "stargazers": [◌, ◌, ◌]
 } ! { "name": "Thi "description "stargazers" } ! ! ! { "name": "Todo", "description": "...", "stargazers": [◌]
 } { } { "viewer": ◌
 } $ { "name": "Neson", "bio":"Yet another geek.", "followers": [◌, ◌, ◌], "repos": [◌, ◌, ◌]
 } $ { "name" "bio": "follo "repos } $ { "name": "Pusheen", "bio":"Nyan nyan nyan~", "followers": [◌], "repos": [◌]
 } $ $ $ $ $ $
  • 16.
    ! ! { "name": "Thi "description "stargazers" } $ { "name": "Pusheen", "bio":"Nyannyan nyan~", "followers": [◌], "repos": [◌]
 } ! ! { "name": "Todo", "description": "...", "stargazers": [◌]
 } ! { "name": "Thing Compiler", "description": "...", "stargazers": [◌, ◌, ◌]
 } { } { "viewer": ◌
 } $ $ $ $ $ $ $ $ $ $ $ / $ { "name" "bio": "follo "repos } $ { "name": "Neson", "bio":"Yet another geek.", "followers": [◌, ◌, ◌], "repos": [◌, ◌, ◌]
 }
  • 17.
    ⬡ GraphQL 簡介 ⬡Relay ⼿手感評測 ⬡ Relay 基本使⽤用 ⬡ 全端案例例:GraphQL & Relay on Rails
 https://github.com/zetavg/RailsRelayTodoMVC 不會講到跟 Apollo 的比較 Outline
  • 18.
    ⬡ GraphQL 簡介 ⬡Relay ⼿手感評測 ⬡ Relay 基本使⽤用 ⬡ 全端案例例:GraphQL & Relay on Rails
 https://github.com/zetavg/RailsRelayTodoMVC 不會講到跟 Apollo 的比較 Outline
  • 19.
    基本查詢 ⬢ 最外層⼀一定是 query ⬢問什什麼得什什麼 { "data": { "viewer": { "name": "Pokai Chang" } } } query { viewer { name } }
  • 20.
    查詢更更多 ⬢ 巢狀狀選取欄欄位 (field) { "data":{ "viewer": { "name": "Pokai Chang", "birthday": { "month": 7, "day": 2 } } } } query { viewer { name birthday { month day } } }
  • 21.
    巢狀狀查更更多 ⬢ 底下的資料 (node)可能是同⼀一種類型 (type) { "data": { "viewer": { "name": "Pokai Chang", "following": [ { "name": "..." }, { "name": "..." }, { "name": "..." } ] } } } query { viewer { name following { name } } }
  • 22.
    巢狀狀查更更多多多多 ⬢ 可以幹奇怪的事⋯⋯ { "data": { "viewer":{ "name": "Pokai Chang", "following": [ { "name": "…", "followers": [ { "name": "Pokai Chang", "following": [ { "name": "…", "followers": [ { "name": "Pokai C query { viewer { name following { name followers { following { name followers { name } } } } } }
  • 23.
    型別定義即⽂文件 query { viewer { name birthday{ month day } following { name } } } type Query { viewer: User } type User { name: String birthday: Date followers: [User] following: [User] } type Date { year: Integer month: Integer day: Integer }
  • 24.
    $ { "name": "Neson", "bio":"Yet anothergeek.", "followers": [◌, ◌, ◌], "repos": [◌, ◌, ◌]
 } $ { "name": "Lucy", "bio":"...", "followers": [◌], "repos": [◌, ◌]
 } $ { "name": "Ja "bio":"..." "followers" "repos": [◌ } $ { "name": "Pusheen", "bio":"Nyan nyan nyan~", "followers": [◌], "repos": [◌]
 } ! { "name": "Thing Compiler", "description": "...", "stargazers": [◌, ◌, ◌]
 } ! { "name": "Hello World", "description": "...", "stargazers": [◌, ◌]
 } ! { "name": "Handy Ut "description": ". "stargazers": [◌] } ! { "name": "Awes "description" "stargazers": } ! { "name": "Todo", "description": "...", "stargazers": [◌]
 } ! { } { "viewer": ◌
 }
  • 25.
    $ { "name" "bio": "follo "repos } $ { "name": "Pusheen", "bio":"Nyan nyannyan~", "followers": [◌], "repos": [◌]
 } ! { "name": "Thing Compiler", "description": "...", "stargazers": [◌, ◌, ◌]
 } ! { "name": "Thi "description "stargazers" } ! ! ! { "name": "Todo", "description": "...", "stargazers": [◌]
 } { } { "viewer": ◌
 } $ { "name": "Neson", "bio":"Yet another geek.", "followers": [◌, ◌, ◌], "repos": [◌, ◌, ◌]
 } query { viewer { name bio } }
  • 26.
    ! { "name": "Thing Compiler", "description":"...", "stargazers": [◌, ◌, ◌]
 } ! { "name": "Thi "description "stargazers" } ! ! ! { "name": "Todo", "description": "...", "stargazers": [◌]
 } { } { "viewer": ◌
 } $ { "name": "Neson", "bio":"Yet another geek.", "followers": [◌, ◌, ◌], "repos": [◌, ◌, ◌]
 } $ { "name" "bio": "follo "repos } $ { "name": "Pusheen", "bio":"Nyan nyan nyan~", "followers": [◌], "repos": [◌]
 } query { viewer { name bio followers { name } } }
  • 27.
    ! ! { "name": "Todo", "description": "...", "stargazers":[◌]
 } $ { "name" "bio": "follo "repos } $ { "name": "Pusheen", "bio":"Nyan nyan nyan~", "followers": [◌], "repos": [◌]
 } ! { "name": "Thing Compiler", "description": "...", "stargazers": [◌, ◌, ◌]
 } ! { "name": "Thi "description "stargazers" } ! { } { "viewer": ◌
 } $ { "name": "Neson", "bio":"Yet another geek.", "followers": [◌, ◌, ◌], "repos": [◌, ◌, ◌]
 } query { viewer { name bio followers { name } repos { name stargazers { name } } } }
  • 28.
    { } { "viewer": ◌
 } ! ! { "name":"Todo", "description": "...", "stargazers": [◌]
 } $ { "name": "Neson", "bio":"Yet another geek.", "followers": [◌, ◌, ◌], "repos": [◌, ◌, ◌]
 } $ { "name" "bio": "follo "repos } $ { "name": "Pusheen", "bio":"Nyan nyan nyan~", "followers": [◌], "repos": [◌]
 } ! { "name": "Thing Compiler", "description": "...", "stargazers": [◌, ◌, ◌]
 } ! { "name": "Thi "description "stargazers" } ! query { viewer { name bio followers { name } repos { name stargazers { name } } girlfriend { name } } } '
  • 29.
    query { viewer { name bio followers{ name } repos { name stargazers { name } } girlfriend { name } } } { "errors": [ { "message": "Field 'girlfriend' doesn't
 exist on type ‘User’", ... } ] } '
  • 30.
    Arguments 參參數 ⬢ 每個field 會定義可⽤用的參參數 query { user(id: 1) { name } }
  • 31.
    Arguments 參參數 ⬢ 也可以做巢狀狀查詢 query{ user(id: 1) { name repo(name: "awesome-graphql") { name description } } }
  • 32.
    Fragment 片段 fragment profileFieldson User { name bio avatarUrl } query { viewer { ...profileFields } user(id: 1) { ...profileFields } } 先把固定會⽤用到的欄欄位 存成有意義的片段
  • 33.
    改資料 ? Mutation ⬢query 改成 mutation,data 放在 arguments ⬢ 其實就像 HTTP GET/POST,只是慣例例,沒有硬性限制 mutation { addComment(input: { subjectId: 1, body: "Hi." }) { subject { comments { body } } } }
  • 34.
  • 35.
    ⬡ GraphQL 簡介 ⬡Relay ⼿手感評測 ⬡ Relay 基本使⽤用 ⬡ 全端案例例:GraphQL & Relay on Rails
 https://github.com/zetavg/RailsRelayTodoMVC 不會講到跟 Apollo 的比較 Outline
  • 36.
    無限捲軸 Caching Prefetch Caching Server DataUpdating Optimistic Update Realtime Update
  • 37.
  • 38.
  • 40.
    const View =(data) => UI
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
    $ { "name": "Neson", "bio":"Yet anothergeek.", "followers": [◌, ◌, ◌], "repos": [◌, ◌, ◌]
 } $ { "name": "Lucy", "bio":"...", "followers": [◌], "repos": [◌, ◌]
 } $ { "name": "Ja "bio":"..." "followers" "repos": [◌ } $ { "name": "Pusheen", "bio":"Nyan nyan nyan~", "followers": [◌], "repos": [◌]
 } ! { "name": "Thing Compiler", "description": "...", "stargazers": [◌, ◌, ◌]
 } ! { "name": "Hello World", "description": "...", "stargazers": [◌, ◌]
 } ! { "name": "Handy Ut "description": ". "stargazers": [◌] } ! { "name": "Awes "description" "stargazers": } ! { "name": "Todo", "description": "...", "stargazers": [◌]
 } ! { } { "viewer": ◌
 } GraphQL
  • 47.
    Relay 的狀狀況 View $ $ $ $ $ dd $$ $ $ dd dd dd Relay Store
  • 48.
    Relay 的狀狀況 View $ $ $ $ dd $$ $ $ dd dd dd Relay Store viewer { name bio } $ Backend query { viewer { name bio } } { "data": { "viewer": { "name": "…", "Bio": "…" } } }
  • 49.
    Relay 的狀狀況 View ㄎㄎㄎㄎ$ $ $ $ dd $$ $ $ dd dd dd Relay Store viewer { name bio } $
  • 50.
    Relay 的狀狀況 View ㄎㄎㄎㄎ dd dd dd dd $ $ $$ $ $ $ viewer { name bio } $ $ viewer { followers { name } } Backend query { viewer { followers { name } } } { "data": { "viewer": { "followers": […] } } } Relay Store Request only the diff!
  • 51.
    Relay 的狀狀況 View ㄎㄎㄎㄎ dd dd dd dd $ $ $$ $ $ $ viewer { name bio } $ $ viewer { followers { name } }
  • 52.
    Relay 的狀狀況 viewer { name bio } View $ ㄎㄎㄎㄎ$ $$ $ $ $ $ $ viewer { followers { name } } dd dd dd dd viewer { repos { name description } } Backend query { viewer { repos { name description } } } { "data": { "viewer": { "repos": […] } } }
  • 53.
    Relay 的狀狀況 viewer { name bio } View $ ㄎㄎㄎㄎ$ $$ $ $ $ $ $ viewer { followers { name } } dd dd dd dd viewer { repos { name description } }
  • 54.
    Relay 的狀狀況 View $ ㄎㄎㄎㄎ$ $ $ $$ $ $ $ dd dd dd dd RenameRepoMutation( repoID: "…", name: "" )
  • 55.
    Relay 的狀狀況 View $ ㄎㄎㄎㄎ$ $ $ $$ $ $ $ dd dd dd dd RenameRepoMutation( repoID: "…", name: "" ) Optimistic Updater
  • 56.
    Relay 的狀狀況 View $ ㄎㄎㄎㄎ$ $ $ $$ $ $ $ dd dd dd dd RenameRepoMutation( repoID: "…", name: "" ) Optimistic UpdaterBackend Updater
  • 57.
    Relay 的狀狀況 View $ ㄎㄎㄎㄎ$ $ $ $$ $ $ $ dd dd dd dd RenameRepoMutation( repoID: "…", name: "" ) Optimistic UpdaterBackend Updater
  • 58.
    想分⾴頁 ? Connections ⬢游標分⾴頁法:Relay Cursor Connections query { viewer { friends(first: 10, after: "someCursor") { edges { cursor node { id name } } pageInfo { hasNextPage } } } } Edge (UserEdgeType) Node (UserType) { … } Cursor 游標 Connection Edges Edge (UserEdgeType) Node (UserType) { … } Cursor Edge (UserEdgeTy Node (UserType { … } Cursor Page Info 起始游標
  • 59.
    ⬢ Offset basedpagination ⬢ Cursor based pagination Why Cursor? page 1 page 2 page 1 page 2 page 3 page 3 ' 壞ㄌ next 5next 5
  • 60.
    ⬡ Relay 可信的Optimistic Updater 機制可以做 「連線問題時重試」甚⾄至離線更更動上線後同步 ⬡ Relay 的 backend 不⼀一定要是 server,也許可以 是裝置上的 database ⬡ 再狂⼀一點,也許我們還能透過⾃自⼰己實作 Relay RecordSource 組出這樣的架構: Fun Things to Try RelayRelay ViewServer Client DB Memory
  • 61.
  • 66.
  • 67.
    適⽤用情況 ⬢ Data driven,data越多越雜,投資越划算 ⬢ React app ⬢ 略略懂後端 ⬢ 踩雷的勇氣 Relay Environment Handler Provider Network Layer Store Record Source GraphQL Endpoint Query Renderer Container Component Fragment Query
  • 68.
  • 69.
    ⬡ GraphQL 簡介 ⬡Relay ⼿手感評測 ⬡ Relay 基本使⽤用 ⬡ 全端案例例:GraphQL & Relay on Rails
 https://github.com/zetavg/RailsRelayTodoMVC 不會講到跟 Apollo 的比較 Outline
  • 70.
    在 React 專案中安裝Relay $ yarn add react-relay
  • 71.
    安裝 GraphQL 語法識別外掛 $yarn add --dev babel-plugin-relay
  • 72.
    記得⼿手動加 Babel Plugin ⬢編輯 .eslintrc 或 package.json "plugins": [ "relay", ]
  • 73.
    安裝 Relay GraphQL編譯器 $ yarn add --dev relay-compiler
  • 74.
    使⽤用 relay-compiler ⬢ 在JavaScript 裡寫的 GraphQL 需要被事先編譯⋯⋯
 
 ⬢ 改了了 graphql`…` tag 裡⾯面的內容後都要跑⼀一次 ⬢ 或是加上 --watch 參參數監看變化 $ relay-compiler --src ./src --schema ./schema.graphql 源碼⽬目錄 GraphQL Schema 檔位置
  • 75.
    import { Environment} from ‘relay-runtime’ const environment = new Environment({ network, store, }) export default environment 環境準備 Relay Environment Handler Provider Network Layer Store Record Source GraphQL Endpoint Network Layer Store ⬢ 先看整個環境⋯⋯
  • 76.
    import { Network} from ‘relay-runtime' const API_ENDPOINT = 'https://api.github.com/graphql' const fetchQuery = (operation, variables) => { return fetch(API_ENDPOINT, { method: 'POST', body: JSON.stringify({ query: operation.text, variables, }), }).then(response => response.json()) } const network = Network.create(fetchQuery) exports default network Relay 網路路層 ⬢ 給⼀一個溝通介⾯面,⼀一般就是包裝 fetch Relay Environment Handler Provider Network Layer Store Record Source GraphQL Endpoint
  • 77.
    import { RecordSource, Store, } from'relay-runtime' const source = new RecordSource() const store = new Store(source) export default store Relay 倉儲 Relay Environment Handler Provider Network Layer Store Record Source GraphQL Endpoint ⬢ Relay 內建具備 garbage collection 的 in-memory record source
  • 78.
  • 79.
    const MyProfile =() => ( <QueryRenderer environment={environment} query={graphql` query MyProfileQuery { viewer { name } } `} render={({ error, props }) => { if (error) { return <Text>{error.message}</Text> } else if (props) { return <Text>Hello, {props.viewer.name}!</Text> } return <Text>Loading...</Text> }} /> ) 基本招 QueryRenderer 可以設計載入中假畫⾯面 注意名字有規定⽤用 "[檔案名稱]Query" Relay Environment Handler Provider Network Layer Store Record Source GraphQL Endpoint Query Renderer Component Query
  • 80.
    const UserProfileComponent =({ user }) => ( <Text>Hello, {user.name}!</Text> ) const UserProfile = createFragmentContainer( UserProfileComponent, graphql` fragment UserProfile_user on User { name login avatarUrl } `, ) export default UserProfile 拆元件 FragmentContainer 注意名字也有規定⽤用 "[檔案名稱]_[prop 名稱]" Relay Environment Store Query Renderer Container Component Fragment Query
  • 81.
    拆元件 FragmentContainer 直接引⽤用定義在 Container裡的 Fragment Relay Environment Store Query Renderer Container Component Fragment Query import MyProfile from '...' const MyProfile = () => ( <QueryRenderer environment={environment} query={graphql` query MyProfileQuery { viewer { ...UserProfile_user } } `} render={({ error, props }) => { if (error) { return <Text>{error.message}</Text> } else if (props) { return <MyProfile user={props.viewer} /> } return <Text>Loading...</Text> }} /> )
  • 82.
    Component Tree 層層疊疊 QueryRenderer Container Fragment Query ContainerContainer Component Component FragmentFragment const UserProfileComponent = ({ user }) => ( <View> <UserName user={user} /> <UserBio user={user} /> </View> ) const UserProfile = createFragmentContainer( UserProfileComponent, graphql` fragment UserProfile_user on User { ...UserName_user ...UserBio_user } `, ) const UserNameComponent = ({ user }) => ( <Text>Hello, {user.name}!</Text> ) const UserName = createFragmentContainer( UserNameComponent, graphql` fragment UserName_user on User { name avatarUrl } `, ) const UserBioComponent = ({ user }) => ( <Text>{user.bio}</Text> ) const UserBio = createFragmentContainer( UserBioComponent, graphql` fragment UserBio_user on User { bio } `, )
  • 83.
    特種 Container ⬢ RefetchContainer ⬡提供 instance method 可以改⽤用不同 arguments 索取資料 ⬢ PaginationContainer ⬡ RefetchContainer 進化版 ⬡ ⽤用 Relay Connection 做分⾴頁專⽤用 ⬡ 可以做出 loadMore()、refresh() 等 method 給無限捲軸和 pull to refresh 使⽤用
  • 84.
    Mutation const mutation =graphql` mutation AddCommentMutation( $input: AddCommentInput! ) { addComment(input: $input) { clientMutationId commentEdge { cursor node { id body } } subject { id commentsCount } } } ` let variables = { input: { subjectId: "aGVsbG8=", body: "Hello Relay", } } commitMutation( environment, { mutation, variables, onCompleted: (response) => { console.log('Success!') }, onError: (err) => { console.error(err) }, }, ) 1 2 3 1 2 3 ⼀一樣有命名規定 宣告 $input 變數以及型別,內容可以在使⽤用時再指定 與 query ⼀一樣列列出欄欄位,這裡需要列列出所有 mutation 執⾏行行後會變更更或新增的資料 2
  • 85.
    ⬢ 還有需要做的事⋯⋯ Mutation Updater constmutation = graphql` mutation AddCommentMutation( $input: AddCommentInput! ) { addComment(input: $input) { clientMutationId commentEdge { cursor node { id body } } subject { id commentsCount } } } ` Mutation 執⾏行行後,我們需要把新的 edge 安插到
 某個 subject 底下的 comments connection 若若在 payload root 發現具有 id 的 node,
 Relay 慣例例會幫我們更更新好 store 中相應的 node
  • 86.
    Mutation Updater commitMutation( environment, { mutation, variables: {... }, updater: (store) => { const payload = store.getRootField('addComment') const newEdge = payload.getLinkedRecord('commentEdge') const subject = payload.getLinkedRecord('subject') const conn = ConnectionHandler.getConnection(subject, ‘commentsConnection') ConnectionHandler.insertEdgeAfter(conn, newEdge) }, optimisticUpdater: (store) => { … }, }, ) 可⾃自訂 store 的 update ⽅方式
  • 87.
    Mutation Updater ConnectionHandler.insertEdgeAfter(conn, newEdge) }, optimisticUpdater:(store) => { const subject = store.get(subjectID) const newCommentID = `client:newComment:${tempID++}` const newNode = store.create(newCommentID, 'Comment') newNode.setValue(newCommentID, 'id') newNode.setValue(variables.input.body, 'body') const newEdge = store.create( `client:newCommentEdge:${tempID++}`, 'TodoItemEdge', ) newEdge.setLinkedRecord(newNode, 'node') const conn = ConnectionHandler.getConnection(subject, 'commentsConnection') ConnectionHandler.insertEdgeAfter(conn, newEdge) subject.setValue( subject.getValue('commentsCount') + 1, 'commentsCount', ) }, }, ) 製作出暫時的 node 製作出暫時的 edge 把暫時的 edge 安插進 connection ⼿手動更更新計數器
  • 88.
    ⬡ GraphQL 簡介 ⬡Relay ⼿手感評測 ⬡ Relay 基本使⽤用 ⬡ 全端案例例:GraphQL & Relay on Rails
 https://github.com/zetavg/RailsRelayTodoMVC 不會講到跟 Apollo 的比較 Outline
  • 89.
  • 90.
    (GraphQL + Relay)on Rails 全端⼀一天可以寫完,拿來來做 RN 更更顯神⼒力力
  • 91.
    (GraphQL + Relay)on Rails Debug 省下的時間可以拿來來調各種 1px
  • 92.
  • 93.