为什么要学习graphl
文档地址:graphql.bootcss.com/ 一种用于
API 的查询语言使用graphql的优点
- 接口聚合
- 字段按需加载,减少网络请求
- 和apollo结合,缓存数据,关注数据的使用而不是获取
- 技多不压身,提高职场竞争力,目前很多大公司都开始使用graphql当作
bff
使用Apollo Server构建一个graphql服务
文档地址:www.apollographql.com/docs/apollo… step1,创建项目
mkdir graphql-server-example
cd graphql-server-example
npm init -y`
step2安装依赖
npm install apollo-server graphql
touch index.js
step3定义graphql schema
const { ApolloServer, gql } = require('apollo-server');
const typeDefs = gql`
type Book {
title: String
author: String
}
type Query {
books: [Book]
}
`;
step4定义解析函数
const books = {
title: 'The Awakening',
author: 'Kate Chopin',
},
{
title: 'City of Glass',
author: 'Paul Auster',
},
];
const resolvers = {
Query: {
books: () => books,
},
};
step5初始化apolo server
const server = new ApolloServer({ typeDefs, resolvers });
// The `listen` method launches a web server.
server.listen().then(({ url }) => {
console.log(`🚀 Server ready at ${url}`);
});
step6启动服务
node index.js
🚀 Server ready at http://localhost:4000/
使用@apollo/client构建一个graphql客户端
在nextjs中使用graphql和apollo(推荐)
git clone https://github.com/vercel/next.js.git
cd next.js/examples/api-routes-apollo-server-and-client
npm i
npm run build
npm run start
使用graphql查询数据
为了了解apollo client中的数据缓存更新策略,让我们稍微修改下源码,去掉user中的id字段
// 在apollo下的typedef.js中定义User和User
import { gql } from '@apollo/client'
export const typeDefs = gql`
type User {
name: String!
status: String!
}
type Query {
viewer: User
}
`
// 在apollo下的resolvers.js定义对应执行的方法
let user = { name: 'John Smith', status: 'cached' }
const resolvers = {
Query: {
viewer(_parent, _args, _context, _info) {
return user
},
},
}
// 定义schema
import { typeDefs } from './type-defs'
import { resolvers } from './resolvers'
export const schema = makeExecutableSchema({
typeDefs,
resolvers,
})
// 在前端页面调用
import { gql, useQuery } from '@apollo/client';
const ViewerQuery = gql`
query ViewerQuery {
viewer {
name
status
}
}
`
const Index = () => {
const {
data: { viewer },
} = useQuery(ViewerQuery)
return (
<div>
You're signed in as {viewer.name} and you're {viewer.status} goto{' '}
<Link href="/about">
<a>static</a>
</Link>{' '}
page.
</div>
)
}
apollo在请求完成graphql数据时,通过内部集成redux,会将数据保存在apollo客户端中。当请求相同的graphql服务时,会优先使用缓存的数据,当然你可以使用不同的cache策略来修改这一行为。文档链接:www.apollographql.com/docs/react/…
// 总是使用网络请求返回的数据
const { loading, error, data } = useQuery(ViewerQuery, {
fetchPolicy: "network-only" // Doesn't check cache before making a network request
});
fetchPolicy: "network-only", // Used for first execution
nextFetchPolicy: "cache-first" // Used for subsequent executions
使用mutation修改数据
// 在apollo/type-defs.js中增加type
type Mutation {
modifyUser(newname: String): User
}
// 在apollo/resolver.js中增加对modifyUser的解析函数
const resolvers = {
....
Mutation: {
modifyUser(_parent, _args, _context, _info) {
user.name = _args.newname
return user
},
}
}
// 在页面中调用
const MODIFY_USERS = gql`
mutation modifyUser($newname: String) {
modifyUser(newname: $newname) {
name
status
}
}
`;
const Index = () => {
const {
data: { viewer },
} = useQuery(ViewerQuery)
const [modifyUser] = useMutation(MODIFY_USERS);
const onClick = () => {
modifyUser({
variables: {
newname: 'Foo Bar',
},
}).catch((err) => {
console.log(err)
})
}
return (
<>
<button onClick={onClick}>修改用户</button>
You're signed in as {viewer.name} and you're {viewer.status} goto{' '}
<Link href="/about">
<a>static</a>
</Link>{' '}
page.
</>
)
}
点击修改数据按钮,打开页面的
network发现,发现前端向graphql服务端发送了一条graphql的网络请求,修改了服务端数据,但是前端页面的展示没有变化,这是因为graphql并不会主动修改当前客户端的数据,如果想要在更新服务端数据的同时也更新前端数据要怎么办呢?
声明式通过__typename和id自动更新
apollo内部通过__typename:id生成唯一的key来保存数据,当接口返回的数据存同时存在__typename和id时,apollo将自动合并接口的数据到客户端的缓存数据中去,并保留其他现有字段。
在前端创建ApolloClient时,apollo通过addTypename字段为每个对象都创建一个__typename,详细文档:addtypename,所以我们想自动更新数据当前用户的姓名就要做两件事情
- 返回的数据包含更新后的姓名(完成)
- 为需要更新的对象生成唯一id
- 可以在代码中硬编码,为每个user
生成唯一的id,比如在graphql服务端拿到数据时,执行
user.id = 1- 通过
defaultDataIdFromObject函数,为每个对象生成唯一的id并返回,文档地址:defaultDataIdFromObjectdefaultDataIdFromObject函数生成唯一id
import { defaultDataIdFromObject } from '@apollo/client'; const cache = new InMemoryCache({ dataIdFromObject(responseObject) { switch (responseObject.__typename) { case 'viewer': return `User:${responseObject.name}`; default: return defaultDataIdFromObject(responseObject); } } });- 通过
typePolicies自动生成id文档地址:typePolicies其中User指的是上述graphql为每一个对象生成的__typename(graphql查询的名字)
cache: new InMemoryCache({ typePolicies: { User: { keyFields: ["name"], }, }, }), - 可以在代码中硬编码,为每个user
如果每条数据都能返回一个id是最好的了,不可以的话也可以能通过上面三种方法来生成id。当然了,除了通过自动生成id来自动更新client中的cache数据之外,apollo还提供了其他两种更新数据方式
自动更新query
重新请求需要更新的query,官方文档:Refetching queries
// Refetches two queries after mutation completes
const [addTodo, { data, loading, error }] = useMutation(ADD_TODO, {
refetchQueries: [ViewerQuery]
});
执行update函数,文档地址:update
const [addUser, addRes] = useMutation(MODIFY_USERS, {
update: (cache, data ) => {
const { viewer = {} } = cache.readQuery({ query: ViewerQuery }) // 如果需要可以之前的数据,可以通过cache.readQuery获得
const modifyUser = data?.data?.modifyUser
cache.writeQuery({
query: ViewerQuery,
data: {
viewer: modifyUser
}
})
}
});
这里的cache.writeQuery,文档地址cache.writeQuery也可以用cache.modify代替,他们之间的区别是
- cache.modify只能修改在
缓存中已经存在的数据 - modify绕过了您定义的任何合并函数,这意味着字段总是被您指定的值完全覆盖。
从官网的例子能猜出大概,因为需要
唯一的id,缓存中没有对应的数据肯定也不存在唯一的id,也就无法通过id更新,所以cache.modify只能修改在缓存中已经存在的数据,指定字段通过field中的函数修改,例如comments
const idToRemove = 'abc123';
cache.modify({
id: cache.identify(myPost),
fields: {
comments(existingCommentRefs, { readField }) {
return existingCommentRefs.filter(
commentRef => idToRemove !== readField('id', commentRef)
);
},
},
});
我的结论是:通过
id和__typename自动更新client中的cache数据是最好的,其次是使用update函数,最后是重新发起query请求,因为重新发起请求可能会有延时,导致页面刷新不及时。优点就是简单方便。关于网络延时,apollo还支持乐观更新,也就是说先将用户的行为展示在页面上,然后在网络请求成功时修改本地的状态。就像我们在进行微信聊天时,点击发送按钮,输入框的消息会立马出现在聊天面板上,如果网络请求成功则不做任何处理,如果失败就会展现红的感叹号!
本地数据处理
apollo不光能管理你的网络数据,也可以管理你的本地数据。管理本地数据大概分为两种方法,文档地址local-resolvers
- 直接调用
client.writeQuery就行数据写入
import React from "react";
import { gql, useQuery } from "@apollo/client";
import Link from "./Link";
const GET_VISIBILITY_FILTER = gql`
query GetVisibilityFilter {
visibilityFilter @client // client标示查询本地数据而不是发起一个网络请求
}
`;
function FilterLink({ filter, children }) {
const { data, client } = useQuery(GET_VISIBILITY_FILTER);
return (
<Link
onClick={() => client.writeQuery({
query: GET_VISIBILITY_FILTER,
data: { visibilityFilter: filter },
})}
active={data.visibilityFilter === filter}
>
{children}
</Link>
)
}
- 通过resolver函数就行本地修改,这也是官方较为推荐的方法。和正常的查询数据写法基本上是一致,这里就不多做介绍,详情可以参考上述文档
部署
关于部署,这里推荐docker,可以和nextjs一起使用
- 通过
dockerfile文件将当前项目打包成一份docker image文件
FROM node:12-alpine
RUN mkdir -p /usr/src/nodejs/
COPY . /usr/src/nodejs/
WORKDIR /usr/src/nodejs/
RUN npm i
RUN npm run build
EXPOSE 3000
CMD ["npm", "start"]
- 通过
ci/cd工具或手动,将docker image文件上传到私有镜像仓库,这里以阿里云镜像库为例子,当然你也可以搭建自己的私有docker仓库,如果能配合k8s最好
docker login --username=xxx -p=xxx registry.cn-hangzhou.aliyuncs.com
docker build --tag graphql:0.0.1 .
docker tag studydocker:0.0.1 registry.cn-hangzhou.aliyuncs.com/xxx/xxx001:0.0.1
docker push registry.cn-hangzhou.aliyuncs.com/xxx/xxx001:0.0.1
- 在静态仓库中直接部署,这里可以手动部署,在对应的服务器先将对应的
image文件下载下来在启动生成容器文件拉取镜像文件,docker pull registry.cn-hangzhou.aliyuncs.com/qwelpcyy/xxx001:[镜像版本号] 通过docker-compose启动服务,以下就是我的docker-compose.yml文件
version: '3'
services:
mydocker:
container_name: graphqlContainer
image: registry.cn-hangzhou.aliyuncs.com/xxxx/xxx001:0.0.1
ports:
- '3000:3000'
restart: 'always'
总结
- 我们可以通过
useQuery和useMutition进行graphql数据的查询和修改 - apollo会将请求好的数据
缓存在client中,我们使用fetchPolicy可以通过制定不同的策略来修改这一行为,比如- fetchPolicy: "network-only"
- fetchPolicy: "cache-only"
- ...
- 当使用
useMutition进行数据修改时,只会修改服务端数据,本地状态并不会自动修改,可以通过以下三种方式就行修改__typename和id(_id)生成唯一key值- 使用
update函数,手动修改逻辑 - 使用
refetchQueries,重新请求需要更新的数据
- 可以使用@client来标示本地数据,修改本地数据的方式
- 直接修改
client.writeQuery - 使用本地
resolver
- 直接修改
- 可以结合
nextjs一起使用,简单方便,支持SSR,SSG(静态资源生成) - 可以通过
docker docker-compose来进行nodejs服务的部署