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

CandyJs是一个健壮可扩展的 Web 框架,它提供了一套优雅的编写代码的规范,使得编写 Web 应用变得得心应手。

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

CandyJs实现了正则路由的合并,多个正则路由我们会将其合成一个大路由,避免了路由逐个匹配带来的巨大性能损失

英文

入门

通过 cli 工具安装 candyjs


$ npm install -g @candyjs/cli
$
$ # 运行命令 按照提示进行操作
$ candyjs-cli

cli 工具目前只能用于创建项目。其他功能未实现

第一次运行程序

执行命令后,进入项目目录安装依赖

上述工具会生成一个 MVC 模式 Hello word 程序,如果只想要写一些接口程序,请看这里

项目目录结构如下


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

进入 PROJECT_NAME 目录,启动程序

$ npm install && npm run start

访问程序

http://localhost:2333

应用结构

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


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 模块
|      |
|      |-- newYearActivity
|      |   |
|      |   |-- controllers 模块控制器目录 其下无子目录
|      |   |   |
|      |   |   |-- IndexController.js
|      |   |
|      |   |-- views 模块模板目录
|      |   |   |
|      |   |   |-- index.html
|      |   |
|      |   |-- 其他目录
|      |
|   -- runtime 缓存目录
|

入口脚本index.js

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

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

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


var CandyJs = require('candyjs');
var App = require('candyjs/web/Application');

var app = new App({
    'id': 1,

    // 定义调试应用
    'debug': true,

    // 定义应用路径
    'appPath': __dirname + '/app',

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

    // 配置日志
    'log': {
        'targets': {
            'file': {
                'classPath': 'candy/log/file/Log'
            }
        }
    }

});
new CandyJs(app).listen(2333, function(){
    console.log('listen on 2333');
});

应用

CandyJs包含两种应用:Web 应用Rest 应用

