来源 | oschina 社区
作者 | 京东云开发者-京东零售 周明亮
前言
玩归玩,闹归闹,别拿 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,比较稳定可靠,而且相对比较熟悉,这个就不玩新的了,毕竟数据库是一切的基石 。
说下我们具体实现步骤:
【必须】没有任何数据库,完成接口请求运行,能够跑起来;
【必须】创建基础数据库 mysql ,接入 @nestjs/sequelize 库 完成 增删改查 功能即:crud
【可选】打算采取 graphql 处理 api 查询,做到精确数据查询,这个已经火了很多了,但是真正使用的很少,我们打算先感受下,后续可以直接用到业务。
【可选】接入 swagger 自动生成 api 文档,快捷进行前端与后端服务联调测试。
◦ swagger 是一个开源工具,用于设计、构建、记录和使用 restful web 服务。
【可选】接口请求,数据库优化处理
◦ 请求分流,数据库写入加锁,处理并发流程
◦ 增加 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.tsimport{ controller, get, query }from'@nestjs/common';import userservices from'@/service/user';import{ getuserdto, getuserinfodto }from'@/dto/user';@controller('user')exportclassusercontroller{ constructor(privatereadonly userservice: userservices){} // get 请求 user/name?name=bricechou @get('name') asyncfindbyname(@query() getuserdto: getuserdto){ returnthis.userservice.read.findbyname(getuserdto.name); } // get 请求 user/info?id=123 @get('info') asyncfindbyid(@query() getuserinfodto: getuserinfodto){ const user =awaitthis.userservice.read.findbyid(getuserinfodto.id); return{ gender: user.gender, job: user.job }; }}// packages/node-server-demo/src/controller/log/add.tsimport{ controller, post, body }from'@nestjs/common';import{ addlogdto }from'@/dto/log';import logservices from'@/service/log';@controller('log')exportclasscreatelogcontroller{ constructor(privatereadonly logservices: logservices){} // post('/log/add') @post('add') create(@body() createlogdto: addlogdto){ returnthis.logservices.create.create(createlogdto); }}
数据转换 data transfer object
// packages/node-server-demo/src/dto/user.tsexportclasscreateuserdto{ name:string; age:number; gender:string; job:string;}// 可以分开写,也可以合并exportclassgetuserdto{ id?:number; name:string;}// 可以分开写,也可以合并exportclassgetuserinfodto{ id:number;}
service 数据库交互处理类
// packages/node-server-demo/src/service/user/read.tsimport{ injectable }from'@nestjs/common';import{ user }from'@/entities/user';@injectable()exportclassreaduserservice{ constructor(){} asyncfindbyname(name:string):promise{ // 可以处理判空,从数据库读取/写入数据,可能会被多个 controller 进行调用 console.info('readuserservice findbyname > ', name); returnpromise.resolve({ id:1, name, job:'程序员', gender:1, age:18}); } asyncfindbyid(id:number):promise{ console.info('readuserservice findbyid > ', id); returnpromise.resolve({ id:1, name:'bricechou', job:'程序员', gender:1, age:18, }); }}
module 模块注册,服务类 / 控制类
// packages/node-server-demo/src/module/user.tsimport{ module }from'@nestjs/common';import userservice,{ readuserservice }from'@/service/user';import{ usercontroller }from'@/controller/user';@module({ providers:[userservice, readuserservice], controllers:[usercontroller],})exportclassusermodule{}// 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, ],})exportclassappmodule{}
main.js 启动注册的所有类
// packages/node-server-demo/src/main.tsimport { appmodule } from '@/module';import { nestfactory } from '@nestjs/core';import { nestexpressapplication } from '@nestjs/platform-express';async function bootstrap() { const app = await nestfactory.create(appmodule); // 监听端口 3000 await app.listen(3000);}bootstrap();
这样一个单机的服务器就启动起来了,我们可以使用 postwoman 进行请求,瞅瞅看返回效果。
控制台也收到日志了,后面可以把这些日志请求保留成 .log 文件,这样请求日志也有了,完美!下一步,我们开始连接数据库,这样就不用单机玩泥巴了~ 第二步、配置 mysql
mysql 安装其实很简单,我电脑是 mac 的,所以下面的截图都是以 mac 为例,先下载对应的数据库。
至于其他系统的,可以网上找教程,这个应该烂大街了,我就不重复搬运教程了。
注意:安装的数据库,一定要设置密码,连接数据库必须要有密码,否则会导致连接数据库失败。
mysql 我们只安装数据库就行,熟悉指令的童鞋,就直接命令行操作就行。
不熟悉的话,那就下载图形化管理工具。
◦ ◦ 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. 创建数据库
数据库存在字符集选择,不同的字符集和校验规则,会对存储数据产生影响,所以大家可以自行查询,按照自己存储数据原则选择,我这里默认选最广泛的。确认好,就选择右下角的应用按钮。
2. 创建表和属性 选项解答:
• primary key 是表中的一个或多个列的组合,它用于唯一标识表中的每一行。
• not null 和 unique 就不解释,就是直译的那个意思。 •
generated 生成列是表中的一种特殊类型的列,它的值不是从插入语句中获取的,而是根据其他列的值通过一个表达式或函数生成的。
createtable 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 ◦
主要用途是存储那些需要按字节进行比较的数据,例如加密哈希值
此外也可顺手传创建一个索引,方便快速查找。
createtable`rrweb`.`test_sys_req_log`( `id`intunsignednotnullauto_increment, `content`textnotnull, `l_level`intunsignednotnull, `l_category`varchar(255)notnull, `l_created_at`timestampnotnulldefaultcurrent_timestamp, `l_updated_at`timestampnotnulldefaultcurrent_timestamp, primarykey(`id`), uniqueindex`id_unique`(`id`asc) visible, index`table_index`(`l_level`asc,`l_category`asc,`l_time`asc) visible);
3. 连接数据库
由于目前 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# 安装 typenpm install --save-dev @types/sequelize
配置数据库基础信息
// packages/node-server-demo/src/module/index.tsimport{ 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, ],})exportclassappmodule{}
实体与数据库一一映射处理
import{ getnow }from'@/common/date';import{ model, table, column, primarykey, datatype,}from'sequelize-typescript';@table({ tablename:'test_sys_req_log'})exportclasslogextendsmodel{ @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.tsimport{ 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],})exportclasslogmodule{}
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()exportclasscreatelogservice{ constructor( @injectmodel(log) private logmodel:typeof log, ){} asynccreate(createlogdto: addlogdto):promise{ console.info('createlogservice create > ', createlogdto); const{ level =1, content ='', category ='info'}= createlogdto ||{}; const str = content.trim(); if(!str){ returngeterrres(500,'日志内容为空'); } const item ={ level, category, // tips: 为防止外部数据进行数据注入,我们可以对内容进行 encode 处理。 // content: encodeuricomponent(str), content: str, }; awaitthis.logmodel.create(item); returngetsucvoidres(); }}一路操作猛如虎,回头一看嘿嘿嘿~终于,我们收到了来自外界的第一条数据! 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()exportclassuserservice{ constructor( @injectmodel(user) private usermodel:typeof user, ){} // 创建新数据 asynccreate(user: user){ const newuser =awaitthis.usermodel.create(user); return newuser; } // 查找所有数据 asyncfindall(){ returnthis.usermodel.findall(); } // 按要求查找单个 asyncfindone(id:string){ returnthis.usermodel.findone({ where:{ id }}); } // 按要求更新 asyncupdate(id:string, user: user){ awaitthis.usermodel.update(user,{ where:{ id }}); returnthis.usermodel.findone({ where:{ id }}); } // 按要求删除 asyncdelete(id:string){ const user =awaitthis.usermodel.findone({ where:{ id }}); await user.destroy(); }}tips: 进行删除的时候,我们可以进行假删除,两个数据库,一个是备份数据库,一个是主数据库。主数据库可以直接删除或者增加标识表示删除。备份数据库,可以不用删除只写入和更新操作,这样可以进行数据还原操作。
此外,为了防止 sql 数据库注入,大家需要对数据来源进行统一校验处理或者直接进行 encode 处理,对于重要数据可以直接进行 md5 加密处理,防止数据库被直接下载泄
露。关于 sql 数据库的安全处理,网上教程有很多,大家找一找就可以啦~
部署就比较简单了,我们就不需要一一赘述了,数据库可以用集团提供的云数据库,而 nest 就是普通的 node 部署。
SVA Assertion有什么优势?
PCB电源供电系统设计
闪送、瓜子二手车与聚美优品被约谈 违规收集用户信息
100G QSFP28 CLR4光模块的详细介绍
如何使用12bit高精度示波器测试电源轨噪声方式
Java SpringBoot项目:Node服务端搭建
量子计算的竞争格局在2018年继续升温
LDO集成电路稳压器的结构及选用技术
联电、GF退出FinFET高级工艺跟进,凸显FD-SOI价值
中国动力电池行业排名第三争夺战
还不快些升级!升级iOS10.3后iPhone可用容量立即暴升
盘点:电机故障五大元凶,及其故障处理
存储产品成为了本次CES的一大亮点,引领市场发展新趋势
Apple提出FastViT:快速卷积和Transformer混合架构
康佳特推出基于英特尔凌动® x6000E系列处理器的五种模块
超级电容器用在儿童玩具上,能起到什么作用?
华为首款台式机MateStation跑分测试
解析SMT生产FPC工艺要点
电源设计进阶阅读:如何处理反激电源变压器漏感
短期未来苹果iPhone或走向裸机状态