Skip to content

Midway

约 2609 字大约 9 分钟

typescriptnode.jsmidway

2024-07-26

Midway 一个类似Springboot的一个框架,也具备控制反转和依赖注入的一些特性

学习文档

midwayjs.org

为什么写这篇文章

官方文档全且复杂,使用过程中无法直接找到解决问题的位置。所以这篇文章主要是为了记录遇到的问题以及解决方式从而避免重复花费时间,还有就是博客重新搭建顺便在这篇文章中学习markdown的一些扩展使用。

安装

运行环境:Node.js

直接安装脚手架 选择koa-v3

cmd
npm init midway@latest -y

安装依赖后运行

npm install
npm run dev

关闭prettier/prettie.eslintrc.json 加即可

"rules": {
	"prettier/prettier": "off"
}

官方详细步骤

快速入门

Controller

从后端角度看这个框架首先考虑就是怎么写接口

这一层需要关注的是

  • 请求方式
  • 接口地址
  • 参数接收
  • 参数校验
  • 响应格式
  • 权限校验

如何成为Controller 控制器 ?

使用装饰器@Controller() 放到对应类上

请求方式

@Get@Post@Put@Del

由上述装饰器放于Controller内的方法上

其中有个特殊装饰器@All 能接收任何方式的请求

可以将多个装饰器绑定同一个方法

接口地址

当装饰器@Controller('/api') 装饰该类时 /api为该类的通用前缀

若装饰器 @Get('/home') 装饰该类的其中一个方法时, 则/api/home 为该方法的请求路径

@Controller('/api')
export class HomeController {
	@Get('/home')
	async home(): Promise<string> {
		return 'Hello Midwayjs!';
	}
}

参数接收

常见接收参数要么从请求url取值,要么从请求体内取值

headertoken 非业务流程,不关注

url取值

装饰器获取

@Query 装饰器的有参数,可以传入一个指定的字符串 key,获取对应的值,赋值给入参,如果不传入,则默认返回整个 Query 对象。

// URL = /?id=1
async getUser(@Query('id') id: string) // id = 1
async getUser(@Query() queryData) // {"id": "1"}

Api获取

// src/controller/user.ts
import { Controller, Get, Inject } from "@midwayjs/core";
import { Context } from '@midwayjs/koa';

@Controller('/user')
export class UserController {

  @Inject()
  ctx: Context;

  @Get('/')
  async getUser(): Promise<User> {
    const query = this.ctx.query;
    // {
    //   uid: '1',
    //   sex: 'male',
    // }
  }
}

路径参数获取

// src/controller/user.ts
// GET /user/1
import { Controller, Get, Param } from '@midwayjs/core';

@Controller('/user')
export class UserController {
  @Get('/:uid')
  async getUser(@Param('uid') uid: string): Promise<User> {
    // xxxx
  }
}
// src/controller/user.ts
// GET /user/1
import { Controller, Get, Inject } from '@midwayjs/core';
import { Context } from '@midwayjs/koa';

@Controller('/user')
export class UserController {

  @Inject()
  ctx: Context;

  @Get('/:uid')
  async getUser(): Promise<User> {
    const params = this.ctx.params;
    // {
    //   uid: '1',
    // }
  }
}

请求体取值

装饰器

async getUser(@body('id') id: string) // id = 1
async getUser(@body() queryData) // {"id": "1"}

API 获取

// src/controller/user.ts
// POST /user/ HTTP/1.1
// Host: localhost:3000
// Content-Type: application/json; charset=UTF-8
//
// {"uid": "1", "name": "harry"}
import { Controller, Post, Inject } from '@midwayjs/core';
import { Context } from '@midwayjs/koa';

@Controller('/user')
export class UserController {

  @Inject()
  ctx: Context;

  @Post('/')
  async getUser(): Promise<User> {
    const body = this.ctx.request.body;
    // {
    //   uid: '1',
    //   name: 'harry',
    // }
  }
}

其他方式

参数获取

参数校验

使用validate 进行参数校验 通过正常执行 不通过会抛出异常

底层支持joi.dev - 17.13.3 API Reference

提示

上述链接文章中joi为midway中的RuleType

安装依赖

npm i @midwayjs/validate@3 --save

或者在 package.json 中增加如下依赖后,重新安装。

{
  "dependencies": {
    "@midwayjs/validate": "^3.0.0"
    // ...
  },
  "devDependencies": {
    // ...
  }
}

开启组件

configuration.ts 中增加组件。

点我查看代码
import { Configuration, App } from '@midwayjs/core';
import * as koa from '@midwayjs/koa';
import * as validate from '@midwayjs/validate';
import { join } from 'path';

@Configuration({
  imports: [koa, validate],
  importConfigs: [join(__dirname, './config')],
})
export class MainConfiguration {
  @App()
  app: koa.Application;

