React庫+GraphQL服務器+Relay架構聯合作戰(上)
【摘要】本系列文章較全面地分析了時下流行的React庫、GraphQL服務器以及Relay架構模式各自的功能特征,并通過一個具體的實例向你展示它們是如何相互配合來完成一款Web應用的開發的。本篇屬于本系列的上篇,側重于React庫、GraphQL服務器和Relay架構各自功能及其關系解析。
概述
不同于如AngularJS和Ember這樣的框架,React是一個客戶端庫,其中提供了一組有限的函數,而且該庫幾乎獨立于構建應用程序其他功能時所可能需要的其他庫。從根本上說,React提供了客戶端UI組件功能,其中提供了一種機制來創建組件、管理組件中的數據,渲染組件,以及復合小組件來構建更大的組件。React可以用于多種情景下,無論數據來自何處、 如何檢索數據或如何把數據作為一個更大的應用程序的一部分進行管理的。為了有效地對付這些問題,需要利用其他的庫和模式。一個用于React應用程序的常見模式是Flux。
人們開發Flux的目的是把它作為MVC(模型-視圖-控制器)模式的一種替代方法,來管理響應動作的數據流。與MVC的雙向數據流動不同,Flux依賴于系統中的各個部件之間的單向數據流。Flux是非死book創建的,因為他們的開發人員發現很難了解采用MVC模式開發的大型應用程序中的數據運動規律。
不同于MVC模式所采用的多環路數據流,Flux使用的是單環路數據流方案。數據僅沿著一條線路流動,這條線路是:動作(Action)->調度器(Dispatcher)->存儲(Store)(或多個存儲)->組件(即視圖)->動作。
其中,動作(Action)表示某種進入系統的事件。這可能是用戶生成的,如請求刷新數據的按鈕單擊事件,或者也可能是通過一個Web套接字接收消息事件。然后,這個動作被傳遞給調度器將它發送到所有的存儲。調度器其實只不過是一個轉發機制。它不懂動作是什么,動作所傳遞的數據的含義,而也不懂得與該動作相關的每個存儲的功能。它只是把動作調度給所有的存儲,然后每個存儲決定是否應該處理這一動作。存儲負責維護數據的本地副本,強制實施業務規則并通知新數據的組件,以便能刷新它們。你可以認為存儲是維護應用程序狀態的,而整個Flux流程本質上就是一個狀態機。因此,React組件利用了狀態機模式。在某種意義上,Flux利用與此相同的狀態機模式來架構整個應用程序。
雖然Flux是解決數據流問題的一種模式,但它本身并沒有提供實現這種模式的技術。若要使用Flux模式,開發人員不得不創建系統中所有的組件,除了非死book提供的調度器之外。創建一個Flux系統是相對比較容易的,但它需要大量的樣板代碼。在這方面,它面臨與Backbone.js同樣的問題。啟動和運行系統是很容易的,但最終需要大量的編碼。
Flux進化過程
當開發人員使用Flux時,他們開始想辦法來把樣板代碼重構到可重用的庫中。此外,他們還能夠確定出Flux中的子模式,這使得對于應用程序中數據流的分析判斷更為容易,而且還在而不犧牲Flux一般優點的情況下減少應用程序的復雜性。這些子模式包括:把應用程序中的多存儲減少到一個存儲;把調度器與存儲結合到相同的組件(當只有一個存儲時這是很重要的)中;而且,還能夠把諸多組件封裝到一個容器中,此容器中可以在黑箱中創建動作、調度和進行存儲管理。如今,許多Flux的衍生產品不再僅僅局限于純粹的Flux功能,而且還能夠在保留其根本要素的同時避免Flux通常的缺點,即大量的樣板代碼的問題。
目前,雖然有很多Flux的衍生產品,但是Redux是更受歡迎的模式之一。Redux純粹基于狀態機概念和不可變數據的思想而構建。在該模式中,動作都由單一的“調度器-存儲”來處理;這些“調度器-存儲”對象使用reducer函數(它們本身是可以組合的)實現把一種狀態轉換到另一種狀態。這就在極大地簡化Flux模式的同時引進很多函數編程特征;一旦你徹底掌握這一技術,React應用程序編碼將容易許多。
Relay是來自于非死book團隊的另一個Flux的衍生產品,此模式日漸普及。關于非死book如何使用Relay以及其與Flux關系的處理方案的更多信息,請參考網址 https://非死book.github.io/react/blog/2015/02/20/introducing-relay-and-graphql.html 。
Relay框架功能分析
雖然Redux簡化了應用程序的管理,但是在實際數據定位方面卻不可知。它可以使用任何數據存儲系統,但是再一次導致更多的樣板代碼(雖然少于Flux)問題。于是,出現了Relay (這是非死book創建的另一個產品,其應用了其他大量的JavaScript開源產品,例如React,Relay,Immutable.js,GraphQL,Jest,Flow等),它的宗旨在于通過重構去除數據訪問相關的樣板代碼部分,同時還引進另一種新的數據服務——GraphQL。GraphQL不同于傳統REST服務的地方在于,它把數據視為一個圖形,并力求以分層方式來描述該圖形,從而使數據消費者自己指定他們需要的數據,而不是傳統的REST服務中那樣提供一組固定的數據集服務而不考慮消費者需求。
那么,Relay到底是做什么的呢?Relay是一個框架,它負責把React組件連接到GraphQL服務器,此連接是通過一個實現了動作、調度器和存儲的容器實現的。開發人員不需要對動作、調度器和存儲進行編程,而可以觸發這些動作并通過Relay API來訪問相應的結果。若要配置容器,開發人員必須提供GraphQL查詢和突變片段(mutation fragments)向容器描述數據的圖結構;此外,Relay還會負責照顧數據管理的所有細節。
Relay的確是一個框架(如Angular),而不是一個庫。它的實現可以說是透明的——它需要通過React實現UI組件而且由GraphQL提供數據服務。一旦GraphQL服務器和React組件配置到位,Relay將接管并執行所有需要的操作。因此,使用Relay的關鍵是掌握配置過程。
此外,與框架Angular(它僅對客戶端有專門需求)不同,Relay還要求GraphQL服務器接口來為Relay容器提供數據查詢和變異操作。但是,只要通過特定的GraphQL接口提供數據,Relay并不在意如何存儲數據。
因此,Relay需要后端和前端兩個開發團隊都要了解它的工作原理,并要求他們弄清程序的的每個部件(無論前端還是后端)是如何編碼和配置的。
Relay與React的配合
本小節中,讓我們從React的角度來研究一下Relay。程序員們可以使用多種語言對GraphQL服務器進行編碼與配置,并部署到多種平臺上。對于Node.js環境下的GraphQL實現來說,有一個稱為graphql-relay( https://www.npmjs.com/package/graphql-relay )的包可以用于簡化GraphQL服務器的編碼和配置要求。在React方面,則存在一個名為relay-react( https://www.npmjs.com/package/react-relay )的包可用于配置Relay容器和路由,還能夠激發動作來實現數據變異操作等。
Relay開發入門
入門Relay是有些難度的。因為該技術是如此之新而且有很多的競爭對手,所以,有關如何使用Relay的參考資源目前還相當有限。而提供資源的地方,一般提供的例子也是很有限的;為此,開發人員最終被迫去閱讀博客文章、翻閱GitHub問題和正式的產品說明書才能創建一個簡單的CRUD應用程序。此外,還需要搭建一個相當復雜的開發環境以及需要有一個正確配置的GraphQL服務器。因此,這樣的任務對于JavaScript/前端開發新手而言可能相當艱巨。
作為準備工作,請首先克隆一下GitHub網站上的存儲倉庫( https://github.com/DevelopIntelligenceBoulder/react-flux-blog )到您的計算機上,并打開相應的文件夾blog-post-5+6。此文件夾包含一個完整的GraphQL/React/Relay應用程序。為了便該應用程序啟動并運行起來,請打開一個終端,導航到文件夾blog-post-5+6中,并運行以下Gulp命令。
$ npm i $ npm i -g gulp eslint eslint-config-airbnb eslint-plugin-react@^4.3.0 webpack babel-cli babel-eslint eslint-plugin-jsx-a11y@^0.6.2 $ gulp $ npm run update-schema $ gulp $ gulp server
現在,請打開微軟的Edge瀏覽器(【譯者注】微軟專門為Windows 10配置的高性能瀏覽器),然后導航到下面的URL: http://localhost:3000 。
你會注意到,頁面中將顯示一個小控件列表,并使用Bootstrap 4風格進行修飾,看起來相當漂亮。(【譯者注】因某種原因原文并沒有提供結果快照)
該項目的基本開發構架是典型的文件夾組織方式。其中,src文件夾中存放可編輯的源代碼文件,而最終的部署文件夾是dist(源代碼文件都要復制至此處),應用程序正是使用此處的文件執行的。復制過程是使用Gulp命令進行的;具體地說,是通過組合一些簡單的復制文件命令、創建一個處理SASS文件的任務,還有一個針對JavaScript的Web打包過程完成的。其中,Web打包處理機制使用Babel轉譯器把RelayQL、JSX和ES2015代碼轉換為 ES5.1兼容的可以在任何瀏覽器中執行的JavaScript代碼。ES2015和JSX轉譯已經不是什么新技術,但對于RelayQL的轉譯卻是一個新課題。
RelayQL與Babel-Relay插件
GraphQL服務器能夠通過使用內省(introspection)機制自動生成結構(【譯者注】原文中用詞是schema,這個詞在數據庫表格設計中常用;而其他許多軟件技術中也廣泛使用這個詞,而且經常譯為“模式”或“架構”。為了區別本文中另兩個詞pattern和architecture,在此特意譯為“結構”)。結構(schema)其實是一個JSON文件,其中描述的所有類型由特定的GraphQL服務器使用。結構中可以包含自定義和內置類型。Babel-Relay插件使用此結構來校驗使用RelayQL編碼形成的GraphQL片段(fragments)。這些片段是使用ES2015字符串模板編碼的,一旦它們通過根據結構定義的校驗就被轉換為JavaScript代碼。這種校驗可以有效地防止 GraphQL錯誤的發生。
配置Babel-Relay插件也是生成結構(schema)最簡單的方法是,直接使用從Relay網站( https://非死book.github.io/relay/docs/guides-babel-plugin.html )或Relay初學者工具包項目( https://github.com/relayjs/relay-starter-kit )中下載的例子。這些文件正是本文對應的Github存儲庫中所使用的,并遵從Relay官網上推薦的開發模式。
從Relay初學者工具包項目中,我們需要使用兩個文件:build/babelRelayPlugin.js和scripts/updateSchema.js。其中,UpdateSchema.js文件將用于生成結構(schema),而babelRelayPlugin.js文件將使用結構文件來校驗證GraphQL片段以及轉換RelayQL代碼。
GraphQL與Relay協作
通常情況下,要使用Relay需要修改標準的GraphQL服務器實現方案。我們可以使用一個名為graphql-relay( https://www.npmjs.com/package/graphql-relay )的包來幫助把基于Node.js的GraphQL服務器配置為Relay兼容型的。要配置成一個Relay特定類型的GraphQL服務器需要三個主要方面:對象標記(Object Identification)、類型連接(Type Connections)和突變(Mutations)。
通過使用一個全局唯一的ID值,對象標記允許Relay從GraphQL服務器查詢實現了節點接口的任何類型。這個全局ID是base64編碼的,其中包含類型名稱和一個后面跟一個冒號的本地ID值。graphql-relay庫提供了分別命名為toGlobalID和fromGlobalID的函數支持在全局 ID之間來回轉換。另外,類型名稱來自于在類型配置中指定的GraphQL自定義類型名稱。通常情況下,本地ID值來自于數據存儲機制,例如關系數據庫中的標記(Identity)。請參考下面的代碼:
import { nodeInterface } from './../node-definitions'; export const widgetType = new GraphQLObjectType({ name: 'Widget', description: 'A widget object', fields: () => ({ id: globalIdField('Widget'), // more fields }), interfaces: () => [nodeInterface] });
在上面代碼中,文件node-definitions.js(及其相關文件type-registry)的作用是:為通過節點接口使對象可用而提供配置與類型注冊。
第二個Relay特定的配置,即類型連接(Type Connections),是建立父類型及其子類型之間的一對多關系的連接。這些連接是使用一種特殊的連接類型結構管理的。這些特殊的連接類型結構支持圖中的邊緣(graph edge)概念和游標(cursor)的概念,用于限制結果集與生成結果頁面。可以配置連接和邊緣類型以支持如元數據這樣的附加屬性,從而允許控制連接或邊緣特性(例如加權的邊緣等)。請參考下面的代碼:
import { widgetType } from './types/widget-type'; import { connectionDefinitions } from 'graphql-relay'; export const { connectionType: widgetConnection, edgeType: WidgetEdge } = connectionDefinitions({name: 'Widget', nodeType: widgetType});
上面代碼中的ConnectionDefinitions函數用于創建Relay期望的結構中的連接類型。請參考下面的代碼:
import { widgetConnection } from '../connections/widget-connection'; // inside of fields function of viewer type declaration widgets: { type: widgetConnection, description: 'A list of widgets', args: connectionArgs, resolve: (_, args) => connectionFromPromisedArray(getWidgets(), args) }
上面代碼中,WidgetConnection類型是從widget-connection.js文件中導入的,用于配置查看器類型中的控件字段。包graphql-relay中還提供了一個名為connectionArgs的對象,該對象中包含通過Relay傳遞進來的用于處理連接的標準參數。這些參數包含的值用于游標操作。
第三和最后一個Relay特定的配置是突變(mutation)配置。graphql-relay包中提供了一個專門的命名為mutationWithClientMutationId的幫助方法用于簡化突變配置。有四個字段是必需的:突變名稱、輸入字段、輸出字段和獲取有效載荷的字段。在GraphQL中,所有的突變都將伴隨一個查詢來獲知任何數據可能已被更改。Relay通過智能地決定突變后需要刷新哪些數據來進一步擴展了這種能力。
突變的名字是當React-Relay應用程序訪問GraphQL服務器時用來調用突變的名字。輸入字段對應于GraphQL中突變的args參數。輸出字段描述了要從突變返回的類型的字段。最后一個獲取有效載荷的字段將執行實際的數據庫操作并返回一個promise對象,該對象將推遲從GraphQL到應用程序的響應時間,直到該promise被解析結束為止。
小結
React和GraphQL聯手,并輔助以Relay,將為構建Web應用程序提供一個很有前途的框架。雖然開發過程中需要不少的安裝及配置工作,但是一旦這些工作完畢,開發過程將流暢地進行下去,消除了樣板代碼,并智能地處理數據管理問題。實踐將會證明Relay框架很可能會成為構建下一代Web應用程序的游戲規則改變者。在本系列下篇文章中,我們將探討使用React并配合以Relay來控制GraphQL資源的問題。
來自:http://developer.51cto.com/art/201608/516331.htm