apollo-client 和 apollo-server的正确打开方式

27 阅读6分钟

开始本文前一定要先学习# GraphQL 的 正确打开方式 (apollo-client前戏)

前面我们学习了graphql的基础知识,知道如果你要在express里面启动grapgql服务,你就要用schema定义类型,用root定义数据,就能启动一个基础服务了。

咱们在前端js里面可以用fetch``和axios启动http服务,把gql语句以参数的形式传入就好了。

这里先使用apollo-server看看。

一. apollo-server

从之前的案例里面可以看出来,你启动的graphql服务是建立在express基础上的。

image.png

我们按照官方的案例建立一个apollo-server案例对比看看

1.创建项目并且初始化

mkdir my-apollo-server
cd my-apollo-server
npm init -y
//顺便把package.json里面的type修改成module模式,支持esModule
npm pkg set type="module"

2.安装包

 npm install @apollo/server graphql

3.创建graphql服务

import { ApolloServer } from '@apollo/server'; // preserve-line
import { startStandaloneServer } from '@apollo/server/standalone'; // preserve-line

const typeDefs = `
  type Query {
    hello: String
  }
`;

const resolvers = {
  Query: {
    hello: () => {
      return "hello world";
    },
  },
};

const server = new ApolloServer({
  typeDefs,
  resolvers,
});

const { url } = await startStandaloneServer(server, {
  listen: { port: 3009 },
});

console.log(`🚀  Server ready at: ${url}`);

启动服务:node server

image.png 访问下http://localhost:3009/ 就会进入这个页面

image.png

它是一个gql可视化页面。有没有觉得apollo-server瞬间比express + graphql高大上了很多。光这个可视化页面就减轻了很多我们写gql的痛苦。

4.增删改查

const { ApolloServer, gql } = require('apollo-server');
 
// 定义GraphQL的类型
const typeDefs = gql`
  type Book {
    title: String
    author: String
  }
 
  type Query {
    books可控性Book]
  }
 
  input BookInput {
    title: String
    author: String
  }
 
  type Mutation {
    createBook(input: BookInput): Book
    deleteBook(title: String): Boolean
    updateBook(title: String, author: String): Book
  }
`;
 
// 模拟数据库
let books = [];
 
// 解析器
const resolvers = {
  Query: {
    books: () => books,
  },
  Mutation: {
    createBook: (_, { input }) => {
      books.push({ title: input.title, author: input.author });
      return input;
    },
    deleteBook: (_, { title }) => {
      const index = books.findIndex(book => book.title === title);
      if (index !== -1) {
        books.splice(index, 1);
        return true;
      }
      return false;
    },
    updateBook: (_, { title, author }) => {
      const book = books.find(book => book.title === title);
      if (book) {
        book.author = author;
        return book;
      }
      return null;
    },
  },
};
 
const server = new ApolloServer({ typeDefs, resolvers });
 
server.listen().then(({ url }) => {
  console.log(`Server ready at ${url}`);
});

注意: 带参数的是函数写法一定要注意,参数是在第二个参数上,不在第一个上

image.png

5.对比并总结

对比文档:spec.graphql.org/June2018/#s… www.apollographql.com/docs/apollo…

1.apollo-server相比于express + graphql而言,语法更加简单。graphqlHTTP利用schema,root创建服务。apollo利用 typeDefs,resolvers创建服务。内核一样,一个定义的是类型,另一个定义的是数据。

2.它还提供了一个可视化界面,帮助你写gql

3.如果appolo-server要进行跨域请求,还是少不了exprss的帮助。

4.graphqlapollo-server 都支持的类型:基础类型:String,Int, Boolean,Float, ID,高级类型:对象,数组,查询Query,突变Mutation,联合union,枚举enum, 关键字有:type和input,指令。

5.graphql可空性和Apollo雷同

image.png