  async onReady() {
    // ...
  }
}

注意

这里只介绍常见的方式,不常见的基本不写 很麻烦不是可取之道

参数接收策略

剔除参数中的未定义属性

其它两个 一个是未定义属性报错(默认) 一个是未定义属性仍然接收 感觉不太合理

官方文档

全局配置

点我查看配置
// src/config/config.default.ts
export default {
  // ...
  validate: {
    validationOptions: {
      stripUnknown: true, // 全局生效
    },
  },
};

校验场景

  • DTO校验
详情
// src/dto/user.ts
import { Rule, RuleType } from '@midwayjs/validate';

export class UserDTO {
  @Rule(RuleType.number().required())
  id: number;

  @Rule(RuleType.string().required())
  firstName: string;

  @Rule(RuleType.string().max(10))
  lastName: string;

  @Rule(RuleType.number().max(60))
  age: number;
}
  • 单个参数校验
详情
// src/controller/home.ts
import { Controller, Get, Query } from '@midwayjs/core';
import { Valid, RuleType } from '@midwayjs/validate';
import { UserDTO } from './dto/user';

@Controller('/api/user')
export class HomeController {
  @get('/')
  async getUser(@Valid(RuleType.number().required()) @Query('id') id: number) {
    // ...
  }
}

校验流程

RuleType

类型校验

数字 字符串 数组 对象 布尔 任意类型

RuleType.number()
RuleType.string()
RuleType.array()
RuleType.object()
RuleType.boolean()
RuleType.any()

根据不同类型对其特别处理

