npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

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

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

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

  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)