注册
web

从0搭建nestjs项目并部署到本地docker

开发目标:快速搭建nestjs项目本地环境,并测试本地打包方便后期部署到服务器。


项目准备:node环境、npm依赖、docker



  1. 创建项目并启动
  2. 使用typeorm连接mysql
  3. 使用class-validate校验入参
  4. 使用全局filter处理异常,使用全局interceptor处理成功信息
  5. 使用ioredis连接redis
  6. 使用swaager文档
  7. 使用docker-compose打包并运行
  8. 总结

一、创建项目并启动


1、全局安装nestjs并创建项目

npm i -g @nestjs/cli
nest new nest-demo

2、使用热更新模式运行项目

npm run start:dev

此时访问 http://localhost:3000就可以看到 Hello World!


3、使用cli一键生成一个user模块

nest g resource system/user

选择REST API和自动生成CURD


4、设置全局api前缀

src/main.ts


async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.setGlobalPrefix('api'); // 设置全局api前缀
await app.listen(3000);
}
bootstrap();

更多nestjs入门教程查看:# 跟随官网学nestjs之入门


二、使用typeorm连接并操作mysq


1、安装依赖

npm i @nestjs/typeorm typeorm mysql @nestjs/config -S

2、在src下创建 config/env.ts 用来判断当前环境,抛出配置文件地址

src/config/env.ts


import * as fs from 'fs';
import * as path from 'path';
const isProd = process.env.NODE_ENV == 'prod';

function parseEnv() {
const localEnv = path.resolve('.env');
const prodEnv = path.resolve('.env.prod');

if (!fs.existsSync(localEnv) && !fs.existsSync(prodEnv)) {
throw new Error('缺少环境配置文件');
}

const filePath = isProd && fs.existsSync(prodEnv) ? prodEnv : localEnv;
return { path: filePath };
}
export default parseEnv();

3、在src下创建.env配置文件

src/.env


# default
PORT=9000

# database
DB_HOST=localhost
DB_PORT=3306
DB_USER=demo_user
DB_PASSWD=123456
DB_DATABASE=demo_db


4、在app.module内挂载全局配置和mysql

src/app.module.ts


import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigService, ConfigModule } from '@nestjs/config';
import envConfig from './config/env';
import { AppService } from './app.service';
import { UserModule } from './system/user/user.module';

@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true, // 设置为全局
envFilePath: [envConfig.path],
}),
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: async (configService: ConfigService) => ({
type: 'mysql',
host: configService.get('DB_HOST', 'localhost'), // 主机,默认为localhost
port: configService.get<number>('DB_PORT', 3306), // 端口号
username: configService.get('DB_USER', 'root'), // 用户名
password: configService.get('DB_PASSWORD', '123456'), // 密码
database: configService.get('DB_DATABASE', 'test_db'), //数据库名
entities: ['dist/**/*.entity{.ts,.js}'],
timezone: '+08:00', //服务器上配置的时区
synchronize: true, //根据实体自动创建数据库表, 生产环境建议关闭
autoLoadEntities: true,
}),
}),
UserModule,
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}

5、定义userEntity实体

src/system/user/entities/user.entity.ts


import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';

@Entity('user_tb')
export class UserEntity {
@PrimaryGeneratedColumn()
s_id: string;

@Column({ type: 'varchar', length: 20, default: '', comment: '名称' })
s_name: string;

@Column({ type: 'int', default: 0, comment: '年龄' })
s_age: number;
}

6、user.module内引入entity实体

import { Module } from '@nestjs/common';
import { UserService } from './user.service';
import { UserController } from './user.controller';
// 引入typeorm和Enetiy实例
import { TypeOrmModule } from '@nestjs/typeorm';
import { UserEntity } from './entities/user.entity';

@Module({
imports: [TypeOrmModule.forFeature([UserEntity])],
controllers: [UserController],
providers: [UserService],
}
)

export class UserModule {}

7、在控制器user.controller修改api地址

@Post('create')
create(@Body() createUserDto: CreateUserDto) {
return this.userService.create(createUserDto);
}

地址拼接为:全局前缀api+模块user+自定义create = localhost:3000/api/user/crtate


image.png


image.png


三、使用class-validato校验入参


1、安装依赖

npm i class-validator class-transformer -S

2、配置校验规则

