MidwayJs Midway (midwayjs.org)、阿里巴巴、node框架、基于koa2、学习分享,官网默认示例也是基于用koa2(不知道为啥不用自家出的egg,不理解)。可能是egg对于ts的支持不是特别友好吧(纯属个人认为)。
话痨时刻
可能会有人回复,文章不错,我选nest 。这这这,咋说呢,其实刚开始了解到这两个框架时,俺也在纠结,不知道该选哪个好,也在网上找了一些关于两者的文章和对比,也很苦劳,不知道选啥好,但最终还是向着"自家人"
了。这里请允许俺解释一下,俺为啥决定选 MidwayJs
:
- 毕竟是
"自家人"
开发的,中文文档比较通俗易懂,比较符合国人的开发习惯,还用官方群交流答疑; - nest是基于express,而Midway是基于koa2的,两者的底层不同(也不能这样说,毕竟人家koa2也是基于express的),俺个人刚开始习惯用koa2去写服务;
- next官网说它借鉴了angular的理念,那时候俺只听说过angular,并没有真正去了解过,nest比较符合国外开发者的习惯,有中文文档但都是英译的或国内个人开发者总结的;
- 都说 技术无国界 ,是真的无国界吗!
以上都是在俺的个人理解下,才决定选用Midway,当然,不管是nest还是Midway,各有各的优势和缺点,根据个人喜好和环境去选择,它们都是很不错的框架;
第一个服务
服务技术选型:
- node
- pnpm
- midway
- typescript
- kao2
- typeorm
- mysql
- redis
- swagger
实现基于用户相关的接口和增删改查的demo
初始化创建
cmd
pnpm create midway
cmd
cd midway-project
pnpm install
pnpm dev
# 启动后浏览器访问:http://127.0.0.1:7001
调整ESLint配置
为了保证代码分隔统一,我们调整下ESLint配置
cmd
// .prettierrc.js
module.exports = {
...require('mwts/.prettierrc.json'),
semi: false,
printWidth: 100,
tabWidth: 2,
useTabs: false,
singleQuote: true,
endOfLine: 'lf',
trailingComma: 'none',
}
ESLint 中文网 (nodejs.cn),不知道如何配置的话可以去官网查。
项目基础结构
bash
├─logs # 日志文件
├─node_modules # 项目依赖
├─src # 源码目录
│ ├─config # 配置
│ ├─controller # 控制器
│ ├─entity # 数据对象模型
│ ├─filter # 过滤器
│ ├─middleware # 中间件
│ ├─service # 服务类
│ ├─configurations.ts # 服务生命周期管理及配置
│ └─interface.ts # 接口定义
├─test # 测试类目录
├─bootstrap.js # 启动入口
├─package.json # 包管理配置
├─tsconfig.json # TypeScript 编译配置文件
使用vscode启动项目并调试
你可以不输入命令行,直接点击小绿三角形,就可以运行项目,当然,你也可以去package.json中可以看到调试字样,点击选择对应的命令即可。(俺只是觉得这个方法更帅一点,就这样😎)
json
// 覆盖.vscode/launch.json初始配置
{
// 使用 IntelliSense 了解相关属性。
// 悬停以查看现有属性的描述。
// 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [{
"name": "Midway Local",
"type": "node",
"request": "launch",
"cwd": "${workspaceRoot}",
"runtimeExecutable": "pnpm",
"windows": {
"runtimeExecutable": "pnpm.cmd"
},
"runtimeArgs": [
"dev"
],
"env": {
"NODE_ENV": "local"
},
"console": "integratedTerminal",
"restart": true,
"autoAttachChildProcesses": true
}]
}
数据库mysql
可能有滴人不会用 docker ,那俺就在本地创建个数据库。
cmd
mysql -u root -p
password: ******
create database twhc;
使用TypeORM
TypeORM:(github.com)是
node.js
现有社区最成熟的对象关系映射器(ORM )。
相关文档:
安装 typeorm 相关依赖,提供数据库ORM:
cmd
pnpm i @midwayjs/typeorm@3 typeorm --save
在 src/configuration.ts
引入 orm 组件:
ts
import { Configuration, App } from '@midwayjs/core';
import * as koa from '@midwayjs/koa';
import * as orm from '@midwayjs/typeorm';
// 略
@Configuration({
imports: [
koa,
orm,
// 略
],
// 略
})
// 略
安装数据库mysql
cmd
pnpm install mysql2 --save
配置src/config/config.default.ts
文件:
ts
import { MidwayConfig } from '@midwayjs/core';
export default {
// use for cookie sign key, should change to your own and keep security
keys: '1701053437454_8538',
koa: {
port: 7001,
},
typeorm: {
dataSource: {
default: {
// 单个数据库
type: 'mysql',
host: 'localhost',
port: 3306,
username: 'root',
password: '123456',
database: 'twhc',
entities: ['**/entity/*{.ts,.js}'], // 扫描entity文件夹
synchronize: true, // 如果第一次使用,不存在表,有同步的需求可以写 true,注意会丢数据
logging: true
}
}
}
} as MidwayConfig;
数据库客户端连接已创建的数据库,俺用的是 DataGrip 2023.2.2
推荐使用Navicat
,它对 mysql
的支持更好,但俺用DataGrip
用习惯了,两个软件都是收费的,免费的推荐 DBeaver
Download | DBeaver Community --- 下载 |DBeaver 社区 这个挺好用的:
创建实体模型
在 entity
文件夹下创建个简单滴 user.ts
,使用 Entity
来定义一个实体模型类:(实现数据库同步更新数据表和字段)
ts
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm'
@Entity('user')
export class User {
@PrimaryGeneratedColumn()
id: number
@Column()
username: string
@Column()
password: string
}
启动服务,查看数据库客户端:
测试一下typeorm,改造src/controller/home.controller.ts
文件:
ts
import { Controller, Get } from '@midwayjs/core';
import { InjectEntityModel } from '@midwayjs/typeorm';
import { User } from '../entity/user';
import { Repository } from 'typeorm';
@Controller('/')
export class HomeController {
// 自动注入模型
@InjectEntityModel(User)
userModel: Repository<User>;
@Get('/')
async home(): Promise<User[]> {
// 查询user表数据
return await this.userModel.find();
}
}
出现以下图示,表示没问题,因为还没有插入数据,所以为空:
手动在数据库中添加一条数据,再测试一下:
缓存redis
使用docker安装redis:
小提示: 如果你不会使用 docker
的话,你可以看看俺之前写的文章:学习前端,还是要会点 docker 的,瞅瞅这篇文章,新手入门必备 - 掘金 (juejin.cn)。
拉取redis镜像:
bash
# 搜索镜像
docker search redis
# 拉取
docker pull redis
# 创建目录用于挂载
mkdir -p /home/redis/myredis /home/redis/myredis/data
myredis.conf 是俺手动上传的 (redis.conf的标准文件在redis官网也可以找到):
conf
# bind 192.168.1.100 10.0.0.1
# bind 127.0.0.1 ::1
#bind 127.0.0.1
protected-mode no
port 6379
tcp-backlog 511
requirepass 123456
timeout 0
tcp-keepalive 300
daemonize no
supervised no
pidfile /var/run/redis_6379.pid
loglevel notice
logfile ""
databases 30
always-show-logo yes
save 900 1
save 300 10
save 60 10000
stop-writes-on-bgsave-error yes
rdbcompression yes
rdbchecksum yes
dbfilename dump.rdb
dir ./
replica-serve-stale-data yes
replica-read-only yes
repl-diskless-sync no
repl-disable-tcp-nodelay no
replica-priority 100
lazyfree-lazy-eviction no
lazyfree-lazy-expire no
lazyfree-lazy-server-del no
replica-lazy-flush no
appendonly yes
appendfilename "appendonly.aof"
no-appendfsync-on-rewrite no
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
aof-load-truncated yes
aof-use-rdb-preamble yes
lua-time-limit 5000
slowlog-max-len 128
notify-keyspace-events ""
hash-max-ziplist-entries 512
hash-max-ziplist-value 64
list-max-ziplist-size -2
list-compress-depth 0
set-max-intset-entries 512
zset-max-ziplist-entries 128
zset-max-ziplist-value 64
hll-sparse-max-bytes 3000
stream-node-max-bytes 4096
stream-node-max-entries 100
activerehashing yes
hz 10
dynamic-hz yes
aof-rewrite-incremental-fsync yes
rdb-save-incremental-fsync yes
启动redis容器:
bash
docker run --restart=always \
--log-opt max-size=100m \
--log-opt max-file=2 \
-p 6379:6379 \
--name myredis \
-v /home/redis/myredis/myredis.conf:/etc/redis/redis.conf \
-v /home/redis/myredis/data:/data \
-d redis redis-server /etc/redis/redis.conf \
--appendonly yes \
--requirepass 123456
--restart=always
总是开机启动--log
是日志方面的-p 6379:6379
将6379端口挂载出去(加粗是容器端口,:前面是宿主机端口)--name
给这个容器取一个名字-v
数据卷挂载: /home/redis/myredis/myredis.conf:/etc/redis/redis.conf 这里是将 liunx路径下的myredis.conf 和redis下的redis.conf 挂载在一起。 /home/redis/myredis/data:/data 这个同上-d redis
表示后台启动redisredis-server /etc/redis/redis.conf
以配置文件启动redis,加载容器内的conf文件,最终找到的是挂载的目录 /etc/redis/redis.conf 也就是liunx下的/home/redis/myredis/myredis.conf--appendonly yes
开启redis 持久化--requirepass 123456
设置密码 (设置密码只有好处,信俺没错😁)
查看容器运行日志:(--since 30m 是查看此容器30分钟之内的日志情况
,同时也表示可成功运行)
bash
docker logs --since 30m myredis
进入容器:(此处跟着的 redis-cli
是直接将命令输在上面了)
bash
docker exec -it myredis redis-cli
进入后,你还需输入密码:(不然会出现如下报错)
bash
auth 123456
当出现OK字样时,就可以正常使用redis相关命令了。
redis常用命令_redis 控制台命令-CSDN博客,该篇文章有redis的一些常用命令(为啥选着用控制台去查看redis呢,啊,这这这,因为穷,那些redis客户端连接工具都要钱呀,晓得不。当下的业务场景还用不到redis那么频繁,只用到了存储就行了)
在项目中安装redis依赖:
cmd
pnpm i @midwayjs/redis@3 --save
引入redis组件,在 src/configuration.ts
中导入:
ts
import { Configuration, App } from '@midwayjs/core';
// 略
import * as redis from '@midwayjs/redis';
import * as validate from '@midwayjs/validate';
// 略
@Configuration({
imports: [
// 略
redis,
validate,
// 略
],
// 略
})
// 略
在src/config/config.default.ts
配置redis:
ts
import { MidwayConfig } from '@midwayjs/core';
export default {
// 略
redis: {
client: {
port: 6379, // Redis port
host: "192.168.169.128", // Redis host
password: "123456",
db: 0,
}
}
} as MidwayConfig;
使用redis服务:
ts
import { Controller, Get, Inject } from '@midwayjs/core';
// 略
import { RedisService } from '@midwayjs/redis';
@Controller('/')
export class HomeController {
// 自动注入redis服务
@Inject()
redisService: RedisService;
// 略
async home(): Promise<string> {
await this.redisService.set('username', '吴某凡');
return await this.redisService.get('username');
}
}
连接的是索引为0的库:
bash
# 切换到该索引下的库
select 0
# 表示查看该库下所有的key
keys *
# get key 查看key的值
get username
swagger接口文档
Swagger框架,用于快速生成、描述、调用和可视化 RESTful 风格的 Web 服务。它可以在线快速生成接口文档,以及快速测试接口。
安装相关依赖:
cmd
pnpm install @midwayjs/swagger@3 --save
# 如果想要在服务器上输出 Swagger API 页面,则需要将 swagger-ui-dist 安装到依赖中
pnpm install swagger-ui-dist --save-dev
开启组件:在 configuration.ts
中(可以配置启用的环境,比如下面的代码指的是"只在 local 环境下启用")
ts
// 略
import * as swagger from '@midwayjs/swagger';
// 略
@Configuration({
imports: [
// 略
{
component: swagger,
enabledEnvironment: ['local']
},
// 略
],
// 略
})
// 略
然后启动项目,访问地址:
路径可以通过 swaggerPath
参数配置。
多语言国际化
cmd
pnpm i @midwayjs/i18n@3 --save
使用组件:在 configuration.ts
中:
ts
// 略
import * as i18n from '@midwayjs/i18n';
// 略
@Configuration({
imports: [
// 略
i18n,
// 略
],
// 略
})
// 略
配置多语言方案:新建 src/locale
目录,用于放置文案文件
bash
# 目录结构
├── src
│ ├── locales
| │ ├── en_US.json # 英文
| │ └── zh_CN.json # 中文
json
// en_US.json
{
"hello": "Hello"
}
json
// zh_CN.json
{
"hello": "你好"
}
在 src/config/config.default.ts
加入这两个 JSON,其中 default
是语言的默认分组:
ts
import { MidwayConfig } from '@midwayjs/core';
export default {
// 略
i18n: {
localeTable: {
en_US: {
default: require('../locales/en_US')
},
zh_CN: {
default: require('../locales/zh_CN')
}
}
}
} as MidwayConfig;
在 home.controller.ts
使用一下:
ts
// 略
import { MidwayI18nService } from '@midwayjs/i18n';
@Controller('/')
export class HomeController {
// 自动注入i18n服务
@Inject()
i18nService: MidwayI18nService;
@Get('/')
async home(): Promise<string> {
return await this.i18nService.translate('hello', {args: {username: '小甜甜'}, locale: 'en_US'})
}
}
将 locale: 'zh_CN'
后:
统一参数校验
选着刚开始的架构创建的项目默认安装了 @midwayjs/validate
。新建 src/dto/user.ts
:
ts
import { Rule, RuleType } from '@midwayjs/validate';
export class UserDTO {
@Rule(RuleType.string().required())
password: string;
}
ts
// src/controller/home.controller.ts
import { Controller, Body, Inject, Post } from '@midwayjs/core';
// 略
import { UserDTO } from '../dto/user';
@Controller('/')
export class HomeController {
// 略
@Post('/')
async home(@Body() user: UserDTO): Promise<void> {
console.log(user);
}
}
使用swagger-ui测试一下,先传一个空对象给后端:
传入个password测试一下:
自定义报错文本:
ts
// src/dto/user.ts
@Rule(RuleType.string().required().error(new Error('密码不能为空!')))
password: string;
统一异常处理
在统一参数校验中,当校验失败时,Response body放回的是html,不想要这要的错误形式,可以换成json格式。
Midway提供了一个内置的异常处理器,负责处理应用程序中所有未处理的异常。当您的应用程序代码抛出一个异常处理时,该处理器就会捕获该异常,然后等待用户处理。
异常处理器的执行位置处于中间件之后,所以它能拦截所有的中间件和业务抛出的错误。
新建 filter/validate.filter.ts
:
ts
import { Catch } from '@midwayjs/decorator';
import { MidwayValidationError } from '@midwayjs/validate';
import { Context } from '@midwayjs/koa';
import { MidwayI18nService } from '@midwayjs/i18n';
@Catch(MidwayValidationError)
export class ValidateErrorFilter {
async catch(err: MidwayValidationError, ctx: Context) {
// 获取国际化服务
const i18nService = await ctx.requestContext.getAsync(MidwayI18nService);
// 翻译
const message = i18nService.translate(err.message) || err.message;
// 未捕获的错误,是系统错误,错误码是500
ctx.status = 422;
return {
code: 422,
message,
};
}
}
在configuration.ts
文件中,注册刚才我们创建的过滤器:
ts
import { Catch } from '@midwayjs/decorator';
import { MidwayValidationError } from '@midwayjs/validate';
import { Context } from '@midwayjs/koa';
import { MidwayI18nService } from '@midwayjs/i18n';
@Catch(MidwayValidationError)
export class ValidateErrorFilter {
async catch(err: MidwayValidationError, ctx: Context) {
// 获取国际化服务
const i18nService = await ctx.requestContext.getAsync(MidwayI18nService);
// 翻译
const message = i18nService.translate(err.message) || err.message;
// 未捕获的错误,是系统错误,错误码是500
ctx.status = 422;
return {
code: 422,
message,
};
}
}
公共业务异常处理
在开发过程中,可能会需要做一些业务校验,业务校验的时候,我们需要对外抛出异常,这时候我们需要封装公共的业务异常类,和业务异常过滤器
创建 src/common/common.error.ts
:
ts
import { MidwayError } from '@midwayjs/core'
export class CommonError extends MidwayError {
constructor(message: string) {
super(message)
}
}
在filter下新建 common.filter.ts
:
ts
import { Catch } from '@midwayjs/decorator'
import { CommonError } from '../common/common.error'
import { Context } from '@midwayjs/koa'
import { MidwayI18nService } from '@midwayjs/i18n'
@Catch(CommonError)
export class CommonErrorFilter {
async catch(err: CommonError, ctx: Context) {
// 获取国际化服务
const i18nService = await ctx.requestContext.getAsync(MidwayI18nService)
// 翻译
const message = i18nService.translate(err.message) || err.message
// 未捕获的错误,是系统错误,错误码是500
ctx.status = 400
return {
code: 400,
message
}
}
}
在src/configuration.ts
中注册过滤器:
ts
// 略
import { CommonErrorFilter } from './filter/common.filter'
// 略
export class MainConfiguration {
@App('koa')
app: koa.Application
async onReady() {
// 略
// add filter
this.app.useFilter([ValidateErrorFilter, CommonErrorFilter])
}
}
因为common.filter.ts
使用了国际化:
json
// zh_CN.json
{
"hello": "你好 {username}",
"error": "出错啦"
}
测试:在home.controller.ts中
ts
// 略
import { CommonError } from '../common/common.error'
@Controller('/')
export class HomeController {
// 略
@Post('/')
async home(): Promise<void> {
throw new CommonError('error')
}
}
打印日志
对于后端来说日志还是很重要的,有利于后期定位线上bug,midway也内置了一套日志组件,用起来很简单
ts
// home.controller.ts
// 略
// 日志打印
import { ILogger } from '@midwayjs/logger'
@Controller('/')
export class HomeController {
@Inject()
logger: ILogger
@Post('/')
async home(@Body() user: UserDTO): Promise<void> {
this.logger.info('hello')
console.log(user)
}
}
测试
测试还是很重要的,默认脚手架中,已经提供了这东西,所以你可以开箱即用的运行测试,仔细了解可看官方文档测试 | Midway (midwayjs.org)。俺这就不举例了,官方文档有举例。复制粘贴就行了。
demo
实现基于用户表增删改查的demo
修改用户实体类
俺将 entity/user.ts
修改为 entity/user.entity.ts
,不想与其他目录下的文件名重复(自己单纯洁癖,不改也行):
详细的参数配置可查看文档 TypeORM | Midway (midwayjs.org)
你可具体对应列的类型,这要可让数据库更加灵活,相关文档 实体 | TypeORM 中文文档 | TypeORM 中文网 (bootcss.com)
ts
import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn } from 'typeorm'
@Entity('user')
export class User {
@PrimaryGeneratedColumn({ comment: '主键ID' })
id: number
@Column({ comment: '用户头像', nullable: true })
avatarUrl: string
@Column({ length: 50, unique: true, comment: '用户名' })
username: string
@Column({ length: 30, comment: '密码' })
password: string
@Column({ length: 11, default: '', comment: '手机号' })
phone: string
@Column({ type: 'bigint', nullable: true, comment: '更新用户ID' })
updaterId: number
@Column({ type: 'bigint', nullable: true, comment: '创建用户ID' })
createrId: number
@Column({ type: 'int', default: 1, comment: '状态 1 正常 0 禁用' })
status: number
@CreateDateColumn({ comment: '创建时间' })
createTime: Date
@UpdateDateColumn({ comment: '更新时间' })
updateTime: Date
}
对应的数据库结构:
用户信息参数校验
俺将 dto/user.ts
文件名改成了 user.dto.ts
:使用到swagger来显示相关信息,使用其中的 ApiProperty
将其中的每个属性都进行了定义
ts
import { Rule, RuleType } from '@midwayjs/validate'
import { ApiProperty } from '@midwayjs/swagger'
export class UserDTO {
@ApiProperty({ description: '用户id' })
@Rule(RuleType.allow(null))
id?: number
@ApiProperty({ description: '用户名' })
@Rule(RuleType.string().required().error(new Error('用户名不能为空!')))
username: string
@ApiProperty({ description: '密码' })
@Rule(RuleType.string().required().error(new Error('密码不能为空!')))
password: string
}
用户服务层
创建 service/user.service.ts
:
你要是看不懂这上面的有些命令,那就在官网上搜,官网上啥都有。
ts
import { Provide } from '@midwayjs/core'
import { InjectEntityModel } from '@midwayjs/typeorm'
import { Repository } from 'typeorm'
import { User } from '../entity/user.entity'
import { IUserOptions } from '../interface'
@Provide()
export class UserService {
@InjectEntityModel(User)
userModel: Repository<User>
// 新增
async cerate(user: User) {
await this.userModel.save(user)
return user
}
async getUser(options: IUserOptions) {
return {
uid: options.uid,
username: 'mockedName',
phone: '12345678901',
email: 'xxx.xxx@xxx.com'
}
}
}
有的代码段是脚手架自带的,俺嫌麻烦这就删除了。
用户控制层
创建 controller/user.controller.ts
文件:(提一嘴,@midwayjs/core和@midwayjs/decorator中都有类似的API,调用那个的都行,俺根据脚手架默认的来用@midwayjs/core里的)
ts
import { Body, Controller, Inject, Post, Provide, ALL } from '@midwayjs/core'
import { Validate } from '@midwayjs/validate'
import { UserDTO } from '../dto/user.dto'
import { UserService } from '../service/user.service'
import { User } from '../entity/user.entity'
@Provide()
@Controller('/user')
export class UserController {
// 注入用户服务层
@Inject()
userService: UserService
// 创建用户
@Post('/')
@Validate()
async create(@Body(ALL) data: UserDTO) {
const user = new User()
user.username = data.username
user.password = data.password
return await this.userService.create(user)
}
}
在http://127.0.0.1:7001/swagger-ui/index.html里进行测试:
在之前的用户信息参数校验中结合了swagger,使得在swagger文档中有校验参数提示:
好啦好啦
好啦好啦,不能再继续写了,再写的话就不是借鉴了,那就是 Ctrl+C+V
了。如果你还想听俺继续唠的话,可以去俺的gitee去看README.md README.md · 逗逗不秃头/midway-stud... ,只要我还在学midway就会持续更新仓库,直到把它做成一个完整的后端服务为止。但是。前端小付 这个博主,反正对于刚接触midway的俺是真喜欢呀。讲滴嘎嘎好,推荐推荐。
借鉴文章
- 后端框架搭建------从零开始搭建一个高颜值后台管理系统全栈框架(二) - 掘金 (juejin.cn) ------ 前端小付 ;真滴,宝藏博主,俺认为😍
- midway3.0的深入浅出的最佳实践分享 - 掘金 (juejin.cn) ------ LetMeTouchTouch ;