前言

玩归玩,闹归闹,别拿 C端 开玩笑! 这里不推荐大家把Node服务作为C端服务,毕竟它是单线程多任务 机制。 这一特性是 Javascript 语言设计之初,就决定了它的使命 - Java >>>【Script】,这里就不多解释了,大家去看看 JavaScript 的历史就知道啦~这也就决定了,它不能像后端语言那样 多线程多任务,用户访问量小还能承受,一旦承受访问量大高并发,就得凉凉~

那为什么我们还要去写 Node 服务? 主要是方便快捷,对于小项目可以迅速完成建设,开发成本小。 其次,主要通过写 Nest 完成下面收获:

  • 学习装饰器语法,感受其简洁优美;
  • 自己学习一门新的开发框架,感受不同框架的优缺点,为以后开发选型打基础;
  • 感受服务端排查问题的复杂性,找找前端设计的灵感。

本篇文章主要是使用 NestJs + Sequelize + MySQL 完成基础运行, 带大家了解 Node 服务端的基础搭建,也可以顺便看看 Java SpringBoot 项目的基础结构,它俩真的非常相似,不信你去问服务端开发同学。

养成好习惯,看文章先一键三连~【点赞,关注,转发】,评论可以看完再吐槽~继续完善填坑~

第一步、项目跑起来

在选择服务端的时候,我之前使用过 Egg.js ,所以这次就不选它了。其次,Egg 也是继承了 Koa 的开发基础,加上 Express 也是基于 Koa 上创新的,两者应该差不多,就不选择 Koa 和 Express 。

所以,我想尝试下 Nest.js 看语法跟 Java 是一样的,加上之前也自己开发过 Java + SpringBoot 的项目,当然更古老的 SSH 2.0 也从无到有搭建过,即:Spring2.0 + Struts2+ Hibernate3.2,想想应该会很容易上手,顺便怀旧下写写。

参考文档:

说下我的想法,首先我们刚入门,估计会有一堆不清楚的坑,我们先简单点,后续我们再继续加深。既然要搞服务端,要搞就多搞点,我们都去尝鲜玩玩。我们打算使用 Nest 作为前端框架,Graphql 作为中间处理层。底层数据库我们用传统的 MySQL,比较稳定可靠,而且相对比较熟悉,这个就不玩新的了,毕竟数据库是一切的基石 。

说下我们具体实现步骤:

  1. 【必须】没有任何数据库,完成接口请求运行,能够跑起来;

  2. 【必须】创建基础数据库 MySQL ,接入 @nestjs/sequelize 库 完成 增删改查 功能即:CRUD

  3. 可选】打算采取 Graphql 处理 API 查询,做到精确数据查询,这个已经火了很多了,但是真正使用的很少,我们打算先感受下,后续可以直接用到业务。

  4. 可选】接入 Swagger 自动生成 API 文档,快捷进行前端与后端服务联调测试。

◦ Swagger是一个开源工具,用于设计、构建、记录和使用RESTful web服务。

  1. 【可选】接口请求,数据库优化处理

◦ 请求分流,数据库写入加锁,处理并发流程

◦ 增加 middleware 中间件统一处理请求及响应,进行鉴权处理,请求拦截等操作

◦ 数据库分割备份,数据库融灾处理,分为:主、备、灾

◦ 数据库读写分离,数据双写,建立数据库缓存机制,使用 redis 处理

也欢迎大家补充更多的优化点,我们一起探讨~有兴趣可以帮忙补充代码哈~

确定了大概方向,我们就开始整。先不追求一步到位,否则越多越乱,锦上添花的东西,我们可以后续增加,基础功能我们要优先保障完成。Nest.js 官网:https://docs.nestjs.com/ ,话不多说,我们直接开整。

# 进入文件夹目录
cd full-stack-demo/packages
# 安装脚手架
npm i -g @nestjs/cli
# 创建基础项目
nest new node-server-demo
# 进入项目
cd new node-server-demo
# 运行项目测试
npm run start:dev

我们移除一些不需要的东西,先简单再复杂,别把自己搞晕了。接下来写一个简单示例感受下这个框架,之后完整的代码,我会公布在后面。废话不多说,开整!调整后目录结构:

common - 公用方法类

config - 配置类文件

controller - 控制器,用于处理前端发起的各类请求

service - 服务类,用于处理与数据库交互逻辑

dto - DTO(Data Transfer Object)可以用于验证输入数据、限制传输的字段或格式。

entities - 实体类,用于描述对象相关的属性信息

module - 模块,用于注册所有的服务类、控制器类,类似 Spring 里面的 bean

◦ 这里不能完全等同哈,两个实现机制上就不同,只是帮助大家理解。

main.ts - nest 启动入口

types - typescript 相关声明类型

只是写 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);
}
}
  • 数据转换 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 数据库交互处理类
// 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,
});
}
}
  • module 模块注册,服务类/控制类
// 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);
// 监听端口 3000
await app.listen(3000);
} bootstrap();

