ReactEurope 2016 小記 - 下

GraphQL at Facebook - Dan Schafer

相信各位熟悉 React 生態的朋友都應該聽說過 GraphQL 的大名。Facebook 從 2012 年開始使用 GraphQL,迄今為止 GraphQL 已經成為了 Facebook 整體技術棧中不可或缺的一環,日均服務調用次數超過 3,000 億次。

來自 GraphQL 團隊的 Dan Schafer 向所有聽眾從以下三個方面介紹了 Facebook 是如何在工程中使用 GraphQL 的。對於 GraphQL 還不是很了解的朋友,推薦先閱讀一下本專欄中另一篇介紹 GraphQL 的文章,相信會對你理解本文以下的內容大有幫助。

How do I implement authorisation?(授權)

在回答關於授權的問題之前,Dan 提出了一個非常有趣的問題,那就是我們如何去定義一個數據模型,或者說什麼是一個數據模型(舉例:在一個簡單的 todoMVC 中什麼是一個待辦事項)?

從目前最流行的 REST 風格的服務架構角度來講,一個數據模型就意味著一個 URL,如:

http://api.todoapp.com/todo/4n

而從傳統的 SQL 資料庫角度來講,一個數據模型就意味著一條 SQL 語句,如:

SELECT * FROM todoitems WHERE id = 4n

以上的這兩種答案都沒有錯,但他們也都存在著各自的問題。我們一起來看下面的三張圖:

如果我們在外部介面的層面上去做授權,那麼需要做的工作就太多了,因為每一個介面都需要鑒權這一環節。而如果我們在數據存儲這一層上去做授權,我們又暴露了太多數據存儲方面的細節,而這些具體的實現邏輯是客戶端所毫不關心的。這個問題的解法就存在於後面的兩張圖中,那就是在業務邏輯層去做授權。筆者見過太多的中小型公司囿於各方面資源的匱乏,很難建立起獨立的業務邏輯層,只是簡單地將後台工程師們做好的表中的數據通過介面的方式暴露出來。這樣的架構當然可以支撐起一個完整的項目,但整個項目的可伸縮性與復用性幾乎為零,僅從結果上來講毫無疑問是一種非常糟糕的架構。從第三張圖中,我們看到了 Facebook 極為推崇的以業務邏輯為核心的架構方式,GraphQL 不過是包在核心業務邏輯之上的一層介面而已。而這樣的架構方式不僅可以應用於軟體工程中,也可以拓展到公司的組織架構層面,當一家公司的業務處於極速膨脹期時,各個業務部門(一個業務部門是許多業務邏輯的總集)之間的協同配合就變得異常重要,如何靈活地復用現有的業務資源,才是滿足日漸膨脹的業務需求的唯一解法,遍地豎煙囪的時代顯然已經過去了。

在這裡我們又要提到三條在 GraphQL 的設計中非常重要的理念:

  • nnThink Graphs, not Endpoints(專註於數據之間的關係,而不是使用數據的過程及終點)

  • nnSingle Source of Truth(單一數據來源,穩定的數據存儲層)

  • nnThin API layer(不涉及任何業務邏輯,適用於任何業務系統)

以下的所有內容都是圍繞著以上這三點設計理念展開的,理解了這三點理念,即使你不在日常生產中使用 GraphQL,你也已經學到了它的精粹。

回到授權的問題上來,我們來看如下代碼:

class TodoItem {n // Single source of truth for fetchingn static async gen(viewer: Viewer, id: number): Promise<?TodoItem> {n const data = await Redis.get("ti:" + id);n if (data === null) return null;n // Single source of truth for authorisationn const canSee = checkCanSee(viewer, data);n return canSee ? new TodoItem(data) : null;n }n}nnfunction checkCanSee(viewer: Viewer, data: Object) {n return (data.creatorID === viewer.userID);n}n

是的,當我們建立了獨立的業務邏輯層後,我們也就有了業務數據的單一數據源,然後我們就可以在 GraphQL 中實現帶有授權機制的數據查詢了。

todoItem: {n type: TodoItemType,n args: { id: { type: GraphQLId } },n resolve: (obj, {id}, viewer) => TodoItem.gen(viewer, id)n}nn// ...nconst viewer = Viewer.fromAuthToken(request.auth_token);ngraphql(schema, "{todoItem(id: $id)}", viewer);n

How do I make GraphQL efficient?(效率)

假設如下的場景,用戶需要查詢自己前五位好友的最親密好友。

如果我們不對請求過程做任何優化的話,我們需要先發送 5 個獨立的請求去查詢自己的前五位好友,然後再分別發送 5 個請求去查詢他們各自的最親密好友。但事實上,以上的這 10 個請求是分兩次同時發生的,我們是否能夠將他們合併成為 2 個獨立的請求呢?

當然可以,但實現這一切的魔法並不來自於 GraphQL,而是 Facebook 的另一個開源項目 DataLoader。DataLoader 接收一個函數,這個函數接收一個用戶 id 的列表(在以上的場景中),並返回非同步的數據。至此,我們對於請求過程的優化就結束了,DataLoader 會處理接下來的所有事情。它將延遲發送這些獨立的請求,合併相同的請求,最後一次性發送給服務端。而且,DataLoader 還會緩存已經發送過的請求,合併請求後並不會向服務端重複發送的已經緩存過的請求。看起來 DataLoader 已經幫我們解決了絕大多數的問題,但我們還有最後一個需求,那就是如果客戶端也希望緩存這些數據,應該如何去做呢?

How do I cache my results?(緩存)

在 GraphQL 中,請求結果與 URL 之間並不存在嚴格的一一對應的關係,所以我們無法使用簡單的 HTTP 緩存。為了實現類似的功能,GraphQL 需要做到以下的三件事:

  • nnGlobal Unique Cache Key(全局唯一的緩存鍵)

  • nnRefetch Identifier(再次請求時的標識符)

  • nnOpaque to Clients(客戶端無感知)

解決以上的三個問題並不算困難,GraphQL 使用添加業務前綴的方式來保證緩存鍵的全局唯一性,再使用 base64 的方法對這些緩存鍵進行加密,以達到客戶端無感知。綜上,當客戶端將一個 base64 加密過的無業務意義的鍵值傳回後,GraphQL 就可以根據這個鍵值再次獲取數據,代碼如下所示:

const userType = new GraphQLObjectType({n name: User,n fileds: () => ({n id: {n type: GraphQLID,n resolve: (user) => base64("user:" + user.getID())n }n })n});n

講到這裡,我們基本已經覆蓋到了 Dan 演講中的絕大多數內容,簡單地談一下筆者的個人感受。

應該說 GraphQL 是 React 生態中離前端最遠的一個領域,各位讀者從上面的介紹中應該也可以看出 GraphQL 的核心在服務端,GraphQL 對服務端提出了比 REST、RPC 等傳統標準更加嚴格的要求,以使得服務端可以滿足客戶端按需取數的需求。也許我們在很長的一段時間內都無法將 GraphQL 應用於生產環境中,也無法享受到服務端返回的數據結構與客戶端渲染所需要的數據結構相同的便利,但 GraphQL 團隊卻成為了我最為敬佩的一個團隊,因為只有他們實現了從前端痛點反推後端技術,並解決了在 Facebook 內部讓整個公司都感到極為痛苦的多維數據(在社交網路的數據世界中,經常會出現十維以上的數據)獲取難題。

Software engineering is all about teamwork and teamwork is all about making your teammates better players.

On the Spectrum of Abstraction - Cheng Lou

當我們在討論軟體工程時,我們在討論些什麼?來自 React 團隊的華裔工程師,react-motion 的作者樓成對於這個問題的回答是:抽象。

但從另一方面來講,軟體工程師的最終任務卻是產出具象的產品,抽象不過是過程中的一個方法,所以抽象到什麼級別,如何抽象就變得異常重要。而且我們還要時刻牢記,抽象是要付出代價的,過度的抽象會增加使用者的認知成本(mental overhead),讓使用者不得不花費更多的力氣將一個抽象的框架/庫具象到最終產品的級別,而最終產品的具象程度又與終端用戶的體驗直接相關,可以說最終產品的具象級別是非常高的,或者說最終產品不應該具有任何程度的抽象,這樣才能保證用戶來到這個頁面就知道如何去完成自己想做的事情。

樓成用了 react 和使用 react 生產的最終產品來舉例,react 很強大(powerful)因為它可以覆蓋到幾乎所有需要跟 DOM 打交道的應用場景,但另一方面,react 卻一點用也沒有,因為終端用戶不可能去使用如此抽象的產品。使用 react 生產的最終產品卻十分有用(useful),但它們並不強大,只能覆蓋到一些特定的含有業務邏輯的用戶場景。

Principle of Least Power: Use least powerful tool to build your concrete products.

正如軟體工程師們每天都會遇到的 library 與 framework 之爭(舉例:jQuery vs. React),其實類似爭論的答案都取決於你到底需要將需求抽象到什麼級別。如果只是為了滿足在某些特定場景下的應用,那麼 library 可能是一個更好的選擇,因為它可以直接拿過來用,快速地解決你所遇到的問題。假設我只是需要拿到一個獨立表單中所有 input 框的數據,那麼我大可以直接使用 jQuery 的選擇器來取值,而不需要先抽象出許多 input 控制項,然後再挨個為它們綁定 onChange 事件(不要忘了,即使你簡單地抽象出了 input 控制項,為了使用它你還需要繼續抽象出表單控制項等等)。

Lots of problems arise from a bad understanding of where we should be in the levels of abstraction.

如果我們一開始就在業務的抽象層級上站錯了隊,那麼我們將人為地給自己創造出許多不必要的困難,也就違反了上面提到的 Principle of Least Power。無論何時,軟體工程的最終目的都是創造出獨一無二的產品,抽象與獨一無二無疑是完全背道而馳的兩種理念,而這也是軟體工程師與產品經理之間的最大矛盾所在。

在我們了解上述關於抽象的理念後,我們來看幾個案例。

Grunt vs. Gulp

Grunt 與 Gulp 之間的競爭,本質上來講是 declarative DSL(聲明式領域專用語言) 與 build system(構建系統)之間的競爭,而從軟體工程這麼多年的發展來看,build system always wins. 如果說 DSL 是 do one thing and do it well,那麼在前端工程化需求日益膨脹的今天,就顯得越來越不夠用了,而在 DSL 的基礎上去改造或擴展它,成本又異常高昂,所以 build system(在函數層級抽象) 獲得最終的勝利就只是時間問題了。

React vs. Templates

是的,React 將 JavaScript 注入了 HTML 中,但 React 並不是一個模板引擎,因為二者在抽象的層級上是不同的。React 是在函數層級上的抽象,而模板引擎則是在數據層級上的抽象,所以模板引擎使用起來更方便,函數支持的場景更多也更靈活。

Diffing on View vs. Diffing on (Model + Computation)

雖然函數層級的抽象更靈活,但它卻幾乎沒有任何優化的空間,也沒有配置的可能,所以 React 將優化的工作都放在了最終的視圖層面,也就是我們經常提到的 virtual DOM diff. 而實現 virtual DOM diff 的前提是,在數據層面的抽象,因為數據是可比較的,而函數是不可比較的。

Immutability vs. Mutability

毫無疑問,可變的數據更強大,能做更多的事情,但這並不是前端工程中的重點。在前端工程中,不可變的數據可以幫助我們寫更少的代碼去實現諸如時間旅行、數據持久化、shouldComponentUpdate 等等這些功能,在這一需求中,我們需要的是更加具象的實現。

JS(Inline) CSS vs. Traditional CSS

傳統的 CSS 有很多優點,比如寫起來很容易,每個屬性也都很具體,可以直接影響到 DOM 元素的表現形式。但正是因為傳統的 CSS 如此具象,它也喪失了很多靈活性,以至於為了實現某些複雜需求,我們不得不在 CSS 內部做許多的 HACK。JS CSS 給予我們的,是一個編程語言(Programming Language)所具備的所有特性,讓我們可以使用更強大的工具去完成複雜的需求,而不再需要在 CSS 內部去做 HACK。

在演講的最後,樓成還總結了四條在開發過程中的個人經驗:

