@crystal1984/bolaa.mvc
v2.4.3
Published
一个自定义的HTTP服务器+MVC框架
Downloads
127
Readme
Bolaa.MVC
这是一个基于Koa构建的MVC框架,提供基本的HTTP服务器、路由、Socket.IO、视图渲染、定时任务等功能
安装
本项目基于[email protected],需要node >= 7.6.0以获得ES2015和async支持
$ npm install @cyrstal1984/bolaa.mvc
项目代码结构
项目根目录
│───start-web.js //项目启动文件
│───package.json
│
│───src //代码文件根目录
│ │
│ │───config //配置文件目录
│ │ │───index.js //主配置
│ │ │───middlewares.js //中间件配置
│ │ │───development.js //开发环境子配置
│ │ └───production.js //生产环境子配置
│ │
│ │───controllers //控制器代码目录
│ │ └───...
│ │
│ │───jobs //定时任务代码目录
│ │ └───...
│ │
│ │───middlewares //自定义中间件代码目录
│ │ └───...
│ │
│ │───services //公共服务代码目录
│ │ └───...
│ │
│ │───sockets //Socket.IO代码目录
│ │ └───...
│ │
│ └───view //视图文件目录
│ └───...
│
└───runtime //运行期文件目录
│
└───wwwroot //web静态文件目录
└───...
在所有可访问app的地方可以通过以下方式获取以下几个目录的完整路径常量
- app.paths.root 项目根目录
- app.paths.source 代码根目录
- app.paths.runtime 运行期文件目录
- app.paths.wwwroot web静态文件目录
运行项目
在开发阶段使用以下命令启动项目
$ npm run dev
生产环境下建议使用pm2启动
$ pm2 start start-web.js
如需在本地调试,通过start-web.js启动
配置
配置文件放置于 项目根目录/src/config 下,以以下顺序加载
- 加载 config/index.js
- 根据当前运行环境加载 development.js 或 production.js
典型的配置文件内容示例
'use strict'
module.exports = {
/**
* 端口配置(可选),可以为一个number或number数组
* 如果提供一个数组,则HTTP服务器会监听其中所有的端口
* 如果未提供此配置项,默认监听3000端口
*/
port: 3000,
/**
* 中间件列表
*/
middlewares: require('./middlewares'),
/**
* 数据库配置
*/
db: {
type: 'mysql', //目前仅支持mysql数据库
mysql: { //mysql数据库配置
host: '',
user: '',
password: '',
database: '',
charset: ''
}
}
}
环境配置文件(development.js和production.js)中的配置项会覆盖主配置文件中的内容
在所有能访问到app的地方可以通过app.config访问配置对象
中间件
Bolaa.MVC兼容Koa的中间件,但需要对其做一些包装。基础库中预定义了一些中间件供基础使用,你也可以自己定义自己需要的中间件
预定义的中间件
- action 必需,负责执行对应控制器上的action方法
- error 可选,捕获处理过程中的错误并将HTTP响应码改为500
- jwt 可选,JsonWebToken解析中间件
- post-body 可选,处理来自HTTP POST的数据
- request-log 可选,记录请求的地址、请求时间并输出到日志
- route 必需,负责解析路由并找到对应的controller和action
- static 开发环境下必需,负责访问静态文件,生产环境下建议交给nginx处理,以降低服务器开销
中间件配置格式(middlewares.js)
中间件配置对象为一个数组,其格式如下
const myMiddleware = require('my-middleware')
module.exports = [
/**
* 配置方法1,只提供中间件名
*/
'request-log',
{
/**
* 中间件定义
* 本例中使用中间件名
*/
handler: 'jwt',
/**
* 中间件配置
* 本例中使用一个静态对象提供中间件配置
*/
options: {
},
/**
* 中间件启用条件,当方法返回值为真时此中间件有效
* 如果忽略此配置项,则中间件永久生效
*/
match: ctx => ctx.path.startsWith('/api')
},
//更复杂的中间件配置对象
{
/**
* 中间件定义
* 使用的是从其他模块中加载的对象
*/
handler: myMiddleware,
/**
* 中间件配置对象
* 通过一个方法获取中间件的配置,方法传递的是应用程序实例app
*/
options: app => {
return {
port: app.config.port
}
},
/**
* 中间件启用条件
*/
match: ctx => ctx.route
}
]
预定义中间件所需的配置对象
jwt
{
secret: ''
passthrouth?: true,
header?: true,
headerName?: 'Authorization',
headerPattern?: /^Bearer (.+)$/i,
cookie?: false,
cookieName?: 'jwt',
getter?: undefined
}
配置项定义
- secret (string) JWT密钥,必需
- passthrouth (boolean) 当无法解析到有效的jwt时是否也继续处理此请求,如为false,则会返回一个HTTP 401错误,默认为true
- header (boolean) 是否从HTTP Header中解析JWT,默认为true
- headerName (string) 从HTTP Header中解析时使用的header头名称,默认为Authorization
- headerPattern (RegExp) 从HTTP Header中解析时的格式,默认为 /^Bearer (.+)$/i
- cookie (boolean) 是否从Cookie中解析JWT,默认为true
- cookieName (string) 从Cookie中解析时使用的键值,默认为jwt
- getter (function) 自定义的获取JWT值的方法,该方法传递当前上下文对象ctx作为参数,需返回string,支持Promise。如果定义了该方法,则整个配置对象中除了secret之外其他参数均不起作用,默认为undefined
被成功解析的JWT内容会放置在ctx.state.jwt上
post-body
{
patchNode?: false,
patchKoa?: true,
jsonLimit?: '1mb',
formLimit?: '56kb',
textLimit?: '56kb',
encoding?: 'utf-8',
multipart?: false,
urlencoded?: true,
text?: true,
json?: true,
formidable?: {
bytesExpected?: null,
maxFields?: 1000,
maxFieldsSize?: 2 * 1024 * 1024,
uploadDir?: '',
keepExtensions?: false,
hash?: false,
multiples?: true
},
onError?: undefined,
strict?: true
}
配置项定义
- patchNode (boolean) 是否将解析成功的内容放置到ctx.req上,默认为false
- patchKoa (boolean) 是否将解析成功的内容放置到ctx.request上,默认为true
- jsonLimit (number|string) 解析JSON内容时的最大字节数,默认为1mb
- formLimit (number|string) 解析表单提交内容时的最大字节数,默认为56k
- textLimit (number|string) 解析文本内容时的最大字节数,默认为56k
- encoding (string) 解析内容使用的字节集,默认为utf-8
- multipart (boolean) 是否解析multipart/form-data类型内容,默认为false
- urlencoded (boolean) 是否解析application/x-www-form-urlencodedl类型内容,默认为true
- text (boolean) 是否解析文本类型内容,默认为true
- json (boolean) 是否解析JSON类型内容,默认为true
- onError (function) 当解析出错时调用的错误处理方法,默认为undefined,格式为 function(err, ctx) { },传递参数如下
- err (Error) 异常实体
- ctx (Context) 上下文Context
- strict (boolean) 如果为true,则不会解析GET、HEAD、DELETE等请求,默认为true
- formidable (object) 解析multipart/form-data数据时的子配置对象
- bytesExpected (number) 要解析的内容体字节数,默认为null
- maxFields (number) 最多解析的字段总数,默认为1000
- maxFieldsSize (number) 解析字段内容(不包括文件字段)时最多分配的内存空间,如果超过此值,会引发一个error事件,默认为2M(2*1024*1024)
- uploadDir (string) 保存上传的文件的临时目录,默认为os.tmpDir(),在Windows环境中建议将此目录与项目根目录放到一个盘符下,否则将无法执行fs.rename操作
- keepExtensions (boolean) 文件保存到临时目录时是否保持其原始扩展名,默认为false
- hash (string) 如果你希望检查上传文件的校验值,可以把此值设为md5或者sha1,默认为false
- multiples (boolean) 是否解析多个上传的文件,默认为true
解析结果
从POST体中解析到的内容会被放置到 ctx.request.body 上
自己编写中间件
自己编写的中间件放置到 项目根目录/src/middlewares 下
示例
'use strict'
module.exports = options => {
/**
* 中间件构造逻辑
*/
...
return async (ctx, next) => {
/**
* 中间件前置处理逻辑
*/
...
await next()
/**
* 中间件后置处理逻辑
*/
...
}
}
使用名称加载中间件的检查顺序
- 项目根目录/src/middlewares/{名称}.js
- 预定义的中间件名
- require({名称})
控制器
控制器代码文件放置在 项目根目录/src/controllers 下
示例代码
'use strict'
const Base = require('@crystal1984/bolaa.mvc').Controller
module.exports = class extends Base {
async index() {
return 'this is index'
}
async hello() {
return {
content: 'hello'
}
}
}
基类属性
- this.app (Application) 访问当前的应用程序实例
- this.ctx (Context) 访问当前HTTP上下文对象
可重载魔术方法
控制器有以下魔术方法可被重载以实现想要的功能
- async __before(action) 当执行控制器动作前执行的前置方法,当该方法的返回值是false或者Promise<false>时,不会执行后续的动作方法
- async __after(action) 当控制器执行动作之后执行的后置方法
- async __action(action) 当控制器实例上找不到路由所定义的动作时执行的方法,该方法的返回值会被放置到ctx.body上
路由
路由通过预定义的route中间件实现
路由会尝试根据访问路径寻找对应的controller和action,如果已经从上一级路径找到对应名称的controller,则不会再从同名的文件夹中寻找
当route中间件找到对应的controller时,会对ctx.route设置一个对象,格式如下
{
controller: , //找到的控制器实例
action: '', //动作方法名
params: [] //后续解析到的路径参数
}
route后的中间件可以通过检测ctx.route属性判断是否已经成功路由
假设存在控制器文件 /src/controllers/my-controller.js
HTTP访问的地址为 /my-controller/my-action/my-par1/mypar-2
则 action 为 'my-action'
params 为 [ 'my-par1', 'my-par2' ]
如果只找到了controller未定义action,则action被设为index
示例
// src/controllers/test.js
module.exports = class extends Base {
async index() {
return 'index'
}
async test1() {
return 'this is test1'
}
async test2(name) {
return 'this is test2 and ' + name
}
}
/test -> 输出 index
/test/index -> 输出 index
/test/test1 -> 输出 this is test1
/test/test2/haha -> 输出 this is test2 and haha
/test/test2 -> 输出 this is test2 and
Service
这是用来定义各模块都可以访问的公共代码
Service对象定义在 /src/services 下
示例
// /src/services/my-service1.js
const Base = require('@crystal1984/bolaa.mvc').Processor
module.exports = class extends Base {
getFullName(name) {
return name + 'abc'
}
}
// /src/controller/my-controller.js
const Base = require('@crystal1984/bolaa.mvc').Controller
module.exports = class extends Controller {
async index() {
let fullname = this.service('my-service1').getFullName('haha')
// fullname = 'hahaabc'
}
}
Controller,Service,Job对象均可使用this.service方法访问已定义的Service
定时任务 Job
创建定时任务有以下2个步骤
1.编写定时任务代码
定时任务对象代码放置在 /src/jobs 下
// /src/jobs/my-job.js
const Base = require('@crystal1984/bolaa.mvc').Job
module.exports = class extends Base {
async run() {
//定时任务执行逻辑
...
}
}
2.在配置文件中配置定时任务
在配置文件中增加jobs数组,用于定义要执行的定时任务
*强烈建议仅在production.js生产环境配置文件中配置定时任务
示例
{
jobs: [
//按时启动的任务
{
handler: 'my-job', //定时任务名称
rule: '42 * * * *', //Cron风格的规则,任务将在指定的时间启动
},
//间隔启动的任务
{
handler: 'my-job',
rule: 600000, //每600秒执行一次
immediately: true, //是否在应用程序启动时立即执行一次
wait: true //是否等待上一次任务执行结束后再计算间隔
}
]
}
数据库
配置数据库
数据库在配置文件中的db属性进行配置,示例:
{
db: {
type: 'mysql',
mysql: {
host: '',
user: '',
password: '',
dababase: '',
charset: ''
}
}
}
访问数据库连接实例
在任何可访问app的地方都可以通过 app.db 访问数据库连接实例
实例方法
async query(sql, values)
执行一段SQL语句,语句中可以使用?占位符表示一个值,用??占位符表示一个标识符,替换占位符的内容放置到values参数中
- sql (string) 要执行的SQL语句
- values (Array) 可选,替换语句中占位符的内容
- 返回值 根据SQL语句不同有不同的返回值
let result = await app.db.query('SELECT * FROM table WHERE id = ?', [ id ])
async queryOne(sql, values)
参数与query方法相同,但仅返回结果集中第一行的数据,如果查询的结果集中没有数据,则返回null
async queryField(sql, values)
参数与query方法相同,但仅返回结果集中第一行的第一个字段,如果结果集中没有数据或数据没有可访问的字段,返回null
async select(table, where)
返回指定表中满足条件的数据
- table (string) 表名
- where (object) 查询条件
await app.db.select('my_table', { id: 1, type: 'running' })
//等效于
SELECT * FROM my_table WHERE id = 1 AND type = 'running'
async get(table, where)
返回指定表中满足条件的第一条数据
- table (string) 表名
- where (object) 查询条件
let result = await app.db.get('my_table', { id: 1, type: 'running' })
等效于
SELECT * FROM my_table WHERE id = 1 AND type = 'running' LIMIT 1
async insert(table, values, replace)
向指定的表中插入数据
- table (string) 表名
- values (object) 要插入的字段
- replace (boolean) 是否使用REPLACE INTO插入
- 返回值 (object)
{ insertId: 1 //插入表的自增键值 }
let result = await app.db.insert('my_table', { name: 'abc', age: 20 })
等效于
INSERT INTO my_table (name, age) VALUES ('abc', 20)
async update(table, fields, where)
更新指定的表
- table (string) 表名
- fields (object) 要更新的字段
- where (object) 查询条件
- 返回值 (object)
{ affectedRows: 1, //生效的行数 changedRows: 1 //被更新的行数 }
let result = await app.db.update('my_table', { name: 'abc', age: 20 }, { id: 8, type: 'person' })
等效于
UPDATE my_table SET name = 'abc', age = 2 WHERE id = 8 AND type = 'person'
事务
使用 beginTransaction 方法开启一个事务
使用方法1:自动处理事务
let result = await app.db.beginTransaction(async conn => {
/**
* 使用conn对象访问操作数据库
* 如果内部方法成功执行,事务会自动commit
* 如果内部发生异常,事务会自动rollback
*/
await conn.query()
return 'result'
})
//result为内部闭包方法的返回值
使用方法2:手动处理事务
let conn = await app.db.beginTransaction()
//具体数据库操作
await conn.query()
//手动提交事务
await conn.commit()
//手动回滚事务
await conn.rollback()
//手动关闭连接
await conn.end()
Socket.IO实现
socket代码放置在 /src/sockets 下
编写socket服务端代码
// /src/sockets/my-socket.js
const Base = require('@crystal1984/bolaa.mvc').SocketIO
module.exports = class extends Base {
onConnection(socket) {
//当有socket连接时进行的操作
}
}
//该socket的路径为 /socket.io/my-socket
向已连接的socket发送消息
app.getSocket('/my-socket').server.emit('evnet', arg1, arg2, arg3)