Nest GraphQL + React 简单学习和例子

940 阅读2分钟

创建项目

nest new app-server # 创建 nest 服务
create-react-app app # 创建 react 项目

# app-server port=4000
yarn dev:start

# app port=3000
yarn start

React 客户端

使用 React 客户端推荐使用 Apollo React Client。

yarn  add apollo-boost @apollo/react-hooks graphql
  • apollo-boost 包含安装Apollo Client所需的一切的软件包
  • @apollo/react-hooks 包含 React hooks 的钩子函数
  • graphql 用于查询

NestJS 服务端

使用脚手架创建的项目,还需要 GrapQL 的相关的支持

yarn add @nestjs/graphql graphql-tools graphql apollo-server-express

因为 GraphQL 是单独的出来一个模块, NestJS 对其进行了支持而已。

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  await app.listen(400);
  console.log(`Application is running on: ${await app.getUrl()}`);
}
bootstrap();

NestJS 服务端有两种写法:代码第一,架构第一,我们使用代码第一的写法。我们需要在 @nestjs 中添加 GraphQLModule, 用于配置 GraphQL

import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { RecipesModule } from './recipes/recipes.module';

@Module({
  imports: [
    RecipesModule,
    GraphQLModule.forRoot({
      installSubscriptionHandlers: true,
      autoSchemaFile: 'schema.gql',
    }),
  ],
})
export class AppModule {}
  • forRoot 方法的参数将传递到 Apollo 中。

  • autoSchemaFile 是运行后自动生成的 GraphQL 的 schema 文件

下面就是要解决 RecipesModule 的 GraphQL 相关的内容了.

// RecipesModule.ts
import { Module } from '@nestjs/common';
import { DateScalar } from '../common/scalars/date.scalar';
import { RecipesResolver } from './recipes.resolver';
import { RecipesService } from './recipes.service';

@Module({
  providers: [RecipesResolver, RecipesService, DateScalar],
})
export class RecipesModule {}

这里值实现一个 Query 查询 recipe 的功能

import { NotFoundException } from '@nestjs/common';
import { Args, Mutation, Query, Resolver, Subscription } from '@nestjs/graphql';
import { Recipe } from './models/recipe';
import { RecipesService } from './recipes.service';

@Resolver(of => Recipe)
export class RecipesResolver {
  constructor(private readonly recipesService: RecipesService) {}

  @Query(returns => Recipe)
  async recipe(@Args('id') id: string): Promise<Recipe> {
    const recipe = await this.recipesService.findOneById(id);
    if (!recipe) {
      throw new NotFoundException(id);
    }
    return recipe;
  }
}

下面是 RecipesResolver 所需所需要的服务层

// /recipes/recipes.module.ts

import { Injectable } from '@nestjs/common';
import { NewRecipeInput } from './dto/new-recipe.input';
import { Recipe } from './models/recipe';

@Injectable()
export class RecipesService {
  // 这里我们自己 Mock 数据
  // 注意这里模拟的事件必须是一个事件对象
  async findOneById(id: string): Promise<Recipe> {
    return {
      id: id,
      title: 'sdf',
      description: 'fsd',
      creationDate: new Date(),
      ingredients: ['sdfd']
    } as any;
  }
}

TypeScript 定义 GraphQL 代码

它依赖 type-graphql

// /recipes/models/recipe.ts

import { Field, ID, ObjectType } from 'type-graphql';

@ObjectType()
export class Recipe {
  @Field(type => ID)
  id: string;

  @Field()
  title: string;

  @Field({ nullable: true })
  description?: string;

  @Field()
  creationDate: Date;

  @Field(type => [String])
  ingredients: string[];
}

它对应的 Schema 是如下这样的

type Recipe {
  id: ID!
  title: String!
  description: String
  creationDate: Date!
  ingredients: [String!]!
}

对比了我们就很容知道 'type-graphql' 提供的装饰器的的作用

  • ObjectType 修饰类,说明是一个类型
  • Field 修饰 GraphQL 中的字段,表示是一个类型
  • type => ID 表示返回值是 ID 类型,ID 本质对应了 String 类型
  • Field 参数为空的时候,表示是可以为非空的
  • @Field({ nullable: true }) 表示可选的
  • @Field(type => [String]) 表示返非空的字符串数组,字符串本身也不能为空。

启动 nest 服务

yarn run dev:start

然后我们就可以在 localhost:4000/graphql 端口上进行数据查询,在浏览器中打开,会得到一个graphql游乐场。

我们游乐场中左边输入, 查询对象

{
  recipe(id: "23") {
    id
    title
    description,
    ingredients,
    creationDate
  }
}

得到的是如下的结果, 与我们的在 Recipe 中自定义的数据是样的。这就是表示 GraphQL已经在浏览器中联通了.

{
  "data": {
    "recipe": {
      "id": "23",
      "title": "sdf",
      "description": "fsd",
      "ingredients": [
        "sdfd"
      ],
      "creationDate": 1591171809755
    }
  }
}

React 客户端使用 Apollo Client

我们先查询数据:

import ApolloClient, { gql } from 'apollo-boost';

const client = new ApolloClient({
  uri: 'http://localhost:3000/graphql',
});

