Nest.js 项目启动指南

Nest.js 项目启动指南

如果一个前端的开发者想要快速的启动一个完整项目,写后端的话,其实现在也有一些不错的企业级的、基于 node.js 服务的开源框架。

早些时候我一直在用阿里的企业级服务端框架 Egg.js,它是一个标准的使用 MVC 架构的框架,基于 node.jsKoa

至于为什么不用了呢?其实主要原因是因为 Egg.js 已经许久没有更新了。我不知道是不是阿里对于它处于一种半放弃的状态,或者说是完全放弃的状态。当后面再遇到问题不知道该怎么解决又没有更多的问答可以参考,所以我也放弃了。

用 Nest.js 也是同事推荐来试试的,简单的看了文档和一些教程以后,我觉得这个东西用起来很顺手。而且完全基于 TypeScript 可以在开发过程中就发现自己犯了什么错误,十分适合我这种手残党。

于是我就整理了一个模板项目,在最近会进行开源。

所以在这里留了一个开源项目的位置。

Module - Service - Controller 模式

Nest.js 使用 Module 区分模块,在模块中引入 ServiceController。在 Module 中还可以把引入的 Service 导出,并且在其他的 Module 中导入来使用。当 Service 中的处理被拆到足够细的情况下,是可以做到节约开发资源的。

Nest.js 官方建议的文件管理模式是把一个 Module 相关的所有 ServiceController 放在一个文件夹里管理。这样的情况下对于每个 Module 相对独立的情况下管理起来会显得干净一些。但是我个人比较喜欢所有的 Module 在一起,所有的 Service 在一起,以及所有的 Controller 在一起的形式来管理文件,像这样:

+- src
|  +- Controllers
|  |  +- passport.controller.ts
|  |  +- user.controller.ts
|  +- Modules
|  |  +- user.module.ts
|  +- Services
|     +- passport.service.ts
|     +- user.service.ts
+- ...

这样做的好处是,在不同场景下可能交叉使用的 Service 不必收到文件夹之间的干扰。
以上例子中,我把用户登录、注册、找回密码等等放进了 passport 这个业务流(Controller -> Service)里,然后把获取用户个人信息、修改个人信息、修改密码等等放进了 user 这个业务流里,这两个业务流都是在处理用户操作或者相关的数据,于是我就把他们都放在一个 Module 里面。

数据库和缓存

用 ORM 操作数据库

Nest.js 推荐的是使用 TypeOrm 进行数据库的 CURD。
方法是从 nestjs-config 依赖中导出 ConfigModuleConfigService,在 imports 中像这样使用:

// ... code clips of App.module.ts
import { ConfigModule, ConfigService } from 'nestjs-config';
import { TypeOrmModule } from '@nestjs/typeorm';
@Modules({
  imports: [
    ConfigModule.load(resolve(__dirname, 'config', '**/!(*.d).{ts,js}')),
    TypeOrmModule.forRootAsync({
      useFactory: (config: ConfigService) => config.get('database'),
      inject: [ConfigService]
    }),
  ]
})

用 Entity 管理表

在上面的例子代码中,可以看到所有 config 是在读取 ~/config/*.ts 作为配置文件的,并且数据库来自文件名是 database。也就是说,config 文件夹里有一个 database.ts 用来管理数据库连接:

import { join } from 'path'

const DB_ENV = {
  development: {
    host: 'rm-develop.mysql.rds.aliyuncs.com',
    database: 'project_db_dev',
    username: 'user_dev',
    password: 'password_dev'
  },
  testing: {
    host: 'rm-testing.mysql.rds.aliyuncs.com',
    database: 'project_db_testing',
    username: 'user_testing',
    password: 'password_testing'
  },
  product: {
    host: 'rm-prod.mysql.rds.aliyuncs.com',
    database: 'project_db_prod',
    username: 'user_prod',
    password: 'password_prod'
  }
}

const useEnv = process.env.NODE_ENV || 'product'

export default {
  type: 'mysql',
  host: DB_ENV[useEnv].host,
  port: 3306,
  username: DB_ENV[useEnv].username,
  password: DB_ENV[useEnv].password,
  database: DB_ENV[useEnv].database,
  entities: [join(__dirname, '../', '**/**.entity{.ts,.js}')],
  synchronize: true,
}

