Nest 实现 GraphQL 版 TodoList

我们经常用 restful 的接口来开发业务。

比如 GET 请求 /students 查询所有学生,/students/1 查询 id 为 1 的学生

发送 POST、PUT、DETETE 请求分别代表增删改。

其实也可以用 GraphQL 的方式来写接口:

查询:

新增:

增删改查都在一个接口里搞定,并且想要什么数据由前端自己取。

今天我们就用 Nest + GrahQL 做一个 TodoList 的增删改查。

数据存在 mysql 里,用 Prisma 作为 ORM 框架。

npm install -g @nestjs/cli nest new graphql-todolist

创建个项目,然后我们首先来实现 restful 接口的增删改查。

用 docker 把 mysql 跑起来:

从 docker 官网下载 docker desktop,这个是 docker 的桌面端:

跑起来后,搜索 mysql 镜像(这步需要科学上网),点击 run:

输入容器名、端口映射、以及挂载的数据卷,还要指定一个环境变量:

端口映射就是把宿主机的 3306 端口映射到容器里的 3306 端口,这样就可以在宿主机访问了。

数据卷挂载就是把宿主机的某个目录映射到容器里的 /var/lib/mysql 目录,这样数据是保存在本地的,不会丢失。

而 MYSQL_ROOT_PASSWORD 的密码则是 mysql 连接时候的密码。

跑起来后,我们用 GUI 客户端连上,这里我们用的是 mysql workbench,这是 mysql 官方提供的免费客户端:

连接上之后,点击创建 database:

指定名字、字符集为 utf8mb4,然后点击右下角的 apply。

创建成功之后在左侧就可以看到这个 database 了:

现在还没有表。

我们在 Nest 里用 Prisma 连接 mysql。

进入项目,安装 prisma

npm install prisma --save-dev

执行 prisma init 创建 schema 文件:

npx prisma init

生成了 schema 文件(用来定义 model 的),和 .env 文件:

改下 .env 的配置:

DATABASE_URL="mysql://root:你的密码@localhost:3306/todolist"

并且修改下 schema 里的 datasource 部分:

datasource db { provider = "mysql" url = env("DATABASE_URL") }

然后创建 model:

generator client { provider = "prisma-client-js" } datasource db { provider = "mysql" url = env("DATABASE_URL") } model TodoItem { id Int @id @default(autoincrement()) content String @db.VarChar(50) createTime DateTime @default(now()) updateTime DateTime @updatedAt }

id 自增,content 是长度为 50 的字符串,还有创建时间 createTime、更新时间 updateTime。

执行 prisma migrate dev,它会根据定义的 model 去创建表:

npx prisma migrate dev --name init

它会生成 sql 文件,里面是这次执行的 sql。

然后还会生成 client 代码,用来连接数据库操作这个表。

可以看到,这次执行的 sql 就是 create table 建表语句:

这时候数据库就就有这个表了:

接下来我们就可以在代码里做 CRUD 了。

生成一个 service:

nest g service prisma --flat --no-spec

改下生成的 PrismaService,继承 PrismaClient,这样它就有 crud 的 api 了:

import { Injectable, OnModuleInit } from '@nestjs/common'; import { PrismaClient } from '@prisma/client'; @Injectable() export class PrismaService extends PrismaClient implements OnModuleInit { constructor() { super({ log: [ { emit: 'stdout', level: 'query' } ] }) } async onModuleInit() { await this.$connect(); } }

在 constructor 里设置 PrismaClient 的 log 参数,也就是打印 sql 到控制台。

在 onModuleInit 的生命周期方法里调用 $connect 来连接数据库。

然后在 AppService 里注入 PrismaService,实现 CRUD:

import { Inject, Injectable } from '@nestjs/common'; import { PrismaService } from './prisma.service'; import { CreateTodoList } from './todolist-create.dto'; import { UpdateTodoList } from './todolist-update.dto'; @Injectable() export class AppService { getHello(): string { return 'Hello World!'; } @Inject(PrismaService) private prismaService: PrismaService; async query() { return this.prismaService.todoItem.findMany({ select: { id: true, content: true, createTime: true } }); } async create(todoItem: CreateTodoList) { return this.prismaService.todoItem.create({ data: todoItem, select: { id: true, content: true, createTime: true } }); } async update(todoItem: UpdateTodoList) { return this.prismaService.todoItem.update({ where: { id: todoItem.id }, data: todoItem, select: { id: true, content: true, createTime: true } }); } async remove(id: number) { return this.prismaService.todoItem.delete({ where: { id } }) } }

