GraphQL學習之實踐篇

前言

兩篇文章學完了GraphQL(基礎篇, 原理篇),接下去即是實踐的過程,這個實踐咱們使用了以下技術棧去實現一套任務管理系統,源碼就不公開了, 等穩定後再發布。效果以下:css

使用的技術棧有:前端

  1. React16全特性
  2. Antd構建UI界面
  3. create-react-app搭建客戶端基礎
  4. react-apollo完成客戶端請求的封裝和響應體的處理
  5. bizcharts(g2)實現圖表
  6. apollo-boost(graphql)完成客戶端數據請求
  7. rxjs完成某部分響應式設計
  8. 全程使用ramda.js作函數式編程
  9. Nest框架作服務器
  10. 數據庫選擇moogoose
  11. passport搭配jsonwebtoken作用戶認證管理
  12. graphql-server(@nest/graphql)實現服務端graphql請求的處理

項目結構

以下圖:react

分爲兩大部分:clientserver,其中client的目錄結構以下:git

各個目錄的解釋在圖中已經體現。github

server端的目錄結構以下:web

各個目錄的含義的解釋在圖中已經體現。typescript

由於咱們主要是講graphql的應用,因此其他的細節忽略不說。至於Nest框架的使用,請參考文檔Nest.js數據庫

GraphQL的實踐

實踐GraphQL咱們不會直接用graphql-js,而是使用功能更加豐富、社區支持更多apollo-graphql。其文檔編寫的也是很詳盡,基本上全部的問題均可以在文檔上找到答案。推薦新手能夠先按照Get started來入手編程

客戶端的實踐

初始化

由於咱們使用了apollo-boost,因此在前端入口文件上,要拿這個包進行一些初始化,獲得apolloClient的實例(無關的代碼已經去掉):json

import React from 'react';
import ReactDOM from 'react-dom';
import { ApolloProvider } from 'react-apollo';
import ApolloClient from 'apollo-boost'

... ...
const GW_BASE_URL = process.env.NODE_ENV === 'production' ? '/graphql' : 'http://127.0.0.1:8888/graphql'

const client = new ApolloClient({
  uri: GW_BASE_URL,
  // 須要設置這個,這樣每次請求的時候認證信息纔會帶上
  fetchOptions: {
    credentials: 'include',
  },
  // 緩存讀取配置
  clientState: {
    typeDefs,
    resolvers,
  },
  // 設置這個是爲了配合jwt
  request: async (operation) => {
    // get the authentication token from local storage if it exists
    const token = localStorage.getItem('token');
    operation.setContext({
      headers: {
        authorization: token ? `Bearer ${token}` : '',
        Origin: location.href,
      },
    });
  },
  // 設置全局錯誤處理信息,這樣就不用每一個請求都進行error處理
  onError: (errObj) => {
    if (errObj.graphQLErrors) {
      const errorMsg = errObj.graphQLErrors[0].message;
      if (errorMsg === '身份信息已過時,請從新登陸') {
        ... ...
        message.info(errorMsg, 3, () => location.hash = '#/user/login');
      } else if (errorMsg && (errorMsg as any).statusCode === 403) {
        message.error('權限不足,請不要重試');
      } else {
        message.error(errorMsg);
      }
    }
  },
});

ReactDOM.render(
  <ApolloProvider client={client}>
    <LocaleProvider>
      <App />
    </LocaleProvider>
  </ApolloProvider>,
  document.getElementById('root'),
);

複製代碼

頁面級別的使用

每一個頁面都會新建三個文件:

graphql.ts
index.scss
index.tsx
複製代碼

其中graphql.ts定義了客戶端的請求,好比:

import gql from 'graphql-tag';

// 用來查詢全部的用戶
export const QUERY_USERS = gql`
  query {
    userList {
      id
      roles
      team
      mobile
      staffCode
      email
      username
    }
  }
`;
複製代碼

然後在index.tsx文件中就可使用這個查詢語句,以下:

整個流程是很清晰的,由於使用了typescript,因此在客戶端能夠引用到服務端定義的返回類型,從而提升了代碼編寫的速度。

