graphql + apollo client + docker使用姿势

1,546 阅读5分钟

为什么要学习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客户端

官方文档:www.apollographql.com/docs/react/…

在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,所以我们想自动更新数据当前用户的姓名就要做两件事情

  1. 返回的数据包含更新后的姓名(完成)
  2. 为需要更新的对象生成唯一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"],
            },
          },
      }),
    

如果每条数据都能返回一个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代替,他们之间的区别是

  1. cache.modify只能修改在缓存中已经存在的数据
  2. 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

  1. 直接调用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>
  )
}
  1. 通过resolver函数就行本地修改,这也是官方较为推荐的方法。和正常的查询数据写法基本上是一致,这里就不多做介绍,详情可以参考上述文档

部署

关于部署,这里推荐docker,可以和nextjs一起使用

  1. 通过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"]
  1. 通过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
  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'
    

总结

  • 我们可以通过useQueryuseMutition进行graphql数据的查询和修改
  • apollo会将请求好的数据缓存在client中,我们使用fetchPolicy可以通过制定不同的策略来修改这一行为,比如
    • fetchPolicy: "network-only"
    • fetchPolicy: "cache-only"
    • ...
  • 当使用useMutition进行数据修改时,只会修改服务端数据本地状态并不会自动修改,可以通过以下三种方式就行修改
    • __typename和id(_id)生成唯一key值
    • 使用update函数,手动修改逻辑
    • 使用refetchQueries重新请求需要更新的数据
  • 可以使用@client来标示本地数据,修改本地数据的方式
    • 直接修改client.writeQuery
    • 使用本地resolver
  • 可以结合nextjs一起使用,简单方便,支持SSRSSG(静态资源生成)
  • 可以通过docker docker-compose来进行nodejs服务的部署