一个面向对象的高效 MVC and REST 框架

CandyJs是一款面向对象、用于开发 Web 应用的高性能 Node 开发框架,最早写于2017年。它不是第三方框架的集成,而是一个全新的框架。

CandyJs实现了一套动态路由规范,您不需要提前注册所需要的路由,只需要输入网址CandyJs会自己找到路由对应的组件。

入门

安装程序

可以通过两种方式安装CandyJs

  1. 使用npm
  2. 下载源代码手动部署

使用npm安装

npm install candyjs

下载源代码手动部署

通过GitHub下载源码放置到 node_modules 目录即可
注意 通过 github 下载的源码可能需要删除其中的 git 仓库信息 否则 npm 安装其他包可能会有问题

第一次运行程序

本例输出一个 Hello word 程序

首先需要创建一个基本应用框架

CandyJsbin目录提供了一个创建应用的工具_candy 可以使用该工具初始化应用

如果CandyJs是通过npm安装的 那么_candy会被安装到node_modules/.bin目录 如果CandyJs是通过下载源码手动安装的 那么需要到node_modules/candyjs/bin目录下找到该工具

./node_modules/.bin/_candy PROJECT_NAME 使用自己想用的应用名字替换掉 PROJECT_NAME 变量即可

这样就会创建一个简单的应用如下


PROJECT_NAME
|
|- index.js
|
|- app
|  |
|  |-- controllers 普通控制器目录
|      |
|      |-- index
|      |   |
|      |   |-- IndexController.js
|      |
|   -- views
|      |
|      |-- index
|      |   |
|      |   |-- index.html
    

进入应用目录启动程序

node index.js

访问程序

http://localhost:8090

应用结构

一个比较完整的应用目录结构如下


PROJECT_NAME
|
|- index.js
|
|- node_modules 依赖模块目录
|
|- public 目录 一般存放静态文件
|
|- app 项目目录
|  |
|  |-- controllers 普通控制器目录
|      |
|      |-- user 用户组目录
|      |   |
|      |   |-- IndexController.js  - host:port/user/index 可以访问到该类
|      |   |-- OtherController.js  - host:port/user/other 可以访问到该类
|      |
|      |-- goods 商品组目录
|      |   |
|      |   |-- IndexController.js  - host:port/goods/index 可以访问到该类
|      |   |-- OtherController.js  - host:port/goods/other 可以访问到该类
|      |
|   -- views 普通控制器模板目录
|      |
|      |-- user 用户组模板 对应上面用户组
|      |   |
|      |   |-- index.html
|      |   |-- other.html
|      |
|   -- goods 商品组模板
|      |   |
|      |   |-- index.html
|      |   |-- other.html
|      |
|   -- modules 模块
|      |
|      |-- reg
|      |   |
|      |   |-- controllers 模块控制器目录 其下无子目录
|      |   |   |
|      |   |   |-- IndexController.js
|      |   |
|      |   |-- views 模块模板目录
|      |   |   |
|      |   |   |-- index.html
|      |   |
|      |   |-- 其他目录
|      |
|   -- runtime 缓存目录
|

入口脚本index.js

入口脚本是应用启动流程中的第一环 一个应用只有一个入口脚本 入口脚本包含启动脚本 程序启动后就会监听客户端的连接

入口脚本主要完成以下工作

  • 加载应用配置
  • 启动应用
  • 注册各种需要组件


var CandyJs = require('candyjs');

new CandyJs({
    'id': 1,
    
    // 定义调试应用
    'debug': true,
    
    // 定义应用路径
    'appPath': __dirname + '/app',
    
    // 注册模块
    'modules': {
        'bbs': 'app/modules/bbs'
    },
    
    // 配置日志
    'log': {
        'targets': {
            'file': {
                'class': 'candy/log/file/Target'
            }
        }
    }
    
}).listen(8090, function(){
    console.log('listen on 8090');
});

应用

一般的框架都包含两种应用Web 应用控制台应用但是CandyJs只有一种Web 应用

应用属性

在入口文件中可以传入各种参数 这些参数最终会被赋值到应用对象上

必要属性
  • candy/web/Application.id 该属性用来标识唯一应用

  • candy/web/Application.appPath 该属性用于指明应用所在的目录