数字
方法说明
min(0)最小值为0
max(100)最大值为100
greater(0)/greater(RuleType.ref("num1"))大于0/动态大于num1
less(10)小于10
integer()整数
port()值为端口
multiple(2)值为2的倍数
字符串
方法说明
alphanum()要求字符串值仅包含a-z、A-Z和0-9
dataUri要求字符串值为有效的数据URI字符串
domain要求字符串值为有效域名。
insensitive()该值在后续比较中忽略大小写
length指定精确字符串长度
lowercase字符串值全部为小写,默认自动转化
uppercase字符串值全部为大写,默认自动转化
max指定字符串字符的最大数目
min指定字符串字符的最小数目
truncate指定是否应将string. max() 限制用作截断。不填为ture
pattern/regex定义正则表达式规则
replace用指定的替换字符串替换匹配给定模式的字符
trim去除字符串前后空格
数组
方法说明
items(RuleType.string())/items(getSchema(SchoolDTO))每个元素是字符串/每个元素都是SchoolDTO类型
has(getSchema(StringParamTestDto))要求数组中至少有一个StringParamTestDto类型数据
length指定数组中项的确切数目
max指定数组中的最大项数
min指定数组中的最小项数
sort({order:'descending'})排序,无参数为升序,参数可增加字段by 作为排序指定字段
unique数组唯一
any
方法说明
equal(1,2,3)值只能为1,2,3
disallow(1,2,3)值不能为1,2,3
cast("number")将值转化为数字(有局限性 Joi官方文档
concat组合其它验证方式,如RuleType.number().concat(RuleType.number().equal(1,2,4))
default为空时设置默认值
empty('')将''视为空值
exist该值必须存在 可以为空
required值必须存在 不能为空
error(new Error("自定义错误信息"))验证失败后 自定义错误信息
custom自定义验证

自定义

@Rule(RuleType.any().custom((value)=>{if (value == 1)throw new Error("不能为1"); return value;}))
any1:number;

不符合条件抛异常 符合返回原值

响应格式(统一返回值)

未设置统一返回值时 错误信息以html格式返回 普通数据以原样输出

重要

需求:统一使用Json格式返回

需要使用中间件来实现这一需求

err_filter

  • middleware 目录中创建对应中间件 format.middleware.ts

  • 使用@Middleware() 装饰器绑定该类 实现IMiddleware

    @Middleware()
    export class FormatMiddleware implements IMiddleware<Context, NextFunction> {
    
    }
  • 重写resolve, matchgetName 方法

    详情
    import { IMiddleware, Middleware } from '@midwayjs/core';
    import { Context, NextFunction } from '@midwayjs/koa';
    
    
    @Middleware()
    export class FormatMiddleware implements IMiddleware<Context, NextFunction> {
    	public static getName(): string {
    		return 'format';
    	}
    
    	public resolve() {
    		return async (ctx: Context, next: NextFunction) => {
    			try {
    				const result = await next();
    				if (result === null) {
    					ctx.status = 200;
    				}
    				return {
    					code: 10000,
    					msg: 'OK',
    					data: result,
    				};
    			} catch (err) {
    				// 所有的异常都在 app 上触发一个 error 事件,框架会记录一条错误日志
    				ctx.app.emit('error', err, ctx);
    				ctx._internalError = err;
    				// 从 error 对象上读出各个属性,设置到响应中
    				ctx.body = { code: 10001,status:err.status, msg: err.message};
    			}
    		};
    	}
    
    	public match(ctx: Context): boolean {
    		//匹配通过才会执行该中间件
    		return /^\/(api|browser)/.test(ctx.path);
    	}
    }
  • configuration.ts 注册中间件

    详情
    @Configuration({
        imports: [
            koa,
            rabbitmq,
            validate,
            view,
            upload,
            {
                component: info,
                enabledEnvironment: ['local'],
            },
        ],
        importConfigs: [join(__dirname, './config')],
    })
    export class MainConfiguration {
        @App('koa')
        app: koa.Application;
        
        async onReady() {
            // add middleware
            this.app.useMiddleware([
                ReportMiddleware,
                FormatMiddleware
            ]);
            // add filter
            this.app.useFilter([DefaultErrorFilter]);
        }
    }

权限校验

示例代码
import {
    IMiddleware,
    httpError,
    Config,
    Inject,
    Middleware,
} from '@midwayjs/core';
import { Context, NextFunction } from '@midwayjs/koa';
import { JwtService } from '@midwayjs/jwt';

import { PathToRegexp } from '../comm/pathToRegexp';

@Middleware()
export class JwtMiddleware implements IMiddleware<Context, NextFunction> {
    @Inject()
    private jwtService: JwtService;

    @Inject()
    pathToRegexp: PathToRegexp;

    @Config('jwtWhitelist')
    whitelist;

    public static getName(): string {
        return 'jwt';
    }

    public resolve() {
        return async (ctx: Context, next: NextFunction) => {
            
            const [, token] = ctx.get('Authorization')?.trim().split(' ') || ['', ''];
            if (!token) {
                throw new httpError.UnauthorizedError();
            }
            try {
                const userStr = await this.jwtService.verify(token,process.env.SECRET_KEY, {complete: true});

                ctx.sysUser = userStr['payload'];

            } catch (error) {
                throw new httpError.UnauthorizedError();
            }
            await next();
        };
    }

    public match(ctx: Context): boolean {
        const { path } = ctx;
        if(/^\/(api|browser)/.test(path)){
            const ignore = this.pathToRegexp.pathMatch(this.whitelist, path, false);
            return !ignore
        }else{
            return false
        }
    }
}

Service

Service层包含了应用程序的核心业务逻辑。例如,处理用户的注册、登录、订单生成等操作。

这些业务逻辑通常涉及多个数据操作,Service层负责协调这些操作,确保业务逻辑的完整性和一致性。

这一层需要关注的是

  • 依赖注入
  • 数据操控 CRUD

依赖注入

如何被可以被注入

@Provide()

只需将该装饰器放到类名上

如何注入

前提条件是该类可以被注入

使用@Inject() 装饰器

@Inject()
adminService: AdminService

即可在该类中使用adminService 需要注意的是正确使用方式this.adminService.xxxx()

如果需要打印日志则需要这样注入

@Logger()
logger: ILogger;

数据操控

主要就是数据库的增删改查

事务提交回滚

async add(body){
    let password = generate(10);
    let sysUser = new SysUser(body)
    sysUser.password = md5(password)
    await sysUser.save();
    return password;
}

SysUser 是数据库对应的实体类

await sysUser.save(); 这行执行后 就会在数据库增加一条数据

警告

通过实体类来进行操作数据库时需要加 await

async delete(body: any) {
    return await SysUser.destroy({
        where:{
            id:{
                [Op.in]:body.ids
            }
        }
    });
}

destroy 即为删除方法

where 中为条件限制

async updatePassword(body: UpdatePasswordSysUserDTO){
    let sysUser = await this.getById(body.id);
    if (!sysUser){
        throw new MyError("用户不存在");
    }
    if (sysUser.password != md5(body.oldPassword)){
        throw new MyError("旧密码不正确");
    }
    return await SysUser.update({password:md5(body.password)},{where:{id:body.id}});
}

update 即为修改方法

第一个参数为需要修改的内容 对应sql语句中的set

第二个参数中的where 为条件限制

主键查询

await SysUser.findByPk(body.id)

返回结果为单个或为空

单个查询

await SysUser.findOne({where:{account:'小明'}})

返回结果为单个或为空

where为对应条件限制

列表查询

await SysUser.findAll({where:{id:{[Op.in]:body.ids}}})

返回结果为集合

where为对应条件限制

分页查询

标准分页查询格式 直接套用

let { count, rows }: any = await SysUser.findAndCountAll({
    where:{
        ...whereObj
    },
    limit: body.rows,
    offset: (body.page - 1) * body.rows,
    order:[
        ['create_time','ASC']
    ]
});
return {
    list: rows,
    totalPage: Math.ceil(count / body.rows),
    totalRow: count,
    curPage: body.page
}