创建基础项目

### 安装nestcli
yarn global add @nestjs/cli

### 创建项目
nest new ctnode
.
├── README.md
├── nest-cli.json
├── package.json
├── src
│   ├── app.controller.spec.ts
│   ├── app.controller.ts
│   ├── app.module.ts
│   ├── app.service.ts
│   └── main.ts
├── test
│   ├── app.e2e-spec.ts
│   └── jest-e2e.json
├── tsconfig.build.json
├── tsconfig.json
└── yarn.lock

运行:yarn start:dev

打开日志

Nest封装好了日志,所以我们不许要做额外多的事情,只需要打开需要的日志类型即可,修改main.ts如下:

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule, {
    logger: ['log'],
  });
  app.setGlobalPrefix('api/v1');
  await app.listen(3000);
}
bootstrap();

写下第一个接口

整个业务的入口文件为main.ts,我们查看下这个文件,

主要的代码有两行

const app = await NestFactory.create(AppModule);
await app.listen(3000);

在这个文件里,使用了Nest的工厂函数创建了一个AppModule,并且设置了启动端口为3000

打开127.0.0.1:3000,会在浏览器看到Hello World!的文字

看一下这串Hello World!在哪~

src/app.service.ts这个文件下可以看到

import { Injectable } from '@nestjs/common';

@Injectable()
export class AppService {
  getHello(): string {
    return 'Hello World!';
  }
}

这里可以看到一个getHello的函数,返回了一个Hello World的字符串

那这个函数在哪里被调用?

app.controller.ts

import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  getHello(): string {
    return this.appService.getHello();
  }
}

在这个文件里可以看到,这里引入了一个AppService类,并且实例化,之后通过@Get修饰AppController的getHello返回了出来

以此构建了一个get方法

修改路由

根据官方文档里路由部分,我们尝试在ControllerGet两个修饰器里加上串字符串,并且复制一个getHello函数出来,如下:

app.controller.ts

import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';

@Controller('/hello')
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get('ping')
  getHello(): string {
    return this.appService.getHello();
  }
  @Get('c2star')
  getC2Stsr(): string {
    return this.appService.getHello();
  }
}

