可以将多个装饰器绑定同一个方法
外观
外观
Midway 一个类似Springboot的一个框架,也具备控制反转和依赖注入的一些特性
学习文档
官方文档全且复杂,使用过程中无法直接找到解决问题的位置。所以这篇文章主要是为了记录遇到的问题以及解决方式从而避免重复花费时间,还有就是博客重新搭建顺便在这篇文章中学习markdown的一些扩展使用。
运行环境:Node.js
直接安装脚手架 选择koa-v3
npm init midway@latest -y
安装依赖后运行
npm install
npm run dev
关闭prettier/prettie
在.eslintrc.json
加即可
"rules": {
"prettier/prettier": "off"
}
官方详细步骤
从后端角度看这个框架首先考虑就是怎么写接口
这一层需要关注的是
如何成为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取值,要么从请求体内取值
从header
取token
非业务流程,不关注
装饰器获取
@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, // 全局生效
},
},
};
// 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 | 数组唯一 |
方法 | 说明 |
---|---|
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格式返回
需要使用中间件来实现这一需求
在middleware
目录中创建对应中间件 format.middleware.ts
使用@Middleware()
装饰器绑定该类 实现IMiddleware
@Middleware()
export class FormatMiddleware implements IMiddleware<Context, NextFunction> {
}
重写resolve
, match
和 getName
方法
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层负责协调这些操作,确保业务逻辑的完整性和一致性。
这一层需要关注的是
如何被可以被注入
@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
}