Graphql實踐——像axios同樣使用Graphql

Graphql嚐鮮

在只學習graphql client端知識的過程當中,咱們經常須要一個graphql ide來提示graphql語法,以及實現graphql的server端來進行練手。
graphql社區提供了graphiql讓咱們使用html

graphiql (npm):一個交互式的運行於瀏覽器中的 GraphQL IDE.

但graphiql提供的live demo基本打不開,難道剛接觸graphql就要本身實現graphql的server端?
好在github用graphql寫了一套api,咱們能夠去這裏,登錄後便可體驗一把graphql。前端

clipboard.png
關於graphql的基礎知識能夠去這裏看看vue

graphql client端選擇

graphql在前端實現有如下方案。node

Relay (github) (npm):Facebook 的框架,用於構建與 GraphQL 後端交流的 React 應用。
Apollo Client (github):一個強大的 JavaScript GraphQL 客戶端,設計用於與 React、React Native、Angular 2 或者原生 JavaScript 一同工做。
graphql-request:一個簡單的彈性的 JavaScript GraphQL 客戶端,能夠運行於全部的 JavaScript 環境(瀏覽器,Node.js 和 React Native)—— 基本上是 fetch 的輕度封裝。
Lokka:一個簡單的 JavaScript GraphQL 客戶端,能夠運行於全部的 JavaScript 環境 —— 瀏覽器,Node.js 和 React Native。
nanogql:一個使用模板字符串的小型 GraphQL 客戶端庫。

從npm download數量上看Apollo Client是最多的,而且Apollo也有服務端的解決方案,因此這裏選擇Apollo Client做爲graphql的client端
apollo client對於web 框架都有具體的實現,可是我更但願能像axios那樣去使用graphql,而不是每套web框架都要去學一下具體實現,那樣會折騰死本身。
clipboard.pngwebpack

初始化項目

// 使用vue-cli初始化項目
vue init webpack-simple my-project
npm i

安裝graphql

npm i apollo-cache-inmemory apollo-client apollo-link apollo-link-http 
npm i graphql graphql-tag

項目結構以下

.
├── index.html
├── package.json
├── package-lock.json
├── README.md
├── src
│   ├── App.vue
│   ├── graphql                            // 接口
│   │   ├── search.graphql
│   │   └── index.js                       // export全部接口
│   ├── main.js
│   └── utils
│       └── graphql.js                    // 對Apollo-client封裝
└── webpack.config.js

apollo-client

接下來對apollo-client進行封裝,加上中間件(實現相似於axios攔截器的效果)。
graphql.jsios

import ApolloClient from 'apollo-client'
import { InMemoryCache } from 'apollo-cache-inmemory'
import { HttpLink } from 'apollo-link-http'
import { onError } from 'apollo-link-error'
import { ApolloLink, from } from 'apollo-link'

const token = '598ffa46592d1c7f57ccf8173e47290c6db0d549'

const Middleware = new ApolloLink((operation, forward) => {
  // request時對請求進行處理
  console.log('Middleware', operation, forward)
})
const Afterware = new ApolloLink((operation, forward) => {
  return forward(operation).map(response => {
    // 服務器返回數據
    console.log('Afterware--response', response)
    return response
  })
})
const errorLink = onError(({ graphQLErrors, networkError }) => {
  if (graphQLErrors)
    graphQLErrors.map(({ message, locations, path }) =>
      console.log(
        `[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`,
      ),
    );
  if (networkError) console.log(`[Network error]: ${networkError}`);
});

const httpLink = new HttpLink({
  uri: 'https://api.github.com/graphql', // 配置請求url 
  headers: {                             // 配置header
    Authorization: `Bearer ${token}`
  }
})
const cache = new InMemoryCache()       // 緩存
export default new ApolloClient({
  link: from([Middleware, Afterware, errorLink, httpLink]),
  cache
})

配置webpack支持.graphql文件git

// 在rules下添加如下規則
      {
        test: /\.(graphql|gql)$/,
        exclude: /node_modules/,
        loader: 'graphql-tag/loader',
      }

search.graphqlgithub

query searchR ($keyword: String!) {
    search (query: $keyword , type: REPOSITORY){
        userCount
    }
}

在/graphql/index.js export全部接口web

import client from '../utils/graphql'
// import gql from 'graphql-tag'
import * as searchGql from './search.graphql'
/** searchGql模塊 **/
export const search = (params) => client.query({query: searchGql.search, variables: params})

到這裏咱們已經能夠直接調用/graphql/下導出的functionsql

使用github接口實現一個簡單的搜索功能

圖片描述
具體實現就不貼出來了,所有代碼已經放到github,歡迎star。
run的時候有記得把token換成本身的,由於個人token有可能已經失效。

Graphql分頁

graphql實現分頁有如下兩種方式:

  1. 基於偏移量,須要提供第幾頁, 每頁的數量
  2. 基於遊標或者id,提供每頁數量,與 遊標/id。

對於遊標分頁Relay(Facebook家的Graphql庫) 定了一套規範 Relay-style cursor pagination

