Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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

Pokai Chang

August 10, 2017
Tweet

More Decks by Pokai Chang

Other Decks in Technology

Transcript

  1. $ { "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
  2. $ { "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": ◌
 }
  3. $ { "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
  4. ! { "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": [◌]
 } $ $ $ $ $ $
  5. ! ! { "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": [◌, ◌, ◌]
 }
  6. ⬡ GraphQL 簡介 ⬡ Relay ⼿手感評測 ⬡ Relay 基本使⽤用 ⬡

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

    全端案例例:GraphQL & Relay on Rails
 https:/ /github.com/zetavg/RailsRelayTodoMVC 不會講到跟 Apollo 的比較 Outline
  8. 查詢更更多 ⬢ 巢狀狀選取欄欄位 (field) { "data": { "viewer": { "name":

    "Pokai Chang", "birthday": { "month": 7, "day": 2 } } } } query { viewer { name birthday { month day } } }
  9. 巢狀狀查更更多 ⬢ 底下的資料 (node) 可能是同⼀一種類型 (type) { "data": { "viewer":

    { "name": "Pokai Chang", "following": [ { "name": "..." }, { "name": "..." }, { "name": "..." } ] } } } query { viewer { name following { name } } }
  10. 巢狀狀查更更多多多多 ⬢ 可以幹奇怪的事⋯⋯ { "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 } } } } } }
  11. 型別定義即⽂文件 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 }
  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": ◌
 }
  13. $ { "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 } }
  14. ! { "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 } } }
  15. ! ! { "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 } } } }
  16. { } { "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 } } } '
  17. query { viewer { name bio followers { name }

    repos { name stargazers { name } } girlfriend { name } } } { "errors": [ { "message": "Field 'girlfriend' doesn't
 exist on type ‘User’", ... } ] } '
  18. Arguments 參參數 ⬢ 也可以做巢狀狀查詢 query { user(id: 1) { name

    repo(name: "awesome-graphql") { name description } } }
  19. Fragment 片段 fragment profileFields on User { name bio avatarUrl

    } query { viewer { ...profileFields } user(id: 1) { ...profileFields } } 先把固定會⽤用到的欄欄位 存成有意義的片段
  20. 改資料 ? Mutation ⬢ query 改成 mutation,data 放在 arguments ⬢

    其實就像 HTTP GET/POST,只是慣例例,沒有硬性限制 mutation { addComment(input: { subjectId: 1, body: "Hi." }) { subject { comments { body } } } }
  21. ⬡ GraphQL 簡介 ⬡ Relay ⼿手感評測 ⬡ Relay 基本使⽤用 ⬡

    全端案例例:GraphQL & Relay on Rails
 https:/ /github.com/zetavg/RailsRelayTodoMVC 不會講到跟 Apollo 的比較 Outline
  22. Redux 資料流 ? View Action Action Action Action Backend State

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

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

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

    $ d d $ $ $ $ d d d d d d Relay Store viewer { name bio } $
  27. 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!
  28. Relay 的狀狀況 View ㄎ ㄎ ㄎ ㄎ d d d

    d d d d d $ $ $ $ $ $ $ viewer { name bio } $ $ viewer { followers { name } }
  29. 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": […] } } }
  30. Relay 的狀狀況 viewer { name bio } View $ ㄎ

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

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

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

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

    $ $ $ $ $ $ d d d d d d d d RenameRepoMutation( repoID: "…", name: "" ) Optimistic Updater Backend Updater
  35. 想分⾴頁 ? 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 起始游標
  36. ⬢ Offset based pagination ⬢ Cursor based pagination Why Cursor?

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

    backend 不⼀一定要是 server,也許可以 是裝置上的 database ⬡ 再狂⼀一點,也許我們還能透過⾃自⼰己實作 Relay RecordSource 組出這樣的架構: Fun Things to Try Relay Relay View Server Client DB Memory
  38. 適⽤用情況 ⬢ Data driven,data 越多越雜,投資越划算 ⬢ React app ⬢ 略略懂後端

    ⬢ 踩雷的勇氣 Relay Environment Handler Provider Network Layer Store Record Source GraphQL Endpoint Query Renderer Container Component Fragment Query
  39. ⬡ GraphQL 簡介 ⬡ Relay ⼿手感評測 ⬡ Relay 基本使⽤用 ⬡

    全端案例例:GraphQL & Relay on Rails
 https:/ /github.com/zetavg/RailsRelayTodoMVC 不會講到跟 Apollo 的比較 Outline
  40. 使⽤用 relay-compiler ⬢ 在 JavaScript 裡寫的 GraphQL 需要被事先編譯⋯⋯
 
 ⬢

    改了了 graphql`…` tag 裡⾯面的內容後都要跑⼀一次 ⬢ 或是加上 --watch 參參數監看變化 $ relay-compiler --src ./src --schema ./schema.graphql 源碼⽬目錄 GraphQL Schema 檔位置
  41. 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 ⬢ 先看整個環境⋯⋯
  42. 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
  43. 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
  44. 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
  45. 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
  46. 拆元件 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> }} /> )
  47. 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 } `, )
  48. 特種 Container ⬢ RefetchContainer ⬡ 提供 instance method 可以改⽤用不同 arguments

    索取資料 ⬢ PaginationContainer ⬡ RefetchContainer 進化版 ⬡ ⽤用 Relay Connection 做分⾴頁專⽤用 ⬡ 可以做出 loadMore()、refresh() 等 method 給無限捲軸和 pull to refresh 使⽤用
  49. 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
  50. ⬢ 還有需要做的事⋯⋯ 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
  51. 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 ⽅方式
  52. 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 ⼿手動更更新計數器
  53. ⬡ GraphQL 簡介 ⬡ Relay ⼿手感評測 ⬡ Relay 基本使⽤用 ⬡

    全端案例例:GraphQL & Relay on Rails
 https:/ /github.com/zetavg/RailsRelayTodoMVC 不會講到跟 Apollo 的比較 Outline