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}
<a href="${value.href}">${value.title}</a>
{@/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`属性即可
// <script>可以,<div>可以,<input>也可以
// 如果没有节点,还是把首参值作为模板内容
args[0] = elem ? (elem.value || elem.innerHTML) : $;
});
}
// 如果是浏览器环境
if(typeof(document) !== 'undefined' && document.body) {
// 先编译`document.body.innerHTML`一次
juicer.compile.call(juicer, document.body.innerHTML);
}
// 如果只传入了模板,仅返回编译结果,而不会立即渲染
if(arguments.length == 1) {
return juicer.compile.apply(juicer, args);
}
// 如果传入了数据,编译之后立即渲染
if(arguments.length >= 2) {
return juicer.to_html.apply(juicer, args);
}
};
juicer.compile
方法是模板内容编译入口,其返回一个编译引擎对象,引擎对象的 render
方法将执行渲染.
juicer.to_html
方法就是执行 juicer.compile
后立即执行 render
。我们在向 juicer
函数传入两个参数的时候,就会立即执行这一方法。
先看 juicer.to_html
juicer.to_html = function(tpl, data, options) {
// 如果没有传入设置或者有新设置,先重新生成设置
if(!options || options !== this.options) {
options = __creator(options, this.options);
}
// 渲染
return this.compile(tpl, options).render(data, options._method);
};
下面看 juicer.compile
是如何编译模板内容的
juicer.compile = function(tpl, options) {
// 如果没有传入设置或者有新设置,先重新生成设置
if(!options || options !== this.options) {
options = __creator(options, this.options);
}
try {
// 构造引擎对象,如果已经缓存则优先使用缓存
var engine = this.__cache[tpl] ?
this.__cache[tpl] :
new this.template(this.options).parse(tpl, options);
// 除非设定`juicer.options.cache`为`false`,否则缓存引擎对象
if(!options || options.cache !== false) {
this.__cache[tpl] = engine;
}
// 返回引擎对象
return engine;
} catch(e) {
// 抛出错误,此方法在本文末介绍
__throw('Juicer Compile Exception: ' + e.message);
// 返回一个新对象,该对象仍有`render`方法,但操作为空
return {
render: function() {}
};
}
};
第四部分: 引擎对象
juicer.compile
方法在正常情况下会返回模板引擎对象,继而执行该对象的 render
方法就可以得到我们的模板编译结果(HTML)。那引擎对象是如何被构造出来的呢?
看这句 new this.template(this.options).parse(tpl, options);
由此,我们进入了 juicer
的核心构造函数,juicer.template
。由于该构造函数篇幅很长,我们先看下简略版的结构,然后拆开来分析。
juicer.template = function(options) {
// 由于`juicer.template`是作为构造器使用的
// 因此`this`引用的是`juicer.template`构造的实例
var that = this;
// 引用选项配置`juicer.options`
this.options = options;
// 变量解析方法
this.__interpolate = function(_name, _escape, options) {};
// 模板解析方法
this.__removeShell = function(tpl, options) {};
// 根据`juicer.options.strip`判断是否清除多余空白
// 而后调用`juicer.template.__convert`
this.__toNative = function(tpl, options) {};
// 词法分析,生成变量和自定义函数定义语句
this.__lexicalAnalyze = function(tpl) {};
// 为`juicer.template.__toNative`所调用
// 将模板解析为可执行的JavaScript字符串
this.__convert = function(tpl, strip) {};
// 渲染模板的入口
this.parse = function(tpl, options) {};
};
好,下面我们一点点地看
juicer.template.__interpolate
this.__interpolate = function(_name, _escape, options) {
/**
* `_define` 切割`_name`
* `_fn`为变量名,这里先暂取值为 `_define[0]`
* 当传入的首参没有`|`分割变量和函数时
* `_fn` === `_define[0]` === `_name`
* 表明是 ${name} 形式
* 当有`|`分割时,`_fn`的初始值会被覆盖
* 形式是 ${name|function} 或 ${name|function,arg1,arg2}
* `_cluster`为函数及传参
*/
var _define = _name.split('|'), _fn = _define[0] || '', _cluster;
// 如果有`|`分割,即有函数和传参
// 举个例子: `VAR|FNNAME,FNVAR,FNVAR2
if(_define.length > 1) {
// VAR
_name = _define.shift();
// [FNNAME,FNVAR,FNVAR2]
_cluster = _define.shift().split(',');
// `[_name].concat(_cluster)`是数组会自动调用`toString()`方法
// 结果就是:_metod.FNNAME.call({},VAR,FNVAR,FNVAR2)
_fn = '_method.' + _cluster.shift() + '.call({}, ' + [_name].concat(_cluster) + ')';
}
/**
* 返回结果
* 如果`_escape`为真,将转义内容
* 如果`juicer.options.detection`为真,将检测变量是否定义
* 返回结果举例(转义内容且检测变量定义)
* <%=_method.__escapehtml.escaping(_method.__escapehtml.detection(`_fn`))%>
*/
return '<%= ' + (_escape ? '_method.__escapehtml.escaping' : '') + '(' +
(!options || options.detection !== false ? '_method.__escapehtml.detection' : '') + '(' +
_fn +
')' +
')' +
' %>';
};
这个方法用来分析变量的。这也允许我们去使用自定义函数。如我们创建自定义函数
// 通过`juicer.register`直接创建
juicer.register('echoArgs',function(a,b){
return a + b;
});
// 或者在模板内通过内联辅助函数间接创建
// 本质仍然是使用了`juicer.register`
{@helper echoArgs}
function(a,b){
return a+b;
}
{@/helper}
我们在模板里就可以这么用了:
// 使用自定义函数
${value.href|echoArgs|value.title}
juicer.template.__removeShell
this.__removeShell = function(tpl, options) {
// 计数器
// 利用计数器避免遍历时创建的临时变量与其他变量冲突
var _counter = 0;
// 解析模板内容
tpl = tpl
// 解析模板里的内联辅助函数并注册
.replace(juicer.settings.helperRegister, function($, helperName, fnText) {
// `annotate`函数返回形参名称和函数语句数组,本文末介绍
var anno = annotate(fnText);
// 内联辅助函数参数
var fnArgs = anno[0];
// 内敛辅助函数语句
var fnBody = anno[1];
// 构造内联辅助函数
var fn = new Function(fnArgs.join(','), fnBody);
// 注册到自定义函数库`juicer.options._method`
juicer.register(helperName, fn);
// 没有清除{@helper}{@/helper}
return $;
})
/**
* 解析each循环语句
* 举个例子: {@each names as name,index}
* `_name` => names
* `alias` => name
* `key` => ,index 注意正则匹配后前面有逗号
*/
.replace(juicer.settings.forstart, function($, _name, alias, key) {
// `alias` 如果木有,取为`value`,如 {@each names as} 情况
// `key` 如果需要属性名,取之
var alias = alias || 'value', key = key && key.substr(1);
// 避免重复
var _iterate = 'i' + _counter++;
/**
* 返回替换结果,举例如下
* <% ~function(){
for(var i0 in names){
if(names.hasOwnProperty(i0)){
var name = names[i0];
var index = i0;
%>
*/
return '<% ~function() {' +
'for(var ' + _iterate + ' in ' + _name + ') {' +
'if(' + _name + '.hasOwnProperty(' + _iterate + ')) {' +
'var ' + alias + '=' + _name + '[' + _iterate + '];' +
(key ? ('var ' + key + '=' + _iterate + ';') : '') +
' %>';
})
// 解析each循环结束
.replace(juicer.settings.forend, '<% }}}(); %>')
// 解析if条件开始
.replace(juicer.settings.ifstart, function($, condition) {
return '<% if(' + condition + ') { %>';
})
// 解析if条件结束
.replace(juicer.settings.ifend, '<% } %>')
// 解析else条件
.replace(juicer.settings.elsestart, function($) {
return '<% } else { %>';
})
// 解析else if条件
.replace(juicer.settings.elseifstart, function($, condition) {
return '<% } else if(' + condition + ') { %>';
})
// 解析禁止对其内容转义的变量
.replace(juicer.settings.noneencode, function($, _name) {
return that.__interpolate(_name, false, options);
})
// 解析变量
.replace(juicer.settings.interpolate, function($, _name) {
return that.__interpolate(_name, true, options);
})
// 清除评论
.replace(juicer.settings.inlinecomment, '')
// 解析辅助循环
.replace(juicer.settings.rangestart, function($, _name, start, end) {
var _iterate = 'j' + _counter++;
return '<% ~function() {' +
'for(var ' + _iterate + '=' + start + ';' + _iterate + '<' + end + ';' + _iterate + '++) {{' +
'var ' + _name + '=' + _iterate + ';' +
' %>';
})
// 载入子模板
.replace(juicer.settings.include, function($, tpl, data) {
// 如果是node.js环境
if(tpl.match(/^file\:\/\//igm)) return $;
// 返回 <% _method.__juicer(tpl,data);%>
return '<%= _method.__juicer(' + tpl + ', ' + data + '); %>';
});
// 当`juicer.options.errorhandling`不为`false`
if(!options || options.errorhandling !== false) {
tpl = '<% try { %>' + tpl;
tpl += '<% } catch(e) {_method.__throw("Juicer Render Exception: "+e.message);} %>';
}
return tpl;
};
计数器的作用已经在注释中说明,其命名方式是字符串 i
或 j
加数字。
因而,如下模板就可能错误:
// 如下模板的写法是不推荐的
// 应当避免遍历中的变量名与计数器创建的变量名冲突
/**
// 模板
${i0}
{@each data as value}
{# i0 可能会因为上面的遍历创建的临时变量而被替换}
${i0}
{@/each}
${i0}
${j1}
{@each i in range(1,5)}
{# j1 可能会因为上面的循环创建的临时变量而被替换}
${j1}
{@/each}
${j1}
// 数据
{
data: {
temp1: 'value1',
temp2: 'value2'
},
i0: 'i0',
j1: 'j1'
}
// 结果
i0 temp1 temp2 i0 j1 1 2 3 4 j1
*/
书写变量在遍历中出现的变量时候,一定要避免与系统创造的临时变量重名。不过,由于在编译模板的时候,遍历是在一个闭包中执行的,因而临时变量不会影响到遍历外的变量。
此外,推荐使用 juicer.register
注册自定义函数,而非使用{@helper}{/@helper}
。因为内联函数的代码在生成HTML的时候没有被清除。
如果要清除之,须将第 #297 行
return $;
更改为
return '';
juicer.template.__toNative
this.__toNative = function(tpl, options) {
// 当`juicer.options.strip`不为`false`时清除多余空白
return this.__convert(tpl, !options || options.strip);
};
juicer.template.__toNative
调用 juicer.template.__convert
方法,juicer.template.__convert
就不作分析了,通过不断替换切割等组成函数语句。
juicer.__lexicalAnalyze
this.__lexicalAnalyze = function(tpl) {
// 变量
var buffer = [];
// 方法,已经存储到`juicer.options.__method`才能被采用
var method = [];
// 返回结果
var prefix = '';
// 保留词汇,因为这些词汇不能用作变量名
var reserved = [
'if', 'each', '_', '_method', 'console',
'break', 'case', 'catch', 'continue', 'debugger', 'default', 'delete', 'do',
'finally', 'for', 'function', 'in', 'instanceof', 'new', 'return', 'switch',
'this', 'throw', 'try', 'typeof', 'var', 'void', 'while', 'with', 'null', 'typeof',
'class', 'enum', 'export', 'extends', 'import', 'super', 'implements', 'interface',
'let', 'package', 'private', 'protected', 'public', 'static', 'yield', 'const', 'arguments',
'true', 'false', 'undefined', 'NaN'
];
// 查找方法
var indexOf = function(array, item) {
// 如果在数组中查找,直接用数组的`indexOf`方法
if (Array.prototype.indexOf && array.indexOf === Array.prototype.indexOf) {
return array.indexOf(item);
}
// 如果在伪数组中查找,遍历之
for(var i=0; i < array.length; i++) {
if(array[i] === item) return i;
}
return -1;
};
// 变量名分析函数
var variableAnalyze = function($, statement) {
statement = statement.match(/\w+/igm)[0];
// 如果没有分析过,并且非保留字符
if(indexOf(buffer, statement) === -1 && indexOf(reserved, statement) === -1 && indexOf(method, statement) === -1) {
// 跳过window内置函数
if(typeof(window) !== 'undefined' && typeof(window[statement]) === 'function' && window[statement].toString().match(/^\s*?function \w+\(\) \{\s*?\[native code\]\s*?\}\s*?$/i)) {
return $;
}
// 跳过node.js内置函数
if(typeof(global) !== 'undefined' && typeof(global[statement]) === 'function' && global[statement].toString().match(/^\s*?function \w+\(\) \{\s*?\[native code\]\s*?\}\s*?$/i)) {
return $;
}
// 如果是自定义函数
if(typeof(juicer.options._method[statement]) === 'function' || juicer.options._method.hasOwnProperty(statement)) {
// 放进 `method`
method.push(statement);
return $;
}
// 存为变量
buffer.push(statement);
}
return $;
};
// 分析出现在for/变量/if/elseif/include中的变量名
tpl.replace(juicer.settings.forstart, variableAnalyze).
replace(juicer.settings.interpolate, variableAnalyze).
replace(juicer.settings.ifstart, variableAnalyze).
replace(juicer.settings.elseifstart, variableAnalyze).
replace(juicer.settings.include, variableAnalyze).
replace(/[\+\-\*\/%!\?\|\^&~<>=,\(\)\[\]]\s*([A-Za-z_]+)/igm, variableAnalyze);
// 遍历要定义的变量
for(var i = 0;i < buffer.length; i++) {
prefix += 'var ' + buffer[i] + '=_.' + buffer[i] + ';';
}
// 遍历要创建的函数表达式
for(var i = 0;i < method.length; i++) {
prefix += 'var ' + method[i] + '=_method.' + method[i] + ';';
}
return '<% ' + prefix + ' %>';
};
juicer.template.parse
this.parse = function(tpl, options) {
// 指向构造的引擎实例
// `that`和`_that`都是一个引用,暂不明为何这样写
var _that = this;
// `juicer.options.loose` 不为 `false`
if(!options || options.loose !== false) {
tpl = this.__lexicalAnalyze(tpl) + tpl;
}
// 编译模板,得到可执行的JavaScript字符串
tpl = this.__removeShell(tpl, options);
tpl = this.__toNative(tpl, options);
// 构造为函数
this._render = new Function('_, _method', tpl);
// 渲染方法
this.render = function(_, _method) {
// 检查自定义函数
if(!_method || _method !== that.options._method) {
_method = __creator(_method, that.options._method);
}
// 执行渲染
return _that._render.call(this, _, _method);
};
// 返回实例,方便链式调用
return this;
};
_that
引起了我疑惑。为什么不直接用 that
?在 juicer.template
分析时我指出,作为构造器被使用的 juicer.template
,var that = this;
的 that
就是指向这个被创建出来的模板引擎对象的,和 _that
起一样的作用。
那为什么要用 _that
或者 that
来替代 this
呢?我想是为了尽可能保证渲染正常。如果我们如此使用:
var render = juicer('#:-.').render;
render({});
代码是可以正常运行的。
但如果我们把语句改为 return this._render.call(this, _, _method);
,则会报错,因为这时候,render
作为全局上下文中的变量,函数中的 this
指针指向了全局对象,而全局对象是没有渲染方法的。
第五部分 辅助函数
最后分析下 juicer
里的一些辅助性函数。
用于转义的对象
var __escapehtml = {
// 转义列表
escapehash: {
'<': '<',
'>': '>',
'&': '&',
'"': '"',
"'": ''',
'/': '/'
},
// 获取要转义的结果,如传入`<`返回`<`
escapereplace: function(k) {
return __escapehtml.escapehash[k];
},
// 对传参进行转义
escaping: function(str) {
return typeof(str) !== 'string' ? str : str.replace(/[&<>"]/igm, this.escapereplace);
},
// 检测,如果传参是undefined,返回空字符串,否则返回传参
detection: function(data) {
return typeof(data) === 'undefined' ? '' : data;
}
};
抛出错误方法
// 接受的参数是`Error`构造的实例
var __throw = function(error) {
// 如果控制台可用
if(typeof(console) !== 'undefined') {
// 如果控制台可以抛出警告
if(console.warn) {
console.warn(error);
return;
}
// 如果控制台可以记录
if(console.log) {
console.log(error);
return;
}
}
// 除此之外都直接抛出错误
throw(error);
};
合并对象方法
传入两个对象,并返回一个对象,这个新对象同时具有两个对象的属性和方法。由于 o
是引用传递,因此 o
会被修改
var __creator = function(o, proto) {
// 如果`o`不是对象,则新建空对象
o = o !== Object(o) ? {} : o;
// 仅在一些高级浏览器中有用
if(o.__proto__) {
o.__proto__ = proto;
return o;
}
// 空函数
var empty = function() {};
// 使用原型模式创建新对象
var n = Object.create ?
Object.create(proto) :
new(empty.prototype = proto, empty);
// 将`o`的自有属性赋给新对象
for(var i in o) {
if(o.hasOwnProperty(i)) {
n[i] = o[i];
}
}
// 返回新对象
return n;
};
字符串形式函数解析方法
传入字符串形式的函数或函数,如果是函数,会利用函数tostring
方法即可获得其字符串形式,继而解析提取函数的参数名和函数代码块内的语句。
var annotate = function(fn) {
// 匹配函数括号里的参数名称
var FN_ARGS = /^function\s*[^\(]*\(\s*([^\)]*)\)/m;
// 匹配逗号,用来分割参数名
var FN_ARG_SPLIT = /,/;
// 匹配参数,如果开头有下划线结尾也得有下划线
// 因此自定义函数应避免使用`_X_`形式作为形参
var FN_ARG = /^\s*(_?)(\S+?)\1\s*$/;
// 匹配函数的代码块里语句
var FN_BODY = /^function[^{]+{([\s\S]*)}/m;
// 匹配注释
var STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg;
// 函数的参数
var args = [],
// 函数字符串形式
fnText,
// 函数代码块内的语句
fnBody,
// 函数的形式参数匹配结果
// 不是直接的参数名称,之后会通过`replace`操作将真正的名称推入`args`
argDecl;
// 如果传入是函数且函数接收参数,`toString`转成字符串
if (typeof fn === 'function') {
if (fn.length) {
fnText = fn.toString();
}
// 如果传入的是字符串,即函数字符串形式
} else if(typeof fn === 'string') {
fnText = fn;
}
// 清除两边空白
// 低版本浏览器没有 `String.prototype.trim`
fnText = fnText.trim();
// 获取函数参数名称数组
argDecl = fnText.match(FN_ARGS);
// 获取函数语句
fnBody = fnText.match(FN_BODY)[1].trim();
// `argDecl[1].split(FN_ARG_SPLIT)` 就是函数的参数名
// 遍历函数参数名称数组
for(var i = 0; i < argDecl[1].split(FN_ARG_SPLIT).length; i++) {
// 赋值为参数名称
var arg = argDecl[1].split(FN_ARG_SPLIT)[i];
// 通过替换操作来将正确的函数名称推入`arg`
arg.replace(FN_ARG, function(all, underscore, name) {
// 过滤下划线前缀
args.push(name);
});
}
// 返回形参名称和函数语句
return [args, fnBody];
};
如果使用 {@helper}
创建自定义函数,_X_
形参将被过滤为 X
,即
// 模板
{@helper userFunc}
function (_X_){
}
{@/helper}
// 编译结果
juicer.options._method.userFunc = function(X){
};
不过,使用 juicer.register
方法将不会过滤下划线。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。