资讯专栏INFORMATION COLUMN

JavaScript > Juicer.js源码解读

mrcode / 2014人阅读

摘要:具体可配置的项可以参看其源代码。那引擎对象是如何被构造出来的呢看这句由此,我们进入了的核心构造函数,。由于该构造函数篇幅很长,我们先看下简略版的结构,然后拆开来分析。此外,推荐使用注册自定义函数,而非使用。

Juicer.js源码解读
  

Version: 0.6.9-stable

Date: 8th of Aug, 2015

个人能力有限,如有分析不当的地方,恳请指正!

第一部分: 参数配置

方法与参数

参数配置方法是 juicer.set,该方法接受两个参数或一个参数:

当传入两个参数时,如 juicer.set("cache",false) ,即是设置 cachefalse

当传入一个参数时,该参数应为一个对象,如 juicer.set({cache:false}),系统将遍历这个对象的属性来设值

可以配置的内容

我们可以配置一些参数选项,包括 cachestriperrorhandlingdetection;其默认值都是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`属性即可
            //                 
阅读需要支付1元查看
<