开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第11天,点击查看活动详情。
前言
继续整理一下当前项目的情况,已满足:
- 启动一个服务,端口为 3000
- 可以处理get、post请求,操作数据库并返回自定义数据结构
- 可以通过接口访问经过 ejs 编译后的 html,并按照 ejs 的规则实现数据渲染
至此,其实项目已经可以投入使用了,但是很明显,他还很不健全!!!而且,和我们平常访问的方式是不一样的。
首先第一步要做到前后端分离。
配置流程
首先作为一个前端,可以很清晰的知道,前后端分离的本质其实就是前端可以通过 ajax。通过 ajax 技术实现页面数据的无感更新。
而 vue/react/webpack 项目也是打包成 静态html,通过ajax和后台进行数据交换。
构建前端应用
前端项目基于前面的 user 数据进行,首先有个列表,点击列表项可以在右侧展示 user 的详细信息。
使用 vite 迅速搭建一个 vue3 + ts +elementPlus 的前端应用,页面效果如下:
这是最常见的一个页面结构了,提供了新增、删除、查看详情三个交互按钮。
针对这个表格,需要四个接口:
- 【GET】user/list
- 【GET】user/info
- 【POST】user/create
- 【POST】user/delete
并将 home 页面放出来,供后面粘贴使用:
<template>
<div class="home-page">
<div class="home-tools">
<el-button type="primary" @click="openCreateUserDialog"
>新增用户</el-button
>
</div>
<div class="home-table">
<el-table :data="state.tableData" border style="width: 100%">
<el-table-column type="index" label="#" width="80" />
<el-table-column prop="username" label="用户名" width="180" />
<el-table-column prop="password" label="密码" width="180" />
<el-table-column prop="createTime" label="创建时间" />
<el-table-column
fixed="right"
align="center"
label="Operations"
width="200"
>
<template #default="{$index, row}">
<el-button type="primary" size="small" @click="handleDelete(row)">Delete</el-button>
<el-button type="primary" size="small">View</el-button>
</template>
</el-table-column>
</el-table>
</div>
<el-dialog v-model="state.dialogVisible" title="创建用户" width="30%">
<el-form
ref="ruleFormRef"
:model="state.ruleForm"
:rules="state.rules"
label-width="120px"
class="demo-state.ruleForm"
status-icon
>
<el-form-item label="用户名" prop="username">
<el-input v-model="state.ruleForm.username" />
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input v-model="state.ruleForm.password" />
</el-form-item>
<el-form-item label="性别" prop="sex">
<el-select v-model="state.ruleForm.sex" placeholder="">
<el-option label="男" value="1" />
<el-option label="女" value="0" />
</el-select>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="state.dialogVisible = false">Cancel</el-button>
<el-button type="primary" @click="handleSubmit">
Confirm
</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { onMounted, reactive, ref } from 'vue'
import axios from 'axios'
import type { FormInstance } from 'element-plus'
const ruleFormRef = ref()
const state = reactive({
tableData: [],
dialogVisible: false,
ruleForm: {
username: '',
password: '',
sex: 1,
},
rules: {
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
password: [{ required: true, message: '请输入密码', trigger: 'blur' }],
},
})
const initTable = async () => {
const { list, total } = await (await axios.get('/api/user/list')).data
console.log(list)
state.tableData = list
}
const openCreateUserDialog = () => (state.dialogVisible = true)
const handleDelete = async (row:any) => {
await axios.post('/api/user/delete', {id: row.id})
initTable()
}
const handleSubmit = async () => {
if (!ruleFormRef) return
await ruleFormRef.value.validate().then(async (v: boolean) => {
if (v) {
await axios.post('/api/user/create', state.ruleForm)
state.dialogVisible = false
initTable()
} else {
console.log('validate error!!!')
}
})
}
onMounted(() => {
initTable()
})
</script>
<style lang="scss" scoped>
.home-page {
width: 960px;
margin: 0 auto;
.home-tools {
border-bottom: 1px solid #eee;
padding-bottom: 16px;
margin: 16px 0;
}
}
</style>
接口添加 api 前缀配置拦截器统一接口返回体
在服务中增加如上 4 个接口并使用 axios 请求数据。
完成对应的几个接口
// user.controller.ts
import { Body, Controller, Get, Post, Query, Render } from '@nestjs/common';
import { UserService } from './user.service';
@Controller('user')
export class UserController {
constructor(private userService: UserService) {}
@Get('listPage')
@Render('page')
getListPage() {
return this.userService.getListPage();
}
@Get('list')
getList() {
return this.userService.getList();
}
@Get('info')
getInfo(@Query() id) {
return this.userService.getInfo(id);
}
@Post('create')
create(@Body() user) {
return this.userService.create(user);
}
@Post('delete')
postDelete(@Body() id) {
return this.userService.postDelete(id);
}
}
// - - - - - - - - - - - - - - - - -
// user.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './user.entity';
@Injectable()
export class UserService {
constructor(
@InjectRepository(User)
private readonly userRepository: Repository<User>,
) {}
async getListPage() {
const userlist = this.userRepository.createQueryBuilder('user').getMany();
const list = await userlist;
return { list };
}
async getList() {
const userBuider = this.userRepository
.createQueryBuilder('user')
.where({ isDelete: false })
.getManyAndCount();
const [list, total] = await userBuider;
return { list, total };
}
async getInfo(id) {
const user = await this.userRepository.findOneBy({ id });
return user;
}
async create(user) {
const createUser = {
username: user.username,
sex: user.sex,
password: user.password,
};
const result = await this.userRepository.save(createUser);
return result;
}
async postDelete(idDto) {
const { id } = idDto;
const deleteItem = await this.userRepository.findOneBy({ id });
deleteItem.isDelete = true;
const result = await this.userRepository.save(deleteItem);
return result;
}
}
两者配合就可以实现接口的使用了,完全可以完成增删改查的功能。
一般情况下,我们使用接口都会使用不同的 code 代表不同的响应结果,然后处理 axios 响应体,可以统一处理 接口返回信息。
结构一般是如下:
{
code: 1,
msg: 'success',
data: any
}
具体操作如下:
创建拦截器:
// src/interceptor/transform.interceptor.ts
import {
CallHandler,
ExecutionContext,
Injectable,
NestInterceptor,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
@Injectable()
export class TransformInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
map((result) => {
if (result && result.__code) {
return {
code: result.__code,
message: result.__message,
data: null,
};
}
return {
code: 200,
message: '请求成功!',
data: result,
};
}),
);
}
}
// - - - - - - - - - - - - - -
// src/filters/http-execption.filter.ts
import {
ExceptionFilter,
Catch,
ArgumentsHost,
HttpException,
} from '@nestjs/common';
import { Response } from 'express';
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const status = exception.getStatus();
const message = exception.message;
// 配置 异常返回值
const exceptionResponse: any = exception.getResponse();
let validatorMessage = exceptionResponse;
console.log(
'🚀 ~ file: http-execption.filter.ts ~ line 22 ~ HttpExceptionFilter ~ validatorMessage',
validatorMessage,
);
if (typeof validatorMessage == 'object') {
validatorMessage = validatorMessage.message;
}
response.status(status).json({
code: status,
message: validatorMessage || message,
});
}
}
这个是我从我的另一个项目中粘过来的,因为和项目耦合度较低,所以直接可以使用。
最近发现一篇日志封装的特别好的,研究一下,再搞这块内容。
并使用 axios 实现数据交换
nest 拦截器配置完成以后,就可以封装 axios 进行处理接口响应体了。
通过判断 code 值,如果是 200,则将 data 返回给业务逻辑,否则通过 message 组件进行提示。这样就不用每次都判断 code 值了。
下一步计划
登录并实现权限管理。