6.apollo-server的缓存分为静态缓存和动态缓存两种,静态缓存是利用 @cacheControl 指令实现,直接把指令跟在类型的后面就好了,这个和浏览器的缓存一样,设置maxAge,scope,inheritMaxAge。动态缓存是利用 @apollo/cache-control-types包的cacheControlFromInfo方法实现的。

7.参数处理上也是不一样的。一定要注意下。

8.如果它要和next集成,利用@as-integrations/next

二. appolo-client

之前的express+graphql启动的服务,我用前端访问接口的时候,直接用的html文件,我用live-server启动一个前端服务,然后调用了接口,相互连接。

image.png

现在 appolo-client 集成了react。

1.初始化项目

利用vite创建一个react项目

npx create vite my-project

2.安装依赖

npm install @apollo/client graphql

3.在main文件集成apollo-client

我们在上面apollo-server里面已经创建了一个graphql服务,现在就启动它,然后在main文件里面使用它

image.png

import { createRoot } from 'react-dom/client';
import { ApolloClient, InMemoryCache, ApolloProvider, gql } from '@apollo/client';
import App from './App.tsx';

const client = new ApolloClient({
  uri: ' http://localhost:3009/',
  cache: new InMemoryCache(),
});

createRoot(document.getElementById('root')!).render(
  <ApolloProvider client={client}>
    <App />
  </ApolloProvider>
);

解释代码:new ApolloClient 创建一个实例,然后利用ApolloProvider传递给他的子孙。ApolloProvider它是react Context Api的封装

4.在组件里面使用

在app.tsx里面

// Import everything needed to use the `useQuery` hook
import { useQuery, gql } from '@apollo/client';

const GET_LOCATIONS = gql`
  query {
    hello
  }
`;

export default function App() {

  const { loading, error, data } = useQuery(GET_LOCATIONS);

  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error : {error.message}</p>;

  console.log(data, 888);

  return <div>{data.hello}</div>;
}

解释代码: GET_LOCATIONS就是grqphqlgql语句,用来获取数据,以前获取数据都用的是fetch请求,现在不用了,直接一个hooks就解决了,你说方便不方便?

apollo-client提供了2个hooksuseQuery和useLazyQuery

5.测试

image.png

6.react-hooks

1.useQuery -- 返回data,loading,error,不返回执行函数,用于页面一进来就获取数据的场景

2.useLazyQuery -- 返回执行函数和对应的值data,如果你想在组件里面,点击了按钮以后才去查询数据,就用useLazyQuery,在使用的时候,记得加 fetchPolicy: 'network-only' 才能立即更新 如下:

image.png

6.总结

apollo-client 完全秒杀了原始js利用http请求获取数据的解决方案。首先它将graphql服务地址集中管理,将获取到的数据集中由context管理,按照 ContextApi的方式将所有的值在父组件里面传递出去。在子组件里面,使用 useQuery和useLazyQuery 配合 gql 获取数据。

三.一个简单的增删改查的案例

image.png

1.apollo-server实现服务端

后端服务利用apollo-server实现如下:

初始化项目后,npm init -y,然后创建一个js文件:server.js,复制以下代码。

import { ApolloServer } from '@apollo/server'; // preserve-line
import { startStandaloneServer } from '@apollo/server/standalone'; //preserve-line
import crypto from 'crypto';

const typeDefs = `
  type User {
    name: String
    sex: String
    age: Int
    id: ID
  }
 
  type Query {
    getList: [User]
    getUser(id: ID): User
  }
 
  type Mutation {
    createUser(name: String, sex: String, age: Int): User
    updateUser(id: ID, name: String, sex: String, age: Int): User
    deleteUser(id: ID): User
  }
`;

class User {
  constructor({ id, name, sex, age }) {
    this.id = id;
    this.name = name;
    this.sex = sex;
    this.age = age;
  }
}

var fakeDatabase = [];

