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

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

隨著前端互動體驗日益受到重視,前端與 app 設計者們無不希望能做出Q彈滑順的使用介面擄獲使用者的心。但對資料導向應用程式的開發者而言,要保持實現各種設計的的靈活度便成了一大難題,經常需要實作資料的快取、分頁、快速回應變動、即時更新更是一份苦差事。GraphQL 是 Facebook 推出的資料查詢語言,佐以 Relay 這個為 React 打造的 data fetching framework,我們就可以告別前後端如何交換資料的頭痛問題,將心力放在打造出令人愛不釋手的 React、React Native app 上。

Video: https://youtu.be/r-ODwIcQeCM
Same thing on SlideShare: https://www.slideshare.net/pokaichang72/graphql-relay-78728604

Abb14b8da9963d69d06677a1c43c3fce?s=128

Pokai Chang

August 10, 2017
Tweet

Transcript

  1. ! # @zetavg
 fb.me/pokaichang72 串串起前後端資料的橋樑
 GraphQL 
 & Relay

  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. /api/v1/posts.json /api/v2/posts.json /api/v3/posts.json /api/v4/posts.json /api/v65535/posts.json ⋯⋯

  6. /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

  7. API 應該是這樣

  8. 不是這樣

  9. Q: 那這個 app 需要多少 API Endpoints 呢?

  10. Ans: 1

  11. Ans: 1 GraphQL

  12. $ { "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
  13. $ { "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": ◌
 }
  14. $ { "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": [◌, ◌, ◌]
 } $ d d d d d d d d
  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":"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": [◌, ◌, ◌]
 }
  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 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": ◌
 }
  25. $ { "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 } }
  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 profileFields on 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. graphql.org

  35. ⬡ GraphQL 簡介 ⬡ Relay ⼿手感評測 ⬡ Relay 基本使⽤用 ⬡

    全端案例例:GraphQL & Relay on Rails
 https:/ /github.com/zetavg/RailsRelayTodoMVC 不會講到跟 Apollo 的比較 Outline
  36. 無限捲軸 Caching Prefetch Caching Server Data Updating Optimistic Update Realtime

    Update
  37. facebook.github.io/relay

  38. Relay
 
 Relay Modern (Relay 1.0)

  39. None
  40. const View = (data) => UI

  41. Redux 資料流 View State subscribe Redux Store

  42. Redux 資料流 View State Reducer Action subscribe prevState

  43. Redux 資料流 View State Reducer Action subscribe prevState Backend ?

  44. Redux 資料流 ? View State Reducer Action subscribe prevState Backend

    Action Action Action
  45. Redux 資料流 ? View Action Action Action Action Backend State

    State State Reducer Reducer Reducer Reducer ??? ???
  46. $ { "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
  47. Relay 的狀狀況 View $ $ $ $ $ d d

    $ $ $ $ d d d d d d Relay Store
  48. Relay 的狀狀況 View $ $ $ $ d d $

    $ $ $ d d d d d d Relay Store viewer { name bio } $ Backend query { viewer { name bio } } { "data": { "viewer": { "name": "…", "Bio": "…" } } }
  49. Relay 的狀狀況 View ㄎ ㄎ ㄎ ㄎ $ $ $

    $ d d $ $ $ $ d d d d d d Relay Store viewer { name bio } $
  50. Relay 的狀狀況 View ㄎ ㄎ ㄎ ㄎ d d d

    d d d d d $ $ $ $ $ $ $ viewer { name bio } $ $ viewer { followers { name } } Backend query { viewer { followers { name } } } { "data": { "viewer": { "followers": […] } } } Relay Store Request only the diff!
  51. Relay 的狀狀況 View ㄎ ㄎ ㄎ ㄎ d d d

    d d d d d $ $ $ $ $ $ $ viewer { name bio } $ $ viewer { followers { name } }
  52. Relay 的狀狀況 viewer { name bio } View $ ㄎ

    ㄎ ㄎ ㄎ $ $ $ $ $ $ $ $ viewer { followers { name } } d d d d d d d d viewer { repos { name description } } Backend query { viewer { repos { name description } } } { "data": { "viewer": { "repos": […] } } }
  53. Relay 的狀狀況 viewer { name bio } View $ ㄎ

    ㄎ ㄎ ㄎ $ $ $ $ $ $ $ $ viewer { followers { name } } d d d d d d d d viewer { repos { name description } }
  54. Relay 的狀狀況 View $ ㄎ ㄎ ㄎ ㄎ $ $

    $ $ $ $ $ $ d d d d d d d d RenameRepoMutation( repoID: "…", name: "" )
  55. Relay 的狀狀況 View $ ㄎ ㄎ ㄎ ㄎ $ $

    $ $ $ $ $ $ d d d d d d d d RenameRepoMutation( repoID: "…", name: "" ) Optimistic Updater
  56. Relay 的狀狀況 View $ ㄎ ㄎ ㄎ ㄎ $ $

    $ $ $ $ $ $ d d d d d d d d RenameRepoMutation( repoID: "…", name: "" ) Optimistic Updater Backend Updater
  57. Relay 的狀狀況 View $ ㄎ ㄎ ㄎ ㄎ $ $

    $ $ $ $ $ $ d d d d d d d d RenameRepoMutation( repoID: "…", name: "" ) Optimistic Updater Backend 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 based pagination ⬢ Cursor based pagination Why Cursor?

    page 1 page 2 page 1 page 2 page 3 page 3 ' 壞ㄌ next 5 next 5
  60. ⬡ Relay 可信的 Optimistic Updater 機制可以做 「連線問題時重試」甚⾄至離線更更動上線後同步 ⬡ Relay 的

    backend 不⼀一定要是 server,也許可以 是裝置上的 database ⬡ 再狂⼀一點,也許我們還能透過⾃自⼰己實作 Relay RecordSource 組出這樣的架構: Fun Things to Try Relay Relay View Server Client DB Memory
  61. Optimistic Update Management

  62. None
  63. None
  64. None
  65. None
  66. { 歡樂 Demo 時間 }

  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 層層疊疊 Query Renderer Container Fragment Query Container Container

    Component Component Fragment Fragment 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 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
  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. (GraphQL + Relay) on Rails

  90. (GraphQL + Relay) on Rails 全端⼀一天可以寫完,拿來來做 RN 更更顯神⼒力力

  91. (GraphQL + Relay) on Rails Debug 省下的時間可以拿來來調各種 1px

  92. Image source: https:/ /blog.codinghorror.com/learn-to-read-the-source-luke/ 請⾒見見 https:/ /github.com/zetavg/RailsRelayTodoMVC

  93. Thanks + Q&A