重要属性
  • candy/web/Application.routesMap 用来自定义路由处理程序

    
    // account 路由使用 app/controllers/user/IndexController 做处理 并传入了一个参数 property
    'account': {
        'class': 'app/controllers/user/IndexController',
        'property': 'value'
    }
    

  • candy/web/Application.modules 用来注册应用模块

    
    // 注册一个 bbs 模块
    'modules': {
        'bbs': 'app/modules/bbs'
    }
    

  • candy/web/Application.encoding 项目编码方式

  • candy/web/Application.debug 是否开启调试

自定义属性

其他在入口文件中传入的参数都会作为自定义参数传入应用对象

应用控制器

控制器是MVC模式中的一部分 是继承candy/core/Controller类的对象 负责处理请求和生成响应

动作

控制器由动作组成 它是执行终端用户请求的最基础的单元 一个控制器有且只有一个入口动作叫做run


'use strict';

var CandyJs = require('candyjs');
var Controller = CandyJs.Candy.include('candy/web/Controller');

class IndexController extends Controller {

    // 入口动作
    run(req, res) {
        res.end('hello');
    }

}

module.exports = IndexController;

动作切面

如果控制器从candy/web/Controller继承而来(在 CandyJs 中控制器可以不从candy/web/Controller继承) 那么就可以使用动作切面在控制器的动作执行前后实现一些业务逻辑


'use strict';

var CandyJs = require('candyjs');
var Controller = CandyJs.Candy.include('candy/web/Controller');

class IndexController extends Controller {

    beforeActionCall(req, res) {
        console.log('beforeActionCall')
    }

    afterActionCall(req, res) {
        console.log('afterActionCall')
    }

    // 入口动作
    run(req, res) {
        res.end('hello');
    }

}

module.exports = IndexController;

注意 由于项目代码中可能存在异步操作afterActionCall()并不能保证在 run() 执行完成后再执行

路由

一般一个路由对应一个控制器 路由格式如下

[route_prefix]/[controllerId]

如果属于模块下的控制器 那么路由格式如下

[moduleId]/[controllerId]

如果用户的请求地址为http://hostname/index会执行index控制器的run入口动作

如果用户的请求地址为http://hostname/bbs/index会执行bbs模块的index控制器的run入口动作 或者执行普通控制器的bbs目录下的index控制器的run入口动作

控制器查找顺序 优先查找模块下的控制器模块控制器 --> 普通控制器

路由拦截

在某些场景下(比如关闭网站) 我们想让所有路由都集中到一个类去处理 这时候就可以使用路由拦截功能

要使用路由拦截功能 需要在入口文件传入一个配置项interceptAll 这样无论访问什么路由 都会被interceptAll指定的类处理


var app = new CandyJs({
    'id': 1,
    'appPath': __dirname + '/app',
    'debug': true,
    
    'interceptAll': 'app/Intercept'
});

模型

模型是MVC模式中的一部分 是代表业务数据的对象

CandyJs暂时没有提供读取数据库的类

视图

视图是MVC模式中的一部分 它用于给终端用户展示页面

视图类一般把模型层提供的数据与静态页面结合生成一个最终的页面展示给用户 CandyJs暂时只在控制器层提供了功能有限的API

模板引擎

一般视图类与模板引擎结合使用 但是CandyJs并没有实现一个模板引擎 用户需要使用已有的模板引擎来实现自己的业务

控制器层的视图 API

如果用户的控制器从candy/web/Controller继承而来 那么可以在控制器中使用getView()方法来获取视图类实例

视图类提供了如下API供用户使用

  • getTemplateFilePath(view) 用于获取一个视图文件的绝对路径
  • getTemplate(view, callback) 用于读取一个视图文件的内容
  • getTemplateFromPath(path, callback) 用于从指定路径读取视图文件内容


'use strict';

var CandyJs = require('candyjs');
var Controller = CandyJs.Candy.include('candy/web/Controller');

class IndexController extends Controller {
    
    run(req, res) {
        this.getView().getTemplate('index', (err, str) => {
            res.end(str);
        });
    }
    
}

module.exports = IndexController;

模块

模块是独立的软件单元由模型 视图 控制器和其他组件组成 终端用户可以访问在应用主体中已注册的模块的控制器 CandyJs在解析路由时优先查找模块中有没有满足条件的控制器

