全栈搭建个人博客(2)--nest+typeorm搭建博客后端

2,600 阅读6分钟

作者 | 周周酱

本文写于2021年5月13日。首发于周周酱个人博客,转载请注明出处。

博客后端选用的是Nest框架,Nest 是一个用于构建高效,可扩展的 Node.js 服务器端应用程序的框架。它使用渐进式 JavaScript,内置并完全支持 TypeScript并结合了 OOP(面向对象编程),FP(函数式编程)和 FRP(函数式响应编程)的元素。风格类似java spring,在服务器端提供开箱即用的应用架构,让前端人员也能够快速创建可扩展、松耦合、易维护的应用。

关于Nest的一些概念,需移步官网查看Nest官方文档

创建应用

npm i -g @nestjs/cli
nest new nest-blog-api

初始项目

微信图片_20210512165816.png

数据库模型

数据库存储:mysql ORM框架:typeorm nest接入typeorm typrorm文档

搭建mysql

首先搭建mysql服务,创建数据库并获取数据库连接以及用户名密码 ​

项目配置文件

nest连接数据库,我使用环境变量的方式,在项目中创建了.env文件,将数据库连接配置以及后续(oss上传相关配置)都放在该文件中 微信图片_20210512171453.png

nest连接数据库

添加@nestjs/typeorm,@nestjs/config(用于读取项目配置),修改app.module.ts 微信图片_20210512172256.png

我的博客站点分为前台和后台管理系统

后台管理系统主要功能

  • 用户鉴权登录(目前只有单一管理员)
  • 文章管理:列表,编辑,创建,详情
  • 项目管理:列表,编辑,创建,详情
  • 标签管理:为文章和项目添加标签,标签的编辑创建
  • 分类:为文章和项目添加分类,分类的编辑创建

前台页面功能

  • 文章列表,详情展示
  • 项目列表,详情展示
  • 评论功能(暂时没做)

数据库设计

微信图片_20210507190631.png 以及一些数据统计,用于辅助的表 微信图片_20210507190638.png

定义模型

接下来使用typeorm定义entity article模型定义如下,其中涉及到关联关系的创建,请看typeorm关联关系

import {
  BeforeUpdate,
  Column,
  Entity,
  JoinTable,
  ManyToMany,
  ManyToOne,
  OneToMany,
  PrimaryGeneratedColumn
} from 'typeorm';
import { CategoryEntity } from './category.entity';
import { CommentEntity } from './comment.entity';
import { TagEntity } from './tag.entity';
import { UserEntity } from './user.entity';

@Entity('article')
export class ArticleEntity {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  slug: string;

  @Column()
  title: string;

  @Column({ default: '' })
  image: string;

  @Column('text')
  description: string;

  @Column('text')
  content: string;

  @Column('text', { nullable: true })
  config: string;

  /**
   *  1 已上架 2 已下架
   */
  @Column({ default: 2 })
  state: number;

  @Column({ default: false })
  isDeleted: boolean;

  @Column({
    type: 'timestamp',
    default: () => 'CURRENT_TIMESTAMP'
  })
  createdAt: Date;

  @Column({
    type: 'timestamp',
    default: () => 'CURRENT_TIMESTAMP'
  })
  updatedAt: Date;

  @BeforeUpdate()
  updateTimestamp() {
    this.updatedAt = new Date();
  }

  @ManyToOne(
    type => UserEntity,
    user => user.articles
  )
  author: UserEntity;

  @ManyToOne(
    type => CategoryEntity,
    category => category.articles
  )
  category: CategoryEntity;

  @ManyToMany(
    type => TagEntity,
    tag => tag.articles
  )
  @JoinTable({
    name: 'article_tag'
  })
  tags: TagEntity[];

  @OneToMany(
    type => CommentEntity,
    comment => comment.article
  )
  comments: CommentEntity[];
}

同理完成其他实体的定义

微信图片_20210513111057.png

启动服务之后,实体自动映射,会将数据模型同步到数据库。