应用属性

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

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

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

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

    
    // account 路由使用 app/controllers/user/IndexController 做处理 并传入了一个参数 property
    'account': {
        'classPath': '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/AbstractController类的对象,负责处理请求和生成响应

业务逻辑一般都应该出现在控制器中,包括但不限于处理用户传递的数据,计算数据等

动作

控制器由动作组成,它是执行终端用户请求的最基础的单元,一个控制器有且只有一个入口动作叫做run,入口动作在控制器被创建后会自动运行


'use strict';

var Controller = require('candyjs/web/Controller');

class IndexController extends Controller {

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

}

module.exports = IndexController;

动作切面与过滤器

动作切面是一种特殊的行为类,用于过滤用户的请求

candyjs提供的切面可以从core/ActionAspect找到,该类主要用于在控制器的动作执行前进行一些预处理操作

ActionAspect类有两个重要的方法:beforeAction(actionEvent)afterAction(actionEvent)方法,方法仅有一个参数actionEvent,该参数有一个valid属性,当前一个 ActionAspect 子类设置了 actionEvent.valid = false;,那么后面的切面类不再执行

切面的使用

1. 编写切面类

使用切面,首先必须编写切面类,切面类需要从candyjs/core/ActionAspect类继承

切面类可以根据具体需求选择实现beforeAction()afterAction()方法

在没有异步操作的情况下,beforeAction()会在控制器的动作执行前运行,afterAction()会在控制器的动作执行后运行

下面的切面类代码简单实现了一个为接口请求添加跨域头的功能,并实现了beforeAction()方法


// app/filters/Cors.js
const ActionAspect = require('candyjs/core/ActionAspect');

module.exports = class Cors extends ActionAspect {
    constructor() {
        super();

        this.cors = {
            // 允许访问该资源的 origin
            'Access-Control-Allow-Origin': '*',
            // 允许使用的请求方法
            'Access-Control-Allow-Methods': 'GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS',
            // 允许请求中使用的头信息
            'Access-Control-Allow-Headers': '',
            // 响应的有效时间为 秒
            'Access-Control-Max-Age': 86400,
            // 如果出现在预检请求的响应中 表示实际请求是否可以使用 credentials
            'Access-Control-Allow-Credentials': true
        };
    }

    /**
     * 实现前置方法,用于添加 http header 头
     */
    beforeAction(actionEvent) {
        let request = actionEvent.request;
        let response = actionEvent.response;

        let headers = this.generateHeaders(request);
        for(let k in headers) {
            response.setHeader(k, headers[k]);
        }

        if('OPTIONS' === request.method) {
            actionEvent.valid = false;

            response.writeHead(200, 'OK');
            response.write('');
            response.end();
        }
    }

    generateHeaders(request) {
        let ret = {};

        // oirigin
        if(undefined !== request.headers['origin'] && undefined !== this.cors['Access-Control-Allow-Origin']) {
            if(this.cors['Access-Control-Allow-Origin'].indexOf(request.headers['origin']) >= 0) {
                ret['Access-Control-Allow-Origin'] = request.headers['origin'];
            }

            // allow origin 为 * 和 credentials 不能同时出现
            if('*' === this.cors['Access-Control-Allow-Origin']) {
                if(this.cors['Access-Control-Allow-Credentials']) {
                    throw new Error('Allowing credentials for wildcard origins is insecure');
                } else {
                    ret['Access-Control-Allow-Origin'] = '*';
                }
            }
        }

        // 客户端请求方法
        if(undefined !== request.headers['access-control-request-method']) {
            ret['Access-Control-Allow-Methods'] = this.cors['Access-Control-Allow-Methods'];
        }

        // 允许的 header
        if(undefined !== this.cors['Access-Control-Allow-Headers']) {
            ret['Access-Control-Allow-Headers'] = this.cors['Access-Control-Allow-Headers'];
        }

        // 认证
        if(undefined !== this.cors['Access-Control-Allow-Credentials']) {
            ret['Access-Control-Allow-Credentials'] = this.cors['Access-Control-Allow-Credentials'] ? 'true' : 'false';
        }

        if('OPTIONS' === request.method && undefined !== this.cors['Access-Control-Max-Age']) {
            ret['Access-Control-Max-Age'] = this.cors['Access-Control-Max-Age'];
        }

        return ret;
    }
}
2. 使用切面

使用切面很简单,只需要在控制器中添加behaviors()方法即可


class IndexController extends Controller {
    // 'cors' 是为切面类指定的别名
    // 'app/filters/Cors' 表示切面的类路径 这里表示 app/filters/Cors.js 类
    behaviors() {
        return [
            ['cors', 'app/filters/Cors']
        ];
    }

    run() {
        // 此控制器动作执行前会先运行 behaviors() 方法指定的切面
    }
}

在 4.18 版本之前,ActionAspect类名为ActionFilter

相较于使用切面过滤用户的请求,更好的做法是使用过滤器

切面不支持异步操作,如果业务逻辑中包含异步逻辑(比如异步读取数据库),那么就不能保证切面执行完成再执行控制器逻辑

所以,需要处理异步业务场景时,应该使用过滤器

过滤器

过滤器是实现了core/IFilter接口的类,或者任何具有doFilter(req, res, chain)函数的类

过滤器的使用和切面类似,但是过滤器的配置是由filters()函数返回的


const Controller = require('candyjs/web/Controller');

class IndexController extends Controller {
    // 'app/filters/MyFilter' 表示过滤器的类路径
    filters() {
        return [
            'app/filters/MyFilter'
        ];
    }

    run() {}
}


// app/filters/MyFilter
module.exports = class MyFilter {
    doFilter(req, res, filterChain) {
        // todo something
        
        // 需要手动调用下一个过滤器
        filterChain.doFilter(req, res);
    }
}

切面和过滤器都可以在控制器执行之前过滤用户的请求,但是切面只支持同步操作,而过滤器支持异步操作

MVC 路由

路由是MVC模式中的一部分,实现控制器寻址

MVC模式中,每一个路由会对应到一个控制器文件

路由与控制器

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

  1. route_prefix[/controllerId]

  2. moduleId[/controllerId]

[/controllerId]可以省略,如果省略,那么默认值为index,即默认控制器为IndexControlelr
  • 直接访问域名 http://localhost/

    会默认映射到 app/controllers/index/IndexControlelr 控制器

  • 访问 http://localhost/user

    首先会查找是否存在 user 模块

    如果存在,会映射到 app/modules/user/controllers/IndexController

    如果不存在,则会映射到 app/controllers/user/IndexController

  • 访问 http://localhost/user/profile

    首先会查找是否存在 user 模块

    如果存在,会映射到 app/modules/user/controllers/ProfileController

    如果不存在,则会映射到 app/controllers/user/ProfileController

  • 访问 http://localhost/user/profile/settings

    首先会查找是否存在 user 模块

    如果存在,会映射到 app/modules/user/controllers/SettingsController

    如果不存在,则会映射到 app/controllers/user/profile/SettingsController

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

自定义路由配置

CandyJs中可以自定义路由的处理程序,这里通过routesMap来实现


var App = require('candyjs/web/Application');

var app = new App({
    id: 'myapp',

    routesMap: {
        // 比如访问 http://www.somedomain.com/account
        // 这时候 account 路由将使用 app/controllers/user/IndexController 做处理
        // 并将 IndexController 的 foo 成员变量赋值为 123
        'account': {
            'classPath': 'app/controllers/user/IndexController',
            'foo': '123'
        }
    }
});

路由拦截

很多时候,网站会需要进行维护,这时候需要关闭网站,candyjs提供了路由拦截功能,使得网站维护变得简单


var App = require('candyjs/web/Application');

var app = new App({
    id: 'myapp',

    // 这条配置使得所有请求都会被转发到 app/Intercept 去处理
    'interceptAll': 'app/Intercept',
});

// Intercept.js
class Intercept {
    run(req, res) {
        res.end('intercepted');
    }
}
module.exports = Intercept;

模型

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

模型并不能直接操作数据库,它只提供数据存储和校验,数据库操作需要借助数据库操作类实现

数据库操作 从这里了解详情

模型示例 从这里了解详情

使用模型

使用模型需要以下几个步骤

  1. 定义模型
  2. 填充数据
  3. 校验数据
1. 定义模型

CandyJs中,模型由属性( attributes )规则( rules )组成

属性是一个 JSON 对象,其中的键名一般与数据库定义的字段一一对应,规则是一个数组,配置了对属性的验证规则

每一个验证规则由三部分组成:规则验证器(由 rule 字段指定,支持对象实例、类配置以及别名路径格式)、需要验证的属性列表(由 attributes 字段指定)、错误时的提示信息(由 messages 字段指定)

如下是一个用户信息模型


const Model = require('candyjs/model/Model');

module.exports = class UserModel extends Model {
    constructor() {
        super();

        // 模型属性
        this.attributes = {
            user_name: '',
            password: '',
            confirming: '',
            email: ''
        };
        
        // 表单与模型字段不一致时 使用此配置来做字段映射
        // 如 模型使用 user_name 而前端表单使用 name
        this.attributesMap = {
            user_name: 'name'
        };
    }

    // 模型规则
    rules() {
        return [
            {
                rule: 'candy/model/RequiredValidator',
                attributes: ['user_name', 'password', 'email'],
                messages: ['用户名不能为空', '密码不能为空', '邮箱不能为空']
            },
            {
                rule: 'candy/model/EqualValidator',
                attributes: ['password', 'confirming'],
                messages: ['两次密码不一致']
            },
            {
                rule: 'candy/model/EmailValidator',
                attributes: ['email'],
                messages: ['邮箱格式不正确']
            }
        ];
    }
}
2. 填充数据

当有了定义好的模型后,接下来就是为模型填充数据,数据一般由用户通过前台页面以表单形式发送到后台,这里以控制器处理为例


const UserModel = require('../../models/UserModel');

module.exports = class PostController {

    run(req, res) {
        let model = new UserModel();
        
        // 数据填充
        model.fill(req);

        // todo ...
        
        res.setHeader('Content-Type', 'text/plain; charset=utf-8');
        res.end('');
    }

}

数据填充时,有可能前台表单的字段名称和模型中的属性名称不一致,这时候数据将会填充失败。

可以在模型定义时增加attributesMap字段,就如上面定义模型的例子那样:this.attributesMap = { user_name: 'name' };

模型使用的是 user_name,而前台表单使用的是 name,通过此种映射方式实现字段不一致时的对应

3. 校验数据

模型提供了validate()方法,调用该方法,模型会执行rules()配置的校验规则对属性进行校验,并返回一个布尔值代表成功或者失败

如果验证失败,可以通过getErrors()获取错误信息,该方法返回一个数组,包含了所有的错误信息


const UserModel = require('../../models/UserModel');

module.exports = class PostController {

    run(req, res) {
        let model = new UserModel();
        
        // 数据填充
        model.fill(req);

        // 执行校验
        let valid = model.validate();
        let errors = model.getErrors();

        let data = JSON.stringify({
            status_code: valid ? 0 : 1,
            messages: errors
        });

        res.setHeader('Content-Type', 'text/plain; charset=utf-8');
        res.end(data);
    }

}

系统内置验证器

CandyJs内置了几个常用的验证器,如上述示例中rule: 'candy/model/EmailValidator',它表示被验证的属性必须是一个邮箱格式

内置验证器如下

  • BooleanValidator
  • EmailValidator
  • EqualValidator
  • RequiredValidator
  • StringValidator

自定义验证器

由于业务的不同,系统内置验证器无法满足所有需求,这时候就可以使用自定义验证器

自定义验证器比较简单,只需要从系统的Vakudatir继承,并实现其中的validate()方法即可

需要注意的是,validate()方法的返回值是字符串,代表了验证失败的错误信息。如果没有错误,需要返回空字符


// 定义一个验证数字范围的验证器
const Validator = require('candyjs/model/Validator');

class MyNumberValidator extends Validator {
    constructor() {
        super();

        this.max = 100;
        this.min = 1;
    }

    validate(attributeName, attributeValue) {
        let info = this.getMessage(attributeName);

        if(attributeValue < this.min || attributeValue > this.max) {
            return '' === info ? 'number is invalid' : info;
        }

        return '';
    }
}

使用自定义验证器


const Model = require('candyjs/model/Model');

class UserModel extends Model {
    constructor() {
        super();

        ...
    }

    rules() {
        return [
            {
                rule: new MyNumberValidator(),
                attributes: ['age'],
                messages: ['年龄不合法']
            }
        ];
    }
}

视图

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

模板引擎

CandyJs提供了@candyjs/template-hbs来负责模板渲染,查看详情

其他方式使用模板引擎

得益于CandyJs灵活的架构设计,使得模板引擎使用非常便捷,这里有如下几种方式:

  • 全局配置方式
  • 局部注入方式
  • 嵌入式

下面将使用Handlebars对以上方式进行逐个讲解

全局配置方式

// 全局配置方式是使用 candyjs 的别名系统实现的
// 这里的代码可以从源代码 examples 目录中找到

// 1. 在 app/libs 目录中建立一个模板引擎文件 CandyTemplate.js
const fs = require('fs');
const Handlebars = require('handlebars');
// 加载系统视图类
const View = require('candyjs/web/View');

class CandyTemplate extends View {
    constructor(context) {
        super(context);
        this.handlebars = Handlebars.create();
    }

    // 模板引擎必须实现这个方法,因为它是渲染模板的入口
    renderFile(file, parameters) {
        fs.readFile(file, 'UTF-8', (err, template) => {
            let compiled = this.handlebars.compile(template);
            this.context.response.end( compiled(parameters) );
        });
    }
}
module.exports = CandyTemplate;

// 2. 经过第 1 步,我们的模板引擎就开发完了,是不是很简单
// 接下来在入口注册我们编写的模板引擎
const App = require('candyjs/web/Application');
const app = new App({
    'id': 'template_test',

    // 配置模板引擎
    'defaultView': 'app/libs/CandyTemplate',

    'appPath': __dirname + '/app'
});
new CandyJs(app).listen(2333, function(){
    console.log('listen on 2333');
});

// 3. 准备模板 html
<html>
<body>
<ul>
{{#each list}}
    <li><a href="/user?uid={{ id }}">{{ name }}</a></li>
{{/each}}
</ul>
</body>
</html>

// 4. 在控制器中使用模板引擎渲染页面
const Controller = require('candyjs/web/Controller');
const User = require('somepath/models/User');

class IndexController extends Controller {
    run(req, res) {
        this.fetchList(res);
    }

    async fetchList(res) {
        const user = new User();
        // 这里 data 是一个用户数组 [{id: xxx, name: xxx}]
        let data = await user.getUserList();

        // 可以获取到模板引擎实例
        // 具体使用方式请参考 handlebars 模板引擎官方文档
        // const handlebars = this.getView().handlebars;
        // handlebars.todo

        // 这里的 render 将使用我们制定的模板引擎渲染页面
        this.render('index', {
            list: data
        });
    }
}
局部注入方式

// 1. 局部注入方式第 1 步也需要编写我们的模板引擎,参考全局配置方式

// 2. 在控制器中动态注入模板引擎
const Controller = require('candyjs/web/Controller');
const User = require('somepath/models/User');
const CandyTemplate = require('somepath/CandyTemplate');

class IndexController extends Controller {
    run(req, res) {
        this.fetchList(res);
    }

    async fetchList(res) {
        const user = new User();
        let data = await user.getUserList();

        // 动态注入模板引擎
        this.setView(new CandyTemplate(this.context));
        this.render('index', {
            list: data
        });
    }
}

module.exports = IndexController;
嵌入式

// 这种方式比较灵活,不需要编写模板引擎

const Controller = require('candyjs/web/Controller');
const Handlebars = require('handlebars');
const User = require('somepath/models/User');

class IndexController extends Controller {
    run(req, res) {
        this.fetchList(res);
    }

    async fetchList(res) {
        const user = new User();
        let data = await user.getUserList();

        this.getView().getViewContent('index', (err, str) => {
            // 直接使用模板引擎对内容进行编译并输出
            let compiled = Handlebars.compile(str);

            res.end( compiled({ list: data }) );
        });
    }
}
module.exports = IndexController;

控制器层的视图 API

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

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

  • findViewFile(view) 用于获取一个视图文件的绝对路径
  • getViewContent(view, callback) 用于读取一个视图文件的内容

'use strict';

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

class IndexController extends Controller {

    run(req, res) {
        this.getView().getViewContent('index', (err, str) => {
            res.end(str);
        });
    }

}

module.exports = IndexController;

模块

模块是独立的软件单元,体量较小,由模型、视图、控制器和其他组件组成。

终端用户可以访问在应用主体中已注册的模块的控制器,CandyJs在解析路由时优先查找模块中有没有满足条件的控制器

如果项目中某个功能相对独立,与项目关联不大,随时准备删除或加入进来,那么可以考虑将其放入模块系统

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

创建模块

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


modules 模块目录
    |
    |-- newyearactive 新年活动页面
    |   |
    |   |-- controllers 模块控制器目录
    |   |   |
    |   |   |-- IndexController.js
    |   |
    |   |-- views 模块视图目录
    |   |   |
    |   |   |-- index.html
    |   |
    |   |-- 其他目录

注册模块

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


var CandyJs = require('candyjs');
var App = require('candyjs/web/Application');

new CandyJs(new App({
    ...

    // 注册模块
    // 这样路由为 /newyearactive 时会优先匹配到该模块
    'modules': {
        'newyearactive': 'app/modules/newyearactive'
    },

    ...

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

组件 & 行为

组件

组件是实现行为 (behavior) 事件 (event)的基类,如果一个类从candy/core/Component或其子类继承而来,那么这个类就拥有组件的特性

组件是candy/core/Component或其子类的实例

CandyJscandy/web/Controller类继承自candy/core/Component,所以控制器具有组件的特性

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

行为

行为是candy/core/Behavior类或其子类的实例,具有如下作用

  • 一个行为类可以用于在不改变原组件代码的情况下增强其功能
  • 行为类能够监听组件的事件并作出响应,这对于拦截框架的核心事件,并对其做出处理提供了方便

事件

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


'use strict';

var Candy = require('candyjs/Candy');
var Controller = 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;

行为的使用

从 4.4.0 开始,我们对行为进行了重构,去除了作用不大的方法注入,保留了事件监听
定义行为

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


'use strict';

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

// 行为类
class MyBehavior extends Behavior {
    constructor() {
        super();
    }

    // 监听控制器的 customEvent 事件
    // 由于一个事件可以有多个处理程序 为保证顺序 这里必须使用数组
    // 格式为 [行为名, 行为处理器]
    events() {
        return [
            ['customEvent', (e) => {
                e.result = 'data processed by behavior';
            }],
            ['customEvent2', (e) => {
                e.result += '--process2';
            }]
        ];
    }
}

module.exports = MyBehavior;

以上代码定义了行为类MyBehavior并监听了一个自定义事件customEvent

附加行为到组件

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

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


'use strict';

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

class IndexController extends Controller {

    // 重写方法
    behaviors() {
        return [
            ['myBehavior', new MyBehavior()]
        ];
    }

    run(req, res) {
        let data = {result: ''};
        this.trigger('customEvent', data);

        // 卸载行为
        this.detachBehavior('myBehavior');
        return data.result;
    }

}

module.exports = IndexController;

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


'use strict';

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

class IndexController extends Controller {

    constructor(context) {
        super(context);

        // 动态附加行为 行为里面会监听 customEvent 事件
        this.attachBehavior('myBehavior', new MyBehavior());
    }

    run(req, res) {
        let data = {result: ''};
        this.trigger('customEvent', data);
        this.trigger('customEvent2', data);

        this.detachBehavior('myBehavior');
        return data.result;
    }

}

module.exports = IndexController;

中间件

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

使用过多的中间件并不是一个好的选择,因为有些程序并不需要中间件处理,好的做法是在控制器中处理相关逻辑

CandyJs中的中间件需要通过Hook进行注册,每一个中间件是一个可以接受三个参数的函数


// 入口文件
var CandyJs = require('candyjs');
var Candy = require('candyjs/Candy');
var App = require('candyjs/web/Application');

var Hook = Candy.include('candy/core/Hook');

Hook.addHook((req, res, next) => {
    // do something
    next();
});
Hook.addHook((req, res, next) => {
    setTimeout(() => {
        next();
    }, 2000);
});

new CandyJs(new App({

    ...

})).listen(2333, function(){
    console.log('listen on 2333');
});
CandyJs暂时只提供了一个处理静态资源的中间件 理论上CandyJs兼容任何express的中间件

静态资源

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


// 入口文件
var CandyJs = require('candyjs');
var Candy = require('candyjs/Candy');
var App = require('candyjs/web/Application');
var Hook = require('candyjs/core/Hook');
var R = require('candyjs/midwares/Resource');

// 使用内置静态服务管理静态资源
// 比如 public 目录下有个 js 文件夹保存了各种业务文件
// 4.20.0 前使用以下方式
// Hook.addHook(R.serve(__dirname + '/public'));
// 4.20.0 后使用以下方式
Hook.addHook(new R(__dirname + '/public').serve());

new CandyJs(new App({

    ...

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


// 之后可以在页面中访问静态资源 真实路径是 /public/js/lib.js
<script src="/js/lib.js"></script>

URI & URL 类

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

candy/web/URI

  • parseUrl() 解析 url

var URI = 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 = 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/http/Requestcandy/http/Response

HTTP 请求 candy/http/Request 类

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

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

  • getQueryString(param) 实例方法获取 GET 请求参数
  • getParameter(param) 实例方法获取 POST 请求参数
  • getCookie(name) 实例方法获取 COOKIE
  • getHeaders() 获取 http headers 集合
CandyJs中使用 getParameter() 获取 POST 参数需要依赖第三方解析 body 的中间件,否则将反回 null

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


const bodyParser = require('body-parser');

Hook.addHook(bodyParser.json());

HTTP 响应 candy/http/Response 类

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

candy/http/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 = Candy.include('candy/http/Response');
var response = new Response(res);
response.setContent('some data from server');
response.send();

使用 response 重定向


var Response = Candy.include('candy/http/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 = Candy.include('candy/helpers/FileHelper');
var StringHelper = Candy.include('candy/helpers/StringHelper');
var TimeHelper = Candy.include('candy/helpers/TimeHelper');

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

// return &lt;script&gt;
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在入口文件中指定
  • @runtime 缓存目录 默认指向@app/runtime
  • @root 网站根目录

类的加载及实例化

类的加载

CandyJs中可以方便的使用别名来加载类


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

上面的Candy.include('candy/web/Controller');可以实现加载系统的Controller

其中开头的candy就是一个系统内置别名。CandyJs在解析路径的时候,首先会尝试将路径的开始部分当做一个别名来解析

虽然Candy.include可以很方便的加载一个类,但是这种方式加载的类是无法识别类型的,所以也就不会有代码提示功能

对于需要代码提示的人推荐使用 require 进行加载const Controller = require('candyjs/web/Controller')

类的实例化

CandyJs中很多类都是使用Candy.createObject()来进行实例化的,他的强大之处在于可以识别别名,自动加载和初始化一个类


// 1. 以字符串方式加载并实例化类,这种方式比较简单
Candy.createObject('candy/utils/LinkedQueue', '还可以传入一些构造函数参数');

// 2. 通过配置进行类的加载和实例化,这种方式除了可以传入构造函数参数外,还能在配置中传入一些配置作为动态属性
Candy.createObject({
    classPath: 'candy/utils/LinkedQueue',
    myProp1: '111',
    myProp2: '222'
}, '构造函数参数');

自定义别名

用户可以自定义别名


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

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

RESTful

在 4.0.0 之前此功能一直是实验性质的

用法

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

var CandyJs = require('candyjs');
var App = require('candyjs/rest/Application');

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

// 简单的请求
app.get('/homepage', (req, res) => {
    res.end('homepage');
});

// 含参数的请求
// id 参数会转译成正则表达式
app.get('/posts/{id}', (req, res, params) => {
    res.end(params.id);
});

// 限制参数类型
// 由于转译字符的解析问题 类型限制需要传入两个反斜线
app.get('/user/{id:\\d+}', (req, res, params) => {
    res.end(params.id);
});

// 使用一个类处理回调
// 这个请求会使用 app/api/Demo 类的 index 方法处理
app.get('/xyz', 'app/api/Demo@index');

var candyJs = new CandyJs(app);
candyJs.listen('2333', () => {
    console.log('listen on 2333')
});

RESTful 中的路由问题

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

MVC 模式接口

使用 mvc 模式也可以写接口程序,而且代码更具有组织性,更便于维护,但是缺少灵活性,传递参数没那么便利。

日志

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

使用日志

使用日志功能前,需要在入口文件注册,CandyJs可以同时配置多种日志,只需要在targets项目中配置即可


'log': {
    'targets': {
        // 文件日志
        'file': {
            // 必须参数,表示类的路径,支持别名路径
            'classPath': 'candy/log/file/Log',
            // 日志输出目录
            'logPath': __dirname + '/logs',
            // 文件名
            'logFile': 'system.log',
            'maxFileSize': 10240  // 10 MB
        }
    },
    'flushInterval': 10  // 每 10 次向文件写一次 log
}

var CandyJs = require('candyjs');

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

targets用于配置日志处理程序,现在只支持文件日志,所以上面配置了file字段,candyjs可以同时配置多个targets,只需要在和file字段平级的地方再添加一个日志配置,那么每次这日志,新配置的日志也会被执行。

flushInterval表示日志写入硬盘的频率。由于 IO 操作比较耗时,candyjs出于优化考虑,并不会每次调用日志接口就写日志,而是先缓存起来,在某一时刻一同写入。这里指定每调用 10 次日志接口向硬盘同步一次

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

日志接口

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

扩展日志

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

// 1. 定义日志类
var Candy = require('candyjs/Candy');  // 助手类
var ILog = Candy.include('candy/log/ILog');

// app/libs/MyDbLog.js
class MyDbLog extends ILog {
    flush(messages) {
        console.log(messages);
    }
}

// 2. 在入口文件配置自己的日志类
var App = require('candyjs/web/Application');

var app = new App({
    'id': 1,
    'appPath': __dirname + '/app',
    // 配置日志
    'log': {
        'targets': {
            'dblog': {
                'classPath': 'app/libs/MyDbLog'
            }
        },
        'flushInterval': 10
    }
});


// 3. 使用日志
var CandyJs = require('candyjs');  // 框架入口类

var log = CandyJs.getLogger();
log.error('This is a error message');
log.flush();

第三方日志

如果上述方式不能满足你的需求,你也可以使用第三方日志,如log4js,只需要按照第三方库的文档使用即可

缓存

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

使用缓存

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


'cache': {
    'file': {
        'classPath': 'candy/cache/file/Cache',
        'cachePath': '...'
    }
}

file用于指定使用文件缓存

缓存接口

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

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

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

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

// 异步
async function getSetData() {
    await c.set('key2', 'value2');

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

国际化

示例 从这里了解详情

异常处理

CandyJs提供了方便的异常处理机制,系统默认使用candy/web/ExceptionHandler处理异常,用户可以通过继承重写该类的handlerException()方法来实现自定义输出,或者提供一个带有handlerException()方法的自定义类来处理错误


// index.js
const app = new App({
    'id': 1,
    'appPath': __dirname + '/app',
    // 定义异常处理类
    'exceptionHandler': 'app/ExceptionHandler'
});

// app/ExceptionHandler.js
module.exports = class ExceptionHandler {
    handlerException(exp, res) {
        res.end('server error');
    }
}

文件上传

CandyJs提供了@candyjs/uploader来负责上传功能,查看详情

数据库

Session

CandyJs提供了 @candyjs/session-cookie 来实现基于 cookie 的 session

此外CandyJs还支持使用第三方 session 库,比如express-session


// index.js
const CandyJs = require('candyjs');
const App = require('candyjs/web/Application');
const Hook = require('candyjs/core/Hook');

const session = require('express-session')

Hook.addHook(session({
    secret: 'some strings'
}));

new CandyJs(new App({
    'id': 1,
    'debug': true,
    'appPath': __dirname + '/app'

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


// 控制器中使用
const Controller = require('candyjs/web/Controller');

class IndexController extends Controller {
    run(req, res) {
        let n = req.session.views || 0;
        req.session.views = ++n;

        res.write('views: ' + n);
        res.end();
    }
}

TypeScript

TypeScript 是一种由微软开发的自由和开源的编程语言,它是 JavaScript 的一个超集。

CandyJs同样支持 typescript 项目以及使用 `tsc` 命令编译项目,但是需要安装@candyjs/tswrapper模块

参考https://gitee.com/candyjs/candyjs-examples这里的 ts-* 或者 typescript 目录示例

或者参考https://github.com/candyframework/candyjs-examples这里的 ts-* 或者 typescript 目录示例

Map