注意 和普通项目目录不同的是 模块中的控制器和视图没有子目录

创建模块

在应用目录的modules目录中建立单独目录创建模块 如下


modules 模块目录
    |
    |-- bbs 创建 bbs 模块
    |   |
    |   |-- controllers 模块控制器目录
    |   |   |
    |   |   |-- IndexController.js
    |   |
    |   |-- views 模块视图目录
    |   |   |
    |   |   |-- index.html
    |   |
    |   |-- 其他目录

注册模块

创建完成的模块还不能被系统识别 需要手动注册一下


var CandyJs = require('candyjs');

new CandyJs({
    ...
    
    // 注册模块
    'modules': {
        'bbs': 'app/modules/bbs'
    },
    
    ...
    
}).listen(8090, function(){
    console.log('listen on 8090');
});

组件 & 行为

组件

组件是实现属性 (property) 行为 (behavior) 事件 (event)的基类 如果一个类继承自组件类 那么这个类就拥有组件类的特性

组件是candy/core/Component或其子类的实例 CandyJscandy/core/Controller类继承自candy/core/Component

行为类一般与组件类同时使用

行为

行为是candy/core/Behavior类或其子类的实例
一个行为类可以用于在不改变原组件代码的情况下增强其功能
当行为附加到组件后它将 注入 它的方法和属性到组件中 然后就可以像访问组件自己的方法和属性一样访问它们
行为类还能够监听组件的事件并作出响应

属性

javascript 类的成员变量也被称为属性 properties

事件

CandyJs中实现了一个观察者模式


'use strict';

var CandyJs = require('candyjs');
var Controller = CandyJs.Candy.include('candy/web/Controller');

class IndexController extends Controller {
    
    constructor(context) {
        super(context);
        
        this.on('myevent', function() {
            console.log('myevent fired');
        });
    }
    
    run(req, res) {
        this.trigger('myevent');
        
        res.end('hello');
    }
    
}

module.exports = IndexController;

行为的使用

定义行为

要定义行为 通过继承candy/core/Behavior或其子类来建立一个类


'use strict';

var CandyJs = require('candyjs');
var Behavior = CandyJs.Candy.include('candy/core/Behavior');

class MyBehavior extends Behavior {
    constructor() {
        super();
        
        this.props1 = 1;
        this.props2 = 2;
    }
    
    myFun() {
        // todo
    }
}

module.exports = MyBehavior;

以上代码定义了行为类MyBehavior并为要附加行为的组件提供了两个属性prop1 prop2和一个方法myFun()

附加行为到组件

可以通过静态配置或者动态方法形式附加行为到组件

要使用静态配置附加行为 重写组件类的behaviors()方法即可behaviors()方法应该返回行为配置列表


'use strict';

var CandyJs = require('candyjs');
var Controller = CandyJs.Candy.include('candy/web/Controller');

class IndexController extends Controller {
    
    // 重写方法
    behaviors() {
    
        // 返回要附加的行为
        return {
            myBehavior: 'app/controllers/index/MyBehavior'
        };
    }
    
    run(req, res) {
        res.end('hello');
    }
    
}

module.exports = IndexController;

要使用动态方法附加行为 在组件里调用attachBehavior()方法即可


'use strict';

var CandyJs = require('candyjs');
var Controller = CandyJs.Candy.include('candy/web/Controller');

class IndexController extends Controller {
    
    constructor(context) {
        super(context);
        
        // 附加组件
        this.attachBehavior('myBehavior', 'app/controllers/index/MyBehavior');
    }
    
    run(req, res) {
        res.end('hello');
    }
    
}

module.exports = IndexController;

使用

一旦行为附加到组件 就可以直接使用它

为了不必要的性能开销CandyJs不会真的执行注入操作 要想使用行为类的功能 必须在调用行为类的功能之前手动调用一下inject()方法


'use strict';

var CandyJs = require('candyjs');
var Controller = CandyJs.Candy.include('candy/web/Controller');

class IndexController extends Controller {
    
    constructor(context) {
        super(context);
        
        this.attachBehavior('myBehavior', 'app/controllers/index/MyBehavior');
    }
    
