
我曾经在公司层面进行了不止一年的基于Graphql的前后端开发,因为积累了一些关于Graphql的相关
Apollo是基于GraphQL的全栈解决方案集合,包括了apollo-client和apollo-server,从后端到前端提供了对应的lib使得开发GraphQL更加方便
apollo-boost 包含启动阿波罗客户端的所有依赖
react-apollo 视图层面的集合
graph-tag 解析查询语句
graphql 也是解析查询语句像大多数前端开发工具一样,Apollo Client 是非常灵活的,比如 Apollo Cache 就是其中一个。Apollo Client 为浏览器提供了获取数据的缓存设置,避免不必要的网络请求,来提升应用的性能。
在使用 Apolllo Query 时,数据是从缓存或者服务端接口中获取,取决于 fetch policy 的设置。fetch policy 表示获取数据的优先级,比如是从服务端拉取最新的数据,还是从缓存中快速读取到数据。理解 fetch policy,有助于更清晰地理解 Apollo GraphQL 应用的数据流,解决一些获取数据时的异常。
cache-first:
Apollo 默认的 fetch policy 是 cache-first(缓存优先),与获取最新数据相比,这种方式会快速获取到数据。如果你不想数据发生变化或者对数据实时性要求不高的情况下,可以使用缓存优先:
cache-and-network:
如果我们需要要显示经常更新的数据时,这是一个很好的 fetch policy。cache first 强调的是快速获取数据,而 cache-and-network 则侧重于让缓存数据跟服务端一样保持最新。如果对数据进行了修改,但是又担心缓存过期时,这个策略会是一个很好的解决方案。
Network-only
如果不想因为显示过期的数据带来的风险时,使用 network-only 会更加合理,这个策略比如快速获取数据,更倾向于保证数据的实时性。不像 cache-and-network,该策略不会从缓存返回可能过期的数据,同时它又能保证缓存的数据是最新的。
no-cache
no-cache 有点类似 network-only,但是它跳过了缓存数据的更新。如果你不想在缓存中存储任何信息时,它会非常合适。
Cache-only
跟 no-cache 恰恰相反,这个策略会避免发起网络请求,但是如果获取数据没在缓存中的话,就会抛出一个错误。如果需要给用户一直显示同个数据而忽略服务端的变化时,或者在离线访问时,这个策略就非常有用了。
你既可以为整个应用设置 fetch policy,也可以单独为某个 query 设置,至于使用哪种策略,还需要根据实际的架构而定。
使用apollo-boost
import ApolloClient from 'apollo-boost'
const client = new ApolloClient({
uri: 'http://localhost:5000/graphql'
})
import { ApolloProvider,Query } from 'react-apollo'
import { Mutation,MutationFunc } from 'react-apollo'安装preset包
# installing the preset package
npm install apollo-boost graphql-tag graphql --save安装@apollo/client
# installing each piece independently
npm install apollo-client apollo-cache-inmemory apollo-link-http graphql-tag graphql --save使用@apollo/client
import ApolloClient from 'apollo-boost';
const client = new ApolloClient();
import React, { ReactElement } from 'react';
import {useQuery, gql } from '@apollo/client';
const GET_AUTHOR = gql`
query Author($id: Int!) {
author(id: $id) {
id
firstName
lastName
posts {
title
author
}
}
}
`
export default function Home({}): ReactElement {
const {data, loading, refetch } = useQuery(GET_AUTHOR, { variables: {id: 1}});
return (
<div>home</div>
)
}import { split } from 'apollo-link';
import { HttpLink } from 'apollo-link-http';
import { WebSocketLink } from 'apollo-link-ws';
import { getMainDefinition } from 'apollo-utilities';
// Create an http link:
const httpLink = new HttpLink({
uri: 'http://localhost:3000/graphql'
});
// Create a WebSocket link:
const wsLink = new WebSocketLink({
uri: `ws://localhost:5000/`,
options: {
reconnect: true
}
});
// using the ability to split links, you can send data to each link
// depending on what kind of operation is being sent
const link = split(
// split based on operation type
({ query }) => {
const definition = getMainDefinition(query);
return (
definition.kind === 'OperationDefinition' &&
definition.operation === 'subscription'
);
},
wsLink,
httpLink,
);node端可以使用graphql-request请求
npm add graphql-request grapghql使用
import { request, gql } from 'graphql-request'
const query = gql`
{
company {
ceo
}
roadster {
apoapsis_au
}
}
`
request('https://api.spacex.land/graphql/', query).then((data) => console.log(data))
import { request, GraphQLClient } from 'graphql-request'
// Run GraphQL queries/mutations using a static function
request(endpoint, query, variables).then((data) => console.log(data))
// ... or create a GraphQL client instance to send requests
const client = new GraphQLClient(endpoint, { headers: {} })
client.request(query, variables).then((data) => console.log(data))使用graphql-code-generate生成hooks代码
安装graphql-code-generator命令行工具
yarn add graphql
yarn add -D @graphql-codegen/cli根据实际项目情况安装生成hooks的plugins
yarn add @graphql-codegen/typescript-react-apollo
yarn add @graphql-codegen/typescript
yarn add @graphql-codegen/typescript-operations然后首先编写graphql文件
mutation CreateAuthor($author: authorInput) {
createAuthor(author: $author)
}
query Author($id: Int!) {
author(id: $id) {
id
firstName
lastName
posts {
title
author
}
}
}再编写codegen的config文件
overwrite: trues
schema: ./schema.gql
documents: 'scr/**/*.graphql'
generates:
src/generated/graphql.tsx:
plugins:
- 'typescript'
- 'typescript-operations'
- 'typescript-react-apollo'
hooks:
afterOneFileWrite:
- prettier --write
schema.graphql:
plugins:
- 'schema-ast'
hooks:
afterOneFileWrite:
- prettier --writeoverwrite - 生成代码时覆盖文件的标志(true默认情况下)
schema(必需) - 指向GraphQLSchema,可以通过本地文件路径、url等多种方式
documents - 指向你的GraphQL文档:query、 mutation、subscription、fragment
generates(必需) - 一个映射,其中键表示生成代码的输出路径,值表示该特定文件的一组相关选项:
配置package.json
{
"scripts": {
"codegen": "graphql-codegen"
},
}graphql上传接口
npm install apollo-upload-client使用
import { gql, useMutation } from "@apollo/client";
const MUTATION = gql`
mutation ($file: Upload!) {
uploadFile(file: $file) {
success
}
}
`;
function UploadFile() {
const [mutate] = useMutation(MUTATION);
function onChange({
target: {
validity,
files: [file],
},
}) {
if (validity.valid) mutate({ variables: { file } });
}
return <input type="file" required onChange={onChange} />;
}@graphql-ws
graphql的websocket客户端
import { GraphQLWsLink } from '@apollo/client/link/subscriptions';
import { createClient } from 'graphql-ws';
const wsLink = new GraphQLWsLink(createClient({
url: 'ws://localhost:4000/subscriptions',
connectionParams: {
authToken: user.authToken,
},
}));React-apollo
react中提供graphql上下文的组件
import { ApolloProvider } from 'react-apollo';
import { ApolloLink } from 'apollo-link';
import ApolloClient from 'apollo-client';
const App = () => {
client = new ApolloClient({
link: ApolloLink.from([onError(() => {}), new SchemaLink({ schema })]),
cache: cache || globalCache || new InMemoryCache(),
});
return <ApolloProvider client={client}>
{children}
</ApolloProvider>;
}https://formidable.com/open-source/urql/docs/
graphQL客户端,支持Vue、React
yarn add urql
# or
npm install --save urql使用
import { Client, Provider, cacheExchange, fetchExchange } from 'urql';
const client = new Client({
url: 'http://localhost:3000/graphql',
exchanges: [cacheExchange, fetchExchange],
});
const App = () => (
<Provider value={client}>
<YourRoutes />
</Provider>
);组件使用
import { gql, useQuery } from 'urql';
const TodosQuery = gql`
query {
todos {
id
title
}
}
`;
const Todos = () => {
const [result, reexecuteQuery] = useQuery({
query: TodosQuery,
});
const { data, fetching, error } = result;
if (fetching) return <p>Loading...</p>;
if (error) return <p>Oh no... {error.message}</p>;
return (
<ul>
{data.todos.map(todo => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
);
};生成graphql ts类型,
安装
npm install gql.tada
npm install --save-dev @0no-co/graphqlsp在tsconfig中添加插件
{
"compilerOptions": {
"strict": true,
"plugins": [
{
"name": "@0no-co/graphqlsp",
"schema": "./schema.graphql",
"tadaOutputLocation": "./src/graphql-env.d.ts"
}
]
}
}使用
import { graphql } from 'gql.tada';
const TodosQuery = graphql(`
query Todos ($limit: Int = 10) {
todos(limit: $limit) {
id
title
completed
}
}
`);安装依赖
npm install apollo-server@2.13.1 graphql@14.6.0 type-graphql@0.17.6引入
import "reflect-metadata"
import {buildSchema,ObjectType,Field,ID,Resolver,Query} from "type-graphql";
import {ApolloServer} from "apollo-server";后端定义schema和resolver
@ObjectType()
class Post{
@Field(type => ID)
id: string;
@Field()
created: Data;
@Field()
content: String;
}
@Resolver(Post)
class PostResolver {
@Query(returns => [Post])
async posts(): Promise<Post[]>{
return [
{
id:"0",
created: new Date(),
content:'aaa'
},
{
id:"1",
created: new Date(),
content:'bbb'
},
{
id:"2",
created: new Date(),
content:'ccc'
},
]
}
}运行项目,在localhost:4444打开graphql的playground进行测试
基于graphql的后端框架
安装
pnpm add graphql-yoga graphql使用
import { createSchema, createYoga } from 'graphql-yoga'
import { createServer } from 'node:http'
const yoga = createYoga({
schema: createSchema({
typeDefs: `
type Query {
hello: String
}
`,
resolvers: {
Query: {
hello: () => 'Hello from Yoga!'
}
}
})
})
const server = createServer(yoga)
server.listen(4000, () => {
console.info('Server is running on http://localhost:4000/graphql')
})类型安全的graph schema
安装
npm install nexus graphql使用
import { queryType, stringArg, makeSchema } from 'nexus'
import { GraphQLServer } from 'graphql-yoga'
const Query = queryType({
definition(t) {
t.string('hello', {
args: { name: stringArg() },
resolve: (parent, { name }) => `Hello ${name || 'World'}!`,
})
},
})
const schema = makeSchema({
types: [Query],
outputs: {
schema: __dirname + '/generated/schema.graphql',
typegen: __dirname + '/generated/typings.ts',
},
})
const server = new GraphQLServer({
schema,
})
server.start(() => `Server is running on http://localhost:4000`)Nexus-prisma
基于prisma的model生成nexus的schema
安装
npm add nexus-prisma nexus graphql @prisma/client使用
import { User } from 'nexus-prisma'
import { makeSchema, objectType } from 'nexus'
export const schema = makeSchema({
types: [
objectType({
name: User.$name
description: User.$description
definition(t) {
t.field(User.id)
// t.field(User.id.name, User.id) <-- For nexus@=<1.0 users
}
})
]
})在koa和express框架中使用graphql的upload接口
安装
npm install graphql-upload graphql使用
import graphqlUploadKoa from "graphql-upload";
app.use(
graphqlUploadKoa({
// Limits here should be stricter than config for surrounding infrastructure
// such as NGINX so errors can be handled elegantly by `graphql-upload`.
maxFileSize: 10000000, // 10 MB
maxFiles: 20,
})
);graphql的内部包
https://the-guild.dev/graphql/tools/docs/resolvers-composition
node端graphql发布订阅
安装
npm install graphql-subscriptions graphql使用
import { PubSub } from 'graphql-subscriptions';
export const pubsub = new PubSub();
const SOMETHING_CHANGED_TOPIC = 'something_changed';
export const resolvers = {
Subscription: {
somethingChanged: {
subscribe: () => pubsub.asyncIterator(SOMETHING_CHANGED_TOPIC),
},
},
}
pubsub.publish(SOMETHING_CHANGED_TOPIC, { somethingChanged: { id: "123" }});graphql做auth验证层的中间件
使用
import { shield, rule, allow } from 'graphql-shield';
const isAuthenticated = rule()((parent, args, ctx) => {
if (ctx.userId) {
return true;
}
return new ServerError({
code: ServerErrorCode.LOGIN_FAILED,
msg: `no user matched`,
});
});
const authMiddleware = shield({
Query: {
users: allow,
},
Mutation: {
admin: allow
},
});个性化的graphql中间件
https://github.com/dimatill/graphql-middleware
限制graphql查询层数
npm install graphql-depth-limit可以配合express-graphql和koa-graphql使用
import depthLimit from 'graphql-depth-limit'
import express from 'express'
import graphqlHTTP from 'express-graphql'
import schema from './schema'
const app = express()
app.use('/graphql', graphqlHTTP((req, res) => ({
schema,
validationRules: [ depthLimit(10) ]
})))将graphql的schema以图形化的形式展示
https://github.com/graphql-kit/graphql-voyager
graphql查询上云
https://github.com/hasura/graphql-engine
graphql请求数据cli
安装
npm install -g graphqurl使用
gq https://my-graphql-endpoint/graphql \
-H 'Authorization: Bearer <token>' \
-q 'query { table { column } }'将graphql转换为sql
https://github.com/join-monster/join-monster
将graphql的api转换为restful的API
安装
yarn add sofa-api
# or
npm install sofa-api使用
import { useSofa } from 'sofa-api';
import express from 'express';
const app = express();
app.use(
'/api',
useSofa({
basePath: '/api',
schema,
})
);
// GET /api/users
// GET /api/messages内省查询就是指graphql的playground查询docs和schema,也可以显示地进行内省查询。
https://half90.top/2022/07/13/graphql-gong-ji-mian-zong-jie/#toc-heading-19
支持批查询的GraphQL允许客户端同时发送多条独立的查询语句,类型主要有两种:json列表批查询、基于名称的批查询:
在 GraphQL 中,存在一个称为查询成本分析的概念,它将权重值分配给解析成本高于其他字段的字段。使用此功能,我们可以创建一个上限阈值来拒绝昂贵的查询。或者,可以实现缓存功能以避免在短时间内重复相同的请求。
嵌套查询:检查深度
基于PostgreSQL一个命令生成GraphQL API
npx postgraphile -c 'postgres://user:pass@localhost/mydb' --watch --enhance-graphiql --dynamic-jsonhttps://github.com/graphql/dataloader
看下面的搜索语句,查找图书列表,以及每本图书的名字、图书作者的 id 和图书作者的名字。
这一条查询语句究竟做了什么查询呢
{
books{
name
author{
id
name
}
}
}在上面的例子中可以看到,如果图书列表有 8 本书,
一条请求语句总共进行了 1 + 8 = 9 次请求。
一条简单的请求语句就对数据库进行了 9 次查询,如果 author 下又对每个 author 创作的图书列表进行请求,那么对数据库的请求次数就会以乘数形式增加。
这就是 graphql 的 n + 1 问题
如果我们把每次请求的作者 id 收集起来,一并去查找作者信息,就只需要对数据库进行一次请求就可以拿到数据了。这也就是使用 dataloader 时 callback 所做的事情
在执行每次请求时候对请求数据的参数进行缓存,在 js 的当前事件循环收集所有参数,在 process.nextTick 拿着所有参数去进行数据处理。
npm install --save dataloader为每一次请求创建一个新的 dataloader 实例,请求结束后实例被垃圾回收
const AuthorLoader = () => new Dataloader(authCallback)
app.use('/graphql', expressGraphQL({
schema,
graphiql: true,
context: {
loaders: {
AuthorLoader: AuthorLoader()
}
}
}))
const BookType = new GraphQLObjectType({
name: 'Book',
description: 'This represents a book written by an author',
fields: () => ({
id: { type: GraphQLNonNull(GraphQLInt) },
name: { type: GraphQLNonNull(GraphQLString) },
authorId: { type: GraphQLNonNull(GraphQLInt) },
author: {
type: AuthorType,
resolve: async (book, args, ctx, info) => {
let { loaders } = ctx
let { AuthorLoader } = loaders
return await AuthorLoader.load(book.authorId)
}
}
})
})