之后可以发现访问之前的`http://127.0.0.1:3000会报错,而下边两个地址返回了之前的HelloWorld

http://127.0.0.1:3000/hello/ping
http://127.0.0.1:3000/hello/c2star

修饰器中使用路由前缀可以使我们轻松地对一组相关的路由进行分组,并最大程度地减少重复代码

这只是在App这个模块里增加了一个路由以及前缀,那如何在全局增加一个路由前缀呢?

只需要在 main.ts 中加上app.setGlobalPrefix('api/v1')

此时如果访问之前的hello/c2star就需要访问api/v1/hello/c2star

这就是Nest中的路由

Nest中的模块

可以看见,在src目录下,有5个文件,抛开main.tsspec文件,还有三个文件

分别是controllerServiceModule

也就是这三个基本文件组成了一个App模块

  • Controller:控制器负责处理传入的 请求 和向客户端返回 响应。也就是说,提供api接口,负责处理路由、中转、验证等一些简洁的业务;

  • Service:提供服务,主要负责处理具体的业务,如数据库的增删改查、事务、并发等逻辑代码;

  • Module:负责将 Controller 和 Service 连接起来,类似于 namespace 的概念;

Nest中如何接受参数

目前,常见的传参方式主要有四种ParamBodyHeadersQuery

  • Param参数所在的位置为URL中,例如/user/1
  • Body通常在post请求中使用
  • Headers 也就是请求时候携带的头部信息,通常会用来携带tokencsrf校验信息等
  • Query就是常见的URL后边?后边的一串参数,比如xxx?user=1&name=殷迦南

在Nest中,这些封装为修饰器放在了@nestjs/common包中,使用的时候直接引入即可

  @Get(':username')
  find(@Param() params) {
    console.log(params);
    return this.usersService.find(params.username);
  }

具体如何使用,会在之后的例子中做具体呈现,关于其余的请求参数可以查看官方文档

创建User模块

通过cli创建user的三个基础文件

nest g service user app
nest g controller user app
nest g module user app

之后,src目录下的曾经结构如下:

.
├── app
│   └── user
│       ├── user.controller.spec.ts
│       ├── user.controller.ts
│       ├── user.module.ts
│       ├── user.service.spec.ts
│       └── user.service.ts
├── app.controller.spec.ts
├── app.controller.ts
├── app.module.ts
├── app.service.ts
└── main.ts

Service

首先我们修改Service,仿照app.service修改一个提供查询功能的service。
如下:
src/app/user/user.service.ts

import { Injectable } from '@nestjs/common';

@Injectable()
export class UserService {
  find(username: string): string {
    if (username === 'k0a1a') {
      return 'k0a1a存在';
    }
    return '未知的用户';
  }
}

Controller

引入Service,创建一个提供查询功能的Api

import { Controller, Get, Param } from '@nestjs/common';
import { UserService } from './user.service';

@Controller('user')
export class UserController {
  constructor(private readonly usersService: UserService) {}
  //   查询单个用户
  @Get(':username')
  find(@Param() params) {
    console.log(params);
    return this.usersService.find(params.username);
  }
}

此时访问http://127.0.0.1:3000/api/v1/user/k0a1a会看见k0a1a存在的文字,如果访问http://127.0.0.1:3000/api/v1/user/xxxx,会返回未知的用户。

这里就是使用了Param的方式进行请求参数的接收,引入了Param修饰器,并修饰了一个params变量,此时,params的内容就为URL中的参数

如果需要携带多个params参数,那么只需要修改路由即可,比如:username/:name,那么请求的URL就必须为/k0a1a/殷迦南

此时,如果在控制器中打印出params,结果如下:
{ username: 'k0a1a', name: '殷迦南' }

至此,一个具有查询逻辑的API就写好了。

重写module

其实截止目前,我们都已经可以正常通过Controller访问到Service里的内容了,那module到底有什么用呢?

我们打开app.module.ts,可以看到这两行代码

  controllers: [AppController, UserController],
  providers: [AppService, UserService],

也就是说,如果我们新增一个模块,就必须在controllersproviders中分别引入

之前已经说过了是类似一个命名空间的东西,所以我们现在通过module来把controllersService链接起来

/app/user/user.module.ts

import { Module } from '@nestjs/common';
import { UserService } from './user.service';
import { UserController } from './user.controller';

@Module({
  controllers: [UserController],
  providers: [UserService],
  exports: [UserService],
})
export class UserModule {}

app.module.ts

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { UserModule } from './app/user/user.module';

@Module({
  imports: [UserModule],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

这样,我们就不需要在app.module.ts中去分别引入controllersService了。

module的作用不止如此,更多的作用可以查看文档,或在之后的demo中进行了解

增加配置化

在实际开发中,会根据环境变量来加载不同配置项,也不会将一些配置信息直接写在代码中,因此,我们需要增加一个配置文件,这里我们之间使用原生的dotenv,(官方也提供了@nestjs/config这个包,也是基于dotenv的封装)

安装

先安装dotenv


yarn add dotenv

引入

之后再app.module.ts进行引入,并使用即可

import * as dotenv from 'dotenv';
dotenv.config({ path: process.cwd() + '/.env' });

在项目根目录创建.env文件

写上如下内容:

# 这是一行注释
APP_NAME=云启星辰的Node项目

之后在user.service.ts的find函数中添加一条console.log

    console.log(process.env.APP_NAME);

再请求接口,发现控制台可以正常打印出:云启星辰的Node项目

使用ORM

在我们的需求里,访问DB必不可少,nest官方文档推荐的第一个orm包就是Typeorm,但是在Google搜索node orm ,可以发现,前几条搜索都是关于Sequelize的。

我对Typeorm有做过简单的尝试性使用,在实际开发中有使用过Sequelize

做了一些简单的对比:

Typeorm给我的感觉很清爽,不管是接口定义,还是代码实现特别易懂,可读性很高。如果你了解过其它 orm 框架,学习成本很低。

但是毕竟是一个较新的工具,功能的完整度还不够,一些高级功能还缺少,经常用的功能还算可以。

没有历史包袱,又借鉴了前人的经验,走得很快。技术栈很新,一开始就使用 ts 开发,很多地方使用了装饰器。

Typeorm在数据库重链接方面有些问题待解决,并且对Mongo等MySQL之外的数据库支持不是很友好

最蛋疼的一点就是,github里,积攒了1.5K的待解决issues,并且很多都未回复甚至连Tag标记都没有

还有就是官网都是基于例子或者教程的形式教你如何用这些特性,并没有一个完整意义上的文档

而相对Typeorm来说,Sequelize更加的稳定和完善,所以在这里,将集成Sequelize到项目中去。

Nest官网也有相对应的文档

安装Sequelize

$ yarn add sequelize sequelize-typescript mysql2
$ yarn add -d @types/sequelize

引入并配置Sequelize

首先我们需要更新我们的env文件,并在env文件中有正确的mysql的连接方式

应用配置
================================================
APP_NAME=云启星辰的Node项目
APP_ENV=production
APP_URL=https://www.cloudtrek.cn
APP_VERSION=1.0.0

数据库配置
================================================
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=ctnode
DB_USERNAME=root
DB_PASSWORD=xxxxxxx

在项目根目录创建config/db.ts文件

(我们之后肯定是会用到REDIS的,所以我们在这里提前写上REDIS的相关配置,具体使用的时候,再挪到.env中)

const MYSQL_CONF = {
  port: Number(process.env.DB_PORT),
  host: process.env.DB_HOST,
  username: process.env.DB_USERNAME,
  password: process.env.DB_PASSWORD,
  database: process.env.DB_DATABASE,
  connectionLimit: 10,
  logging: false,
};

const REDIS_CONF = {
  port: 6379,
  host: '127.0.0.1',
};

export default { MYSQL_CONF, REDIS_CONF };

在src目录中创建database/db.tsdatabase/sync.tsmodel/index.ts三个文件

  • database/db.ts用来导出Sequelize的模型类

  • database/sync.ts用来编写升级db的脚本

  • model/index.ts 用来批量导出所有的数据模型

model/index.ts

const Models = [];
export { Models };

database/db.ts

import { SequelizeModule } from '@nestjs/sequelize';
import { Models } from '../model/index';
import CONFIG from '../../config/db';
export default (() => {
  const Sequelize = SequelizeModule.forRoot({
    dialect: 'mysql',
    ...CONFIG.MYSQL_CONF,
    models: Models,
    synchronize: true,
  });
  return Sequelize;
})();

database/sync.ts

import * as dotenv from 'dotenv';
dotenv.config({ path: process.cwd() + '/.env' });
import { Sequelize } from 'sequelize-typescript';
import { Models } from '../model/index';
import CONFIG from '../../config/db';
const DB = CONFIG.MYSQL_CONF;
const seq = new Sequelize({
  database: DB.database,
  username: DB.username,
  password: DB.password,
  port: DB.port,
  dialect: 'mysql',
});

seq
  .authenticate()
  .then(() => {
    console.log('auth ok');
  })
  .catch(() => {
    console.log('auth err');
  });

seq.addModels(Models);

// 执行同步
seq.sync({ force: false }).then(() => {
  console.log('sync ok');
  process.exit();
});

database/sync.ts文件是一个独立的同步脚本,所以dotenv需要单独引入

之后修改package.json文件,把同步脚本的执行命令加入到scripts

"db:sync": "ts-node src/database/sync.ts"

创建数据模型

Sequelize引入之后,我们就可以尝试编写数据模型、自动创建表结构并使用了。

在src中新建目录model,并创建user.model.tsindex.ts两个文件

*.model.ts用来编写数据模型,`index.ts用来批量导出所有的数据模型