微信图片_20210513111421.png

接下来可以愉快地打业务代码了。 ​

功能模块

接下来按业务对象区分功能模块,组织应用程序结构。 微信图片_20210513141008.png 一般一个功能模块下会包含 controller、service、module三个文件。

service

复杂的任务应该委托给 providers,我们这里的service就是一个provider,该服务负责数据存储和检索,然后在controller中调用,service类声明之前带有 @Injectable()装饰器,可以在controller中通过 constructor 注入依赖关系。下述代码是相对较典型的一个增删查改操作

//category.service.ts
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { PaginationData } from 'core/models/common';
import { CategoryEntity } from 'entity/category.entity';
import { In, Repository } from 'typeorm';

@Injectable()
export class CategoryService {
  constructor(
    @InjectRepository(CategoryEntity)
    private readonly categoryRepository: Repository<CategoryEntity>
  ) {}

  /**
   * 获取分类列表
   *
   */
  async findAll(
    index: number,
    size: number,
    module?: string
  ): Promise<PaginationData<CategoryEntity>> {
    let res = null;
    if (module) {
      res = await this.categoryRepository.findAndCount({
        where: { module: In([module, 'common']), isDeleted: false },
        take: size,
        skip: (index - 1) * size
      });
    } else {
      res = await this.categoryRepository.findAndCount({
        where: { isDeleted: false },
        take: size,
        skip: (index - 1) * size
      });
    }

    return { index, size, list: res[0], total: res[1] };
  }

  /**
   * 获取分类详情
   *
   */
  async findOne(id: number): Promise<CategoryEntity> {
    const res = await this.categoryRepository.findOne({ id });
    return res;
  }

  /**
   * 创建
   *
   */
  async create(
    title: string,
    description: string,
    module: string
  ): Promise<number> {
    const category = new CategoryEntity();
    category.title = title;
    category.description = description;
    category.module = module;
    const newCategory = await this.categoryRepository.save(category);
    return newCategory.id;
  }

  /**
   * 修改
   *
   */
  async update(
    id: number,
    title: string,
    description: string,
    module: string
  ): Promise<number> {
    const category = await this.categoryRepository.findOne({ id });
    if (!category) {
      throw new HttpException('该分类不存在', HttpStatus.BAD_REQUEST);
    }
    category.title = title;
    category.description = description;
    category.module = module;
    await this.categoryRepository.save(category);
    return id;
  }
  /**
   * 删除
   *
   */
  async delete(id: number): Promise<void> {
    const category = await this.categoryRepository.findOne({ id });
    if (!category) {
      throw new HttpException('该分类不存在', HttpStatus.BAD_REQUEST);
    }
    category.isDeleted = true;
    await this.categoryRepository.save(category);
  }
}

controller

controller控制器负责处理传入的请求和向客户端返回响应 。路由机制控制哪个控制器接收哪些请求。通常,每个控制器有多个路由,不同的路由可以执行不同的操作。控制器使用@Controller装饰器,上面定义的categoryService 是通过类构造函数注入的,意味着我们已经在controller中创建并初始化了 categoryService 成员。

//category.controller.ts

import {
  Body,
  Controller,
  Delete,
  Get,
  Param,
  Post,
  Put,
  Query
} from '@nestjs/common';
import { CategoryService } from './category.service';

@Controller('category')
export class CategoryController {
  constructor(private readonly categoryService: CategoryService) {}

  /**
   * 获取分类列表
   * @param index
   * @param size
   * @param module
   */
  @Get('all')
  async findAll(
    @Query('index') index: number,
    @Query('size') size: number,
    @Query('module') module?: string
  ) {
    return this.categoryService.findAll(
      Number(index) || 0,
      Number(size),
      module
    );
  }

  /**
   * 获取分类详情
   * @param id
   */
  @Get(':id')
  async findOne(@Param('id') id: number) {
    return this.categoryService.findOne(id);
  }