  • nnDon』t cover every use-case

  • nnRepeating your code is fine

  • nnDon』t be swayed by elegance

  • nnWhen in doubts, use examples

在這裡限於篇幅,而且以上的這四條經驗實在有些太「濕」了,相信每個開發者所遇到的情況都不盡相同,所以我們就不一一展開討論了。

Code is work, architecture is experience, abstraction is art.

小結

至此,我們的 React Europe 2016 之旅就暫時告一段落了,感謝大家一路的陪伴,也希望大家可以從這些業界大牛的演講中吸取到一些優秀的解決問題的思路。

最後,我想借用樓成演講中的一句話來總結我們的這兩篇文章:

最強大的工具都是抽象且無用的,正如我的這個演講,也許它是整個 React Europe 中最無用的一個。

但我想,樓成所提到的關於抽象層級的思考方式卻可以滲透到我們日常工作中的方方面面。HACK 永遠只是一時之策,真正考驗軟體工程師(架構師)能力的還是如何在問題發生之前預料並解決掉它。對於優秀的軟體工程師而言,寫代碼是一件非常容易的事情,如何將具象的業務需求抽象為與業務無關的技術方案才是真正的難點。

如果讀者在抽象需求方面有非常好的最佳實踐,也歡迎在專欄下留言,我們可以一起探討。

======

感謝您的閱讀,React Europe 2017 我們再見!


推薦閱讀:

High Order Component
React + Redux 性能優化
React 父組件引發子組件重渲的時候,如何保持子組件的狀態更新不受影響?
React V16 錯誤處理(componentDidCatch 示例)

TAG:React | 前端开发 | 软件架构 |