src/system/user/dto/create-user.dto.ts


import { IsNotEmpty } from 'class-validator';

export class CreateUserDto {
@IsNotEmpty({ message: '名称不能为空' })
readonly s_name: string;
}

image.png


更多校验规则查看:git文档


四、使用filter全局错误过滤、interceptor全局成功过滤


1、使用cli自动生成过滤器


nest g filter common/http-exception
nest g interceptor common/transform

2、编写过滤器


src/common/http-exception/http-exception.filter.ts


import {
ArgumentsHost,
Catch,
ExceptionFilter,
HttpException,
} from '@nestjs/common';

@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp(); // 获取请求上下文
const response = ctx.getResponse(); // 获取请求上下文中的 response对象
const status = exception.getStatus(); // 获取异常状态码

let resultMessage = exception.message;

// 拦截class-validate错误信息
try {
const exceptionResponse = exception.getResponse() as any;
if (Object.hasOwnProperty.call(exceptionResponse, 'message')) {
resultMessage = exceptionResponse.message;
}
} catch (e) {}

const errorResponse = {
data: null,
message: resultMessage,
code: '9999',
};

// 设置返回的状态码, 请求头,发送错误信息
response.status(status);
response.header('Content-Type', 'application/json; charset=utf-8');
response.send(errorResponse);
}
}

src/common/transform/transform.interceptor.ts


import {
CallHandler,
ExecutionContext,
Injectable,
NestInterceptor,
} from '@nestjs/common';
import { map, Observable } from 'rxjs';

@Injectable()
export class TransformInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
map((data) => {
return {
data,
code: '0000',
msg: '请求成功',
};
}),
);
}
}

3、在main.ts里挂载


import { HttpExceptionFilter } from './common/http-exception/http-exception.filter';
import { TransformInterceptor } from './common/transform/transform.interceptor';

async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalFilters(new HttpExceptionFilter()); // 全局注册错误的过滤器(错误异常)
app.useGlobalInterceptors(new TransformInterceptor()); // 全局注册成功过滤器
await app.listen(3000);
}
bootstrap();

手动抛出异常错误只需在service的方法里


throw new HttpException('message', HttpStatus.BAD_REQUEST)


五、使用idredis连接redis


1、安装依赖

npm i ioredis -S

2、在.env文件添加reids配置

# redis
REDIS_HOST=localhost
REIDS_PORT=6379
REIDS_PASSWD=
REIDS_DB=3

3、在common目录下创建cache模块,连接redis

nest g mo cache common && nest g s cache common

src/common/cache/cache.service.ts


import { Injectable, Logger } from '@nestjs/common';
import { Redis } from 'ioredis';
import { ConfigService } from '@nestjs/config';

@Injectable()
export class CacheService {
public client;
constructor(private readonly configService: ConfigService) {
this.getClient();
}

async getClient() {
const client = new Redis({
host: this.configService.get('REDIS_HOST', 'localhost'), // 主机,默认为localhost
port: this.configService.get<number>('REIDS_PORT', 6379), // 端口号
password: this.configService.get('REIDS_PASSWD', ''), // 密码
db: this.configService.get<number>('REIDS_DB', 3),
});
// 连接成功提示
client.on('connect', () =>
Logger.log(
`redis连接成功,端口${this.configService.get<number>(
'REIDS_PORT',
3306,
)}
`
,
),
);
client.on('error', (err) => Logger.error('Redis Error', err));

this.client = client;
}

public async set(key: string, val: string, second?: number) {
const res = await this.client.set(key, val, 'EX', second);
return res === 'OK';
}

public async get(key: string) {
const res = await this.client.get(key);
return res;
}
}

在cache.module内抛出service
src/common/cache/cache.module.ts


@Module({
providers: [CacheService],
exports: [CacheService],
})

4、在user.module内引入cacheModule并在user.service内使用

src/system/user/user.module.ts


import { CacheModule } from 'src/common/cache/cache.module';
@Module({
imports: [CacheModule],
controllers: [UserController],
providers: [UserService],
})

export class UserModule {}

src/system/user/user.service.ts


import { CacheService } from '@src/common/cache/cache.service';

