前段时间,分享了一篇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,并不会有什么问题。因此,适合本身的才是最好的! 最后,有任何问题,欢迎留言讨论,一块儿学习。