@Inject 注入 PrismaService,用它来做 CRUD,where 是条件、data 是数据,select 是回显的字段:

然后创建用到的两个 dto 的 class

todolist-create.dto.ts

export class CreateTodoList { content: string; }

todolist-update.dto.ts

export class UpdateTodoList { id: number; content: string; }

在 AppController 里引入下,添加几个路由:

import { Body, Controller, Delete, Get, Post, Query } from '@nestjs/common'; import { AppService } from './app.service'; import { CreateTodoList } from './todolist-create.dto'; import { UpdateTodoList } from './todolist-update.dto'; @Controller() export class AppController { constructor(private readonly appService: AppService) {} @Get() getHello(): string { return this.appService.getHello(); } @Post('create') async create(@Body() todoItem: CreateTodoList) { return this.appService.create(todoItem); } @Post('update') async update(@Body() todoItem: UpdateTodoList) { return this.appService.update(todoItem); } @Get('delete') async delete(@Query('id') id: number) { return this.appService.remove(+id); } @Get('list') async list() { return this.appService.query(); } }

添加增删改查 4 个路由,post 请求用 @Body() 注入请求体,@Query 拿路径中的参数:

把服务跑起来试一下:

npm run start:dev

首先是 list,现在没有数据:

然后添加一个:

服务端打印了 insert into 的 sql:

数据库也有了这条记录:

再加一个:

然后查一下:

接下来试下修改、删除:

再查一下:

没啥问题。

这样,todolist 的 restful 版接口就完成了。

接下来实现 graphql 版本:

安装用到的包:

npm i @nestjs/graphql @nestjs/apollo @apollo/server graphql

然后在 AppModule 里引入下:

import { Module } from '@nestjs/common'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { PrismaService } from './prisma.service'; import { GraphQLModule } from '@nestjs/graphql'; import { ApolloDriver } from '@nestjs/apollo'; @Module({ imports: [ GraphQLModule.forRoot({ driver: ApolloDriver, typePaths: ['./**/*.graphql'], }) ], controllers: [AppController], providers: [AppService, PrismaService], }) export class AppModule {}

typePaths 就是 schema 文件的路径:

添加一个 todolist.graphql

type TodoItem { id: Int content: String } input CreateTodoItemInput { content: String } input UpdateTodoItemInput { id: Int! content: String } type Query { todolist: [TodoItem]! queryById(id: Int!): TodoItem } type Mutation { createTodoItem(todoItem: CreateTodoItemInput!): TodoItem! updateTodoItem(todoItem: UpdateTodoItemInput!): TodoItem! removeTodoItem(id: Int!): Int }

语法比较容易看懂,就是定义数据的结构。

在 Query 下定义查询的接口,在 Mutation 下定义增删改的接口。

然后实现 resolver,也就是这些接口的实现:

nest g resolver todolist --no-spec --flat

import { Args, Mutation, Query, Resolver } from '@nestjs/graphql'; import { PrismaService } from './prisma.service'; import { Inject } from '@nestjs/common'; import { CreateTodoList } from './todolist-create.dto'; import { UpdateTodoList } from './todolist-update.dto'; @Resolver() export class TodolistResolver { @Inject(PrismaService) private prismaService: PrismaService; @Query("todolist") async todolist() { return this.prismaService.todoItem.findMany(); } @Query("queryById") async queryById(@Args('id') id) { return this.prismaService.todoItem.findUnique({ where: { id } }) } @Mutation("createTodoItem") async createTodoItem(@Args("todoItem") todoItem: CreateTodoList) { return this.prismaService.todoItem.create({ data: todoItem, select: { id: true, content: true, createTime: true } }); } @Mutation("updateTodoItem") async updateTodoItem(@Args('todoItem') todoItem: UpdateTodoList) { return this.prismaService.todoItem.update({ where: { id: todoItem.id }, data: todoItem, select: { id: true, content: true, createTime: true } }); } @Mutation("removeTodoItem") async removeTodoItem(@Args('id') id: number) { await this.prismaService.todoItem.delete({ where: { id } }) return id; } }

用 @Resolver 声明 resolver,用 @Query 声明查询接口,@Mutation 声明增删改接口,@Args 取传入的参数。

具体增删改查的实现和之前一样。

浏览器访问 http://localhost:3000/graphql 就是 playground,可以在这里查询:

左边输入查询语法,右边是执行后返回的结果。

当然,对新手来说这个 playground 不够友好,没有提示。

我们换一个:

import { Module } from '@nestjs/common'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { PrismaService } from './prisma.service'; import { GraphQLModule } from '@nestjs/graphql'; import { ApolloDriver } from '@nestjs/apollo'; import { TodolistResolver } from './todolist.resolver'; import { ApolloServerPluginLandingPageLocalDefault } from '@apollo/server/plugin/landingPage/default'; @Module({ imports: [ GraphQLModule.forRoot({ driver: ApolloDriver, typePaths: ['./**/*.graphql'], playground: false, plugins: [ApolloServerPluginLandingPageLocalDefault()], }) ], controllers: [AppController], providers: [AppService, PrismaService, TodolistResolver], }) export class AppModule {}

试一下新增:

查询:

修改:

单个查询:

删除:

查询:

基于 GraphQL 的增删改查都成功了!

然后在 react 项目里调用下。

npx create-vite

进入项目,安装 @apollo/client

npm install npm install @apollo/client

改下 main.tsx

import * as ReactDOM from 'react-dom/client'; import { ApolloClient, InMemoryCache, ApolloProvider } from '@apollo/client'; import App from './App'; const client = new ApolloClient({ uri: 'http://localhost:3000/graphql', cache: new InMemoryCache(), }); const root = ReactDOM.createRoot(document.getElementById('root')!); root.render( <ApolloProvider client={client}> <App /> </ApolloProvider>, );

创建 ApolloClient 并设置到 ApolloProvider。

然后在 App.tsx 里用 useQuery 发请求:

import { gql, useQuery } from '@apollo/client'; const getTodoList = gql` query Query { todolist { content id } } `; type TodoItem = { id: number; content: string; } type TodoList = { todolist: Array<TodoItem>; } export default function App() { const { loading, error, data } = useQuery<TodoList>(getTodoList); if (loading) return 'Loading...'; if (error) return `Error! ${error.message}`; return ( <ul> { data?.todolist?.map(item => { return <li key={item.id}>{item.content}</li> }) } </ul> ); }

把服务跑起来:

npm run dev

这里涉及到的跨域,现在后端服务里开启下跨域支持:

可以看到,返回了查询结果:

然后加一下新增:

用 useMutation 的 hook,指定 refetchQueries 也就是修改完之后重新获取数据。

调用的时候传入 content 数据。

import { gql, useMutation, useQuery } from '@apollo/client'; const getTodoList = gql` query Query { todolist { content id } } `; const createTodoItem = gql` mutation Mutation($todoItem: CreateTodoItemInput!) { createTodoItem(todoItem: $todoItem) { id content } } `; type TodoItem = { id: number; content: string; } type TodoList = { todolist: Array<TodoItem>; } export default function App() { const { loading, error, data } = useQuery<TodoList>(getTodoList); const [createTodo] = useMutation(createTodoItem, { refetchQueries: [getTodoList] }); async function onClick() { await createTodo({ variables: { todoItem: { content: Math.random().toString().slice(2, 10) } } }) } if (loading) return 'Loading...'; if (error) return `Error! ${error.message}`; return ( <div> <button onClick={onClick}>新增</button> <ul> { data?.todolist?.map(item => { return <li key={item.id}>{item.content}</li> }) } </ul> </div> ); }

测试下:

数据库里也可能看到新增的数据:

这样,我们就能在 react 项目里用 graphql 做 CRUD 了。

案例代码上传了 github。

后端代码: https://github.com/QuarkGluonPlasma/nestjs-course-code/tree/main/graphql-todolist

前端代码:https://github.com/QuarkGluonPlasma/nestjs-course-code/tree/main/graphql-todolist-client

总结

我们实现了 Restful 和 GraphQL 版的 CRUD。

前端用 React + @apollo/client。

后端用 Nest + GraphQL + Prisma + MySQL。

GraphQL 主要是定义 schema 和 resolver 两部分,schema 是 Query、Mutation 的结构,resolver 是它的实现。

可以在 playground 里调用接口,也可以在 react 里用 @appolo/client 调用。

相比 restful 的版本,graphql 只需要一个接口,然后用查询语言来查,需要什么数据取什么数据,更加灵活。

业务开发中,你会选择用 GraphQL 开发接口么?

#前端# #后端#
0个评论
点击登录,快来和大家讨论吧~
表情
图片
暂无评论
下载 APP