在只学习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;
},
},
};
复制代码