摘要:具体可配置的项可以参看其源代码。那引擎对象是如何被构造出来的呢看这句由此,我们进入了的核心构造函数,。由于该构造函数篇幅很长,我们先看下简略版的结构,然后拆开来分析。此外,推荐使用注册自定义函数,而非使用。
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 + "eachs*([^}]*?)s*ass*(w*?)s*(,s*w*?)?" + juicer.tags.operationClose; // each循环结束 var forend = juicer.tags.operationOpen + "/each" + juicer.tags.operationClose; // if条件开始 var ifstart = juicer.tags.operationOpen + "ifs*([^}]*?)" + 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 ifs*([^}]*?)" + juicer.tags.operationClose; // 匹配变量 var interpolate = juicer.tags.interpolateOpen + "([sS]+?)" + juicer.tags.interpolateClose; // 匹配不对其内容转义的变量 var noneencode = juicer.tags.noneencodeOpen + "([sS]+?)" + juicer.tags.noneencodeClose; // 匹配模板内容注释 var inlinecomment = juicer.tags.commentOpen + "[^}]*?" + juicer.tags.commentClose; // for辅助循环 var rangestart = juicer.tags.operationOpen + "eachs*(w*?)s*ins*range(([^}]+?)s*,s*([^}]+?))" + juicer.tags.operationClose; // 引入子模板 var include = juicer.tags.operationOpen + "includes*([^}]*?)s*,s*([^}]*?)" + juicer.tags.operationClose; // 内联辅助函数开始 var helperRegisterStart = juicer.tags.operationOpen + "helpers*([^}]*?)s*" + juicer.tags.operationClose; // 辅助函数代码块内语句 var helperRegisterBody = "([sS]*?)"; // 辅助函数结束 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`属性即可 //