@crystal1984/bolaa.mvc
TypeScript icon, indicating that this package has built-in type declarations

2.4.3 • Public • Published

Bolaa.MVC

这是一个基于Koa构建的MVC框架,提供基本的HTTP服务器、路由、Socket.IO、视图渲染、定时任务等功能

安装

本项目基于Koa@2.0,需要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()

        /**
         * 中间件后置处理逻辑
         */
        ...
    }
}

使用名称加载中间件的检查顺序

  1. 项目根目录/src/middlewares/{名称}.js
  2. 预定义的中间件名
  3. 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)

Dependencies (13)

Dev Dependencies (0)

    Package Sidebar

    Install

    npm i @crystal1984/bolaa.mvc

    Weekly Downloads

    3

    Version

    2.4.3

    License

    ISC

    Unpacked Size

    108 kB

    Total Files

    61

    Last publish

    Collaborators

    • crystal1984