  /**
   * 创建分类
   * @param title
   * @param description
   * @param module
   */
  @Post()
  async create(
    @Body('title') title: string,
    @Body('description') description: string,
    @Body('module') module: string
  ) {
    return this.categoryService.create(title, description, module);
  }

  /**
   * 编辑分类
   * @param id
   * @param title
   * @param description
   * @param module
   */
  @Put(':id')
  async update(
    @Param('id') id: number,
    @Body('title') title: string,
    @Body('description') description: string,
    @Body('module') module: string
  ) {
    return this.categoryService.update(id, title, description, module);
  }

  /**
   * 删除
   * @param id
   */
  @Delete(':id')
  async delete(@Param('id') id: number) {
    return this.categoryService.delete(id);
  }
}

module

模块是具有 @Module() 装饰器的类,每个 Nest 应用程序都一定会有一个根模块即app.module。然后根据应用的实际功能划分出多个功能模块,比如category就是一个相对独立的功能模块,category.module.ts可以说是这个功能模块的一个出口。

//category.module.ts

import { AuthMiddleware } from '@/common/middleware/auth';
import { UserModule } from '@/user/user.module';
import {
  MiddlewareConsumer,
  Module,
  NestModule,
  RequestMethod
} from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { CategoryEntity } from 'core/entity/category.entity';
import { CategoryController } from './category.controller';
import { CategoryService } from './category.service';

@Module({
  imports: [TypeOrmModule.forFeature([CategoryEntity]), UserModule],
  providers: [CategoryService],
  controllers: [CategoryController]
})
export class CategoryModule implements NestModule {
  public configure(consumer: MiddlewareConsumer) {
    consumer.apply(AuthMiddleware).forRoutes(
      {
        path: 'category/all',
        method: RequestMethod.GET
      },
      {
        path: 'category/:id',
        method: RequestMethod.GET
      },
      {
        path: 'category',
        method: RequestMethod.POST
      },
      {
        path: 'category/:id',
        method: RequestMethod.PUT
      },
      {
        path: 'category/:id',
        method: RequestMethod.DELETE
      }
    );
  }
}

@module() 装饰器接受一个描述模块属性的对象:

providers由 Nest 注入器实例化的提供者,并且可以至少在整个模块中共享
controllers必须创建的一组控制器
imports导入模块的列表,这些模块导出了此模块中所需提供者
exports由本模块提供并应在其他模块中可用的提供者的子集。在模块共享的时候需要用到,比如这里的userModule,因为在所有模块中都需要依赖user模块的用户鉴权公共方法,所以需要在userModule中导出userService,然后在其他模块的imports 数组中增加userModule,这样其他模块就可以访问userService。

同时需要在跟模块app.module.ts中注册所有的功能模块

import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { ScheduleModule } from '@nestjs/schedule';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ArticleModule } from './article/article.module';
import { CategoryModule } from './category/category.module';
import { DashboardModule } from './dashboard/dashboard.module';
import { LogModule } from './log/log.module';
import { ProjectModule } from './project/project.module';
import { ShareModule } from './share/share.module';
import { StatisModule } from './statis/statis.module';
import { TagModule } from './tag/tag.module';
import { TaskModule } from './task/task.module';
import { UserModule } from './user/user.module';

@Module({
  imports: [
    TypeOrmModule.forRoot(),
    ConfigModule.forRoot({
      envFilePath: '.env',
      isGlobal: true
    }),
    ScheduleModule.forRoot(),
    ArticleModule,
    ProjectModule,
    TagModule,
    CategoryModule,
    UserModule,
    ShareModule,
    DashboardModule,
    LogModule,
    StatisModule,
    TaskModule
  ],
  controllers: [AppController],
  providers: [AppService]
})
export class AppModule {}

这样一个最基本的功能模块就跑通了,接下来就按照业务功能去实现业务代码,同时完善基础建设,比如增加用户统一鉴权控制器,全局错误过滤器,全局接口返回结果处理器,完善swagger定义等。 ​

完整代码请访问 zzj-admin-api