Juicer.js源码解读
Version: 0.6.9-stable
Date: 8th of Aug, 2015
个人能力有限,如有分析不当的地方,恳请指正!
第一部分: 参数配置
方法与参数
参数配置方法是 juicer.set
,该方法接受两个参数或一个参数:
当传入两个参数时,如
juicer.set('cache',false)
,即是设置cache
为false
当传入一个参数时,该参数应为一个对象,如
juicer.set({cache:false})
,系统将遍历这个对象的属性来设值
可以配置的内容
我们可以配置一些参数选项,包括 cache
、strip
、errorhandling
、detection
;其默认值都是true
;我们还可以修改模板的语法边界符,如 tag::operationOpen
等。具体可配置的项可以参看其源代码。
工作原理
juicer.options = {
// 是否缓存模板编译结果
cache: true,
// 是否清除空白
strip: true,
// 是否处理错误
errorhandling: true,
// 是否检测变量是否定义
detection: true,
// 自定义函数库
_method: __creator({
__escapehtml: __escapehtml,
__throw: __throw,
__juicer: juicer
}, {})
};
选项解析如下:
-
cache
是否缓存编译结果(引擎对象)。缓存的结果存于juicer.__cache
-
strip
是否清除模板中的空白,包括换行、回车等 -
errorhandling
是否处理错误 -
detection
开启后,如果变量未定义,将用空白字符串代替变量位置,否则照常输出,所以如果关闭此项,有可能造成输出undefined
-
_method
存储的是用户注册的自定义函数,系统内部创建的自定义函数或对象有__escapehtml
处理HTML转义、__throw
抛出错误、__juicer
引用juicer
。__creator
方法本文最末讲解
在 Node.js 环境中,cache
默认值是 false
,请看下面代码
if(typeof(global) !== 'undefined' && typeof(window) === 'undefined') {
juicer.set('cache', false);
}
这段代码在结尾处可以找到。
此外,还有一个属性是 juicer.options.loose
,默认值为 undefined
(没有设置),当其值不为 false
(此亦系统默认)时,将对 {@each}
、{@if}
、{@else if}
、${}
、{@include}
等中的变量名和自定义函数名进行校验,给其中使用到的变量、函数定义并添加到模板的开头,以保证能够顺利使用。
所以,如果我们更改此设置,可能造成系统错误
// 这些操作应当避免,否则会造成系统错误
// 将`juicer.options.loose`设为`false`
// juicer.set('loose',false);
下面来看 juicer.set
方法的源代码
juicer.set = function(conf, value) {
// 引用`juicer`
var that = this;
// 反斜杠转义
var escapePattern = function(v) {
// 匹配 $ ( [ ] + ^ { } ? * | . *
// 这些符号都需要被转义
return v.replace(/[\$\(\)\[\]\+\^\{\}\?\*\|\.]/igm, function($) {
return '\\' + $;
});
};
// 设置函数
var set = function(conf, value) {
// 语法边界符匹配
var tag = conf.match(/^tag::(.*)$/i);
if(tag) {
// 由于系统这里没有判断语法边界符是否是系统所用的
// 所以一定要拼写正确
that.tags[tag[1]] = escapePattern(value);
// 重新生成匹配正则
// `juicer.tagInit`解析见下面
that.tagInit();
return;
}
// 其他配置项
that.options[conf] = value;
};
// 如果传入两个参数,`conf`表示要修改的属性,`value`是要修改的值
if(arguments.length === 2) {
set(conf, value);
return;
}
// 如果传入一个参数,且是对象
if(conf === Object(conf)) {
// 遍历该对象的自有属性设置
for(var i in conf) {
if(conf.hasOwnProperty(i)) {
set(i, conf[i]);
}
}
}
};
注释里面已经提示,通过 juicer.set
方法可以覆盖任何属性。
如果修改了语法边界符设定,将会重新生成匹配正则,下面看匹配正则的源代码
juicer.tags = {
// 操作开
operationOpen: '{@',
// 操作闭
operationClose: '}',
// 变量开
interpolateOpen: '\\${',
// 变量闭标签
interpolateClose: '}',
// 禁止对其内容转义的变量开
noneencodeOpen: '\\$\\${',
// 禁止对其内容转义的变量闭
noneencodeClose: '}',
// 注释开
commentOpen: '\\{#',
// 注释闭
commentClose: '\\}'
};
juicer.tagInit = function() {
/**
* 匹配each循环开始,以下都是OK的
* `each VAR as VALUE`, 如 {@each names as name}
* `each VAR as VALUE ,INDEX`,如 {@each names as name,key}
* `each VAR as`,如 {@each names as}
* 需要说明后两种情况:
* `,key` 是一起被捕获的,所以在编译模板的时候,系统会用`substr`去掉`,`
* as 后没有指定别名的话,默认以`value`为别名,所以
* {@each names as} 等价于 {@each names as value}
*/
var forstart = juicer.tags.operationOpen + 'each\\s*([^}]*?)\\s*as\\s*(\\w*?)\\s*(,\\s*\\w*?)?' + juicer.tags.operationClose;
// each循环结束
var forend = juicer.tags.operationOpen + '\\/each' + juicer.tags.operationClose;
// if条件开始
var ifstart = juicer.tags.operationOpen + 'if\\s*([^}]*?)' + juicer.tags.operationClose;
// if条件结束
var ifend = juicer.tags.operationOpen + '\\/if' + juicer.tags.operationClose;
// else条件开始
var elsestart = juicer.tags.operationOpen + 'else' + juicer.tags.operationClose;
// eles if 条件开始
var elseifstart = juicer.tags.operationOpen + 'else if\\s*([^}]*?)' + juicer.tags.operationClose;
// 匹配变量
var interpolate = juicer.tags.interpolateOpen + '([\\s\\S]+?)' + juicer.tags.interpolateClose;
// 匹配不对其内容转义的变量
var noneencode = juicer.tags.noneencodeOpen + '([\\s\\S]+?)' + juicer.tags.noneencodeClose;
// 匹配模板内容注释
var inlinecomment = juicer.tags.commentOpen + '[^}]*?' + juicer.tags.commentClose;
// for辅助循环
var rangestart = juicer.tags.operationOpen + 'each\\s*(\\w*?)\\s*in\\s*range\\(([^}]+?)\\s*,\\s*([^}]+?)\\)' + juicer.tags.operationClose;
// 引入子模板
var include = juicer.tags.operationOpen + 'include\\s*([^}]*?)\\s*,\\s*([^}]*?)' + juicer.tags.operationClose;
// 内联辅助函数开始
var helperRegisterStart = juicer.tags.operationOpen + 'helper\\s*([^}]*?)\\s*' + juicer.tags.operationClose;
// 辅助函数代码块内语句
var helperRegisterBody = '([\\s\\S]*?)';
// 辅助函数结束
var helperRegisterEnd = juicer.tags.operationOpen + '\\/helper' + juicer.tags.operationClose;
juicer.settings.forstart = new RegExp(forstart, 'igm');
juicer.settings.forend = new RegExp(forend, 'igm');
juicer.settings.ifstart = new RegExp(ifstart, 'igm');
juicer.settings.ifend = new RegExp(ifend, 'igm');
juicer.settings.elsestart = new RegExp(elsestart, 'igm');
juicer.settings.elseifstart = new RegExp(elseifstart, 'igm');
juicer.settings.interpolate = new RegExp(interpolate, 'igm');
juicer.settings.noneencode = new RegExp(noneencode, 'igm');
juicer.settings.inlinecomment = new RegExp(inlinecomment, 'igm');
juicer.settings.rangestart = new RegExp(rangestart, 'igm');
juicer.settings.include = new RegExp(include, 'igm');
juicer.settings.helperRegister = new RegExp(helperRegisterStart + helperRegisterBody + helperRegisterEnd, 'igm');
};
具体语法边界符的用法请参照官方文档:http://www.juicer.name/docs/docs_zh_cn.html
一般地,不建议对默认标签进行修改。当然,如果默认语法边界符规则与正在使用的其他语言语法规则冲突,修改 juicer
的语法边界符就很有用了。
需要注意,{@each names as}
等价于 {@each names as value}
,尽管我们仍要保持正确书写的规则,避免利用系统自动纠错机制
// 如下模板的写法是不推荐的
/**
{@each list as}
${value.title}
{@/each}
*/
第二部分: 注册自定义函数
上面说,juicer.options._method
存储了用户的自定义函数,那么我们如何注册以及如何使用自定义函数呢?
注册/销自定义函数
juicer.register
方法用来注册自定义函数
juicer.unregister
方法用来注销自定义函数
// `fname`为函数名,`fn`为函数
juicer.register = function(fname, fn) {
// 自定义函数均存储于 `juicer.options._method`
// 如果已经注册了该函数,不允许覆盖
if(_method.hasOwnProperty(fname)) {
return false;
}
// 将新函数注册进入
return _method[fname] = fn;
};
juicer.unregister = function(fname) {
var _method = this.options._method;
// 没有检测是否注销的是系统自定义函数
// 用户不要注销错了
if(_method.hasOwnProperty(fname)) {
return delete _method[fname];
}
};
自定义函数都是存储在juicer.options._method
中的,因此以下方法可以跳过函数是否注册的检验强行更改自定义函数,这些操作很危险:
// 这些操作应当避免,否则会造成系统错误
// 改变`juicer.options._method`
// juicer.set('_method',{});
// juicer.unregister('__juicer');
// juicer.unregister('__throw');
// juicer.unregister('__escapehtml');
第三部分: 编译模板
先看下 juicer
的定义部分。
var juicer = function() {
// 将传递参数(伪数组)切成数组后返回给`args`,以便调用数组的方法
var args = [].slice.call(arguments);
// 将`juicer.options`推入`args`,表示渲染使用当前设置
args.push(juicer.options);
/**
* 下面将获取模板内容
* 模板内容取决于我们传递给`juicer`函数的首参数
* 可以是模板节点的id属性值
* 也可以是模板内容本
*/
// 首先会试着匹配,匹配成功就先当作id处理
// 左右两侧的空白会被忽略
// 如果是`#`开头,后面跟着字母、数字、下划线、短横线、冒号、点号都可匹配
// 所以这么写都是可以的:`id=":-."`
if(args[0].match(/^\s*#([\w:\-\.]+)\s*$/igm)) {
// 如果传入的是模板节点的id,会通过`replace`方法准确匹配并获取模板内容
// 回调函数的首参`$`是匹配的全部内容(首参),$id是匹配的节点id
args[0].replace(/^\s*#([\w:\-\.]+)\s*$/igm, function($, $id) {
// node.js环境没有`document`,所以会先判断`document`
var _document = document;
// 找寻节点
var elem = _document && _document.getElementById($id);
// 如果该节点存在,节点的`value`或`innerHTML`就是模板内容
// 即是说,存放模板的内容节点只要有`value`或`innerHTML`属性即可
//