user.model.ts


import { Column, Model, Table } from 'sequelize-typescript';

@Table
export class User extends Model {
  @Column({
    allowNull: false,
    unique: true,
    comment: '用户名,唯一',
  })
  UserName: string;

  @Column({
    comment: '用户昵称',
  })
  nickName: string;

  @Column({
    allowNull: false,
    comment: '密码',
  })
  Password: string;

  @Column({
    type: 'JSON',
    comment: '权限',
  })
  Roles: Array<number>;

  @Column({ defaultValue: true, comment: '是否激活' })
  isActive: boolean;

  @Column({
    comment: '头像',
    defaultValue: 'https://',
  })
  avatarUrl: string;

  @Column({
    allowNull: false,
    defaultValue: 3,
    comment: '性别(1 男性,2 女性,3 未知)',
  })
  gender: boolean;
}

在这个文件中,我们定义了一个User用户表,具体表结构的定义方式,可以参考官方文档

更新index.ts文件

index.ts


import { User } from '../model/user.model';

const Models = [User];
export { Models };

至此,我们的一个数据模型及编写好了,我们尝试执行sync命令来升级数据库

yarn db:sync

之后就可以在终端中看见升级db的相关日志,之后使用工具查看我们的数据库

请输入图片描述

接下来我们手动在数据库写入一条数据,以方便我们之后修改find接口来查询数据

INSERT INTO `Users` (`id`, `UserName`, `nickName`, `Password`, `Roles`, `isActive`, `avatarUrl`, `gender`, `createdAt`, `updatedAt`)
VALUES
    (1, 'k0a1a', '殷迦南', '******', '[1]', 1, 'https://', 3, '2021-04-08 18:56:00', '2021-04-08 18:56:00');

修改之前的find接口

user.controller.ts

import { Controller, Get, Param } from '@nestjs/common';
import { UserService } from './user.service';

@Controller('user')
export class UserController {
  constructor(private readonly usersService: UserService) {}
  //   查询单个用户
  @Get('/find/:username')
  find(@Param() params) {
    return this.usersService.find(params.username);
  }
}

user.service.ts

import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/sequelize';
import { User } from '../../model/user.model';
@Injectable()
export class UserService {
  constructor(
    @InjectModel(User)
    private userModel: typeof User,
  ) {}
  find(UserName: string): Promise<User> {
    return this.userModel.findOne({
      where: {
        UserName,
      },
    });
  }
}

此时,我们请求之前的查询接口,可以看到如下返回:

{
"id": 1,
"UserName": "k0a1a",
"nickName": "殷迦南",
"Password": "******",
"Roles": [1],
"isActive": true,
"avatarUrl": "https://",
"gender": 3,
"createdAt": "2021-04-08T18:56:00.000Z",
"updatedAt": "2021-04-08T18:56:00.000Z"
}

标签: none

添加新评论