CandyJs
是一个健壮可扩展的 Web 框架,它提供了一套优雅的编写代码的规范,使得编写 Web 应用变得得心应手。
CandyJs
实现了一套动态路由
规范,您不需要提前注册所需要的路由,只需要输入网址CandyJs
会自己找到路由对应的组件。
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;
,那么后面的切面类不再执行
使用切面,首先必须编写切面类,切面类需要从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;
}
}
使用切面很简单,只需要在控制器中添加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
模式中,每一个路由会对应到一个控制器文件
一般一个路由对应一个控制器 路由有如下两种格式
route_prefix[/controllerId]
moduleId[/controllerId]
[/controllerId]
可以省略,如果省略,那么默认值为index
,即默认控制器为IndexControlelr
直接访问域名 http://localhost/
会默认映射到 app/controllers/index/IndexControlelr
控制器
首先会查找是否存在 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
模式中的一部分,是代表业务数据和规则的对象
模型并不能直接操作数据库,它只提供数据存储和校验,数据库操作需要借助数据库操作类实现
数据库操作 从这里了解详情
模型示例 从这里了解详情
使用模型需要以下几个步骤
在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: ['邮箱格式不正确']
}
];
}
}
当有了定义好的模型后,接下来就是为模型填充数据,数据一般由用户通过前台页面以表单形式发送到后台,这里以控制器处理为例
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,通过此种映射方式实现字段不一致时的对应
模型提供了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;
如果用户的控制器从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
或其子类的实例
CandyJs
中candy/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>
candy/web/URI 及 candy/web/URL
类提供了对 uri 和 url 操作的方法
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');
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/Request
和candy/http/Response
用于处理 http 请求
该对象提供了对诸如请求参数 HTTP头 cookies
等信息的访问方法
candy/http/Request
类提供了一组实例和静态方法来操作需要的数据
getQueryString(param)
实例方法获取 GET 请求参数getParameter(param)
实例方法获取 POST 请求参数getCookie(name)
实例方法获取 COOKIEgetHeaders()
获取 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());
主要用户向客户端输出响应消息
candy/http/Response
类提供了一组实例和静态方法来操作响应数据
setStatusCode(value[, text])
设置 http status codesetHeader(name, value)
设置 headersetContent(content)
设置实体内容setCookie(name, value[, options])
设置一条 cookiesend([content])
发送 HTTP 响应到客户端redirect(url[, statusCode = 302])
页面重定向
var Response = Candy.include('candy/http/Response');
var response = new Response(res);
response.setContent('some data from server');
response.send();
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 <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
在入口文件中指定@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');
在 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 中的路由是用正则表达式来实现的,它可以实现非常灵活的路由配置 但是相对于 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
来负责上传功能,查看详情
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 是一种由微软开发的自由和开源的编程语言,它是 JavaScript 的一个超集。
CandyJs
同样支持 typescript 项目以及使用 `tsc` 命令编译项目,但是需要安装@candyjs/tswrapper
模块
参考https://gitee.com/candyjs/candyjs-examples这里的 ts-* 或者 typescript 目录示例
或者参考https://github.com/candyframework/candyjs-examples这里的 ts-* 或者 typescript 目录示例