實踐出來的問題和想法

  1. react-apollo目前發現了個bug,若是我返回的數據的層級太深,好比達到了4層以上,數據更新到緩存的時候便會出錯。
  2. 關於graphql的本地狀態的管理略微複雜,若是有個請求的結果從一開始就一直被全部的頁面使用,通常一些公共的信息,好比用戶名等,這種狀況下想要直接拿到的話是不大可能的,須要繞一大圈去實現,有點蛋疼~
  3. 分頁的功能分爲遊標式和skip式,很明顯遊標式並不適用於web端,雖然遊標式對數據是很是友好的。在移動端用遊標式更加適合。
  4. 本地狀態管理是graphql的一個很厲害的功能,直接不須要任何數據管理框架,就能夠實現數據的各類操做,這是一大亮點!
  5. apollo-graphql也提供了開發者工具,能夠在瀏覽器實時預覽當前緩存的全部數據Developer tools

服務端實踐

由於服務端使用了Nest.js,因此沒有直接用apollo提供的服務器,而是用了Nest框架封裝出來適用於Nest框架的graphql包graphql。該包仍是提供了不少功能的。

服務端GraphQL的初始化

app.modules.ts中,咱們要去初始化graphql模塊(無關代碼已忽略):

@Module({
  imports: [
    GraphQLModule.forRoot({
      // 指定服務端schema存放的位置
      typePaths: ['graphql/schema.graphql'],
      // 配置了該選項,能夠自動根據代碼生成schema
      autoSchemaFile: path.join(__dirname, 'graphql/schema.graphql'),
      buildSchemaOptions: {
      },
      // 能夠自動生成types文件
      // definitions: {
      //   path: path.join(__dirname, 'types/graphql.ts'),
      // },
      debug: true,
      playground: true,
      context: ({ req }) => ({ req }), // 必定要這裏設置req到上下文中,不然在guard中是拿不到這個req參數的
    }),
  ],
  controllers: [],
  providers: []
})
複製代碼

服務端業務層級的實現

每一個業務目錄都會存在這麼些文件:

咱們在dto目錄下定義三種類型文件:xx.args.ts/xx.input.ts/xx.model.ts,分別定義下面三種狀況

  1. args對應請求不是Input類型的
  2. input對應請求是Input類型的
  3. model對應請求的響應體

然後在xx.resolver.ts中實現resolve函數,藉助於修飾器,好比:

import { Query, Resolver, Args, Mutation } from '@nestjs/graphql'

@Resolver('User')
export class UserResolver {
  @Query(returns => [UserItem])
  ... ...
  async userList(): Promise<UserItem[]>{
    return this.userService.getUserList();
  }
}
複製代碼

UserItem在這裏(user.model.ts)定義:

import { ObjectType, Field, ID, registerEnumType, Int } from 'type-graphql'
... ...

@ObjectType()
export class UserItem {
  @Field(type => ID, {nullable: true})
  _id?: number;

  @Field({nullable: true})
  username?: string;

  @Field({nullable: true})
  email?: string;

  ... ...
}
複製代碼

如此便完成了整個服務端數據流的過程。看着是否是很easy啊?

服務端實踐的思考

  1. 數據庫的model和graphQL定義的model大體相同,兩者如何更好地契合在一塊兒?目前社區並無給出對應的解決方案。
  2. 由於graphql只有一個endpoint,因此打印請求就不能像以前restful那樣,須要一個與之對應的打印方案
  3. 如何結合swagger實現文檔級別的呈現?亦或是不須要swagger,而是依靠schema去呈現文檔給客戶端,值的深刻研究,並給出解決方案
  4. graphql號稱解決版本的兼容性問題是垂手可得的,目前在本項目中並無體現到。

最後

至此,三篇關於GraphQL的文章到此結束了,花了不少時間斷斷續續地學習,但願能夠給你們呈現一份不同地文章,供你們思考。後續我所在的公司網關團隊會持續實踐GraphQL,爭取貢獻出更多的解決方案。

相關文章
相關標籤/搜索