@Injectable()
export class UserService {
constructor(
private readonly cacheService: CacheService,
) {}

async create(createUserDto: CreateUserDto) {
const redisTest = await this.cacheService.get('redisTest');

Logger.log(redisTest, 'redisTest');
if (!redisTest) {
await this.setRedis();
return this.create(createUserDto);
}

...
}
async setRedis() {
const res = await this.cacheService.set(
'redisTest',
'test_val',
12 * 60 * 60,
);
if (!res) {
Logger.log('redis保存失败');
} else {
Logger.log('redis保存成功');
}
}
}

image.png


image.png


六、使用swagger生成文档


1、安装依赖

npm i @nestjs/swagger swagger-ui-express -S

2、在main.ts引入并配置

import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';

async function bootstrap() {
const app = await NestFactory.create(AppModule);
// 设置swaager
const options = new DocumentBuilder()
.setTitle('nest-demo example')
.setDescription('The nest demo API description')
.setVersion('1.0')
.build();
const document = SwaggerModule.createDocument(app, options);
SwaggerModule.setup('swagger', app, document);

...
}
bootstrap();

此时访问http://wwww.localhost:9000/swagge就可以看到文档


image.png


3、在控制器为业务模块和api打上标签

src/system/user/user.controller.ts


import { ApiTags, ApiOperation } from '@nestjs/swagger';

@ApiTags('user')
@Controller('user')
export class UserController {
constructor(private readonly userService: UserService) {}

@ApiOperation({
summary: '创建用户',
})
@Post('create')
create(@Body() createUserDto: CreateUserDto) {
return this.userService.create(createUserDto);
}
}

4、在dto内为字段设置名称

src/system/user/dto/create-user.dto.ts


import { ApiProperty } from '@nestjs/swagger';

export class CreateUserDto {
@ApiProperty({ type: 'string', example: '用户名称' })
@IsNotEmpty({ message: '名称不能为空' })
readonly s_name: string;

@ApiProperty({ type: 'number', example: '用户年龄' })
readonly s_age: number;
}

这时刷新浏览器,就能看到文档更新了


image.png


更多swaager配置查看:官方文档


七、使用docker-compose自动部署到本地docker


1、在根目录下创建docker-compose.yml

version: "3.0"

services:
# docker容器启动的redis默认是没有redis.conf的配置文件,所以用docker启动redis之前,需要先去官网下载redis.conf的配置文件
redis_demo: # 服务名称
container_name: redis_demo # 容器名称
image: daocloud.io/library/redis:6.0.3-alpine3.11 # 使用官方镜像
# 配置redis.conf方式启动
# command: redis-server /usr/local/etc/redis/redis.conf --requirepass 123456 --appendonly yes # 设置redis登录密码 123456、--appendonly yes:这个命令是用于开启redis数据持久化
# 无需配置文件方式启动
command: redis-server --appendonly yes # 开启redis数据持久化
ports:
- 6379:6379 # 本机端口:容器端口
restart: on-failure # 自动重启
volumes:
- ./deploy/redis/db:/data # 把持久化数据挂载到宿主机
- ./deploy/redis/conf/redis.conf:/usr/local/etc/redis/redis.conf # 把redis的配置文件挂载到宿主机
- ./deploy/redis/logs:/logs # 用来存放日志
environment:
- TZ=Asia/Shanghai # 解决容器 时区的问题
networks:
- my-server_demo

mysql_demo:
container_name: mysql_demo
image: daocloud.io/library/mysql:8.0.20 # 使用官方镜像
ports:
- 3306:3306 # 本机端口:容器端口
restart: on-failure
environment:
MYSQL_DATABASE: demo_db
MYSQL_ROOT_PASSWORD: 123456
MYSQL_USER: demo_user
MYSQL_PASSWORD: 123456
MYSQL_ROOT_HOST: '%'
volumes:
- ./deploy/mysql/db:/var/lib/mysql # 用来存放了数据库表文件
- ./deploy/mysql/conf/my.cnf:/etc/my.cnf # 存放自定义的配置文件
# 我们在启动MySQL容器时自动创建我们需要的数据库和表
# mysql官方镜像中提供了容器启动时自动docker-entrypoint-initdb.d下的脚本的功能
- ./deploy/mysql/init:/docker-entrypoint-initdb.d/ # 存放初始化的脚本
networks:
- my-server_demo

