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

8,855 阅读5分钟

开发目标:快速搭建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-compose up -d --force-recreate --no-deps --build --remove-orphans server_demo

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

docker stop server_demo
npm run start:dev