玩归玩,闹归闹,别拿C端开玩笑!这里不推荐大家把Node服务作为C端服务,毕竟它是单线程多任务机制。这一特性是Javascript语言设计之初,就决定了它的使命——Java>>>【Script】,这里就不多解释了,大家去看看JavaScript的历史就知道啦~这也就决定了,它不能像后端语言那样多线程多任务,用户访问量小还能承受,一旦承受访问量大高并发,就得凉凉~
那为什么我们还要去写Node服务?主要是方便快捷,对于小项目可以迅速完成建设,开发成本小。其次,主要通过写Nest完成下面收获:
学习装饰器语法,感受其简洁优美;
自己学习一门新的开发框架,感受不同框架的优缺点,为以后开发选型打基础;
感受服务端排查问题的复杂性,找找前端设计的灵感。
本篇文章主要是使用NestJs+Sequelize+MySQL完成基础运行,带大家了解Node服务端的基础搭建,也可以顺便看看Java SpringBoot项目的基础结构,他俩真的非常相似,不信去问服务端的开发同学。
在选择服务端的时候,我之前使用过 Egg.js ,所以这次就不选它了。其次,Egg 也是继承了 Koa 的开发基础,加上 Express 也是基于 Koa 上创新的,两者应该差不多,就不选择 Koa 和 Express 。
https://www.geeksforgeeks.org/best-nodejs-frameworks-for-app-development/
https://anywhere.epam.com/business/best-node-js-frameworks
# 进入文件夹目录
cd full-stack-demo/packages
# 安装脚手架
npm i -g @nestjs/cli
# 创建基础项目
nest new node-server-demo
# 进入项目
cd new node-server-demo
# 运行项目测试
npm run start:dev
// packages/node-server-demo/src/controller/user/index.ts
import { Controller, Get, Query } from '@nestjs/common';
import UserServices from '@/service/user';
import { GetUserDto, GetUserInfoDto } from '@/dto/user';
@Controller('user')
export class UserController {
constructor(private readonly userService: UserServices) {}
// Get 请求 user/name?name=bricechou
@Get('name')
async findByName(@Query() getUserDto: GetUserDto) {
return this.userService.read.findByName(getUserDto.name);
}
// Get 请求 user/info?id=123
@Get('info')
async findById(@Query() getUserInfoDto: GetUserInfoDto) {
const user = await this.userService.read.findById(getUserInfoDto.id);
return { gender: user.gender, job: user.job };
}
}
// packages/node-server-demo/src/controller/log/add.ts
import { Controller, Post, Body } from '@nestjs/common';
import { AddLogDto } from '@/dto/log';
import LogServices from '@/service/log';
@Controller('log')
export class CreateLogController {
constructor(private readonly logServices: LogServices) {}
// post('/log/add')
@Post('add')
create(@Body() createLogDto: AddLogDto) {
return this.logServices.create.create(createLogDto);
}
}
// packages/node-server-demo/src/dto/user.ts
export class CreateUserDto {
name: string;
age: number;
gender: string;
job: string;
}
// 可以分开写,也可以合并
export class GetUserDto {
id?: number;
name: string;
}
// 可以分开写,也可以合并
export class GetUserInfoDto {
id: number;
}
// packages/node-server-demo/src/service/user/read.ts
import { Injectable } from '@nestjs/common';
import { User } from '@/entities/User';
@Injectable()
export class ReadUserService {
constructor() {}
async findByName(name: string): Promise<User> {
// 可以处理判空,从数据库读取/写入数据,可能会被多个 controller 进行调用
console.info('ReadUserService findByName > ', name);
return Promise.resolve({ id: 1, name, job: '程序员', gender: 1, age: 18 });
}
async findById(id: number): Promise<User> {
console.info('ReadUserService findById > ', id);
return Promise.resolve({
id: 1,
name: 'BriceChou',
job: '程序员',
gender: 1,
age: 18,
});
}
}
// packages/node-server-demo/src/module/user.ts
import { Module } from '@nestjs/common';
import UserService, { ReadUserService } from '@/service/user';
import { UserController } from '@/controller/user';
@Module({
providers: [UserService, ReadUserService],
controllers: [UserController],
})
export class UserModule {}
// packages/node-server-demo/src/module/index.ts 根模块注入
import { Module } from '@nestjs/common';
import { UserModule } from './user';
import { LogModule } from './log';
@Module({
imports: [
UserModule,
LogModule,
],
})
export class AppModule {}
// packages/node-server-demo/src/main.ts
import { AppModule } from '@/module';
import { NestFactory } from '@nestjs/core';
import { NestExpressApplication } from '@nestjs/platform-express';
async function bootstrap() {
const app = await NestFactory.create<NestExpressApplication>(AppModule);
// 监听端口 3000
await app.listen(3000);
}
bootstrap();
MySQL 安装其实很简单,我电脑是 Mac 的,所以下面的截图都是以 mac 为例,先下载对应的数据库。
CREATE TABLE people (
first_name VARCHAR(100),
last_name VARCHAR(100),
full_name VARCHAR(200) AS (CONCAT(first_name, ' ', last_name))
);
CREATE TABLE `rrweb`.`test_sys_req_log` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`content` TEXT NOT NULL,
`l_level` INT UNSIGNED NOT NULL,
`l_category` VARCHAR(255) NOT NULL,
`l_created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`l_updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE INDEX `id_UNIQUE` (`id` ASC) VISIBLE,
INDEX `table_index` (`l_level` ASC, `l_category` ASC, `l_time` ASC) VISIBLE);
由于目前 node-oracledb 官方尚未提供针对 Apple Silicon 架构的预编译二进制文件。导致我们无法在 Mac M1 芯片上使用 TypeORM 链接数据库操作,它目前只支持 Mac x86 芯片。哎~折腾老半天,查阅各种文档,居然有这个坑,没关系我们换个方式打开。
我们不得不放弃,从而选用 https://docs.nestjs.com/techniques/database#sequelize-integration
哐哐哐~一顿操作猛如虎,盘它!
# 安装连接库
npm install --save @nestjs/sequelize sequelize sequelize-typescript mysql2
# 安装 type
npm install --save-dev @types/sequelize
// packages/node-server-demo/src/module/index.ts
import { Module } from '@nestjs/common';
import { UserModule } from './user';
import { LogModule } from './log';
import { Log } from '@/entities/Log';
import { SequelizeModule } from '@nestjs/sequelize';
@Module({
imports: [
SequelizeModule.forRoot({
dialect: 'mysql',
// 按数据库实际配置
host: '127.0.0.1',
// 按数据库实际配置
port: 3306,
// 按数据库实际配置
username: 'root',
// 按数据库实际配置
password: 'hello',
// 按数据库实际配置
database: 'world',
synchronize: true,
models: [Log],
autoLoadModels: true,
}),
LogModule,
UserModule,
],
})
export class AppModule {}
import { getNow } from '@/common/date';
import {
Model,
Table,
Column,
PrimaryKey,
DataType,
} from 'sequelize-typescript';
@Table({ tableName: 'test_sys_req_log' })
export class Log extends Model<Log> {
@PrimaryKey
@Column({
type: DataType.INTEGER,
autoIncrement: true,
field: 'id',
})
id: number;
@Column({ field: 'content', type: DataType.TEXT })
content: string;
@Column({ field: 'l_level', type: DataType.INTEGER })
level: number; // 3严重,2危险,1轻微
@Column({ field: 'l_category' })
category: string; // 模块分类/来源分类
@Column({
field: 'l_created_at',
type: DataType.NOW,
defaultValue: getNow(),
})
createdAt: number;
@Column({
field: 'l_updated_at',
type: DataType.NOW,
defaultValue: getNow(),
})
updatedAt: number;
}
// packages/node-server-demo/src/module/log.ts
import { Module } from '@nestjs/common';
import { SequelizeModule } from '@nestjs/sequelize';
import { Log } from '@/entities/Log';
import LogServices, {
CreateLogService,
UpdateLogService,
DeleteLogService,
ReadLogService,
} from '@/service/log';
import {
CreateLogController,
RemoveLogController,
UpdateLogController,
} from '@/controller/log';
@Module({
imports: [SequelizeModule.forFeature([Log])],
providers: [
LogServices,
CreateLogService,
UpdateLogService,
DeleteLogService,
ReadLogService,
],
controllers: [CreateLogController, RemoveLogController, UpdateLogController],
})
export class LogModule {}
import { Log } from '@/entities/Log';
import { Injectable } from '@nestjs/common';
import { AddLogDto } from '@/dto/log';
import { InjectModel } from '@nestjs/sequelize';
import { ResponseStatus } from '@/types/BaseResponse';
import { getErrRes, getSucVoidRes } from '@/common/response';
@Injectable()
export class CreateLogService {
constructor(
@InjectModel(Log)
private logModel: typeof Log,
) {}
async create(createLogDto: AddLogDto): Promise<ResponseStatus<null>> {
console.info('CreateLogService create > ', createLogDto);
const { level = 1, content = '', category = 'INFO' } = createLogDto || {};
const str = content.trim();
if (!str) {
return getErrRes(500, '日志内容为空');
}
const item = {
level,
category,
// Tips: 为防止外部数据进行数据注入,我们可以对内容进行 encode 处理。
// content: encodeURIComponent(str),
content: str,
};
await this.logModel.create(item);
return getSucVoidRes();
}
}
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/sequelize';
import { User } from './user.model';
@Injectable()
export class UserService {
constructor(
@InjectModel(User)
private userModel: typeof User,
) {}
// 创建新数据
async create(user: User) {
const newUser = await this.userModel.create(user);
return newUser;
}
// 查找所有数据
async findAll() {
return this.userModel.findAll();
}
// 按要求查找单个
async findOne(id: string) {
return this.userModel.findOne({ where: { id } });
}
// 按要求更新
async update(id: string, user: User) {
await this.userModel.update(user, { where: { id } });
return this.userModel.findOne({ where: { id } });
}
// 按要求删除
async delete(id: string) {
const user = await this.userModel.findOne({ where: { id } });
await user.destroy();
}
}
Tips: 进行删除的时候,我们可以进行假删除,两个数据库,一个是备份数据库,一个是主数据库。主数据库可以直接删除或者增加标识表示删除。备份数据库,可以不用删除只写入和更新操作,这样可以进行数据还原操作。
此外,为了防止 SQL 数据库注入,大家需要对数据来源进行统一校验处理或者直接进行 encode 处理,对于重要数据可以直接进行 MD5 加密处理,防止数据库被直接下载泄露。关于 SQL 数据库的安全处理,网上教程有很多,大家找一找就可以啦~
部署就比较简单了,我们就不需要一一赘述了,数据库可以用集团提供的云数据库,而 Nest 就是普通的 node 部署。