server_demo: # nestjs服务
container_name: server_demo
build: # 根据Dockerfile构建镜像
context: .
dockerfile: Dockerfile
ports:
- 9003:9003
restart: on-failure # 设置自动重启,这一步必须设置,主要是存在mysql还没有启动完成就启动了node服务
networks:
- my-server_demo
depends_on: # node服务依赖于mysql和redis
- redis_demo
- mysql_demo

# 声明一下网桥 my-server。
# 重要:将所有服务都挂载在同一网桥即可通过容器名来互相通信了
# 如nestjs连接mysql和redis,可以通过容器名来互相通信
networks:
my-server_demo:

2、在根目录创建Dockerfile文件

FROM daocloud.io/library/node:14.7.0

# 设置时区
ENV TZ=Asia/Shanghai \
DEBIAN_FRONTEND=noninteractive
RUN ln -fs /usr/share/zoneinfo/${TZ} /etc/localtime && echo ${TZ} > /etc/timezone && dpkg-reconfigure --frontend noninteractive tzdata && rm -rf /var/lib/apt/lists/*

# 创建工作目录
RUN mkdir -p /app

# 指定工作目录
WORKDIR /app

# 复制当前代码到/app工作目录
COPY . ./

# npm 源,选用国内镜像源以提高下载速度
RUN npm config set registry https://registry.npm.taobao.org/

# npm 安装依赖
COPY package.json /app/package.json
RUN rm -rf /app/package-lock.json
RUN cd /app && rm -rf /app/node_modules && npm install

# 打包
RUN cd /app && rm -rf /app/dist && npm run build

# 启动服务
# "start:prod": "cross-env NODE_ENV=production node ./dist/src/main.js",
CMD npm run start:prod

EXPOSE 9003

3、修改.env.prod正式环境配置

# default
PORT=9003
HOST=localhost

# database
DB_HOST=mysql_demo #使用容器名称连接
DB_PORT=3306
DB_USER=demo_user
DB_PASSWD=123456
DB_DATABASE=demo_db

# redis
REDIS_HOST=redis_demo #使用容器名称连接
REIDS_PORT=6379
REIDS_PASSWD=
REIDS_DB=3

4、修改main.ts启动端口

import { ConfigService } from '@nestjs/config';

async function bootstrap() {
const app = await NestFactory.create(AppModule);
const configService = app.get(ConfigService); // 获取全局配置
const PORT = configService.get<number>('PORT', 9000);
const HOST = configService.get('HOST', 'localhost');
await app.listen(PORT, () => {
Logger.log(`服务已经启动,接口请访问:http://wwww.${HOST}:${PORT}`);
});
}
bootstrap();

5、前台运行打包

docker-compose up


运行完成后大概率会报错,因为我们使用的mysql账号没有权限,所以需要进行设置


image.png


// 进入mysql容器命令
docker ecex -it mysql_demo /bin/bash
// 登录mysql
mysql -uroot -p123456
// 查询数据库后进入mysql查询数据表
show databases;
use mysql;
show tables;
// 查看user表中的数据
select User,Host from user;
// 刚创建的用户表没有我们设置连接的用户和host,所以需要创建
CREATE USER 'demo_user'@'%' IDENTIFIED BY '123456';
// 给创建的用户赋予权限
GRANT ALL ON *.* TO 'demo_user'@'%';
// 刷新权限
flush privileges;

如果还报错修改下密码即可
Pasted Graphic 1.png


ALTER USER 'demo_user'@'%' IDENTIFIED WITH mysql_native_password BY '123456';

此时项目应该能正常启动并成功访问


image.png


image.png


6、切换后台运行

// Ctrl+C 终止程序后执行后台运行命令
docker-compose up -d

八、总结


docker-compose up正常用来测试本地打包,和第一次构建redismysql容器,后续需要在本地运行开发模式只需保证redismysql容器正常运行即可,如需再次打包,删除server容器和镜像再次执行即可


docker ps -a // 查询docker容器
docker rm server_demo // 删除server容器
docker images // 查询镜像
docker rmi nest-demo_server_demo // 删除server镜像, server镜像名称:项目名称_容器名称
docker-compose up -d // 重新打包

本地开发模式只需关闭server容器,然后在项目内只需 start:dev即可


docker stop server_demo
npm run start:dev

作者:jjggddb
来源:juejin.cn/post/7215844385614528549

0 个评论

要回复文章请先登录注册