Share
Embed
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
Transcript
93 slides
-
$ { "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": [◌, ◌, ◌] } $ d d d d d d d d -
! { "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 參參數 ⬢ 也可以做巢狀狀查詢 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 簡介 ⬡ Relay ⼿手感評測 ⬡ Relay 基本使⽤用 ⬡
全端案例例:GraphQL & Relay on Rails https:/ /github.com/zetavg/RailsRelayTodoMVC 不會講到跟 Apollo 的比較 Outline -
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 $ $ $ $ $ d d
$ $ $ $ d d d d d d Relay Store -
Relay 的狀狀況 View $ $ $ $ d d $
$ $ $ d d d d d d Relay Store viewer { name bio } $ Backend query { viewer { name bio } } { "data": { "viewer": { "name": "…", "Bio": "…" } } } -
Relay 的狀狀況 View ㄎ ㄎ ㄎ ㄎ $ $ $
$ d d $ $ $ $ d d d d d d Relay Store viewer { name bio } $ -
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! -
Relay 的狀狀況 View ㄎ ㄎ ㄎ ㄎ d d d
d d d d d $ $ $ $ $ $ $ viewer { name bio } $ $ viewer { followers { name } } -
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": […] } } } -
Relay 的狀狀況 viewer { name bio } View $ ㄎ
ㄎ ㄎ ㄎ $ $ $ $ $ $ $ $ viewer { followers { name } } d d d d d d d d viewer { repos { name description } } -
Relay 的狀狀況 View $ ㄎ ㄎ ㄎ ㄎ $ $
$ $ $ $ $ $ d d d d d d d d RenameRepoMutation( repoID: "…", name: "" ) -
Relay 的狀狀況 View $ ㄎ ㄎ ㄎ ㄎ $ $
$ $ $ $ $ $ d d d d d d d d RenameRepoMutation( repoID: "…", name: "" ) Optimistic Updater -
Relay 的狀狀況 View $ ㄎ ㄎ ㄎ ㄎ $ $
$ $ $ $ $ $ d d d d d d d d RenameRepoMutation( repoID: "…", name: "" ) Optimistic Updater Backend Updater -
Relay 的狀狀況 View $ ㄎ ㄎ ㄎ ㄎ $ $
$ $ $ $ $ $ d d d d d d d d RenameRepoMutation( repoID: "…", name: "" ) Optimistic Updater Backend 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 5 next 5 -
⬡ Relay 可信的 Optimistic Updater 機制可以做 「連線問題時重試」甚⾄至離線更更動上線後同步 ⬡ Relay 的
backend 不⼀一定要是 server,也許可以 是裝置上的 database ⬡ 再狂⼀一點,也許我們還能透過⾃自⼰己實作 Relay RecordSource 組出這樣的架構: Fun Things to Try Relay Relay View Server Client DB Memory -
適⽤用情況 ⬢ 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 -
使⽤用 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 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 } `, ) -
特種 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