前段時間,分享了一篇GraphQL & Relay 初探,主要介紹了 GraphQL 的設計思想和 Relay 的基本應用。
目前,筆者在實際項目中應用 GraphQL+Relay 已經有段時間了,併發布了一個正式版本。整個過程當中,踩了很多坑,也摸索出了一些經驗,特此作一下總結分享。html
對於架構設計與角色分工,必定程度上,依賴於團隊人員的配置。因爲咱們團隊主要由後端研發組成,前端人數有限,因此仍是以「前」和「後」爲分界來分工,即前端負責純 Web 端部分的開發,後端來實現後端邏輯以及 GraphQL 層的封裝。
具體而言,每一個後端研發負責一個或多個業務模塊,每一個模塊都微服務化,並起一個 GraphQL 或 RESTful API 服務。後端同時還負責維護一個 API Gateway 模塊,用來轉發前端過來的請求、鑑權、統一錯誤處理等工做。整個架構以下圖: 前端
因爲人員限制,採用了上面提到的第一種,後端微服務化的架構設計,便不可避免的存在一些溝通成本。對此,結合社區已有的解決方案,設計了一個半自動的工做流,以下圖: react
其中,核心點在於,腳本自動化地獲取各 GraphQL 微服務的 Schema,而後作合併,彙總成一個總的 Schema。這個總的 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
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,並不會有什麼問題。因此,適合本身的纔是最好的! 最後,有任何問題,歡迎留言討論,一塊兒學習。