基於偏移量的分頁實現簡單,但存在如下問題:

  • 性能問題,雖然可使用 「延遲關聯」 解決,但會使sql語句變得複雜

    # 假設 有一個 product商品表,當商品表數量足夠多時,這個查詢會變得很是緩慢,
    SELECT id, name FROM product LIMIT 1000, 20;
    # 若是咱們提供一個邊界值,好比id,不管翻頁到多麼後面,其性能都會很好
    SELECT id, name FROM product WHERE id > 1000 LIMIT 20;
  • 刪除列表數據時,致使獲取下一頁的數據缺失

    # 假設 總共有11條數據,一頁顯示10條,總頁數爲 2 頁。
    # 當調用接口刪除 第 1 頁的 1 條數據,而後進行翻頁時,由於只剩下10條數據,因此下面的sql會查不到數據。
    SELECT id, name FROM product LIMIT 10, 10;

基於遊標/ID 的分頁,也存在硬傷:

  • 如何實現跳往第 n 頁的功能
    難道要獲取 相應的遊標再進行翻頁? 因此它更適用於無限加載,或者只有 上一頁/下一頁 的情景上,對於跳往第n頁仍是須要用到基於偏移量的分頁。

因此咱們須要同時支持這兩種分頁。
圖片描述

Relay 式的遊標分頁

Relay 定義了 PageInfoEdgesEdge TypesNodeCursor等對象 用於實現靈活的分頁。👇是Relay給出的一個query例子。

{
  user {
    id
    name
    friends(first: 10, after: "opaqueCursor") {
      edges {
        cursor
        node {
          id
          name
        }
      }
      pageInfo {
        hasNextPage
      }
    }
  }
}

friends 鏈接會返回一個對象,這個對象的名稱會以 Connection結尾,如friendConnection, Connection中必需包含PageInfoEdges

PageInfo

Relay 在返回的遊標鏈接上提供了一個 PageInfo 對象,其必需包含 hasPreviousPage, hasNextPage。

  • startCursor : 列表中 第一個項的遊標id,字符串
  • endCursor : 最後一個項的遊標id
  • hasPreviousPage : 是否有上一頁,布爾值
  • hasNextPage :是否有下一頁
遊標是不透明的,而且它們的格式不該該被依賴,建議用 base64 編碼它們。

Edges

Edges:類型爲 LIST ,必需包含Edge Types
Edge Types:類型爲 Object,必需包含 NodeCursor
Node: 類型能夠爲 標量,枚舉,對象,接口,聯合類型,此字段沒法返回列表。
Cursor: 類型爲String

經過Edges,列表數據中每一項都包含一個Cursor、Node,但咱們基本不多須要Cursor。

上一頁/下一頁

下一頁分頁,須要兩個參數。

  • first 頁數大小,必需爲一個非負整數。
  • after 遊標id。

服務器最多返回 first 條 遊標id以後(不包括遊標id) 的數據。

上一頁分頁,須要兩個參數。

  • last 頁數大小,必需爲一個非負整數。
  • before 遊標id。

服務器最多返回 last 條 遊標id以前(不包括遊標id) 的數據。

first跟last不該該同時使用,這會使判斷 上一頁/下一頁 變得麻煩。

支持基於偏移量的分頁

咱們須要在query時,把跳過多少條記錄 這個參數給到 service 端,後端根據這個值 是否 存在 去使用不一樣分頁方式。Prisma把這個參數命名爲 skip,這裏咱們與其保持一致。

{
  user {
    id
    name
    friends(first: 10, after: "opaqueCursor", skip: 1) {
      edges {
        cursor
        node {
          id
          name
        }
      }
      pageInfo {
        hasNextPage
      }
    }
  }
}

基於egg.js實現基於偏移量 與 Relay 式的遊標分頁

👉分頁例子:包含先後端
後端項目目錄結構如👇

./app/
├── extend
├── graphql
│   ├── common                  # 定義公用的 Schema 和類型,如pageInfo
│   │   ├── resolver.js
│   │   ├── scalars
│   │   │   └── cursor.js       # 定義cursor數據類型
│   │   └── schema.graphql
│   ├── mutation
│   │   └── schema.graphql
│   ├── query
│   │   └── schema.graphql
│   └── user                    # user
│       ├── connector.js
│       ├── resolver.js
│       └── schema.graphql

定義 PageInfo對象,Cursor標量 。

# graphql/common/schema.graphql
scalar Cursor
type PageInfo {
    hasNextPage: Boolean!
    hasPreviousPage: Boolean!
    startCursor: Cursor
    endCursor: Cursor
}

PageInforesolver

// graphql/common/resolver
'use strict';
module.exports = {
  Cursor: require('./scalars/cursor'), // eslint-disable-line
  PageInfo: {
    hasNextPage(root) {
      // 在Connector層(如UserConnector)返回PageInfo對象時,咱們能夠返回 function 或 boolean,function可以支持更加複雜的判斷
      if (typeof root.hasNextPage === 'function') return root.hasNextPage();
      return root.hasNextPage;
    },
    hasPreviousPage(root) {
      if (typeof root.hasPreviousPage === 'function') return root.hasPreviousPage();
      return root.hasPreviousPage;
    },
  },
};
相關文章
相關標籤/搜索