【NestJS实战】前端Vue+后端Nest,实现你的第一个CURD目标 写一个表格,支持添加、查询、删除、修改表格数据
目标
写一个表格,支持添加、查询、删除、修改表格数据,同时也支持翻页
准备
-
数据库用mysql
-
数据可视化工具用vscode的插件
-
操作数据库用前端的 TypeOrm 。它是TypeScript中最成熟的对象关系映射器(ORM)。因为它是用 TypeScript 编写的,所以可以很好地与 Nest 框架集成
-
将 Nest 与数据库连接起来。在app.module.ts中,使用 imports 引入 TypeOrmModule
- 准备实体文件,例如 user.entity.ts。它是 Nest 与数据库通信的桥梁,本质上是一个映射到数据库表的类。
- 在 user.entity.ts 中用 typeorm 的一系列装饰器,定义数据库将来会有的数据,例如下图的 id、username、pwd、createTime 字段
- 将实体文件 user.entity.ts 与数据库关联:在 user.module.ts 中,在imports中,导入 User 实体类和 TypeOrmModule
数据库生成如下:
后端
在 user.service.ts 中,引入实体、typeorm,把他们和服务 Service 关联起来后,就能利用typeorm的API向数据中存数据、读数据
控制器
在控制器中使用一些列装饰器分别对应请求,并将请求数据提取后,交给服务去处理;服务返回的结果,会交还给控制器,它再响应给浏览器
- user.controller.ts
import { Controller, Get, Post, Body, Patch, Param, Delete, Query } from '@nestjs/common';
import { UserService } from './user.service';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
interface SearchParams {
keyWord?: string
page?: number
pageSize?: number
}
@Controller('user')
export class UserController {
constructor(private readonly userService: UserService) { }
// 接收 POST
@Post()
create(@Body() createUserDto: CreateUserDto) {// 用 @Body() 提取请求体
return this.userService.create(createUserDto);
}
// 接收 GET
@Get()
findAll(@Query() query: SearchParams) { // 用 @Query() 提取查询参数
return this.userService.findAll(query);
}
// 接收 Delete
@Delete(':id')
remove(@Param('id') id: string) {// 用 @Query() 提取斜杠后的参数
return this.userService.remove(Number(id));
}
// 接收 Patch
@Patch(':id')
update(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) {
// 用 @Param('id') 提取斜杠后的参数; 用 @Body() 提取请求体
return this.userService.update(Number(id), updateUserDto);
}
@Get(':id')
findOne(@Param('id') id: string) {
return this.userService.findOne(+id);
}
}
服务
在服务中,接收数据,使用 typeorm 操作数据库
- user.service.ts
import { Injectable } from '@nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { Repository, Like } from 'typeorm';
import { InjectRepository } from '@nestjs/typeorm'; // 导入装饰器
import { User } from './entities/user.entity' // 导入实体
interface SearchParams {
keyWord?: string
page?: number
pageSize?: number
}
@Injectable()
export class UserService {
//! 用 InjectRepository 装饰 user
constructor(@InjectRepository(User) private readonly user: Repository<User>) {
//
}
// A. 添加新用户
create(createUserDto: CreateUserDto) {
// 1. 实例化实体 User
const obj = new User()
// 2. 把前端传来的数据添加到实体对象上
obj.username = createUserDto.username
obj.pwd = createUserDto.pwd
// 3. 保存到数据库,并作为响应返回
return this.user.save(obj)
}
// B. 搜索用户(模糊查询)
async findAll(query: SearchParams) { // 参数 query 就是用户的查询参数
const searchObj = {
// 根据关键词模糊查询
where: {
username: Like(`%${query.keyWord}%`)
},
// 根据分页信息查询
skip: (query.page - 1) * query.pageSize, // 从第x页开始,需要x条数据
take: query.pageSize
}
// 总数据条数
const totalNum = {
where: {
username: Like(`%${query.keyWord}%`)
}
}
const data = await this.user.find(searchObj)
const total = await this.user.count(totalNum)
// 从数据库中获取结果,并作为响应返回
return {
data,
total
}
}
// C. 删除用户
remove(id: number) {
// 从数据库中删除,并作为响应返回
return this.user.delete(id)
}
// D. 更新用户
update(id: number, updateUserDto: UpdateUserDto) {
// 将id和新数据放入数据库,并作为响应返回
return this.user.update(id, updateUserDto)
}
findOne(id: number) {
return `This action returns a #${id} user`;
}
}
前端
整体如下:表格 + 分页 + 搜索框 + 添加按钮
- 请求封装
import axios from 'axios'
axios.defaults.baseURL = 'http://localhost:3000'
export const addUser = (data) => {
return axios.post('/user', data).then(res => res.data)
}
export const getList = (data) => {
return axios.get('/user', { params: data }).then(res => res.data)
}
export const delUser = (data) => {
return axios.delete(`/user/${data.id}`).then(res => res.data)
}
export const updateUser = (data) => {
return axios.patch(`/user/${data.id}`, data).then(res => res.data)
}
- 结构
<template>
<div class="wraps">
<!-- 搜索栏 -->
<div>
<h2 class="title">用户管理:增、删、改、查</h2>
<el-input v-model="search.keyWord" style="width:300px;" placeholder="请输入用户名"></el-input>
<el-button @click="init" style="margin-left:10px;">搜索</el-button>
<el-button @click="openDialog" type="primary" style="margin-left: 527px;">添加新用户</el-button>
</div>
<!-- 表格 -->
<el-table border :data="tableData" style="width: 100%; margin-top: 30px;">
<el-table-column prop="username" label="用户名" />
<el-table-column prop="pwd" label="密码" />
<el-table-column prop="id" label="id" />
<el-table-column>
<template #default="scope">
<el-button @click="edit(scope.row)">编辑</el-button>
<el-button @click="deleteRow(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
@current-change="changeSize"
background
layout="prev, pager, next"
:total="total"
style="float:right; margin-top:10px;"
/>
</div>
<!-- 新增和编辑的弹框 -->
<el-dialog v-model="dialogVisible" :title="title" width="30%">
<el-form :model="form">
<el-form-item prop="username" label="用户名">
<el-input v-model="form.username" placeholder="用户名" />
</el-form-item>
<el-form-item prop="pwd" label="密码">
<el-input v-model="form.pwd" placeholder="密码">
</el-input>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="close">关闭</el-button>
<el-button type="primary" @click="save">
保存
</el-button>
</span>
</template>
</el-dialog>
</template>
<style lang='less'>
* {
padding: 0;
margin: 0;
}
html,
body {
background: #ccc;
}
.wraps {
width: 1000px;
height: 600px;
padding: 30px;
.title {
margin-bottom: 30px;
}
}
</style>
- CURD的js
<script setup>
import { ref, reactive, computed } from 'vue'
import { addUser, updateUser, delUser, getList } from './http'
import { ElMessage, ElMessageBox } from 'element-plus'
// 表格数据
const tableData = ref([])
// 数据总条数
const total = ref(0)
// 搜索框
const search = reactive({
keyWord: "",
page: 1,
pageSize: 10
})
// 表单
const form = reactive({
username: "",
pwd: "",
id: 0
})
const title = computed(() => {
return form.id ? '编辑用户' : '新增用户'
})
// 获取数据填充表格数据
const init = async () => {
const { result } = await getList(search)
console.log('result', result)
tableData.value = result.data
total.value = result.total
}
init()
// 清空数据
const resetForm = reactive({ ...form })
// 弹框
const dialogVisible = ref(false)
const openDialog = () => {
dialogVisible.value = true;
Object.assign(form, resetForm)
}
const changeSize = (page) => {
search.page = page
init()
}
// 新增、编辑用户
const save = async () => {
if (form.id) { // 编辑用户
await updateUser(form)
ElMessage.success('编辑成功')
} else { // 新增用户
await addUser(form)
ElMessage.success('添加成功')
}
close() // 关闭弹窗
init() // 重新获取数据更新表格
}
// 删除用户
const deleteRow = (row) => {
const id = row.id
ElMessageBox.confirm(
'确认删除?',
'Warning',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}
)
.then(async () => {
await delUser({ id })
init()
ElMessage({
type: 'success',
message: '删除成功',
})
})
}
// 编辑用户
const edit = (row) => {
dialogVisible.value = true
form.username = row.username
form.pwd = row.pwd
form.id = row.id
}
// 关闭弹框
const close = () => {
dialogVisible.value = false;
}
</script>
可能遇到的问题
Nest 返回的数据不一致怎么办?难道每个接口的响应都要手动写成json对象吗? 使用全局响应拦截器,它一般用于格式化响应数据
- 创建响应拦截器 formatt.interceptor.ts
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
import { map, Observable } from 'rxjs';
@Injectable()
export class FormattInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
// 格式化响应数据
const doMap = map((data) => {
return {
code: 200,
message:'成功',
result: data
}
})
return next.handle().pipe(doMap);
}
}
- 在 main.ts中注册为全局。如果遇到跨域问题,可直接使用 NestJS 内置的.enableCors(),代码如下
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { FormattInterceptor } from './formatt/formatt.interceptor'
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// 启用CORS
app.enableCors({
origin: 'http://localhost:5173'
});
// 注册全局的响应拦截器
app.useGlobalInterceptors(new FormattInterceptor())
await app.listen(3000);
}
bootstrap();
转载自:https://juejin.cn/post/7400584538626080806