前段時間,分享了一篇GraphQL & Relay 初探,主要介紹了 GraphQL 的設計思想和 Relay 的基本應用。
目前,筆者在實際項目中應用 GraphQL+Relay 已經有段時間了,併發布了一個正式版本。整個過程當中,踩了很多坑,也摸索出了一些經驗,特此作一下總結分享。html
對於架構設計與角色分工,必定程度上,依賴於團隊人員的配置。因爲咱們團隊主要由後端研發組成,前端人數有限,因此仍是以「前」和「後」爲分界來分工,即前端負責純 Web 端部分的開發,後端來實現後端邏輯以及 GraphQL 層的封裝。
具體而言,每一個後端研發負責一個或多個業務模塊,每一個模塊都微服務化,並起一個 GraphQL 或 RESTful API 服務。後端同時還負責維護一個 API Gateway 模塊,用來轉發前端過來的請求、鑑權、統一錯誤處理等工做。整個架構以下圖: 前端
因爲人員限制,採用了上面提到的第一種,後端微服務化的架構設計,便不可避免的存在一些溝通成本。對此,結合社區已有的解決方案,設計了一個半自動的工做流,以下圖: react
const schemaPath = path.resolve(__dirname, "../schema/schema.graphql"); const urls = Object.keys(APIGraphQL).map(item => APIGraphQL[item]); // APIGraphQL記錄微服務地址 const links = urls.map(uri => { let link = new HttpLink({ uri, fetch }); link = setContext((request, previousContext) => ({ headers: {} })).concat(link); return link; }); const main = async () => { const schemas = await Promise.all(links.map(link => introspectSchema(link))); // 在根查詢節點添加一個id字段,解決Relay框架限制 const HackSchemaForRelay = makeExecutableSchema({ typeDefs: ` type HackForRelay { id: ID! } type Query { _hackForRelayById(id: ID!): HackForRelay } ` }); fs.writeFileSync( schemaPath, printSchema( mergeSchemas({ schemas: [HackSchemaForRelay, ...schemas] }) ) ); console.log("Wrote " + schemaPath); }; main(); 複製代碼
在合併 Schema 時,有個問題須要注意:
不一樣微服務間的 Schema 不能存在相同名稱的 Type,不然在合併中會被同名的 Type 覆蓋。
在筆者開發中,是經過與後端研發約定一個命名規則來規避這類問題的。後續優化,能夠考慮自動添加微服務名稱做爲前綴以解決此類問題。webpack
如下爲項目目錄結構以供參考:git
├── package.json
├── publish.sh
├── src
│ ├── index.ejs
│ ├── index.js
│ ├── index.less
│ ├── js
│ │ ├── __generated__
│ │ ├── api
│ │ ├── app.js
│ │ ├── assets
│ │ ├── common
│ │ ├── components
│ │ ├── config
│ │ ├── mutations
│ │ ├── routes.js
│ │ ├── service
│ │ └── utils
│ ├── public
│ │ ├── favicon.ico
│ │ └── fonts
│ ├── schema
│ │ ├── mock
│ │ └── schema.graphql
│ ├── scripts
│ │ └── updateSchema.js
│ └── theme.config.js
├── webpack.config.creator.js
├── webpack.config.js
└── yarn.lock
複製代碼
其中,src/scripts/updateSchema.js是獲取與合併 schema 的腳本,Schema 與 Mock 服務一併放在src/schema目錄中。其他前端組件、包含 Relay 組件,所有放在src/js目錄下。
一個前端組件能夠建立一個目錄,目錄由至少三個文件組成:純 React 組件、組件的樣式以及 Relay 的封裝 Container,以下: github
import { createRefetchContainer, graphql } from "react-relay"; import ProjectList from "./ProjectList"; export default createRefetchContainer( ProjectList, { projectInfoList: graphql` fragment ProjectListContainer_projectInfoList on ProjectInfo @relay(plural: true) { createdTime descInfo jobProfileInfo { ... } ... } ` }, graphql` query ProjectListContainer_RefetchQuery { projectInfoList { ...ProjectListContainer_projectInfoList } } ` ); 複製代碼
關於前端路由,Relay 官方文檔中在路由章節中提到了一些解決方案,但不是很詳細。
筆者在項目中,採用的是相對比較推薦的Found Relay。web
部分配置代碼參考:數據庫
const routesConf = makeRouteConfig( <Route> <Route path="login" Component={Login} /> <Route path="logout" render={() => { api.logout({ payload: {}, api: "" }); throw new RedirectException({ pathname: "/login" }); }} /> <Route path="/" Component={MainLayout}> <Route path="exception/:statusCode" Component={Exception} /> <Redirect from="/" to="/project" /> <Route path="project" Component={ProjectListContainer} query={ProjectListQuery} prepareVariables={params => ({})} > <Route path="job/:projectId" Component={JobListContainer} query={JobListQuery} /> </Route> </Route> </Route> ); const Router = createFarceRouter({ historyProtocol: new BrowserProtocol(), historyMiddlewares: [queryMiddleware], routeConfig: routesConf, render: createRender({ renderError: ({ error }) => { const { status } = error; if (status) { throw new RedirectException({ pathname: `/exception/${status}` }); } } }) }); const mountNode = document.getElementById("root"); ReactDOM.render(<Router resolver={new Resolver(environment)} />, mountNode); 複製代碼
在結合 Relay 框架使用路由過程當中,有幾點須要注意:
一、因爲 Relay 組件只有請求到了後端數據纔會開始渲染,因此儘可能不要將整個頁面做爲 Relay 組件,不然切換路由的時候,會產生相似「全屏刷新」的效果,影響用戶體驗,以下圖: json
Route 所接受的組件都是Fragment,也就是 Relay 框架所提供的 Fragment Container、Refetch Container 和 Pagintion Container。這三種類型的組件,Relay 自己提供的方法使用起來已經比較簡潔方便了。
可是,若是想要封裝一個能夠本身單獨獲取數據的Relay組件,也就是使用QueryRenderer,官方卻沒有提供一個封裝函數。因此,咱們能夠本身來寫一個:後端
import { QueryRenderer, graphql } from "react-relay"; import { message, Spin } from "antd"; import environment from "../../config/environment"; const createContainer = ({ query = "", variables = {}, propsName = "" }) => Target => class RelayContainer extends React.Component { render() { return ( <QueryRenderer environment={environment} query={query} variables={variables} render={({ error, props }) => { if (error) { return null; } else if (props) { return <Target {...this.props} data={props[propsName]} />; } return <Spin spinning={true} />; }} /> ); } }; export { createContainer }; 複製代碼
在具體使用的時候,能夠結合ES7的Decorator,很是簡潔:
@createContainer({ query: graphql` ... `, propsName: "propsName" }) class MyComponent extends React.Component { static defaultProps = { ... }; render() { ... } } 複製代碼
GraphQL+Relay框架的設計思路很是好,也確實能在項目後期迭代中,解放很多生產力。可是,在前期的腳手架搭建以及工做流的梳理、先後端人員配合上,須要多花一點的時間來設計一下。但願本文能給準備使用GraphQL的同窗掃清一些障礙。 此外,任何框架和技術都要切忌爲了用而用,仍是要根據實際需求來決定最佳實踐。好比,即便是一個Relay的項目,也並不必定要求全部的API都是GraphQL,依然能夠結合RESTful API,並不會有什麼問題。因此,適合本身的纔是最好的! 最後,有任何問題,歡迎留言討論,一塊兒學習。