    run(req, res) {
        // 手动调用
        this.inject();
        
        // 使用行为类的功能
        this.myFun();
        console.log(this.props1);
        console.log(this.props2);
        
        res.end('hello');
    }
    
}

module.exports = IndexController;

行为中处理事件

行为类可以处理组件触发的事件 只需要重写行为类的events()方法即可


// CandyJs 在调用控制器入口动作前后会触发 beforeActionCall afterActionCall 事件 可以使用行为类进行处理
'use strict';

var CandyJs = require('candyjs');
var Behavior = CandyJs.Candy.include('candy/core/Behavior');

class MyBehavior extends Behavior {
    events() {
        return {
            'beforeActionCall': function(){
                console.log('before');
            },
            'afterActionCall': function() {
                console.log('after');
            }
        }
    }
}

module.exports = MyBehavior;

中间件

中间件是处理请求的第一个环节 可以对请求做过滤处理并调用下一个中间件

CandyJs暂时只提供了一个处理静态资源的中间件 理论上CandyJs兼容任何express的中间件

静态资源

CandyJs默认是不处理静态资源的 需要使用中间件


// 入口文件
var CandyJs = require('candyjs');

var Hook = CandyJs.Candy.include('candy/core/Hook');
var R = CandyJs.Candy.include('candy/midwares/Resource');

Hook.getInstance().addHook(new R(__dirname + '/public').serve());

new CandyJs({

    ...
    
}).listen(8090, function(){
    console.log('listen on 8090');
});

URI & URL 类

candy/web/URI 及 candy/web/URL类提供了对 uri 和 url 操作的方法

candy/web/URI

  • parseUrl() 解析 url


var URI = CandyJs.Candy.include('candy/web/URI');

var uri = new URI();

/*
{
    source: 'http://xxx.com:8080/abc?q=1#anchor',
    scheme: 'http',
    user: undefined,
    password: undefined,
    host: 'xxx.com',
    port: '8080',
    path: '/abc',
    query: 'q=1',
    fragment: 'anchor'
}
*/
uri.parseUrl('http://xxx.com:8080/abc?q=1#anchor');

candy/web/URL

  • getReferer() 获取先前网页的地址
  • getHostInfo() 获取 URI 协议和主机部分
  • getCurrent() 获取当前网址 不包含锚点部分
  • to(url[, params = null]) 创建一个 url


var URL = CandyJs.Candy.include('candy/web/URL');

var url = new URL(req);

// return scheme://host/index/index
url.to('index/index');

// return scheme://host/index/index?id=1#anchor
url.to('index/index', {id: 1, '#': 'anchor'})

请求与响应

CandyJs提供了处理请求和响应的类 candy/web/Requestcandy/web/Response

HTTP 请求 candy/web/Request 类

用于处理 http 请求 该对象提供了对诸如请求参数 HTTP头 cookies等信息的访问方法

candy/web/Request类提供了一组实例和静态方法来操作需要的数据

  • static parseUrl(request) 简单解析 url
  • static getClientIp(request) 获取客户端 IP
  • static getQueryString(request, param) 获取 GET 请求参数
  • static getParameter(request, param) 获取 POST 请求参数
  • static getCookie(request, name) 获取 COOKIE
  • getQueryString(param) 实例方法获取 GET 请求参数
  • getParameter(param) 实例方法获取 POST 请求参数
  • getCookie(name) 实例方法获取 COOKIE
CandyJs中使用 getParameter() 获取 POST 参数暂时需要依赖第三方解析 body 的中间件 否则将反回 null


var Request = CandyJs.Candy.include('candy/web/Request');
var request = new Request(req);
var id = request.getQueryString('id');
...

HTTP 响应 candy/web/Response 类

主要用户向客户端输出响应消息

candy/web/Response类提供了一组实例和静态方法来操作响应数据

  • setStatusCode(value[, text]) 设置 http status code
  • setHeader(name, value) 设置 header
  • setContent(content) 设置实体内容
  • setCookie(name, value[, options]) 设置一条 cookie
  • send([content]) 发送 HTTP 响应到客户端
  • redirect(url[, statusCode = 302]) 页面重定向
使用 response 输出内容


var Response = CandyJs.Candy.include('candy/web/Response');
var response = new Response(res);
response.setContent('some data from server');
response.send();

