GraphQL & Relay 實戰

前段時間,分享了一篇GraphQL & Relay 初探,主要介紹了 GraphQL 的設計思想和 Relay 的基本應用。
目前,筆者在實際項目中應用 GraphQL+Relay 已經有段時間了,併發布了一個正式版本。整個過程當中,踩了很多坑,也摸索出了一些經驗,特此作一下總結分享。html

架構&角色分工

對於架構設計與角色分工,必定程度上,依賴於團隊人員的配置。因爲咱們團隊主要由後端研發組成,前端人數有限,因此仍是以「前」和「後」爲分界來分工,即前端負責純 Web 端部分的開發,後端來實現後端邏輯以及 GraphQL 層的封裝。
具體而言,每一個後端研發負責一個或多個業務模塊,每一個模塊都微服務化,並起一個 GraphQL 或 RESTful API 服務。後端同時還負責維護一個 API Gateway 模塊,用來轉發前端過來的請求、鑑權、統一錯誤處理等工做。整個架構以下圖: 前端

角色分工架構圖
若是對於先後端人員配置均等或者「大前端」團隊來講,就比較適合按組件/模塊來分工了。
也就是說,前端負責 Web 端開發以及 GraphQL 的封裝,後端則負責設計數據庫並提供後端業務操做接口。架構圖能夠設計成這樣:
角色分工架構圖 2
這樣設計的好處,能夠最大程度下降先後端之間用於溝通、聯調上的時間成本,使得開發效率最大化。

工做流

因爲人員限制,採用了上面提到的第一種,後端微服務化的架構設計,便不可避免的存在一些溝通成本。對此,結合社區已有的解決方案,設計了一個半自動的工做流,以下圖: react

工做流
其中,核心點在於,腳本自動化地獲取各 GraphQL 微服務的 Schema,而後作合併,彙總成一個總的 Schema。這個總的 Schema 主要有三個做用:
一、供 Relay 框架編譯 Relay 組件;
二、前端 Mock 服務;
三、提供 API 文檔(含類型校驗)這樣一來,只要後端開發完成了 schema 的定義,並運行 Server(能夠暫時只是假數據),前端便可以一鍵跑起 Mock 服務,開始開發前端組件,並且後端任何的變動,也能夠及時同步到前端。
具體實現上,採用了 Apollo graphql-toolsremote schemaschema stitching工具完成微服務 schema 的獲取與合併。同時,使用 Mocking根據生成的 Schema 來運行 Mock 服務。
附:Schema 獲取與合併代碼參考

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

項目目錄
其中的 ProjectListContainer.js 部分代碼參考:

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 Relayweb

部分配置代碼參考:數據庫

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

路由
二、根據實際狀況,選擇封裝成 QueryRendererFragment Container
好比,某個彈窗內的表格數據,能夠考慮使用 QueryRenderer,在觸發了打開彈窗操做後,再由組件主動請求數據,而非 Fragment Container,由路由 Container 一口氣拉到全部數據,這樣會影響頁面加載速度,並且也沒有必要;
三、在一般的單頁應用裏,除非是有切換用戶的功能,通常 Relay 的 environment 應只在一處配置,全部 Relay 組件共享。
(關於 QueryRenderer、Fragment Container、environment 能夠參考 Relay 官方文檔

組件封裝

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,並不會有什麼問題。因此,適合本身的纔是最好的! 最後,有任何問題,歡迎留言討論,一塊兒學習。

相關文章
相關標籤/搜索