如果一个前端的开发者想要快速的启动一个完整项目,写后端的话,其实现在也有一些不错的企业级的、基于 node.js
服务的开源框架。
早些时候我一直在用阿里的企业级服务端框架 Egg.js,它是一个标准的使用 MVC 架构的框架,基于 node.js
和 Koa
。
至于为什么不用了呢?其实主要原因是因为 Egg.js 已经许久没有更新了。我不知道是不是阿里对于它处于一种半放弃的状态,或者说是完全放弃的状态。当后面再遇到问题不知道该怎么解决又没有更多的问答可以参考,所以我也放弃了。
用 Nest.js 也是同事推荐来试试的,简单的看了文档和一些教程以后,我觉得这个东西用起来很顺手。而且完全基于 TypeScript 可以在开发过程中就发现自己犯了什么错误,十分适合我这种手残党。
于是我就整理了一个模板项目,在最近会进行开源。
所以在这里留了一个开源项目的位置。
Module - Service - Controller 模式
Nest.js 使用 Module
区分模块,在模块中引入 Service
和 Controller
。在 Module
中还可以把引入的 Service
导出,并且在其他的 Module
中导入来使用。当 Service
中的处理被拆到足够细的情况下,是可以做到节约开发资源的。
Nest.js 官方建议的文件管理模式是把一个 Module
相关的所有 Service
和 Controller
放在一个文件夹里管理。这样的情况下对于每个 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
依赖中导出 ConfigModule
和 ConfigService
,在 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()
方法,依次传入 code
,message
和 data
三个参数即可。甚至可以不穿参数直接返回。
Nest.js 一样可以使用 axios
从服务端发异步请求,这得益于 axios
在 nodejs 方面也有很好的兼容性。
总结
以上大概就是 Nest.js 这个框架在比较小型的项目中的使用以及一些开发技巧。后续我会尽快的把模版项目开源,更新到文章中。