Nest项目(四)-统一处理接口返回体并实现前后端分离

1,250 阅读3分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第11天,点击查看活动详情

前言

继续整理一下当前项目的情况,已满足:

  • 启动一个服务,端口为 3000
  • 可以处理get、post请求,操作数据库并返回自定义数据结构
  • 可以通过接口访问经过 ejs 编译后的 html,并按照 ejs 的规则实现数据渲染

至此,其实项目已经可以投入使用了,但是很明显,他还很不健全!!!而且,和我们平常访问的方式是不一样的。

首先第一步要做到前后端分离。

配置流程

首先作为一个前端,可以很清晰的知道,前后端分离的本质其实就是前端可以通过 ajax。通过 ajax 技术实现页面数据的无感更新。

而 vue/react/webpack 项目也是打包成 静态html,通过ajax和后台进行数据交换。

构建前端应用

前端项目基于前面的 user 数据进行,首先有个列表,点击列表项可以在右侧展示 user 的详细信息。

使用 vite 迅速搭建一个 vue3 + ts +elementPlus 的前端应用,页面效果如下:

image.png

这是最常见的一个页面结构了,提供了新增、删除、查看详情三个交互按钮。

针对这个表格,需要四个接口:

  • 【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 值了。

下一步计划

登录并实现权限管理。

参考文档

vite 官方文档