const resolvers = {
  Query: {
    getList() {
      return fakeDatabase;
    },
    getUser({ id }) {
      const user = fakeDatabase.find((item) => item.id === id);

      return user;
    },
  },
  Mutation: {
    createUser(_, { name, sex, age }) {
      var id = crypto.randomBytes(10).toString("hex");
      const user = new User({
        id,
        name,
        sex,
        age
      });
      fakeDatabase.push(user);

      return user;
    },
    updateUser(_, { id, name, sex, age }) {
      const user = new User({ id, name, sex, age });
      const userList = fakeDatabase.map((item) => {
        if (item.id === id) {
          return user;
        }
        return item;
      });

      fakeDatabase = [...userList];

      return user;
    },
    deleteUser(_, { id }) {
      const userList = fakeDatabase.filter((item) => item.id !== id);
      fakeDatabase = [...userList];

      return userList;
    },
  }
};

const server = new ApolloServer({
  typeDefs,
  resolvers,
});

const { url } = await startStandaloneServer(server, {
  listen: { port: 3009 },
});

console.log(`🚀  Server ready at: ${url}`);

利用 node 启动服务,一般你直接用node启动的服务是不能帮你打印console.log的,你需要用debug模式开启服务,可以帮你在前端访问接口的时候,node打印console.log

image.png

2.apollo-client实现前端

利用vite初始化项目

  npx create vite my-project

安装包

image.png

main.tsx

graphql服务的端口号是3009,就是apollo- server的服务。

import { createRoot } from 'react-dom/client';
import { ApolloClient, InMemoryCache, ApolloProvider, gql } from '@apollo/client';
import App from './App.tsx';

const client = new ApolloClient({
  uri: ' http://localhost:3009/',
  cache: new InMemoryCache(),
});

createRoot(document.getElementById('root')!).render(
  <ApolloProvider client={client}>
    <App />
  </ApolloProvider>
);

gql.ts

import { gql } from '@apollo/client';
export const GET_LIST = gql`query  {
 getList {
  id
  name
  sex
  age
 }
}
`;

export const CREATE_USER = gql`mutation  CreateUser($name: String, $sex: String, $age: Int) {
  createUser(name: $name, sex: $sex, age: $age){
    id
    name
    sex
    age
  }
}`;

export const UPDATE_USER = gql`mutation  UpdateUser($id: ID, $name: String, $sex: String, $age: Int) {
  updateUser(id: $id, name: $name, sex: $sex, age: $age){
    id
    name
    sex
    age
  }
}`;

export const DELETE_USER = gql`mutation  DeleteUser($id: ID) {
  deleteUser(id: $id){
    id
    name
    sex
    age
  }
}`;

app.tsx

import { useLazyQuery } from '@apollo/client';
import UserList from './UserList';
import CreateUser from './CreateUser';
import { Button } from 'antd';
import { GET_LIST } from './gql';
import './App.css';
import { useEffect } from 'react';

export default function App() {
  const [getList, { loading, data }] = useLazyQuery(GET_LIST, {
    fetchPolicy: 'network-only' //用于实时更新
  });

  const getData = () => {
    getList();
  };

  useEffect(() => {
    getList();
  }, []);

  return <div>
    <div className='user-list'>
      <h1>人员信息表</h1>
      <div className='btn-container'>
        <CreateUser mode="add" refresh={getList}></CreateUser>
        <Button type="primary" onClick={getData} style={{ marginLeft: "8px" }}>查询</Button>
      </div>

      <UserList {...{ loading, data, refresh: getList }}></UserList>
    </div>
  </div >;
}

CreateUser.tsx

import React, { useState, useRef } from 'react';
import { useMutation } from '@apollo/client';
import { Button, Modal, Form, Input, Radio, InputNumber } from 'antd';
import { CREATE_USER, UPDATE_USER } from './gql';

interface IProps {
  mode?: string;
  record?: Record<string, any>;
  refresh?: () => void;
}

