【基于Nest.js+React的全栈项目-01篇】基础篇:快速跨过新手村

发布于:2025-07-02 ⋅ 阅读:(22) ⋅ 点赞:(0)

该文章是【基于Nest.js+React的全栈项目】系列文章之一,整体信息详见:

开篇

本文是系列文章第一篇,不想再系统的讲一遍nestjs如何入门,但如果没使用过nestjs的话,确实需要一个过程,自己之前也跟着别人的教程撸过一个demo项目,正好把文章和我自己的demo都贴出来,方便大家快速跨过新手村:

掘友文章:

我的demo项目: nestjs-starter

  • 上面的四篇文章从nestjs的基础使用到jwt认证、typeorm、mysql、redis等都串了一遍。写的比较早,依赖库版本可能也比较老,所以可以与我写的demo一起使用。
  • 上面文章有人反馈坑多,我在coding的时候也发现一些问题,所以文章其实只是思路和大纲,具体代码肯定是不能照搬的,结合ai编程,其实一些小细节也无伤大雅。
  • 接下来我会结合上面的文章和自己的demo项目做一些必要的讲解,不过还是希望还是先把上面的四篇文章过一遍。

推荐两种阅读顺序:

  • 高效版:从我这篇文章开始读,读完 clone 项目先跑起来,再把代码捋一遍,如果不理解且没解释到位,再去翻下上面掘友的文章。
  • 渐进版:一点点把上面掘优的文章读完,想动手操练的时候再直接clone本文章demo。

nestjs-starter 项目使用

  • 该项目是基于 nest 脚手架一步步coding的,如果不想直接clone,也可以先 nest new nest-starter 看下基础的模板代码,这样能更清晰一些。
  • git clone git@github.com:hellozhangran/nestjs-starter.git 拉取项目
  • cd nestjs-starter && pnpm install 安装依赖
  • 启动 本地 mysql 和 redis 服务。
    • 如何使用 mysql 可自行查阅,我这里用的 8.4.0 的macOS 版本
    • 如何使用 redis 可参考我之前的记录:工具篇-Redis
    • 可视化工具可以使用:Navicat Premium Lite 一个软件可同时管理 mysql 和 redis
    • 在 mysql 中创建一个名为 typeorm-test 的数据库备用
  • 启动完数据库服务后,需要修改项目根目录的 .env 文件,改成自己的配置,参考如下:
// 数据库相关配置
DB_HOST=localhost
DB_PORT=3306
DB_USER=root
DB_PWD=12345678
DB_DATABASE=typeorm-test

//Redis
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_DB=0
// 故意空着,一般redis不需要设置密码
REDIS_PWD=

// 这个secret可以随便写,一般会使用crypto生成一个
JWT_SECRET=33b28fb03f649f0b053b1f15f86017cf56ff1ecfacf3fe772d7fe05c3e2963ea
  • pnpm start 启动服务
  • 服务启动后浏览器直接访问:http://localhost:3000/api/hello 就可以调到一个最基础的get请求里,返回 “Hello World!”
  • 推荐使用 Apifox 这个工具来访问自己的本地请求,它有个快捷请求功能,感觉很好用。还可以统一设置 Header Authorization 方便模拟登陆后的 jwt 权限认证。

Tips:该项目没有加 Swagger Api 功能,防止增加太多代码增加理解成本,后续可按需自己加。

核心逻辑梳理

登陆与鉴权逻辑

该 demo 项目最复杂的逻辑就是登陆与鉴权逻辑了,现在把这块单独拿出来串一下:

  • /api/user/register 第一步用户注册,把用户名/密码传给接口
    • 简单的非空校验,进入信息存储到数据库的环节。
    • user.entity.ts 中定义了预操作,会把passport自动加密后存入数据库
@BeforeInsert()
	async encryptPassword() {
	this.password = await bcrypt.hash(this.password, 10);
}
  • /api/auth/login 登陆逻辑
    • 登陆时通过通过前置守卫 @UseGuards(AuthGuard('local')) 先检查用户名密码是否合法
    • 检查合法后,会创建使用 jwtService 创建 token ,并返回给前端
    • 前端接收到 token 后会存放到 localStorage 中,后续所有操作都会默认通过 Header Authoration 字段带上该 token 。
  • 请求其他常规接口。通过 @UseGuards(AuthGuard('jwt')) 守卫设置当前接口需要jwt校验。
    • 后续可以通过全局Guard把所有接口都加上 jwt 守卫
    • 访问到该接口时先校验jwt token是否合法,如果合法则直接把user信息注入到 request.user对象上,后续操作如何需要用户信息就不用单独取了。
  • api/post/create 该接口通过 @UseGuards(AuthGuard('jwt'), RoleGuard) 加了jwt 校验和角色权限校验
@Roles('root', 'author')
@UseGuards(AuthGuard('jwt'), RoleGuard)
create(@Body() createPostDto: CreatePostDto, @Req() req: { user: UserEntity }) {
	return this.postService.create(req.user, createPostDto);
}
  • 除了之前说的jwt校验,会从user信息中拿到role信息,比对下是否有 root 或 author 权限。