这样一个单机的服务器就启动起来了,我们可以使用 Postwoman [https://hoppscotch.io/] 进行请求,瞅瞅看返回效果。

控制台也收到日志了,后面可以把这些日志请求保留成 .log 文件,这样请求日志也有了,完美!下一步,我们开始连接数据库,这样就不用单机玩泥巴了~

第二步、配置 MySQL

MySQL 安装其实很简单,我电脑是 Mac 的,所以下面的截图都是以 mac 为例,先下载对应的数据库。

下载地址:https://dev.mysql.com/downloads/mysql/ 至于其他系统的,可以网上找教程,这个应该烂大街了,我就不重复搬运教程了。

  • 注意:安装的数据库,一定要设置密码,连接数据库必须要有密码,否则会导致连接数据库失败。
  • MySQL 我们只安装数据库就行,熟悉指令的童鞋,就直接命令行操作就行。
  • 不熟悉的话,那就下载图形化管理工具。

◦ Mysql 官方控制台 https://dev.mysql.com/downloads/workbench/

◦ Windows 也可以使用 https://www.heidisql.com/download.php?download=installer

PS:安装 workbench 时发现要求 MacOS 13以上,我的电脑是 MacOS 12

白白下载,所以只能 https://downloads.mysql.com/archives/workbench/ 从归档里面找低版本 8.0.31。对于数据库服务也有版本要求,大家按照自己电脑版本,选择支持的版本即可。 https://downloads.mysql.com/archives/community/。我这边选择的是默认最新版本:8.0.34,下载好直接安装,一路 Next 到底,记住自己输入的 Root 密码!!!

确认好当前数据库是否已经运行起来了,启动 Workbench 查看状态。

1.创建数据库

数据库存在字符集选择,不同的字符集和校验规则,会对存储数据产生影响,所以大家可以自行查询,按照自己存储数据原则选择,我这里默认选最广泛的。确认好,就选择右下角的应用按钮。

  1. 创建表和属性

选项解答:

PRIMARY KEY 是表中的一个或多个列的组合,它用于唯一标识表中的每一行。

Not NULLUnique 就不解释,就是直译的那个意思。

GENERATED 生成列是表中的一种特殊类型的列,它的值不是从插入语句中获取的,而是根据其他列的值通过一个表达式或函数生成的。

CREATE TABLE people (
first_name VARCHAR(100),
last_name VARCHAR(100),
full_name VARCHAR(200) AS (CONCAT(first_name, ' ', last_name))
);
  • UNSIGNED 这个数值类型就只能存储正数(包括零),不会存储负数。
  • ZEROFILL 将数值类型的字段的前面填充零,他会自动使字段变为 UNSIGNED,直到该字段达到声明的长度,如:00007
  • BINARY 用于存储二进制字符串,如声明一个字段为 BINARY(5),那么存储在这个字段中的字符串都将被处理为长度为 5 的二进制字符串。

◦ 如尝试存储一个长度为 3 的字符串,那么它将在右侧用两个空字节填充。

◦ 如果你尝试存储一个长度为 6 的字符串,那么它将被截断为长度为 5

◦ 主要用途是存储那些需要按字节进行比较的数据,例如加密哈希值

  • 此外也可顺手传创建一个索引,方便快速查找。
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);
  1. 连接数据库

由于目前 node-oracledb 官方尚未提供针对 Apple Silicon 架构的预编译二进制文件。导致我们无法在 Mac M1 芯片上使用 TypeORM 链接数据库操作,它目前只支持 Mac x86 芯片。哎~折腾老半天,查阅各种文档,居然有这个坑,没关系我们换个方式打开。

我们不得不放弃,从而选用 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
  • 配置数据库基础信息
// 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;
}
  • 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 操作数据库处理数据
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();
}
}

一路操作猛如虎,回头一看嘿嘿嘿~终于,我们收到了来自外界的第一条数据! hello world!

连接及创建数据成功!此时已经完成基础功能啦~

第三步、实现 CRUD 基础功能

剩下的内容,其实大家可以自行脑补了,就是调用数据库的操作逻辑。先说说什么是 CRUD

  • C create 创建
  • R read 读取
  • U update 更新
  • D delete 删除

下面给个简单示例,大家看看,剩下就去找文档,实现业务逻辑即可:

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 部署。

作者:京东零售 周明亮

来源:京东云开发者社区 转载请注明来源

