前言
玩歸玩,鬧歸鬧,別拿 C端
開玩笑! 這里不推薦大家把Node服務(wù)作為C端服務(wù),畢竟它是單線程多任務(wù)
機制。 這一特性是 Javascript
語言設(shè)計之初,就決定了它的使命 - Java >>>【Script】,這里就不多解釋了,大家去看看 JavaScript 的歷史就知道啦~這也就決定了,它不能像后端語言那樣 多線程多任務(wù)
,用戶訪問量小還能承受,一旦承受訪問量大高并發(fā),就得涼涼~
那為什么我們還要去寫 Node 服務(wù)? 主要是方便快捷,對于小項目可以迅速完成建設(shè),開發(fā)成本小。 其次,主要通過寫 Nest
完成下面收獲:
- 學(xué)習(xí)裝飾器語法,感受其簡潔優(yōu)美;
- 自己學(xué)習(xí)一門新的開發(fā)框架,感受不同框架的優(yōu)缺點,為以后開發(fā)選型打基礎(chǔ);
- 感受服務(wù)端排查問題的復(fù)雜性,找找前端設(shè)計的靈感。
本篇文章主要是使用 NestJs + Sequelize + MySQL 完成基礎(chǔ)運行, 帶大家了解 Node 服務(wù)端的基礎(chǔ)搭建,也可以順便看看 Java SpringBoot 項目的基礎(chǔ)結(jié)構(gòu),它倆真的非常相似,不信你去問服務(wù)端開發(fā)同學(xué)。
養(yǎng)成好習(xí)慣,看文章先一鍵三連~【點贊,關(guān)注,轉(zhuǎn)發(fā)】,評論可以看完再吐槽~繼續(xù)完善填坑~
第一步、項目跑起來
在選擇服務(wù)端的時候,我之前使用過 Egg.js ,所以這次就不選它了。其次,Egg 也是繼承了 Koa 的開發(fā)基礎(chǔ),加上 Express 也是基于 Koa 上創(chuàng)新的,兩者應(yīng)該差不多,就不選擇 Koa 和 Express 。
所以,我想嘗試下 Nest.js 看語法跟 Java 是一樣的,加上之前也自己開發(fā)過 Java + SpringBoot 的項目,當(dāng)然更古老的 SSH 2.0 也從無到有搭建過,即:Spring2.0 + Struts2+ Hibernate3.2,想想應(yīng)該會很容易上手,順便懷舊下寫寫。
參考文檔:
- https://www.geeksforgeeks.org/best-nodejs-frameworks-for-app-development/
- https://anywhere.epam.com/business/best-node-js-frameworks
說下我的想法,首先我們剛?cè)腴T,估計會有一堆不清楚的坑,我們先簡單點,后續(xù)我們再繼續(xù)加深。既然要搞服務(wù)端,要搞就多搞點,我們都去嘗鮮玩玩。我們打算使用 Nest
作為前端框架,Graphql
作為中間處理層。底層數(shù)據(jù)庫我們用傳統(tǒng)的 MySQL
,比較穩(wěn)定可靠,而且相對比較熟悉,這個就不玩新的了,畢竟數(shù)據(jù)庫是一切的基石 。
說下我們具體實現(xiàn)步驟:
-
【必須】沒有任何數(shù)據(jù)庫,完成接口請求運行,能夠跑起來;
-
【必須】創(chuàng)建基礎(chǔ)數(shù)據(jù)庫
MySQL
,接入@nestjs/sequelize
庫 完成增刪改查
功能即:CRUD
-
【可選】打算采取
Graphql
處理 API 查詢,做到精確數(shù)據(jù)查詢,這個已經(jīng)火了很多了,但是真正使用的很少,我們打算先感受下,后續(xù)可以直接用到業(yè)務(wù)。 -
【可選】接入
Swagger
自動生成 API 文檔,快捷進行前端與后端服務(wù)聯(lián)調(diào)測試。
? Swagger是一個開源工具,用于設(shè)計、構(gòu)建、記錄和使用RESTful web服務(wù)。
- 【可選】接口請求,數(shù)據(jù)庫優(yōu)化處理
? 請求分流,數(shù)據(jù)庫寫入加鎖,處理并發(fā)流程
? 增加 middleware
中間件統(tǒng)一處理請求及響應(yīng),進行鑒權(quán)處理,請求攔截等操作
? 數(shù)據(jù)庫分割備份,數(shù)據(jù)庫融災(zāi)處理,分為:主、備、災(zāi)
? 數(shù)據(jù)庫讀寫分離,數(shù)據(jù)雙寫,建立數(shù)據(jù)庫緩存機制,使用 redis
處理
也歡迎大家補充更多的優(yōu)化點,我們一起探討~有興趣可以幫忙補充代碼哈~
確定了大概方向,我們就開始整。先不追求一步到位,否則越多越亂,錦上添花的東西,我們可以后續(xù)增加,基礎(chǔ)功能我們要優(yōu)先保障完成。Nest.js 官網(wǎng):https://docs.nestjs.com/ ,話不多說,我們直接開整。
# 進入文件夾目錄
cd full-stack-demo/packages
# 安裝腳手架
npm i -g @nestjs/cli
# 創(chuàng)建基礎(chǔ)項目
nest new node-server-demo
# 進入項目
cd new node-server-demo
# 運行項目測試
npm run start:dev
我們移除一些不需要的東西,先簡單再復(fù)雜,別把自己搞暈了。接下來寫一個簡單示例感受下這個框架,之后完整的代碼,我會公布在后面。廢話不多說,開整!調(diào)整后目錄結(jié)構(gòu):
? common
- 公用方法類
? config
- 配置類文件
? controller
- 控制器,用于處理前端發(fā)起的各類請求
? service
- 服務(wù)類,用于處理與數(shù)據(jù)庫交互邏輯
? dto
- DTO(Data Transfer Object)可以用于驗證輸入數(shù)據(jù)、限制傳輸?shù)淖侄位蚋袷健?/p>
? entities
- 實體類,用于描述對象相關(guān)的屬性信息
? module
- 模塊,用于注冊所有的服務(wù)類、控制器類,類似 Spring 里面的 bean
? 這里不能完全等同哈,兩個實現(xiàn)機制上就不同,只是幫助大家理解。
? main.ts
- nest 啟動入口
? types
- typescript 相關(guān)聲明類型
只是寫 demo, 搞快點就沒有怎么寫注釋了,我感覺是一看就懂了,跟 Java SpringBoot 的寫法非常一致,部分代碼展示:
- 控制器 controller
// 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);
}
}
- 數(shù)據(jù)轉(zhuǎn)換 Data Transfer Object
// 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;
}
- service 數(shù)據(jù)庫交互處理類
// 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> {
// 可以處理判空,從數(shù)據(jù)庫讀取/寫入數(shù)據(jù),可能會被多個 controller 進行調(diào)用
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,
});
}
}
- module 模塊注冊,服務(wù)類/控制類
// 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 {}
- main.js 啟動注冊的所有類
// 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);
// 監(jiān)聽端口 3000
await app.listen(3000);
}
bootstrap();
這樣一個單機的服務(wù)器就啟動起來了,我們可以使用 Postwoman
[https://hoppscotch.io/] 進行請求,瞅瞅看返回效果。
控制臺也收到日志了,后面可以把這些日志請求保留成 .log
文件,這樣請求日志也有了,完美!下一步,我們開始連接數(shù)據(jù)庫,這樣就不用單機玩泥巴了~
第二步、配置 MySQL
MySQL 安裝其實很簡單,我電腦是 Mac 的,所以下面的截圖都是以 mac 為例,先下載對應(yīng)的數(shù)據(jù)庫。
下載地址:https://dev.mysql.com/downloads/mysql/ 至于其他系統(tǒng)的,可以網(wǎng)上找教程,這個應(yīng)該爛大街了,我就不重復(fù)搬運教程了。
- 注意:安裝的數(shù)據(jù)庫,一定要設(shè)置密碼,連接數(shù)據(jù)庫必須要有密碼,否則會導(dǎo)致連接數(shù)據(jù)庫失敗。
- MySQL 我們只安裝數(shù)據(jù)庫就行,熟悉指令的童鞋,就直接命令行操作就行。
- 不熟悉的話,那就下載圖形化管理工具。
? Mysql 官方控制臺 https://dev.mysql.com/downloads/workbench/
? Windows 也可以使用 https://www.heidisql.com/download.php?download=installer
PS:安裝 workbench 時發(fā)現(xiàn)要求 MacOS 13
以上,我的電腦是 MacOS 12
。
白白下載,所以只能 https://downloads.mysql.com/archives/workbench/ 從歸檔里面找低版本 8.0.31
。對于數(shù)據(jù)庫服務(wù)也有版本要求,大家按照自己電腦版本,選擇支持的版本即可。 https://downloads.mysql.com/archives/community/
。我這邊選擇的是默認最新版本:8.0.34
,下載好直接安裝,一路 Next
到底,記住自己輸入的 Root 密碼!!!
確認好當(dāng)前數(shù)據(jù)庫是否已經(jīng)運行起來了,啟動 Workbench 查看狀態(tài)。
1.創(chuàng)建數(shù)據(jù)庫
數(shù)據(jù)庫存在字符集選擇,不同的字符集和校驗規(guī)則,會對存儲數(shù)據(jù)產(chǎn)生影響,所以大家可以自行查詢,按照自己存儲數(shù)據(jù)原則選擇,我這里默認選最廣泛的。確認好,就選擇右下角的應(yīng)用按鈕。
- 創(chuàng)建表和屬性
選項解答:
? PRIMARY KEY
是表中的一個或多個列的組合,它用于唯一標(biāo)識表中的每一行。
? Not NULL
和 Unique
就不解釋,就是直譯的那個意思。
? GENERATED
生成列是表中的一種特殊類型的列,它的值不是從插入語句中獲取的,而是根據(jù)其他列的值通過一個表達式或函數(shù)生成的。
CREATE TABLE people (
first_name VARCHAR(100),
last_name VARCHAR(100),
full_name VARCHAR(200) AS (CONCAT(first_name, ' ', last_name))
);
-
UNSIGNED
這個數(shù)值類型就只能存儲正數(shù)(包括零),不會存儲負數(shù)。 -
ZEROFILL
將數(shù)值類型的字段的前面填充零,他會自動使字段變?yōu)?UNSIGNED
,直到該字段達到聲明的長度,如:00007 -
BINARY
用于存儲二進制字符串,如聲明一個字段為 BINARY(5),那么存儲在這個字段中的字符串都將被處理為長度為 5 的二進制字符串。
? 如嘗試存儲一個長度為 3 的字符串,那么它將在右側(cè)用兩個空字節(jié)填充。
? 如果你嘗試存儲一個長度為 6 的字符串,那么它將被截斷為長度為 5
? 主要用途是存儲那些需要按字節(jié)進行比較的數(shù)據(jù),例如加密哈希值
- 此外也可順手傳創(chuàng)建一個索引,方便快速查找。
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);
- 連接數(shù)據(jù)庫
由于目前 node-oracledb
官方尚未提供針對 Apple Silicon 架構(gòu)的預(yù)編譯二進制文件。導(dǎo)致我們無法在 Mac M1 芯片上使用 TypeORM
鏈接數(shù)據(jù)庫操作,它目前只支持 Mac x86 芯片。哎~折騰老半天,查閱各種文檔,居然有這個坑,沒關(guān)系我們換個方式打開。
我們不得不放棄,從而選用 https://docs.nestjs.com/techniques/database#sequelize-integration 哐哐哐~一頓操作猛如虎,盤它!
- 安裝
Sequelize
# 安裝連接庫
npm install --save @nestjs/sequelize sequelize sequelize-typescript mysql2
# 安裝 type
npm install --save-dev @types/sequelize
- 配置數(shù)據(jù)庫基礎(chǔ)信息
// 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',
// 按數(shù)據(jù)庫實際配置
host: '127.0.0.1',
// 按數(shù)據(jù)庫實際配置
port: 3306,
// 按數(shù)據(jù)庫實際配置
username: 'root',
// 按數(shù)據(jù)庫實際配置
password: 'hello',
// 按數(shù)據(jù)庫實際配置
database: 'world',
synchronize: true,
models: [Log],
autoLoadModels: true,
}),
LogModule,
UserModule,
],
})
export class AppModule {}
- 實體與數(shù)據(jù)庫一一映射處理
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嚴(yán)重,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;
}
- module 注冊實體
// 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 {}
- service 操作數(shù)據(jù)庫處理數(shù)據(jù)
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, '日志內(nèi)容為空');
}
const item = {
level,
category,
// Tips: 為防止外部數(shù)據(jù)進行數(shù)據(jù)注入,我們可以對內(nèi)容進行 encode 處理。
// content: encodeURIComponent(str),
content: str,
};
await this.logModel.create(item);
return getSucVoidRes();
}
}
一路操作猛如虎,回頭一看嘿嘿嘿~終于,我們收到了來自外界的第一條數(shù)據(jù)! hello world!
連接及創(chuàng)建數(shù)據(jù)成功!此時已經(jīng)完成基礎(chǔ)功能啦~
第三步、實現(xiàn) CRUD
基礎(chǔ)功能
剩下的內(nèi)容,其實大家可以自行腦補了,就是調(diào)用數(shù)據(jù)庫的操作邏輯。先說說什么是 CRUD
-
C
create 創(chuàng)建 -
R
read 讀取 -
U
update 更新 -
D
delete 刪除
下面給個簡單示例,大家看看,剩下就去找文檔,實現(xiàn)業(yè)務(wù)邏輯即可:
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,
) {}
// 創(chuàng)建新數(shù)據(jù)
async create(user: User) {
const newUser = await this.userModel.create(user);
return newUser;
}
// 查找所有數(shù)據(jù)
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:
進行刪除的時候,我們可以進行假刪除,兩個數(shù)據(jù)庫,一個是備份數(shù)據(jù)庫,一個是主數(shù)據(jù)庫。主數(shù)據(jù)庫可以直接刪除或者增加標(biāo)識表示刪除。備份數(shù)據(jù)庫,可以不用刪除只寫入和更新操作,這樣可以進行數(shù)據(jù)還原操作。
此外,為了防止 SQL 數(shù)據(jù)庫注入,大家需要對數(shù)據(jù)來源進行統(tǒng)一校驗處理或者直接進行 encode 處理,對于重要數(shù)據(jù)可以直接進行 MD5 加密處理,防止數(shù)據(jù)庫被直接下載泄露。關(guān)于 SQL 數(shù)據(jù)庫的安全處理,網(wǎng)上教程有很多,大家找一找就可以啦~
部署就比較簡單了,我們就不需要一一贅述了,數(shù)據(jù)庫可以用集團提供的云數(shù)據(jù)庫,而 Nest 就是普通的 node 部署。
作者:京東零售 周明亮
來源:京東云開發(fā)者社區(qū) 轉(zhuǎn)載請注明來源