@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)

    Keywords

    Install

    npm i @crystal1984/bolaa.mvc

    DownloadsWeekly Downloads

    1

    Version

    2.4.3

    License

    ISC

    Unpacked Size

    108 kB

    Total Files

    61

    Last publish

    Collaborators

    • crystal1984