我在前端写Java SpringBoot项目的更多相关文章

  1. Java SpringBoot 项目构建 Docker 镜像调优实践

    PS:已经在生产实践中验证,解决在生产环境下,网速带宽小,每次推拉镜像影响线上服务问题,按本文方式构建镜像,除了第一次拉取.推送.构建镜像慢,第二.三-次都是几百K大小传输,速度非常快,构建.打包.推 ...

  2. 【转载】JAVA SpringBoot 项目打成jar包供第三方引用自动配置(Spring发现)解决方案

    JAVA SpringBoot 项目打成jar包供第三方引用自动配置(Spring发现)解决方案 本文为转载,原文地址为:https://www.cnblogs.com/adversary/p/103 ...

  3. JAVA - SpringBoot项目引用MyBatis操作数据库

    JAVA - SpringBoot项目引用MyBatis操作数据库 1. 创建SpringBoot项目,参考:https://www.cnblogs.com/1285026182YUAN/p/1232 ...

  4. JAVA - SpringBoot项目引用generator生成 Mybatis文件

    JAVA - SpringBoot项目引用generator生成 Mybatis文件  在spring官网https://start.spring.io/自动生成springboot项目,这里选择项目 ...

  5. JAVA - SpringBoot项目跨域访问

    JAVA - SpringBoot添加支持CORS跨域访问 CORS(Cross-Origin Resource Sharing)“跨域资源共享”,是一个W3C标准,它允许浏览器向跨域服务器发送Aja ...

  6. JAVA SpringBoot 项目打包(JAR),在打包成 docker 镜像的基本方法

    1,打包 SpringBoot 项目,使用 IDEA 如下图 2,将 JAR 包上传到安装了 Docker 的 linux 服务器上,并且在相容目录下创建一个名为 Dockerfile 的文件 3,在 ...

  7. JAVA SpringBoot 项目打成jar包供第三方引用自动配置(Spring发现)解决方案

    本项目测试环境 JDK: 1.8 SpringBoot: 2.1 需求描述 当我们想要利用SpringBoot封装一套组件并发布给第三方使用时,我们就不得不考虑我们的组件能否被使用者正确引入使用,此处 ...

  8. java springboot项目树结构递归查询

    记录工作 本文记录树结构递归查询,像菜单栏和部门 首先需要一张表 CREATE TABLE `sys_dict` ( `id` int NOT NULL AUTO_INCREMENT, `parent ...

  9. 一个完整Java Web项目背后的密码

    前言 最近自己做了几个Java Web项目,有公司的商业项目,也有个人做着玩的小项目,写篇文章记录总结一下收获,列举出在做项目的整个过程中,所需要用到的技能和知识点,带给还没有真正接触过完整Java ...

  10. 做一个完整的Java Web项目需要掌握的技能[转]

    转自:http://blog.csdn.net/JasonLiuLJX/article/details/51494048 最近自己做了几个Java Web项目,有公司的商业项目,也有个人做着玩的小项目 ...

随机推荐

  1. 全球唯一云厂商!华为云高分入选2023Gartner Peer Insights™云数据库管理系统“客户之选”

    本文分享自华为云社区<华为云高分入选2023Gartner Peer Insights云数据库管理系统"客户之选">,作者:GaussDB 数据库 . 近日,Gartn ...

  2. WPF 入门笔记 - 06 - 命令

    我们把世界看错,反说它欺骗了我们. --飞鸟集 前言 相较而言,命令对我来说是一个新概念,因为在Winform中压根没有所谓的命令这个概念.从文字角度理解,"命令"可以指代一种明确 ...

  3. Vue3从入门到精通(三)

    vue3插槽Slots 在 Vue3 中,插槽(Slots)的使用方式与 Vue2 中基本相同,但有一些细微的差异.以下是在 Vue3 中使用插槽的示例: // ChildComponent.vue ...

  4. 实时阴影技术(Real-time Shadows)

    目录 Shadow Mapping 基本实现 Shadow Bias Peter Panning 问题 & 简单 Trick Slope Scale Based Depth Bias Casc ...

  5. python学习笔记:第七章面向对象

    与java类似,python作为一种面向对象的编程语言,也可以创建自定义的对象和类. 它的特性主要有:继承,封装,多态,方法,属性,超类 1.变量的作用域 c = 50 #全局变量, 作用域为整个模块 ...

  6. 【小小Demo】在线聊天小🌰子

    easy-chat 一个简单的即时通讯demo. 环境 jdk1.8 idea maven springboot 2.1.1.RELEASE websocket 项目启动 修改maven设置 打开ID ...

  7. .NET周刊【7月第3期 2023-07-16】

    国内文章 揭秘 .NET 中的 TimerQueue(上) https://www.cnblogs.com/eventhorizon/p/17557821.html TimerQueue 是.NET中 ...

  8. 牛客小白月赛65 D题 题解

    原题链接 题意描述 一共有两堆石子,第一堆有 \(a\) 个,第二堆有 \(b\) 个,牛牛和牛妹轮流取石子,牛牛先手,每次取石子的时候只能从以下 \(2\) 种方案种挑一种来取(对于选择的方案数必须 ...

  9. 原来TypeScript中的接口和泛型这么好理解

    "接口"和"泛型"是 TypeScript 相比于 JavaScript 新增的内容,都用于定义数据类型 前面两篇文章总结了TypeScript中的 类型注解. ...

  10. Java学习之进制之间的转换

    import java.util.*; public class Jinzhi { public static void main(String[] args) { /** * 总结: * 二进制转换 ...