# YoyoThink **Repository Path**: Y_oyo/yoyo-think ## Basic Information - **Project Name**: YoyoThink - **Description**: 一个封装的 koa 动态路由控制器框架, 你可以用ThinkPHP 那种 控制器 模型 中间件 去开发 你的项目 项目内置了 websocket框架 ws 库 软件架构 参考ThinkPHP6 框架进行模仿架构 并支持 plugins 自定义中间件扩展 - **Primary Language**: TypeScript - **License**: MIT - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 8 - **Forks**: 2 - **Created**: 2024-11-07 - **Last Updated**: 2025-07-31 ## Categories & Tags **Categories**: webframework **Tags**: yoyo, Koa, MVC, ORM, TypeScript ## README # YoyoThink #### 介绍 一个封装的 koa 动态路由控制器框架, 你可以用ThinkPHP 那种 控制器 模型 中间件 去开发 你的项目 项目内置了 websocket框架 ws 库 软件架构 参考ThinkPHP6 框架进行模仿架构 并支持 plugins 自定义中间件扩展 #### 软件目录说明 ``` markdown ├── @types - 类型定义目录 ├── App - 项目目录 | | ├── controller - 控制器目录 | | ├── middleware - 中间件目录 | | ├── model - 模型目录 | | ├── view - 视图目录(暂定) | | ├── schedule - 定时任务目录 | | ├── validate - 验证器目录 | | ├── common.ts - 控制器公共函数文件 (G.方法调用) | | ├── WebSocket.ts - websocket 控制器 ├── dist - 构建目录(默认执行 npm run build 后构建的js文件) ├── Config │ ├── Base.ts - 基础配置(项目的基础) │ ├── Cache.ts - 缓存配置 │ ├── DataBase.ts - 数据库配置 │ ├── Log.ts - 日志系统配置 │ ├── Route.ts - 路由配置 │ └── WebSocket.ts - websocket 配置 ├── Frame - 框架目录(通常不需要修改) | ├── Plugins - 插件目录(新增或者修改插件) | ├── CacheBase.ts - 缓存基类 | ├── ClusterManager.ts - 集群管理 | ├── ControllerBase.ts - 控制器基类 | ├── Log4.ts - 日志系统 | ├── viewBase.ts - 视图基类 | ├── CronBase.ts - 定时任务基类 | ├── WebSocketBase.ts - websocket 基类 | ├── ModelBase.ts - 模型基类 | ├── RouteBase.ts - 路由基类 | ├── ValidateBase.ts - 验证器基类 ├── Public - 公共目录 ├── Routes - 路由目录 ├── Runtime - 运行时目录 | ├── logs - 日志目录 | ├── temp - 临时目录 | | ├── uploads - 临时上传目录 ├── Views - 视图目录 | ├── xxx.ejs - 视图文件 ├── Index.ts - 入口文件 ├── jsToJsc.js - js转jsc的脚本(先执行npm run build编译js) ├── package.json - 项目配置文件 ├── tsconfig.json - ts 配置文件 ``` #### 安装教程 1. 先拉取项目 ``` git clone https://gitee.com/Y_oyo/yoyo-think.git ``` 2. 再安装依赖 ``` npm install ``` 3. 然后启动项目 ``` npm run dev ``` 4. 最后访问项目 ``` http://localhost:3000 ``` 5. 如果需要打包项目 ``` npm run build ``` 6. 打包后的项目在 dist 目录下,只需要复制里面的文件到你的项目目录即可 ``` npm run start ``` 这样您的项目就在服务器正式运行起来了 #### 控制器 controller 控制器文件通常放在`controller`下面,类名和文件名保持大小写一致 目前框架是单应用模式,那么控制器的类的定义如下: ```ts import * as C from "/Frame/ControllerBase"; export default class index implements C.ControllerBase { index(ctx: C.Context,params: C.Params) { return '你好YoyoThink'; } } ``` 控制器类文件的实际位置则变成 ``` /App/controller/index.ts ``` 访问URL地址是(假设没有开启只允许强路由的情况下) ``` http://localhost/index/index ``` 控制器和中间件都包含 `ctx.isRouteStrong` 当前是否强路由 ,以及 `ctx.reqPath` 里面是当前路由的路径以及访问的控制器和方法,注意一点(强路由函数`ctx.reqPath`是空白的) ```ts Context.reqPath: { /**当前访问的控制器名字 */ controller: string; /**当前访问的方法名字 */ apiName: string; /**当前访问的路径到控制器 */ filePath: string; } ``` ##### 渲染输出 默认情况下,控制器的输出全部采用`return`的方式,无需进行任何的手动输出,系统会自动完成渲染内容的输出。 下面都是有效的输出方式: ```ts import * as C from "/Frame/ControllerBase"; export default class index implements C.ControllerBase { hello(ctx: C.Context,params: C.Params) { return '你好YoyoThink'; } json(ctx: C.Context,params: C.Params) { return ctx.json('我是变量'); } read(ctx: C.Context,params: C.Params) { // 渲染默认模板输出 return ctx.view({data:'我是传递的变量'}); } } ``` ##### 多层控制器 支持任意层次的控制器,并且支持路由,例如: ```ts import * as C from "/Frame/ControllerBase"; export default class user implements C.ControllerBase { index(ctx: C.Context,params: C.Params) { return '你好YoyoThink'; } } ``` 该控制器类的文件位置为: ``` /App/controller/v1/user.ts ``` 访问地址可以使用 ``` http://localhost/v1/user/index ``` #### 强路由 Routes 因为默认是动态路由是基于控制器 `App/controller`下的文件结构来实现的,当然如果您在路由配置`Config/Route.ts`开启了`Reinforce: true`只允许强路由那么控制器的动态路由将不会注册。只会采用您配置的强路由。强路由位于根目录 `Routes`里面 这里也可也多层文件夹格式、我们来看看具体格式举例吧: ```ts import { Route } from '/Frame/RouteBase'; /** 指向index控制器里面的index方法 */ Route.all('/', 'index/index'); /** 指向v1文件夹下的index控制器里面的index方法 */ Route.get('/v1', 'v1/index/index'); Route.get('/ql/test2', '/index/test2'); Route.get('/ql/test3', '/index/test3'); /** 你也可以自定义路由实现 */ Route.all('/dynamic/:id/:name', 'index/test'); Route.get('/test/:id', async (ctx) => { logger.info(ctx.reqPath,'--',ctx.isRouteStrong); return ctx.params; }); /** 创建一条重定向 */ Route.redirect('/redirect', '/index/1'); /** 创建一个404页面 */ Route.get('/404', async (ctx) => { ctx.throw(404, '这是路由创建的404页面!'); }); /** 你可以连续创建路由 */ Route.delete('/1', 'index/index') .post('/2', 'index/index') .put('/3', 'index/index') .get('/4', async (ctx) => { throw [7000, 'Token过期'];//逻辑拒绝 }); ``` #### 控制器中间件 middleware 支持为控制器定义中间件,你只需要在你的控制器中定义`_middleware`属性,例如: ```ts import * as C from "/Frame/ControllerBase"; import { isLogin } from "/App/middleware/index"; export default class index implements C.ControllerBase { _middleware = [isLogin]; index(ctx: C.Context,params: C.Params) { return '你好YoyoThink'; } } ``` 当执行`index`控制器的时候就会调用`_middleware`里面的中间件 ##### 中间件详细 下列是中间件的详情 ```ts import { Context, Next } from "koa"; export async (ctx: Context, next: Next) => { // console.log('中间件:',ctx.reqPath); await next();//这个需要放行才可以执行控制器的方法 } ``` 获取你也可以这样取做一个中间件工厂 ```ts import { Context, Next } from "koa"; export function isLogin(filterName:Array) { // console.log('中间件工厂:', a); return async (ctx: Context, next: Next) => { // console.log('中间件:',ctx.reqPath); await next(); } } ``` #### 数据库模型 model 就是用于数据库操作和控制器交互数据的位于 `/App/model`同样支持多层文件夹构造,该功能实现与`yoyomysql`我们看看模型具体基础的使用 ```ts import db from "/Frame/ModelBase"; export default new class index { constructor(){ // 在服务启动时,被控制器引入即可执行 } async yoyo() { return db.table("user").select(); } } ``` 具体教程文档点击我 [YoyoMysql](https://www.npmjs.com/package/yoyomysql) #### 验证器 validate 为具体的验证场景或者数据表定义好验证器类,直接调用验证类的`check`方法即可完成验证,下面是一个例子: 我们定义一个`/App/validate/User`验证器类用于`User`的验证。 ```ts import { ValidateBase, Scene, checkType } from "/Frame/ValidateBase"; class User extends ValidateBase { rule = { email: 'require|email|between:5,30', password: 'require|numberEn|min:5|max:32', test:'require|regular:^[\\w\\u4E00-\\u9FA5]{2,15}$',//还支持自定义正则 testObj:{ require: true, regular: '^(qq|wx)$' },//或者这种方式的写法 }; message = { email: '邮箱地址格式不正确', password: '密码不符合规则', 'password.min': '密码最小${1}个字符', 'password.max': '密码最大${1}个字符', }; /** * 快捷场景(优先级低于方法的场景) * 数组选择的字段只能是rule已经配置好的(没有rule配置的将无效) */ scene: Scene = { Login: ['email', 'password'], }; } /** * (装饰器)验证 * @param sceneName 场景名字 * @param (header|body|query|params) header请求头部 body请求体 query URL请求参数 params路由参数 * @returns */ export const Decorate = (sceneName: string, checkType: checkType) => index.Decorate(index, sceneName, checkType); /** * 方法验证(需要try做异常处理) * @param sceneName 场景名字 * @returns validate 可以再用check(data)方法进行验证 */ export const Validate = (sceneName: string) => index.validate(sceneName); ``` ##### 数据验证 在需要进行`User`验证的控制器方法中,添加如下代码即可: ```ts import * as C from "/Frame/ControllerBase"; import { Decorate as DUser, Validate as VUser } from '/App/validate/User'; export default class User implements C.ControllerBase { //(装饰器方式) 场景名 和 对应的参数集合 @DUser('Login', 'query') index(ctx: C.Context,params: C.Params) { return '验证成功'; } index2(ctx: C.Context,params: C.Params) { try { // 引入指定验证场景名称 再调用 check 取检查指定对象值 VUser('Login').check(ctx.query); } catch (error: any) { // 出现错误 这里用这个可以自定义抛出json错误 error.message 就是您验证器对应的错误信息 throw [7000, error.message]; } return '验证成功2'; } } ``` ##### 高级的验证场景 上面scene属性指定的是选用上方存在的字段,但是会出现同个字段复用,条件又不同怎么办?就可以尝试这种方案 `sceneLogin(that: ValidateBase)`自定义scene开头的方法 后面拼接您的场景名注意首字母大写 ```ts import { ValidateBase, Scene, checkType } from "/Frame/ValidateBase"; class User extends ValidateBase { rule = { email: 'require|email|between:5,30', password: 'require|numberEn|min:5|max:32', }; message = { email: '邮箱地址格式不正确', password: '密码不符合规则', 'password.min': '密码最小${1}个字符', 'password.max': '密码最大${1}个字符', }; /** * 快捷场景(优先级低于方法的场景) * 数组选择的字段只能是rule已经配置好的(没有rule配置的将无效) */ scene: Scene = { Login: ['email', 'password'], }; /** * 登录的验证场景(必须scene开头的方法)(优先级高于scene属性) * @returns */ sceneLogin(that: ValidateBase) { return that.only(['email'])// 'password', 'age' // .append('email', 'max:15')//增加一个规则(字符串) // .append('age', 'require|between:1,3|int')//增加多个规则(字符串) // .remove('password', 'numberEn')//移除指定的 // .remove('password', 'numberEn|min')//移除指定的多个(字符串) // .remove('password', ['numberEn','max'])//移除指定的多个(数组) // .remove('password')//移除该字段全部 .append('password', { min: 1, max: 10, isYoyo: true });//增加一个规则(对象) } } /** * (装饰器)验证 * @param sceneName 场景名字 * @param (header|body|query|params) header请求头部 body请求体 query URL请求参数 params路由参数 * @returns */ export const Decorate = (sceneName: string, checkType: checkType) => index.Decorate(index, sceneName, checkType); /** * 方法验证(需要try做异常处理) * @param sceneName 场景名字 * @returns validate 可以再用check(data)方法进行验证 */ export const Validate = (sceneName: string) => index.validate(sceneName); ``` ##### 自定义验证规则 系统内置了一些常用的规则(参考后面的内置规则),如果不能满足需求,可以在验证器重添加额外的验证方法,例如: ```ts import { ValidateBase, Scene, checkType } from "/Frame/ValidateBase"; class User extends ValidateBase { rule = { email: 'require|email|between:5,30', password: 'isYoyo',//直接在这里引用这个方法名即可 }; message = { email: '邮箱地址格式不正确', password: '密码不符合规则', 'password.min': '密码最小${1}个字符', 'password.max': '密码最大${1}个字符', }; /** * 快捷场景(优先级低于方法的场景) * 数组选择的字段只能是rule已经配置好的(没有rule配置的将无效) */ scene: Scene = { Login: ['email', 'password'], }; /** * 自定义方法进行验证(后端|不支持异步|否则抛出会报错) * @param {Any} value 指定字段的值 * @param {String} field 字段名 * @param {Object} data 需要验证的所有数据 */ isYoyo(value: any, field: string, data: any) { // return '自定义拦截'; return true; } } /** * (装饰器)验证 * @param sceneName 场景名字 * @param (header|body|query|params) header请求头部 body请求体 query URL请求参数 params路由参数 * @returns */ export const Decorate = (sceneName: string, checkType: checkType) => index.Decorate(index, sceneName, checkType); /** * 方法验证(需要try做异常处理) * @param sceneName 场景名字 * @returns validate 可以再用check(data)方法进行验证 */ export const Validate = (sceneName: string) => index.validate(sceneName); ``` #### 计划任务 schedule 以下这个计划任务位于 `/App/schedule/index.ts`我们看看里面的具体基础格式如何底层是基于 cron 库 这个文件位置同样支持`多层文件夹` ```ts import { Cron, CronControlType, cronJob } from "@/Frame/CronBase"; /** * index 计划任务示例 */ export default class index { /** * 清除过期订单 * @param cronTime — cron表达式 * @param start — 是否立即启动 默认为true * @param runOnInit — 初始化后立即触发 onTick 函数。默认值为 false */ @Cron.from('0/2 * * * * *',false) public clearingOverdueOrders(cc: CronControlType, job: cronJob) { logger.trace('清除过期订单'); } @Cron.from('*/10 * * * * *', false) public kkkll(cc: CronControlType) { logger.trace('我启动了'); setTimeout(() => { cc.stop('index/kkkll'); }, 1000); } /** * 没有被装饰的方法不会执行 */ public test() { logger.trace ('test'); } } ``` 提供的支持的几个基础方法如下: ```ts /** * 启动一个任务 * @param taskName 任务名称 */ start(taskName: string): boolean; /** * 停止一个任务 * @param taskName 任务名称 */ stop(taskName: string): boolean; /** * 检查一个任务是否正在运行 * @param taskName 任务名称 */ isRun(taskName: string): boolean | undefined; /** * 获取一个任务的上下文对象 * @param taskName 任务名称 */ context(taskName: string): Record | null; /** * 获取指定任务的CronJob对象 * @param taskName 任务名称 */ getCronJob(taskName: string): cronJob | undefined; ``` #### *公共函数库* common 该文件是自己写的方法可以映射到全局,文件位于 `/App/common.ts` 格式如下 ```ts /** * 公共函数库 * 其它模块使用方法(由于是实例所以static不可用) * G.方法 */ export default class { public log(msg: string) { logger.trace(`全局方法 common: ${msg}`); } } ``` #### *全局中间件* 这个中间件不同于控制器的,是全局就是每个路由请求都会执行调用,文件位于 `/App/middleware.ts` 格式如下 ```ts import { Context, Next } from "koa"; /** * 全局中间件 */ export default async (ctx: Context, next: Next) => { logger.trace('全局中间件 强路由:',ctx.isRouteStrong,' 请求路径:',ctx.reqPath); await next(); } ``` #### WebSocket 这个不用多说了吧,底层基于 ws 库、进行了一些封装,因为这个框架是多进程的,所以为了不同进程的ws实例都能正常通信所以给每个新连接新增了一个uuid 调用就是 `ws.uuid`后续广播发送也会采用这个uuid作为标识 ```ts import WebSocketBase, { WebSocketWithId } from "/Frame/WebSocketBase"; /** * 自定义验证函数 * @param info 客户端连接信息 * @returns {boolean} 是否允许连接 */ WebSocketBase.init((info) => { let url = info?.req?.url; let Params = new URLSearchParams(String(url).split('?')[1]); // 连接验证 return Params.get('key') == '123456'; }).then(socket => { WebSocketBase.close(['uuid']); // 关闭指定连接 WebSocketBase.terminate(['uuid']); // 终止指定连接 WebSocketBase.sendTo('123456','uuid'); // 发送消息 给指定客户端 WebSocketBase.sendAll('123456',['uuid']); // 发送消息 给所有客户端 过滤指定uuid socket.onConnection((ws, req) => { console.log('连接成功', ws.uuid); ws.data = { name: '张三' };// 可以给当前连接对象添加自定义属性 }); socket.onClose((ws, code, reason) => { console.log('关闭连接', ws.data, code); }); socket.onMessage((ws, message) => { console.log('收到消息', message); }); // socket.wss.clients 可以获取当前所有连接的客户端 // 底层已经封装了 on 方法,这里可以不用再写(除非有自己需要处理的) // 监听连接事件 // Socket.wss.on('connection', (ws, req) => { // // 接收消息 // ws.on('message', (message) => { // console.log('自己的: %s', message); // }); // // 断开连接 // ws.on('close', function close() { // console.log('Client disconnected.'); // }); // }); }); ``` 为了让websocket通信更加简单,这边封装了一套json交互格式,消息格式如下 ```json { "id": "消息id字符串", "data": { "yoyo": 6666 }, "method": "index" } ``` 这个就是调用的 如下 是基于 `socket`里面封装的方法`onMethod`实现,在这个里面`return`如果客户端消息携带了消息id,就会以 `{resultId:xxx,data:xxx}`返回 ```ts socket.onMethod('index', (id,data) => { console.log('index_id: %s index_data: %s',id, data); return '哈哈哈接收到了';// 返回值() }); ``` #### 视图模型 view 该文件夹位于根目录的 `Views`里面你可以按照控制器的文件夹结构创建,有助于控制器的`ctx.view`自动取寻找(当然是可以指定路径的),这里底层是基于`EJS`实现的 下面是`EJS`基础符号标签说明 1. `<%` 和 `%>`: - 这是用于嵌入执行JavaScript代码的标签。在这些标签之间的任何内容都会被当作JavaScript代码执行,但不会输出到最终的HTML文档中。 2. `<%= %>`: - 用于输出数据到模板中。这个标签内的JavaScript表达式的计算结果会被自动转义(防止XSS攻击),然后插入到HTML中。 3. `<%- %>`: - 类似于`<%= %>`, 但是不会对输出的内容进行HTML转义,即原始内容将直接输出到HTML中。 4. `<%# %>`: - 注释标签,用于注释掉一段代码。这部分内容既不会被执行也不会出现在输出的HTML中。 5. `<%_ %>` 和 `<%_%>`: - 这些是去除空白符的标签。它们与对应的`<%` 和 `%>`、`<%= %>`等价,但是在处理后会移除前面或后面的空白字符,这有助于控制输出中的空格和换行。 6. `<%%` 和 `%%>`: - 当你需要在EJS模板中显示实际的`<%` 或 `%>`符号时,可以使用这两个标签来转义。 ##### 还有一些增强功能 1. **局部变量和全局变量**: - EJS允许你定义局部变量(在模板内部使用)和访问全局变量。局部变量可以通过`var`或`let`关键字定义,而全局变量通常是通过传递给渲染函数的对象属性来访问。 2. **包含其他模板文件**: - 使用`<%- include('path/to/template') %>`语句可以将另一个EJS模板的内容嵌入到当前模板中。这有助于代码重用和组织大型项目。请注意,`include`是无转义的,如果需要转义内容,请确保被包含的文件内容安全。 3. **循环和条件语句**: - 你可以使用标准的JavaScript控制结构如`for`、`while`、`if`、`else if`和`else`来创建条件性的内容或重复生成HTML片段。 4. **布局和块**: - EJS本身不直接提供像某些其他模板引擎那样的布局功能,但你可以通过`include`或自定义逻辑来实现类似的效果。例如,你可以有一个主模板,然后在适当的位置使用`include`来引入不同的 #### 项目配置文件 Config 主要是整个框架的配置文件目录、这个这里不用多说、提示都有,而且都有中文注释 位于 根目录的 `Config`文件夹下