其中,useEnv 是用于切换环境的,可以在启动项目时候添加 process.env.NODE_ENV 参数进行获取,默认为 product

上面的配置也提示了可以在任何的文件夹里使用 *.entity.ts 这样的形式配置实体(也就是表在系统里的表现)。

那么我们在 ~/entity/ 目录下建立实体文件,就可以对数据库中的表进行管理了。

// user.entity.ts
import { Column, Entity, PrimaryColumn } from 'typeorm'

/**
 * 实体:用户列表
 */
@Entity({
  name: 'user',
})
export class UserEntity {
  @PrimaryColumn({ length: 128 })
  id: string // 唯一识别码

  @Column({ length: 512 })
  headImgUrl: string // 用户头像

  @Column({ length: 18 })
  name: string // 昵称

  @Column({ length: 128 })
  openId: string // openId

  @Column({ length: 128 })
  unionId: string // unionId

  @Column('datetime')
  createdAt: string

  @Column('datetime')
  updatedAt: string
}

Redis

要使用一个 @chenjm/nestjs-redis 的依赖来代替官网推荐的 @nestjs/redis。主要原因是官网推荐的包没有跟着 nestjs 进行更新,会报错。

装饰器

整个 Nest.js 的核心思想就是装饰器,它看起来是一个以 @ 开头的函数执行。
它在项目里无处不在:

// passport.controller.ts
import { Body, Controller, Post } from '@nestjs/common'
import { PassportService } from 'src/service/passport.service'
import { getUser } from 'src/utils/lark.utils'
import { JSONResponse } from '../utils/response.utils'

@Controller('lark')
export class PassportController {
  constructor(
    private readonly passportService: PassportService
  ) {}
  // 用户登录
  @Post('Token')
  async userGetToken(@Body() { code, state }) {
    if (!code) return JSONResponse(-1, '缺少参数:code')
    if (!state) return JSONResponse(-1, '缺少参数:state')
    const userInfo: any = await getUser(code, state)
    
    if (userInfo.code !== 0) return JSONResponse(-userInfo.code, userInfo.msg)
    return this.passportService.createAndLogin(userInfo.data)
  }
}

@Controller('lark')@Post('Token') 两个装饰器引导当前 userGetToken 这个函数是透过 [POST] /lark/Token 这个接口执行的,有两个 request body 参数,是透过 @Body() { code, state } 接收

中间件

我们可以在任何场景下使用中间件,例如:

我们可以在收到接口请求是验证 JWT,或者打印请求日志等等:

// code clips of App.module.ts
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(Logger)
      .forRoutes('') // 表示所有接口都执行 logger
      .apply(Jwt)
      .forRoutes('') // 表示所有接口都执行 Jwt
      .apply(AuthAdmittance)
      .forRoutes('user*', 'order*') // 表示 user 和 order 开头的接口执行 AuthAdmittance
  }
}

工具类/工具函数

我还有有一些前端开发的习惯,比如会把请求封装成工具(类)等等。所以我在这里也建立了一个叫做 'utils' 的目录用于存放工具类。

例如,所有的返回:

// response.utils.ts
export function JSONResponse (
  code: number = 0,
  message: string = 'ok',
  data: any = []
): string {
  const ret = DefaultResponse(code, message, data)
  return JSON.stringify(ret)
}

这样我只需要在返回数据处调用这个 JSONResponse() 方法,依次传入 codemessagedata 三个参数即可。甚至可以不穿参数直接返回。

Nest.js 一样可以使用 axios 从服务端发异步请求,这得益于 axios 在 nodejs 方面也有很好的兼容性。

总结

以上大概就是 Nest.js 这个框架在比较小型的项目中的使用以及一些开发技巧。后续我会尽快的把模版项目开源,更新到文章中。

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×