基于vue3+nestjs+prisma+graphql实现CRUD

251 阅读2分钟
  • 相较于传统的restful风格来开发crud。
  • 现在尝试下graphql风格来试试。

Nestjs+Graphql来实现一个todolist

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

然后我们使用docker 跑一个mysql

docker pull mysql
docker run -d -name mysql-c -p 3306:3306 -e MYSQL_ROOT_PASSWORD=123456 mysql

然后可以使用vscode插件Database Client JDBC查看连接查看自己数据

CREATE DATABASE todolist
    DEFAULT CHARACTER SET = 'utf8mb4';

在nest里面使用prisma连接mysql

npm i prisma --save-dev

初始化prisma创建schema文件

npx prisma init

然后再.env文件修改配置

DATABASE_URL="mysql://root:123456@localhost:3306/todolist"

在prisma/schema.prisma 添加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
}

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

npx prisma migrate dev --name init

接下来可以在nest里面写crud了 使用nest 生成一个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()
    }
    
}

接下来安装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';
import { TodolistResolver } from './todolist.resolver';

@Module({
  imports: [
    GraphQLModule.forRoot({
      driver: ApolloDriver,
      typePaths: ['./**/*.graphql']
    })
  ],
  controllers: [AppController],
  providers: [AppService, PrismaService, TodolistResolver],
})
export class AppModule { }

添加一个 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
}

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

nest g resolver todolist --no-spec --flat
import { Inject, } from '@nestjs/common';
import { Args, Mutation, Query } from "@nestjs/graphql";
import { Resolver } from '@nestjs/graphql';
import { PrismaService } from './prisma.service';
import { UpdateTodoList } from './dto/todolist-update.dto';
import { CreateTodoList } from './dto/todolist-create.dto';

@Resolver()
export class TodolistResolver {

    @Inject(PrismaService)
    private prismaService: PrismaService


    @Query('todolist')
    todolist() {
        return this.prismaService.todoItem.findMany()
    }

    @Query('queryById')
    async queryById(@Args('id') id) {
        return await this.prismaService.todoItem.findUnique({
            where: {
                id
            }
        })
    }

    @Mutation('updateTodoItem')
    updateTodoItem(@Args('todoItem') todoItem: UpdateTodoList) {
        return this.prismaService.todoItem.update({
            where: {
                id: todoItem.id
            },
            data: todoItem,
        })
    }


    @Mutation('createTodoItem')
    createTodoItem(@Args('todoItem') todoItem: CreateTodoList) {
        return this.prismaService.todoItem.create({
            data: todoItem,
        })
    }

    @Mutation('removeTodoItem')
    async removeTodoItem(@Args('id') id: number) {
        const res = await this.prismaService.todoItem.findFirst({
            where: {
                id
            }
        })
        if (res) {
            await this.prismaService.todoItem.delete({
                where: {
                    id
                }
            })

            return id
        } else {
            return 'id is not exist'
        }

    }
}

用 @Resolver 声明 resolver,用 @Query 声明查询接口,@Mutation 声明增删改接口,@Args 取传入的参数。 具体增删改查的实现和之前一样。 浏览器访问 http://localhost:3000/graphql 就是 playground,可以在这里查询

基于Graphql的crud完成了

vue实现

npm create vite@latest

初始化之后再继续安装

npm install --save graphql graphql-tag @apollo/client @vue/apollo-composable

创建apolloClient.ts文件

touch apploClient.ts
import { ApolloClient, HttpLink, InMemoryCache } from '@apollo/client/core'
const httpLink = new HttpLink({
    // You should use an absolute URL here
    uri: 'http://localhost:3000/graphql',
})
// Create the apollo client
export const apolloClient = new ApolloClient({
    link: httpLink,
    cache: new InMemoryCache(),
    connectToDevTools: true,
})

引入main.ts

import { createApp, h, provide } from 'vue'
import './style.css'
import App from './App.vue'
import { apolloClient } from "./apolloClient";
import { DefaultApolloClient } from '@vue/apollo-composable'



const app = createApp({
    setup() {
        provide(DefaultApolloClient, apolloClient)
    },
    render: () => h(App)
})

app.mount('#app')

实现crud