使用 response 重定向


var Response = CandyJs.Candy.include('candy/web/Response');
var response = new Response(res);
response.redirect('http://foo.com');

助手类

助手类封装了一些常用操作

文件助手类FileHelper

  • getDirname(dir) 获取路径的 dir 部分
  • normalizePath(path[, directorySeparator = '/']) 正常化一个路径
  • createDirectory(dir[, mode = 0o777[, callback = null]]) 异步创建文件夹
  • createDirectorySync(dir[, mode = 0o777]) 同步创建文件夹

字符串助手类StringHelper

  • nIndexOf(str, find, n) 查找某字符串在另一个字符串中第 N 次出现的位置
  • trimChar(str, character) 删除两端字符
  • lTrimChar(str, character) 删除左侧字符
  • rTrimChar(str, character) 删除右侧字符
  • ucFirst(str) 首字母大写
  • htmlSpecialChars(str[, flag = 0[, doubleEncode = true]]) 转化特殊 html 字符到实体
  • filterTags(str[, allowed = '']) 过滤 html 标签

时间助手类TimeHelper

  • format(formats[, timestamp = Date.now()]) 格式化时间


var Response = CandyJs.Candy.include('candy/helpers/FileHelper');
var Response = CandyJs.Candy.include('candy/helpers/StringHelper');
var Response = CandyJs.Candy.include('candy/helpers/TimeHelper');

// return /a/c
var path = FileHelper.normalizePath('/a/./b/../c');

// return <script>
var str = StringHelper.htmlSpecialChars('<script>');

// return abcxyz
var strTag = StringHelper.filterTags('<a>abc</a>xyz');

// 格式化当前时间 return xxxx-xx-xx xx:xx:xx 
var time = TimeHelper.format('y-m-d h:i:s');

别名系统

为了方便类的管理 实现自动加载 初始化等CandyJs提供了一套别名系统

别名是一个以@符号开头的字符串 每一个别名对应一个真实的物理路径

CandyJs中加载类以及创建类的实例都是用的别名

系统内置别名

  • @candy 指向 CandyJs 目录
  • @app 项目目录 由appPath指定CandyJs.Candy.app.getAppPath()可得到该值
  • @runtime 缓存目录 默认指向@app/runtime CandyJs.Candy.app.getRuntimePath()可得到该值
  • @root 网站根目录CandyJs.Candy.app.getRootPath()可得到该值

自定义别名

用户可以自定义别名


// 注册别名
CandyJs.Candy.setPathAlias('@lib', '/home/www/library');

// 加载并创建 /home/www/library/MyClass 类
var obj = CandyJs.Candy.createObject('lib/MyClass');

RESTful

由于设计不够优雅 npm 包从 2.0.0 开始重构了 REST 模式架构 此部分可能还会持续更新

npm 包 2.0.0 之前用法

在 RESTful 模式中可用的请求方法如下以下方法都是静态的

  • get(route, handler)
  • post(route, handler)
  • put(route, handler)
  • delete(route, handler)
  • patch(route, handler)
  • head(route, handler)
  • options(route, handler)


// 增加 useRestful 参数启用 RESTful
var app = new CandyJs({
    'id': 1,
    'debug': true,
    'appPath': __dirname + '/app',
    
    'useRestful': true
});
app.listen(8090, function(){
    console.log(8090)
});

var Restful = CandyJs.Candy.include('candy/web/Restful');
// get 路由 并指定 id 参数必须为数字
Restful.get('/abc/{id:\\d+}', function(req, res, id){
    var Request = CandyJs.Candy.include('candy/web/Request');
    var r = new Request(req);
    
    console.log(r.getQueryString('id'));
    console.log(id);
    
    res.end('api get');
});

// 多方法路由 id 参数可为字母或数字
Restful.addRoute(['GET', 'POST'], '/def/{id:}', function(req, res, id){
    res.end(id);
});

// 使用 app/api/User 类的 index 方法处理请求
Restful.get('/xyz/{id:}', 'app/apis/User@index');

// 其中 User 的定义如下
'use strict';
class User {
    index(req, res, id) {
        res.end(id);
    }
}
module.exports = User;

npm 包 2.0.0 之后用法