上面就是目前项目中的鉴权相关逻辑,很简单直接。当然还不完善,会把角色对应的接口权限给抽出来,实现可配置,而不是像现在这样写死。

Redis 使用场景

当使用了 mysql 存储数据的时候,再看到 redis 肯定会有一个疑问为啥用了 mysql 还要用 redis 呢,主要因为它是内存数据库读写延迟低至微秒级,有很多原子性操作适合实现分布式锁等;这里不长篇大论只说几个我们项目可能用到的场景:

  • 缓存常用且更新率低的数据,如用户用户信息数据。
  • 利用其过期属性,设置 token 并可管理 token 生命周期。
  • 分布式锁用以处理资源竞争问题。(未来会用上,如多实例下的定时任务等)
    还有比如秒杀、排行、消息队列等作用,但目前在该项目尚无清晰的使用场景。

Nest 几个疑问

nest 是如何注册和使用环境变量的?

  • 可以看到代码里有使用 process.env.DB_USER 获取 .env 文件里的变量的情况,也有通过 configService.get('JWT_SECRET') 获取 .env 文件里的变量的情况。
    • 首先在 app.module.ts 文件中使用了 ConfigModule.forRoot 方法,该方法的 envFilePath 可以指定一个环境变量配置文件,如果不指定就是默认根目录下的 .env 文件。
    • 该 .env 文件就会被nest首先注册到 process.env对象里,然后又组册到自己的ConfigService 模块里,这样上面的两种获取方式才得以成功。
  • 另外,项目里有通过 configService.get('mysql') 的情况,这个mysql 变量不存在.env里。
    • 首先可以看 src/config/mysql.config.ts 文件,里面通过 registerAs 注册了 mysql 变量
    • 然后 ConfigModule.forRoot 方法里通过 load 属性指定了要加载的变量。这样便可以访问configService.get('mysql') 了。

nest中请求的生命周期是怎样的?

这里其实可以分为正常的生命周期 和 出现异常报错时的生命周期,尤其是第二种官网也没说的特别清晰。

  • 情况一:正常时的生命周期
    1. 收到请求
    2. 全局绑定的中间件
    3. 模块绑定的中间件
    4. 全局守卫
    5. 控制层守卫
    6. 路由守卫
    7. 全局拦截器(控制器之前)
    8. 控制器层拦截器 (控制器之前)
    9. 路由拦截器 (控制器之前)
    10. 全局管道
    11. 控制器管道
    12. 路由管道
    13. 路由参数管道
    14. 控制器(方法处理器) 15。服务(如果有)
    15. 路由拦截器(请求之后)
    16. 控制器拦截器 (请求之后)
    17. 全局拦截器 (请求之后)
    18. 服务器响应
  • 情况二:异常时候的生命周期
    • 首先请求依旧按照 请求一 中的流程走,但假如某个环节报错了,则会立马跳出上面的流程。那具体跳到后面的哪个环节呢?或者说被哪个环节捕获呢?
    • 有两个地方可以捕获到异常,一个是 after拦截器 里的 catchError,一个是Filter。下面重点看这两者的差异:
    • Filter 会拦截来自各个阶段的异常,比如中间件、守卫、管道、拦截器等,如果"任意"阶段异常发生则请求不会继续执行后面的环节,而是直接跳到 Filter 中,但有意外情况。
    • 当 Pipe、Controller、Service 中抛出异常后,请求会先流转到 after Interceptor中的catchError中,如果在after Interceptor中处理了这个异常,则会直接返回 response,如果没处理则会走到 Filter。
    • Filter 只拦截没有被捕获的异常,如果用try catch 捕获了,也不会流转到Filter 中,当然也不会进入到catchError中。所以,可以直接理解成手动 try catch 的异常就不再是异常了,会直接按照你catch中的逻辑走,除非catch中又手动抛出一个异常。

Module 配置中的各项时什么作用?

以app.module.ts文件中的Module配置为例说下作用,查了很多文章都说的不够清楚或全面。

@Module({
	providers: [AppService],
	controllers: [AppController],
	exports: [AppService],
	imports: [UserModule],
})
  • providers: []
    • 首先写在这里服务要被@Injection修饰
    • 这里面放的服务有俩作用,一让这个服务有能力使用其他服务,二是让这个服务有能力被其他服务使用
      • 比如,如果AppService不写在providers里,那controller里就没法使用
      • 如果AppService不写在providers里,那exports里也没法把这个服务导出去,让其他模块里的服务或controller使用。
  • controllers: []
    • @Controller修饰的类才能放到这里
    • 放到这里,或放到providers里后,类才能调用其他服务
  • exports: []
    • 为了让其他模块也使用当前模块下的Service,需要把你想共享的Service放到这里。如果某服务只是想让自己模块使用,那只放到 providers 中即可,无需 exports。
  • imports: []
    • 导入其他模块,如UserModule,导入后,自己的controller 和 providers里的类就可以使用UserModule中配置在exports: [] 中的所有服务类。

@Gloabl 有的模块配置文件里有这个标识,说明这个模块是全局的,意味着别的模块想使用该模块的服务时就不用再手动配置 imports 了。


网站公告

今日签到

点亮在社区的每一天
去签到