Ready
安装(使用脚手架): $ npm i -g @nestjs/cli
新建一个项目 $ nest new project-name
或者,使用 Git 安装 TypeScript 起始项目$ git clone https://github.com/nestjs/typescript-starter.git project-name
$ cd project-name
$ npm install
$ npm run start
如果不使用脚手架,使用 npm/yarn/pnpn 手动从头开始创建一个新项目,安装核心和支持包。$ npm i --save-dev @nestjs/core @nestjs/common rxjs reflect-metadata
-
@nestjs/core
: Nest.js框架的核心包,提供了构建Nest.js应用程序所需的基本功能,包括应用程序实例化、模块加载、路由处理、中间件管理。Controllers、Modules、Dependency Injection、Middleware、Exception Filters、Guards、Interceptors、Pipes、Websocks。 - **
@nestjs/common
: **Nest.js框架的通用模块包,包含了一些常用的装饰器、异常类和工具函数,用于简化开发过程。如, **@Controller
装饰器用于定义控制器类,@Injectable
装饰器用于定义可注入的服务类,HttpException
**异常类用于抛出HTTP异常,等等。 -
@rxjs
:响应式编程库,提供了丰富的操作符和工具函数,用于处理异步数据流。在 Nest.js中,rxjs
通常用于处理异步操作,如触发HTTP请求、处理数据库查询等。它提供了方便的方式来处理异步数据流和事件,并且与Nest.js的异步编程模型紧密集成。 -
reflect-metadata
: 用于获取和定义元数据的库。在Nest.js中,reflect-metadata
用于支持基于装饰器的元编程。它允许开发人员在类、方法、参数等级别上添加元数据,以便在运行时进行反射和元数据的访问。这在实现依赖注入、路由处理和其他装饰器相关的功能时非常有用。
CRUD
快捷命令: $ nest g resource [name]
- 生成一个module:
$ nest g mo [name]
- 生成一个controller
$ nest g co [name]
- 生成一个service
$ nest g s [name]
基础知识
Nest CLI
数据传输
url param
query
form-urlencoded
form-data
json
IOC(Inverse of Control)
实现思路: 在class上声明依赖后,让工具去分析声明的依赖关系,根据先后顺序自动把对象创建好后组装起来。
它有一个放对象的容器,程序初始化的时候,会扫描class上声明的依赖关系,然后把这些class都给new一个实例放到容器里。创建对象的时候,还会把它们依赖的对象也注入进去。这种依赖注入的方式叫做 Dependency Injection 。
常用装饰器
- @Module: 声明 Nest 模块
- @Controller:声明模块里的 controller
- @Injectable:声明模块里可以注入的 provider
- @Inject:通过 token 手动指定注入的 provider,token 可以是 class 或者 string
- @Optional:声明注入的 provider 是可选的,可以为空
- @Global:声明全局模块
- @Catch:声明 exception filter 处理的 exception 类型
- @UseFilters:路由级别使用 exception filter
- @UsePipes:路由级别使用 pipe
- @UseInterceptors:路由级别使用 interceptor
- @SetMetadata:在 class 或者 handler 上添加 metadata
- @Get、@Post、@Put、@Delete、@Patch、@Options、@Head:声明 get、post、put、delete、patch、options、head 的请求方式
- @Param:取出 url 中的参数,比如 /aaa/:id 中的 id
- @Query: 取出 query 部分的参数,比如 /aaa?name=xx 中的 name
- @Body:取出请求 body,通过 dto class 来接收
- @Headers:取出某个或全部请求头
- @Session:取出 session 对象,需要启用 express-session 中间件
- @HostParm: 取出 host 里的参数
- @Req、@Request:注入 request 对象
- @Res、@Response:注入 response 对象,一旦注入了这个 Nest 就不会把返回值作为响应了,除非指定 passthrough 为true
- @Next:注入调用下一个 handler 的 next 方法
- @HttpCode: 修改响应的状态码
- @Header:修改响应头
- @Redirect:指定重定向的 url
- @Render:指定渲染用的模版引擎
Nest原理
通过装饰器给类或对象添加元数据metadata,并且开启ts的**emitDecoratorMetadata **来自动添加类型相关的metadata,初始化的时候取出这些元数据进行依赖分析,然后创建对应的实例对象即可。所以核心就是 Reflector metadata的api。
Nest的装饰器都是依赖reflect-metadata来实现的,而且还提供了一个 @SetMetadata 的装饰器用于给class、method添加一些元数据metatdata。
Dynamic Module
Module 可以传入 options 动态产生,这叫做动态 Module,你还可以把传入的 options 作为 provider 注入到别的对象里。
建议的动态产生 Module 的方法名有 register、forRoot、forFeature 3种。
- register:用一次注册一次
- forRoot:只注册一次,用多次,一般在 AppModule 引入
- forFeature:用了 forRoot 之后,用 forFeature 传入局部配置,一般在具体模块里 imports
并且这些方法都可以写 xxxAsync 版本,也就是传入 useFactory 等 option,内部注册异步 provider。
这个过程也可以用 ConfigurableModuleBuilder 来生成。通过 setClassMethodName 设置方法名,通过 setExtras 设置额外的 options 处理逻辑。
并且返回的 class 都有 xxxAsync 的版本。
这就是动态模块的定义方式,后面用到 typeorm、mongoose 等模块会大量见到这种模块。
Express
Express是处理请求、响应的库。
Nest核心概念
- IOC
- AOP
- 全局模块
- 动态模块
- 自定义provider
- 生命周期
Docker
FROM node:latest
WORKDIR /app
COPY . .
RUN npm config set registry https://registry.npmmirror.com/
RUN npm install -g http-server
EXPOSE 8080
CMD ["http-server", "-p", "8080"]
解释:
- FROM: 基于一个基础镜像来修改
- WORKDIR: 指定当前工作目录
- COPY: 把容器外的内容复制到容器内
- EXPOSE: 声明当前容器要访问的网络端口号,比如这里起服务会用到8080
- RUN: 在容器内执行命令
- CMD:容器启动的时候执行的命令
我们先通过 FROM 继承了 node 基础镜像,里面就有 npm、node 这些命令了。
通过 WORKDIR 指定当前目录。
然后通过 COPY 把 Dockerfile 同级目录下的内容复制到容器内,这里的 . 也就是 /app 目录
之后通过 RUN 执行 npm install,全局安装 http-server
通过 EXPOSE 指定要暴露的端口
CMD 指定容器跑起来之后执行的命令,这里就是执行 http-server 把服务跑起来。
生成镜像docker build -t aaa:ccc .
其中,aaa 是镜像名,ccc是镜像的标签。
FROM node:18 # 继承node:18基础镜像
WORKDIR /app # 指定当前目录为 /app
COPY package.json . # 复制宿主机的package.json和lock文件到容器的当前目录,也就是 /app下
COPY *.lock .
RUN npm config set registry https://registry.npmmirror.com/
RUN npm install # 执行 npm install
COPY . . #复制其余的文件到当前容器内
RUN npm run build
EXPOSE 3000 # 指定容器需要暴露的端口是3000
CMD [ "node", "./dist/main.js" ] # 指定容器跑起来时执行的命令是 node ./dist/main.js
两种登录状态保存方式: session+cookie、jwt
- 服务端session+cookie
session + cookie 的给 http 添加状态的方案是服务端保存 session 数据,然后把 id 放入 cookie 返回,cookie 是自动携带的,每个请求可以通过 cookie 里的 id 查找到对应的 session,从而实现请求的标识。这种方案能实现需求,但是有 CSRF、分布式 session、跨域等问题,不过都是有解决方案的。
- 客户端存储的token
token 的方案常用 json 格式来保存,叫做 json web token,简称 JWT。
JWT 是保存在 request header 里的一段字符串(比如用 header 名可以叫 authorization),它分为三部分: header、payload、verify signature
header 部分保存当前的加密算法,payload 部分是具体存储的数据,verify signature 部分是把 header 和 payload 还有 salt 做一次加密之后生成的。(salt,盐,就是一段任意的字符串,增加随机性)
这三部分会分别做 Base64,然后连在一起就是 JWT 的 header,放到某个 header 比如 authorization 中:
makefile
复制代码authorization: Bearer xxxxx.xxxxx.xxxx
请求的时候把这个 header 带上,服务端就可以解析出对应的 header、payload、verify signature 这三部分,然后根据 header 里的算法也对 header、payload 加上 salt 做一次加密,如果得出的结果和 verify signature 一样,就接受这个 token。
把状态数据都保存在 payload 部分,这样就实现了有状态的 http:
JWT 的方案是把状态数据保存在 header 里,每次请求需要手动携带,没有 session + cookie 方案的 CSRF、分布式、跨域的问题,但是也有安全性、性能、没法控制等问题。
实战 session-cookie
npm install express-session
npm install @types/express-session -D
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import * as session from 'express-session';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.use(session({
secret: 'guang',
resave: false,
saveUninitialized: false
}));
await app.listen(3000);
}
bootstrap();
- secret: 加密的密钥
- resave: ‘true’ 每次访问都会更新 session,不管有没有修改 session 的内容,而 false 是只有 session 内容变了才会去更新 session。
- saveUninitalized: 设置为 ‘true’ 是不管是否设置 session,都会初始化一个空的 session 对象。比如你没有登录的时候,也会初始化一个 session 对象,这个设置为 false 就好。
import { Controller, Get, Session } from '@nestjs/common';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
getHello(): string {
return this.appService.getHello();
}
@Get('sss')
session(@Session() session) {
console.log(session);
// Session {
// cookie: { path: '/', _expires: null, originalMaxAge: null, httpOnly: true },
// count: 3
// }
session.count = session.count ? session.count + 1 : 1;
return session.count;
}
}
jwt
npm install @nestjs/jwt
引入
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { AppController } from './app.controller';
import { AppService } from './app.service';
@Module({
imports: [
// JwtModule.register({
// secret: 'wangyibo',
// signOptions: {
// expiresIn: '7d',
// },
// }),
JwtModule.registerAsync({
async useFactory() {
await 1;
return {
secret: 'wangyibo',
signOptions: { expiresIn: '7d' },
};
},
}),
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
在 controller 里注入 JwtModule 里的 JwtService:
import { Controller, Get, Inject, Res } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { Response } from 'express';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Inject(JwtService) private jwtService: JwtService;
}
然后在加一个 handler:
@Get('ttt')
ttt(@Res({ passthrough: true }) response: Response) {
const newToken = this.jwtService.sign({ count: 1 });
response.setHeader('token', newToken);
return 'hello';
}
注意: 因为注入 response 对象之后,默认不会把返回值作为 body 了,需要设置 passthrough 为 true 才可以。
import {
Controller,
Get,
Inject,
Res,
Headers,
UnauthorizedException,
} from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { Response } from 'express';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Inject(JwtService) private jwtService: JwtService;
@Get()
getHello(): string {
return this.appService.getHello();
}
@Get('ttt')
ttt(
@Headers('authorization') authorization: string,
@Res({ passthrough: true }) response: Response,
) {
if (authorization) {
try {
const token = authorization.split(' ')[1];
const data = this.jwtService.verify(token);
const newToken = this.jwtService.sign({ count: data.count + 1 });
response.setHeader('token', newToken);
return data.count + 1;
} catch (error) {
throw new UnauthorizedException();
}
} else {
const newToken = this.jwtService.sign({ count: 1 });
response.setHeader('token', newToken);
return 1;
}
}
}
通过 @Headers 装饰器取出 autorization 的 header,然后通过 jwtService.verify 对它做验证。
如果验证失败,那就抛出 UnauthorizedException 异常,让 Nest 内置的 Exception Filter 来处理。
验证成功就重新生成 jwt 放到 header 里返回。
如果没有 autorization 的 header,那就生成一个 jwt 放到 header 里返回。