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

相关文章
相关标签/搜索