以下方法都改为了实例方法

  • get(route, handler)
  • post(route, handler)
  • put(route, handler)
  • delete(route, handler)
  • patch(route, handler)
  • head(route, handler)
  • options(route, handler)


var Rest = require('candyjs/restful');

var rest = new Rest({
    appPath: __dirname + '/app',
    debug: true
});

rest.get('/abc/{id:\\d+}', function(req, res, id){
    res.end(String(id));
});

rest.listen('8090', () => {
    console.log('listen on 8090')
});

RESTful 中的路由问题

RESTful 中的路由是用正则表达式来实现的 它可以实现非常灵活的路由配置 但是相对于 MVC 中的路由性能要差 ( mvc 模式中的路由不是用正则实现的 )

日志

CandyJs提供了对日志处理的功能 目前只支持文件日志

使用日志

使用日志功能前 需要在入口文件注册


'log': {
    'targets': {
        'file': {
            'class': 'candy/log/file/Target',
            'logPath': __dirname + '/logs'
        },
        'other': {...}
    },
    'flushInterval': 10
}

targets用于配置日志处理程序 现在只支持文件日志 所以上面配置了file字段 用于配置文件日志所需要的环境 其中配置了所用的日志类和日志路径

flushInterval表示日志写入硬盘的频率 这里指定每调用 10 次日志接口向硬盘同步一次

用户手动调用 flush() 也会触发同步日志到硬盘的操作

日志接口

  • error(message) 记录错误日志
  • warning(message) 记录警告日志
  • info(message) 记录信息日志
  • trace(message) 记录追踪日志
  • flush() 输出日志


var CandyJs = require('candyjs')
var Logger = CandyJs.Candy.include('candy/log/Logger');

var log = Logger.getLogger();
log.error('This is a error message');
log.flush();  // 写入硬盘

CandyJs暂时只提供了文件日志功能 如果想实现诸如数据库日志等的功能必须自己进行日志扩展 CandyJs完善的代码设计使得进行日志扩展非常容易 只需要让扩展的日志类继承candy/log/ITarget并实现其中的flush()方法即可

缓存

CandyJs提供了数据缓存处理的功能 目前只支持文件缓存

使用缓存

使用缓存功能前 需要在入口文件注册


'cache': {
    'file': {
        'class': 'candy/cache/file/Target',
        'cachePath': '...'
    }
}

file用于指定使用文件缓存

缓存接口

  • setSync(key, value, duration) 同步写入缓存
  • set(key, value, duration, callback) 异步写入缓存
  • getSync(key) 同步读取缓存
  • get(key, callback) 异步读取缓存
  • deleteSync(key) 同步删除缓存
  • delete(key, callback) 异步删除缓存


var CandyJs = require('candyjs');
var Cache = CandyJs.Candy.include('candy/cache/Cache');

var c = Cache.getCache('file');

// 同步
c.setSync('key', 'value');
var data = c.getSync('key');

// 异步
c.set('key2', 'value2', undefined, (err) => {
    c.get('sync', (err, data) => {
        res.end(data);
    });
});

CandyJs暂时只提供了文件缓存功能 如果想实现诸如数据库缓存等的功能必须自己进行缓存扩展 CandyJs完善的代码设计使得进行缓存扩展非常容易 只需要让扩展的缓存类继承candy/cache/ITarget并实现其中的方法即可

异常处理

CandyJs提供了异常处理的能力 默认在调试模式下直接输出错误信息 在错误出现时 你可以定制自己的错误页面 这在正式环境下会很有用

如果要定制自己的错误页面 需要在入口文件传入一个配置项exceptionHandler 如果没有 那么将使用默认值candy/web/ExceptionHandler

自己定制的异常处理类必须提供一个handlerException(response, exception)方法


new YNode({
    'id': 1,
    'debug': true,
    'appPath': __dirname + '/app',
    
    // 添加异常处理程序
    'exceptionHandler': 'app/error/MyError'
    
}).listen(8090, function(){
    console.log('listen on 8090');
});

app/error/MyError.js 内容如下
class MyError {
    
    handlerException(response, exception) {
        response.setHeader('Content-Type', 'text/plain');
        response.writeHead(500);
        
        response.end('The server encountered an internal error...');
    }
    
}

module.exports = MyError;