<script setup lang="ts">
import { ref, onMounted, watchEffect, unref, reactive, nextTick, computed, watch } from "vue";
import { useQuery, provideApolloClient, useMutation } from "@vue/apollo-composable";
import { apolloClient } from "./apolloClient";
import gql from "graphql-tag";


const todoListRef = ref()
const loadingRef = ref<boolean>(false)
const tableRefetch = ref()
const getTodoList = () => {
  const GET_TODOITEM_LIST = gql`
    query queryTodolist {
      todolist {
        content
        id
      }
    }
  `

  const { loading, onResult, refetch } = useQuery(GET_TODOITEM_LIST)
  loadingRef.value = loading.value
  tableRefetch.value = refetch

  onResult((res) => {
    if (!res.loading) {
      const { data: result } = res

      todoListRef.value = result?.todolist
    }
  })

}


const inputModel = ref()
const getTodoItemById = async (id: number) => {
  const GET_TODOITEM = gql`
    query queryTodoItem($queryById: Int!) {
      queryById(id: $queryById){
      content
    }
  }
  `

  const variables = {
    "queryById": id
  }
  const { onResult } = provideApolloClient(apolloClient)(() => useQuery(GET_TODOITEM, variables))

  onResult((res) => {
    if (!res.loading) {
      const { data: result } = res

      inputModel.value = unref(result).queryById.content
    }
  })
}


// addFunc

const addFunc = async () => {
  const ADD_TODOITEM = gql`
    mutation Mutation($todoItem: CreateTodoItemInput!) {
      createTodoItem(todoItem: $todoItem){
        content,
        id
      }
    }
  `

  const variables = {
    "todoItem": {
      content: inputModel.value
    }
  }
  const { mutate, onDone, loading } = provideApolloClient(apolloClient)(() => useMutation(ADD_TODOITEM, {
    variables,
  }))

  await mutate()

  tableRefetch.value()
}

// delFunc
const delFunc = async (id: number) => {
  const DEL_TODOITEM = gql`
    mutation Mutation($removeTodoItemId: Int!) {
      removeTodoItem(id: $removeTodoItemId)
    }
  `

  const variables = {
    "removeTodoItemId": id
  }
  const { mutate, onDone, loading } = provideApolloClient(apolloClient)(() => useMutation(DEL_TODOITEM, {
    variables,
  }))

  await mutate()
  tableRefetch.value()
}

// updateFunc
const updateFunc = async (id: number) => {
  const UPDATE_TODOITEM = gql`
    mutation Mutation($todoItem: UpdateTodoItemInput!) {
      updateTodoItem(todoItem: $todoItem){
        content,
        id
      }
    }
  `

  const variables = {
    "todoItem": {
      content: inputModel.value,
      id
    }
  }
  const { mutate, onDone, loading } = provideApolloClient(apolloClient)(() => useMutation(UPDATE_TODOITEM, {
    variables,
  }))

  await mutate()
}

onMounted(() => {
  getTodoList()

})


</script>

<template>
  <button @click="addFunc">add</button>
  <div v-for="item in todoListRef">
    <span @click="getTodoItemById(item!.id)">{{ item?.content }}-{{ item?.id }}</span>
    <button @click="delFunc(item!.id)">del</button>
    <button @click="updateFunc(item!.id)">edit</button>
  </div>

  <input v-model="inputModel" />
</template>

<style scoped>
.logo {
  height: 6em;
  padding: 1.5em;
  will-change: filter;
  transition: filter 300ms;
}

.logo:hover {
  filter: drop-shadow(0 0 2em #646cffaa);
}

.logo.vue:hover {
  filter: drop-shadow(0 0 2em #42b883aa);
}
</style>

配置代码生成

npm add -D typescript @graphql-codegen/cli @graphql-codegen/client-preset
# 初始化配置文件
npx graphql-code-generator init

配置生成参考codegen.ts


import type { CodegenConfig } from '@graphql-codegen/cli';

const config: CodegenConfig = {
  overwrite: true,
  schema: "http://localhost:3000",
  documents: "src/**/*.vue",
  generates: {
    "src/gql/": {
      preset: "client",
      plugins: []
    },
    "./graphql.schema.json": {
      plugins: ["introspection"]
    }
  }
};

export default config;

修改package.json,在scripts中增加

"generate": "graphql-codegen"