const CreateUser: React.FC = ({ mode, record, refresh }: IProps) => {
  const [isModalOpen, setIsModalOpen] = useState(false);
  const formRef = useRef(null) as any;
  const [createUser] = useMutation(CREATE_USER);
  const [updateUser] = useMutation(UPDATE_USER);

  const showModal = () => {
    setIsModalOpen(true);
  };

  const handleOk = () => {
    const formVal = formRef.current;
    if (formVal) {
      const values = formVal?.getFieldsValue();
      if (mode === 'add') {
        createUser({
          variables: values
        }).finally(() => {
          setIsModalOpen(false);
          refresh && refresh();
        });

        return;
      }

      updateUser({
        variables: {
          id: record?.id,
          ...values
        }
      }).finally(() => {
        setIsModalOpen(false);
        refresh && refresh();
      });

    }
  };

  const handleCancel = () => {
    setIsModalOpen(false);
  };

  return (
    <>
      {mode === 'add' ? <Button type="primary" onClick={showModal}>
        新增
      </Button> : <a onClick={showModal}>修改</a>}
      <Modal
        title={mode === 'add' ? "新增人员" : "修改人员"}
        cancelText="取消"
        okText="保存"
        open={isModalOpen}
        onOk={handleOk}
        onCancel={handleCancel}>
        <Form
          name="basic"
          labelCol={{ span: 8 }}
          wrapperCol={{ span: 16 }}
          initialValues={{ remember: true }}
          // onFinish={onFinish}
          // onFinishFailed={onFinishFailed}
          autoComplete="off"
          ref={formRef}
        >
          <Form.Item
            label="姓名"
            name="name"
            rules={[{ required: true, message: '请输入姓名!' }]}
            initialValue={record?.name}
          >
            <Input />
          </Form.Item>
          <Form.Item
            label="性别"
            name="sex"
            rules={[{ required: true, message: '请输入性别' }]}
            initialValue={record?.sex}
          >
            <Radio.Group name="radiogroup" >
              <Radio value='male'></Radio>
              <Radio value='female'></Radio>
            </Radio.Group>
          </Form.Item>
          <Form.Item
            label="年龄"
            name="age"
            rules={[{ required: true, message: '请输入年龄!' }]}
            initialValue={record?.age}
          >
            <InputNumber />
          </Form.Item>
        </Form>
      </Modal>
    </>
  );
};

export default CreateUser;

UserList.tsx

import { Table, Space, Modal } from 'antd';
import CreateUser from './CreateUser';
import { DELETE_USER } from './gql';
import { useMutation } from '@apollo/client';
import { ExclamationCircleOutlined } from '@ant-design/icons';

const { confirm } = Modal;

export default function UserList({ loading, data, refresh }: any) {
  const dataSource = data?.getList;
  const [deleteUser] = useMutation(DELETE_USER);

  const handleDelete = (id: string) => {
    confirm({
      title: '删除',
      icon: <ExclamationCircleOutlined />,
      content: '你确定要删除这个人员吗?',
      okText: '确定',
      okType: 'danger',
      cancelText: '取消',
      onOk() {
        deleteUser({
          variables: { id }
        });
        refresh();
      },
      onCancel() {
        console.log('Cancel');
      },
    });
  };

  const columns = [
    {
      title: 'ID',
      dataIndex: 'id',
      key: 'id',
    },
    {
      title: '姓名',
      dataIndex: 'name',
      key: 'name',
    },
    {
      title: '性别',
      dataIndex: 'sex',
      key: 'sex',
    },
    {
      title: '年龄',
      dataIndex: 'age',
      key: 'age',
    },
    {
      title: 'Action',
      key: 'action',
      render: (_: any, record: any) => (
        <Space size="middle">
          <CreateUser mode="edit" record={record} refresh={refresh}></CreateUser>
          <a onClick={() => handleDelete(record?.id)}>删除</a>
        </Space>
      ),
    },
  ];

  return <Table dataSource={dataSource} columns={columns} loading={loading} />;
}

项目目录

image.png

总结

这是一个简单的增删改查的小页面,它包含了apollo-serverapollo-client所有的基础知识。通过这个小项目你能充分的理解apollo-serverapollo-client组合而成的项目。