在只學習graphql client端知識的過程當中,咱們經常須要一個graphql ide來提示graphql語法,以及實現graphql的server端來進行練手。 graphql社區提供了graphiql讓咱們使用html
graphiql (npm):一個交互式的運行於瀏覽器中的 GraphQL IDE.前端
但graphiql提供的live demo基本打不開,難道剛接觸graphql就要本身實現graphql的server端? 好在github用graphql寫了一套api,咱們能夠去這裏,登錄後便可體驗一把graphql。vue
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 客戶端庫。webpack
從npm download數量上看Apollo Client是最多的,而且Apollo也有服務端的解決方案,因此這裏選擇Apollo Client做爲graphql的client端 apollo client對於web 框架都有具體的實現,可是我更但願能像axios那樣去使用graphql,而不是每套web框架都要去學一下具體實現,那樣會折騰死本身。ios
// 使用vue-cli初始化項目
vue init webpack-simple my-project
npm i
複製代碼
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
│ │ └── search.js
│ ├── main.js
│ └── utils
│ └── graphql.js // 對Apollo-client封裝
└── webpack.config.js
複製代碼
接下來對apollo-client進行封裝,加上中間件(實現相似於axios攔截器的效果)。 graphql.jsgit
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文件github
// 在rules下添加如下規則
{
test: /\.(graphql|gql)$/,
exclude: /node_modules/,
loader: 'graphql-tag/loader',
}
複製代碼
search.graphqlweb
query searchR ($keyword: String!) {
search (query: $keyword , type: REPOSITORY){
userCount
}
}
複製代碼
search.jssql
import client from '../utils/graphql'
// import gql from 'graphql-tag'
import { searchR } from './search.graphql'
export const search = (params) => client.query({
query: searchR,
variables: params
})
複製代碼
到這裏咱們已經能夠直接調用/graphql/下導出的function
graphql實現分頁有如下兩種方式:
基於偏移量的分頁實現簡單,但存在如下問題:
性能問題,雖然可使用 「延遲關聯」 解決,但會使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 的分頁,也存在硬傷:
因此咱們須要同時支持這兩種分頁。
Relay 定義了 PageInfo
,Edges
,Edge Types
,Node
,Cursor
等對象 用於實現靈活的分頁。👇是Relay給出的一個query例子。
{
user {
id
name
friends(first: 10, after: "opaqueCursor") {
edges {
cursor
node {
id
name
}
}
pageInfo {
hasNextPage
}
}
}
}
複製代碼
friends 鏈接會返回一個對象,這個對象的名稱會以 Connection
結尾,如friendConnection
, Connection
中必需包含PageInfo
,Edges
。
Relay 在返回的遊標鏈接上提供了一個 PageInfo 對象,其必需包含 hasPreviousPage, hasNextPage。
遊標是不透明的,而且它們的格式不該該被依賴,建議用 base64 編碼它們。
Edges
:類型爲 LIST
,必需包含Edge Types
Edge Types
:類型爲 Object
,必需包含 Node
,Cursor
Node
: 類型能夠爲 標量,枚舉,對象,接口,聯合類型,此字段沒法返回列表。 Cursor
: 類型爲String
經過Edges,列表數據中每一項都包含一個Cursor、Node,但咱們基本不多須要Cursor。
下一頁分頁,須要兩個參數。
上一頁分頁,須要兩個參數。
first跟last不該該同時使用,這會使判斷 上一頁/下一頁 變得麻煩。
咱們須要在query時,把跳過多少條記錄 這個參數給到 service 端,後端根據這個值 是否 存在 去使用不一樣分頁方式。Prisma把這個參數命名爲 skip
,這裏咱們與其保持一致。
{
user {
id
name
friends(first: 10, after: "opaqueCursor", skip: 1) {
edges {
cursor
node {
id
name
}
}
pageInfo {
hasNextPage
}
}
}
}
複製代碼
👉分頁例子:包含先後端
後端項目目錄結構如👇
./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
}
複製代碼
PageInfo
的resolver
層
// 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;
},
},
};
複製代碼