一步一步实现一个Javascript模板引擎(三)

前两篇文章介绍了js模板引擎的基本原理,同时给出了核心代码,本文列出重构后的代码,当没提供数据的时候,返回一个渲染函数,具体如下:

/**
 * 模板引擎
 * @name    myTemplate
 * @param   {String}  模板字符串
 * @param   {Object}  数据
 * @return  {String, Function}  渲染好的HTML字符串或者渲染方法
 */
var myTemplate = function template() {
    'use strict';
    
    var args = Array.prototype.slice.call(arguments),
        tmpl = args.shift().replace(/\r|\n/g, "").replace(/"/g, '\\"'), //转义"号
        funcBody = 'var result = "";',
        func;

    funcBody += ' result += "' + tmpl + '";'; 
    funcBody = funcBody.replace(/<%=\s*([^>]*)\s*%>/g, function(match, $1) {
        return '" + ' + $1.replace(/\\"/g, '"') + ' + "'; //替换的同时,恢复<%=%>中被转义的"号
    });
    funcBody = funcBody.replace(/<%\s*([^>]*)\s*%>/g, function(match, $1) {
        return '";' + $1.replace(/\\"/g, '"') + 'result += "'; //替换的同时,恢复<%=%>中被转义的"号
    });

    funcBody += " return result;";

    func = new Function(funcBody);
    if (args.length > 0) {
        return func.apply(args.shift(), args); //返回渲染好的HTML字符串
    }
    
    return function() { //返回渲染方法
        args = Array.prototype.slice.call(arguments);
        return func.apply(args.shift(), args);
    }
};

myTemplate.version = '0.1.0';

该项目已经开源在Github上了,感兴趣的童鞋可以去查看源码及相关文档和demo。

项目名称:myTemplate
项目地址:https://github.com/guoyao/myTemplate

Posted in Web前端开发. Tags: , . 没有评论 »

一步一步实现一个Javascript模板引擎(二)

上一篇文章实现了一个简单的js模板引擎,但当时我们只是考虑了模板中的输出,没考虑模板中的js逻辑,下面我们来处理模板中的js逻辑问题。我们来处理一个复杂的模板,模板代码如下:

<script type="text/tmpl" id="template">
    <p><strong>Name:</strong> <%= this.name %></p>
    <p><strong>Age:</strong> <%= this.age %></p>
    <% if (this.gender) { %>
        <p><strong>Gender:</strong> <%= this.gender %></p>
    <% } %>
    <% if (this.children) { %>
        <p><strong>Children:</strong></p>
        <% for (var i = 0, len = this.children.length, child; i < len; i++) { 
            child = this.children[i]; 
        %>
            <ul style="background-color: #f5f5f5;">
                <li><strong>Name:</strong> <%= child.name %></li>
                <li><strong>Age:</strong> <%= child.age %></li>
                <% if (child.gender == "female") { %>
                    <li><strong>Gender:</strong> <%= child.gender %></li>
                <% } %>
            </ul>
        <% } %>
    <% } %>
</script>

以上模板中包含了较多的js逻辑代码,看起来很复杂,不好解析,其实不然,原理还是一样的,使用正则表达式替换,功能增强后的模板引擎代码如下:

var guiTemplate = (function () {
    function template(tmpl, data) {
        var funcBody = 'var result = "";',
            func;

        tmpl = tmpl.replace(/\r|\n/g, "").replace(/"/g, '\\"'); //转义"号
        funcBody += ' result += "' + tmpl + '";'; 
        funcBody = funcBody.replace(/<%=\s*([^>]*)\s*%>/g, function(match, $1) {
            return '" + ' + $1.replace(/\\"/g, '"') + ' + "'; //替换的同时,恢复<%=%>中被转义的"号
        });
        funcBody = funcBody.replace(/<%\s*([^>]*)\s*%>/g, function(match, $1) {
            return '";' + $1.replace(/\\"/g, '"') + 'result += "'; //替换的同时,恢复<%=%>中被转义的"号
        });

        funcBody += " return result;";

        func = new Function(funcBody);
        return func.call(data);
    }
    return {
      template: template
    };
})();

调用代码如下:

var template = document.getElementById("template").innerHTML,
    html = guiTemplate.template(template, {
        name: 'guoyao',
        age: 26,
        gender: 'male',
        children: [
            {
                name: 'child 1',
                age: 5,
                gender: 'female'
            },
            {
                name: 'child 2',
                age: 3,
                gender: 'male'
            }
        ]
    });

生成的html如下:

<p><strong>Name:</strong> guoyao</p>        
<p><strong>Age:</strong> 26</p>                    
<p><strong>Gender:</strong> male</p>                            
<p><strong>Children:</strong></p>                            
<ul style="background-color: #f5f5f5;">                    
    <li><strong>Name:</strong> child 1</li>                    
    <li><strong>Age:</strong> 5</li>                                            
    <li><strong>Gender:</strong> female</li>                                    
</ul>                            
<ul style="background-color: #f5f5f5;">                    
    <li><strong>Name:</strong> child 2</li>                    
    <li><strong>Age:</strong> 3</li>                                    
</ul>

本文我们基本上实现了一个比较完整的js模板引擎,当然跟目前流行的开源模板引擎比较起来,我们的还不够完善,有空的时候我会继续完善它,不久将开源在Gitbub上,有兴趣的可以关注一下。

Posted in Web前端开发. Tags: , . 没有评论 »

一步一步实现一个Javascript模板引擎(一)

目前Web前端越来越复杂,Javascript MVC / MVVM思想也开始流行起来。Javascript模板引擎作为数据与界面分离工作中最重要一环,越来越受开发者关注。那么什么是模板引擎?简单点说,模板引擎的功能就是将一个字符串中的待定变量用js对象的对应属性来替换。目前流行的js模板引擎有:Mustache、Underscore、doT、Handlebars、Juicer等。其实要自己实现一个模板引擎并不太困难,其原理都大同小异。接下来就跟着我一步一步来实现一个js模板引擎。

示例模板字符串如下:

<p>Name: <%= this.name %></p>
<p>Age: <%= this.age %></p>

模板引擎处理后的字符串如下:

<p>Name: Guoyao</p>
<p>Age: 27</p>

模板引擎代码如下:

var guiTemplate = (function () {
    function template(tmpl, data) {
        var funcBody = 'var result = "";',
            func;

        tmpl = tmpl.replace(/\r|\n/g, "").replace(/"/g, '\\"'); //转义"号
        funcBody += ' result += "' + tmpl + '";'; 
        funcBody = funcBody.replace(/<%=\s*([^>]*)\s*%>/g, function(match, $1) {
            return '" + ' + $1.replace(/\\"/g, '"') + ' + "'; //替换的同时,恢复<%=%>中被转义的"号
        });

        funcBody += " return result;";

        func = new Function(funcBody);
        return func.call(data);
    }
    return {
      template: template
    };
})();

调用代码如下:

var tmpl = '<p>Name: <%= this.name %></p><p>Age: <%= this.age %></p>';
guiTemplate.template(tmpl, {name: 'Guoyao', age: 27});

至此,我们只是考虑了模板中的输出,没考虑模板中的js逻辑,下一步我们来处理模板中的js逻辑问题。

Posted in Web前端开发. Tags: , . 没有评论 »

js双向数据绑定的简单实现

双向数据绑定是指在UI上绑定数据对象的某个属性,当该对象的此属性发生变化时,所有绑定了该对象此属性的UI元素同时更新;同理,当绑定了对象属性的UI更新时(如用户输入),数据对象同时被更新,同时其它绑定了此属性的UI也同样被更新。许多流行的JS MVC框架例如Ember.js,AngularJS以及KnockoutJS都实现了双向数据绑定。总的来说,实现双向绑定的基本思想是一样的:

1、哪些UI元素绑定了对象的哪个属性
2、监听对象属性的变化,变化后通知到所有绑定了该对象属性的UI元素
3、监听UI元素的变化,变化后通知对象,更新对象对应属性和其它所有绑定了同样属性的UI元素

<!DOCTYPE html>
<html>
<head>
    <title>Two Way Binding</title>
</head>
<body>
    <input type="text" data-bind="user.name"/>
    <input type="text" data-bind="user.name"/>
    <input type="text" data-bind="user.name"/>
    <input type="text" data-bind="user.name"/>
    <input type="text" data-bind="user.name"/>
    <textarea data-bind="user.name"></textarea>
    <input id="changeModelPropertyButton" type="button" value="Change Model Property"/>
    <script src="js/two-way-binding.js"></script>
</body>
</html>

js/two-way-binding.js

(function () {

    var _ = {
            toArray: function (value) {
                return Array.prototype.slice.call(value);
            },
            isFunction: function (value) {
                return Object.prototype.toString.call(value) === "[object Function]";
            },
            extend: function (destination, source) {
                for (var property in source) {
                    destination[property] = source[property];
                }
                return destination;
            }
        },
        Class = (function () { //类工厂
            var tempFunc = function () {},
                emptyInitializeFunc = function () {};

            function Class() {
                var superclass = null,
                    properties = _.toArray(arguments);

                if (_.isFunction(properties[0])) {
                    superclass = properties.shift();
                }

                function klass() {
                    this.initialize.apply(this, arguments);
                }

                if (superclass) {
                    tempFunc.prototype = superclass.prototype;
                    klass.prototype = new tempFunc();
                    klass.superclass = superclass.prototype;
                }

                for (var i = 0, length = properties.length; i < length; i++) {
                    _.extend(klass.prototype, properties[i]);
                }

                if (!klass.prototype.initialize) {
                    klass.prototype.initialize = emptyInitializeFunc;
                }

                klass.prototype.constructor = klass;

                return klass;
            }

            return Class;
        })(),
        EventUtil = {
            on: function (element, type, handler) {
                if (document.addEventListener) {
                    element.addEventListener(type, handler, false);
                } else if (document.attachEvent) {
                    element.attachEvent("on" + type, handler);
                } else {
                    element["on" + type] = handler;
                }
            },
            getEvent: function (e) {
                return e || window.event;
            },
            getTarget: function (e) {
                var event = this.getEvent(e);
                return event.target || event.srcElement;
            }
        },
        DataBinder = function (objectId) {
            var binder = {
                    callbacks: {},
                    subscribe: function (message, callback) {
                        this.callbacks[message] || (this.callbacks[message] = []);
                        this.callbacks[message].push(callback);
                    },
                    publish: function (message) {
                        this.callbacks[message] || (this.callbacks[message] = []);
                        for (var i = 0, len = this.callbacks[message].length; i < len; i++) {
                            this.callbacks[message][i].apply(this, arguments);
                        }
                    }
                },
                message = objectId + ":change";

            EventUtil.on(document, "input", function (e) {
                var target = EventUtil.getTarget(e),
                    bindInfo = target.getAttribute("data-bind");
                if (bindInfo.indexOf(objectId) !== -1) {
                    binder.publish(message, bindInfo.split(".")[1], target.value);
                }
            });

            binder.subscribe(message, function (message, prop, value) {
                var elements = document.querySelectorAll("[data-bind='" + objectId + "." + prop + "']"),
                    i, element, tagName;
                for (i = 0; element = elements[i]; i++) {
                    tagName = element.tagName.toLowerCase();
                    if (tagName === "input" || tagName === "textarea") {
                        element.value = value;
                    } else {
                        element.innerHTML = value;
                    }
                }
            });

            return binder;
        },
        Bindable = new Class({ //所有可绑定对象的基类
            initialize: function (objectId) {
                this._objectId = objectId;
                this._attributes = {};
                this._binder = new DataBinder(objectId);
            },
            set: function (prop, value) {
                this._attributes[prop] = value;
                this._binder.publish(this._objectId + ":change", prop, value);
            },
            get: function (prop) {
                return this._attributes[prop];
            }
        }),
        User = new Class(Bindable),
        user = new User("user");

    user.set("name", "hello world");

    EventUtil.on(document.getElementById("changeModelPropertyButton"), "click", function () {
        user.set("name", "hello " + Math.round(Math.random() * 100));
    });

})();