client
  .query({
    query: gql`
      {
        recipe(id: "23") {
          id
          title
          description,
          ingredients,
          creationDate
        }
      }
    `,
  })
  .then((result) => console.log(result));

result 是一个返回的对象,这个对象数据结构如下:

const result ={
  data: {recipe: {
    creationDate: 1591171953828,
    description: "fsd",
    id: "23",
    ingredients: ["sdfd"],
    title: "sdf",
    __typename: "Recipe"
  }},
  loading: false,
  networkStatus: 7,
  stale: false
}

上面是一个简单的例子,下面我们使用更加符合 React 的开发习惯的 API。

  • client 作为顶层组件传入内部组件使用,类似于 react-redux 的 Provider,其实就是提供一个上下文环境。
import { ApolloProvider } from '@apollo/react-hooks';
import ApolloClient, { gql } from 'apollo-boost';

const client = new ApolloClient({
  uri: 'http://localhost:3000/graphql',
});

function App() {
  return (
    <ApolloProvider client={client}></ApolloProvider>
  )
}
  • 在组件中使用查询 useQuery
import React from 'react';
import { useQuery } from '@apollo/react-hooks';
import { gql } from 'apollo-boost';

const EXCHANGE_RATES = gql`
  {
    rates(currency: "USD") {
      currency
      rate
    }
  }
`;

function ExchangeRates() {
  const { loading, error, data } = useQuery(EXCHANGE_RATES);

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

  return data.rates.map(({ currency, rate }) => (
    <div key={currency}>
      <p>
        {currency}: {rate}
      </p>
    </div>
  ));
}
  • 在组件中使用编译
import gql from 'graphql-tag';
import { useMutation } from '@apollo/react-hooks';

const ADD_TODO = gql`
  mutation AddTodo($type: String!) {
    addTodo(type: $type) {
      id
      type
    }
  }
`;

function AddTodo() {
  let input;
  const [addTodo, { data }] = useMutation(ADD_TODO);

  return (
    <div>
      <form
        onSubmit={e => {
          e.preventDefault();
          addTodo({ variables: { type: input.value } });
          input.value = '';
        }}
      >
        <input
          ref={node => {
            input = node;
          }}
        />
        <button type="submit">Add Todo</button>
      </form>
    </div>
  );
}

本地状态管理

执行本地状态突变的主要方法有两种。

  • 第一种方法是通过调用直接写入缓存cache.writeData。对于不依赖于缓存中当前数据的一次性突变(例如,写入单个值),直接写入非常有用。
  • 第二种方法是利用useMutation带有调用本地客户端解析器的GraphQL突变的钩子。如果您的突变取决于缓存中的现有值,例如将项目添加到列表或切换布尔值,我们建议使用解析器。

直接写

import React from "react";
import { useApolloClient } from "@apollo/react-hooks";

import Link from "./Link";

function FilterLink({ filter, children }) {
  const client = useApolloClient();
  return (
    <Link
      onClick={() => client.writeData({ data: { visibilityFilter: filter } })}
    >
      {children}
    </Link>
  );
}

本地解析

import { ApolloClient } from 'apollo-client';
import { InMemoryCache } from 'apollo-cache-inmemory';

const client = new ApolloClient({
  cache: new InMemoryCache(),
  resolvers: {
    Mutation: {
      toggleTodo: (_root, variables, { cache, getCacheKey }) => {
        const id = getCacheKey({ __typename: 'TodoItem', id: variables.id })
        const fragment = gql`
          fragment completeTodo on TodoItem {
            completed
          }
        `;
        const todo = cache.readFragment({ fragment, id });
        const data = { ...todo, completed: !todo.completed };
        cache.writeData({ id, data });
        return null;
      },
    },
  },
});
import React from "react"
import { useMutation } from "@apollo/react-hooks";
import gql from "graphql-tag";

const TOGGLE_TODO = gql`
  mutation ToggleTodo($id: Int!) {
    toggleTodo(id: $id) @client
  }
`;

function Todo({ id, completed, text }) {
  const [toggleTodo] = useMutation(TOGGLE_TODO, { variables: { id } });
  return (
    <li
      onClick={toggleTodo}
      style={{
        textDecoration: completed ? "line-through" : "none",
      }}
    >
      {text}
    </li>
  );
}

订阅

除了使用查询获取数据和使用突变来修改数据外,GraphQL规范还支持第三种操作类型,称为subscription。

GraphQL订阅是一种将数据从服务器推送到选择侦听服务器实时消息的客户端的方法。订阅与查询相似,因为订阅指定了要传递给客户端的一组字段,但是,每次在服务器上发生特定事件时,都会发送结果,而不是立即返回单个答案。

订阅的一个常见用例是向客户端通知特定事件,例如,创建新对象,更新字段等。

type Subscription {
  commentAdded(repoFullName: String!): Comment
}

分页

基本上有两种获取分页数据的方式:带编号的页和游标。 还有两种显示分页数据的方式:离散页面和无限滚动。要更深入地了解两者之间的区别以及何时使用一种与另一种之间的差异,建议您阅读我们的博客文章,主题为:了解分页

片段

fragment NameParts on Person {
  firstName
  lastName
}

query GetPerson {
  people(id: "7") {
    ...NameParts
    avatar(size: LARGE)
  }
}

接下来

接下来我们就需要深入的学 Apollo-React 和 NestJS GraphQL