2013年9月

第五章:视图和模板《Developing JavaScript Web Applications》学习笔记

模板

jQuery.tmpl是jQuery官方指定的jQuery模板库,这个库有一个主要的函数jQuery.tmpl(),可以给它传入一个模板和一些数据,函数会返回渲染好的元素节点,可以将渲染的结果追加至页面里。如果数据是数组的话,对于数组中的每个数据项都会生成渲染好的模板,否则,将只会渲染一个模板:

var object = {
url: "http://example.com",
getName: function(){ return "Trevor"; }
};
var template = '<li><a href="${url}">${getName()}</a></li>';
var element = jQuery.tmpl(template, object);
// 得到的结果: <li><a href="http://example.com">Trevor</a></li>
$("body").append(element);

这里你可以看到我们使用${} 语法来书写插见的变量。不管括号中的变量名是什么,都会根据传入jQuery.tmpl() 的对象来计算得出要填充的文本,不考虑它是一种属性还是一个函数。
然而模板的功能要比这种纯粹的插值替换强大很多。很多模板库都具有一些高级功能,诸如条件流(conditional flow)和迭代。你可以通过使用if 和else 语句来实现条件流,就像用纯粹的JavaScript 写出的代码一样。惟一和JavaScript 语法不同的地方是,这里需将关键字用双括号括起来,以便模板引擎能正确识别它们:

{{if url}}
${url}
{{/if}}

遍历是所有模板类库都提供的基础功能。使用模板类库的{{each}} 关键字可以遍历任何JavaScript 类型,包括Object 和Array。你可以使用$value 变量来访问当前正被遍历的值

var object = {
foo: "bar",
messages: ["Hi there", "Foo bar"]
};

然后使用接下来的模板来遍历这个message 数组,显示每条消息。此外,数组元素的索引也可以使用$index 变量来输出:

<ul>
{{each messages}}
<li>${$index + 1}: <em>${$value}</em></li>
{{/each}}
</ul>

模板Helpers

有时在视图内部使用“通用helper 函数”(generic helper function)是非常好用的,比如格式化一个日期或数字。将它抽象出来,并用命名空间进行管理,而不是直接将函数掺杂进视图中,这样才能保持逻辑和视图之间的解耦。例如:

// helper.js
var helper = {};
helper.autoLink = function(data){
var re = /((http|https|ftp):\/\/[\w?=&.\/-;#~%-]+(?![\w\s?&.\/;#~%"=-]*>))/g;
return(data.replace(re, '<a target="_blank" href="$1">$1</a> ') );
};
// template.html
<div>
${ helper.autoLink(this.data) }
</div>

这里还有一个额外的好处,autoLink() 函数是通用的,在应用的任何地方都可以重用它。

模板存储

说到模板存储,有这样一些内容需要考虑:

  1. 在JavaScript 中以行内形式存储。---违背了MVC 架构的原则
  2. 在自定义 script 标签里以行内形式存储。--推荐这种方式,浏览器不必对它们进行渲染,而仅将它们解析为内容文本
  3. 远程加载。--影像UI 的渲染
  4. 在 HTML 中以行内形式存储。--增加了初始页面的体积

其中一些非常适合在MVC 架构中使用。作者推荐使用第2种方式,即在自定义script标签里以行内形式存储模板。例如:

<script type="text/x-jquery-tmpl" id="someTemplate">
<span>${getName()}</span>
</script>
<script>
var data = {
getName: function(){ return "Bob" }
};
var element = $("#someTemplate").tmpl(data);
element.appendTo($("body"));
</script>

绑定

我们希望,当模型记录创建、更新或销毁时,都会触发change 事件,并重新渲染视图。在下面的例子中,我们创建了一个基础的
User 类,新建了事件绑定和触发,最后监听了change 事件,当触发change 事件时重新渲染视图:

<script>
var User = function(name){
this.name = name;
};
User.records = []
User.bind = function(ev, callback) {
var calls = this._callbacks || (this._callbacks = {});
(this._callbacks[ev] || (this._callbacks[ev] = [])).push(callback);
};
User.trigger = function(ev) {
var list, calls, i, l;
if (!(calls = this._callbacks)) return this;
if (!(list = this._callbacks[ev])) return this;
jQuery.each(list, function(){ this() })
};
User.create = function(name){
this.records.push(new this(name));
this.trigger("change")
};
jQuery(function($){
User.bind("change", function(){
var template = $("#userTmpl").tmpl(User.records);
$("#users").empty();
$("#users").append(template);
});
}):
</script>
<script id="userTmpl" type="text/x-jquery-tmpl">
<li>${name}</li>
</script>
<ul id="users">
</ul>

现在无论何时修改User 的记录,User 的模型的change 事件都会被触发,调用我们模板的回调函数并重绘用户列表。这很有帮助,因为我们只需关注创建和更新用户的记录,而不必担心视图的更新,视图的更新是自动的。比如,我们创建一个新的User :

User.create("Sam Seaborn");

第四章:控制器和状态《Developing JavaScript Web Applications》学习笔记

模块模式

模块模式是用来封装逻辑并避免全局命名空间污染的好方法。使用匿名函数可以做到这一点,匿名函数也是JavaScript 中被证明最优秀的特性之一。通常是创建一个匿名函数并立即执行它。在匿名函数里的逻辑都在闭包里运行,为应用中的变量提供了局部作用域和私有的运行环境:

(function(){
/* ... */
})();

在执行这个匿名函数之前,我们用一对括号() 将它包起来。这样才能让JavaScript 解释器正确地将这段代码解析为一个语句。

全局导入和导出

可以将页面的window 导入我们的模块,直接给它定义属性,通过这种方式可以暴露全局变量:

var exports=this;
(function($){
    exports.Foo = "wem";
})(jQuery);

这里我们使用的变量名叫exports,用它来暴露全局变量,这样代码看起来更干净易读,可以直接看出模块创建了哪些全局变量。

一个完整的控制器

(function($, exports){
      var mod = {};
  mod.create = function(includes){
    var result = function(){
      this.init.apply(this, arguments);
    };
    
    result.fn = result.prototype;
    result.fn.init = function(){};
    
    result.proxy = function(func){ return $.proxy(func, this); };
    result.fn.proxy = result.proxy;

    result.include = function(ob){ $.extend(this.fn, ob); }; 
    result.extend = function(ob){  $.extend(this, ob); };
    if (includes) result.include(includes);
    
    return result;
  };
  
  exports.Controller = mod;
})(jQuery, window);

var exports = this;

$(function($){
  exports.SearchView = Controller.create({
    elements: {
      &quot;input[type=search]&quot;: &quot;searchInput&quot;,
      &quot;form&quot;: &quot;searchForm&quot;
    },
    
    init: function(element){
      this.el = $(element);
      this.refreshElements();
      this.searchForm.submit(this.proxy(this.search));
    },
    
    search: function(){
      alert(&quot;Searching: &quot; + this.searchInput.val());
      return false;
    },
    
    // Private
    
    $: function(selector){
      return $(selector, this.el);
    },
    
    refreshElements: function(){
      for (var key in this.elements) {
        this[this.elements[key]] = this.$(key);
      }
    }
  });
  
  new SearchView(&quot;#users&quot;);
});</code></pre>

See the Pen CaEbA by buer (@buer) on CodePen.

状态机

考虑这样一个场景,应用中存在一些视图,它们的显示是相互独立的,比如一个视图用来显示联系人,另一个视图用来编辑联系人。这两个视图一定是互斥的关系,其中一个显示时另一个一定是隐藏的。这个场景就非常适合引入状态机,因为它能确保每个时刻只有一种视图是激活的。的确,如果我们想添加一些新视图,比如一个承载设置操作的视图,用状态机来处理这种场景绰绰有余。

一个完整的状态机

 var Events = {
      bind: function(){
        if ( !this.o ) this.o = $({});
        this.o.bind.apply(this.o, arguments);
      },
  trigger: function(){
    if ( !this.o ) this.o = $({});
    this.o.trigger.apply(this.o, arguments);
  }
};

var StateMachine = function(){};
StateMachine.fn  = StateMachine.prototype;
$.extend(StateMachine.fn, Events);

StateMachine.fn.add = function(controller){
  this.bind(&quot;change&quot;, function(e, current){
    if (controller == current)
      controller.activate();
    else
      controller.deactivate();
  });
  
  controller.active = $.proxy(function(){
    this.trigger(&quot;change&quot;, controller);
  }, this);
};

var con1 = {
  activate: function(){ 
    console.log(&quot;controller 1 activated&quot;);
  },
  deactivate: function(){ 
    console.log(&quot;controller 1 deactivated&quot;);
  }
};

var con2 = {
  activate: function(){ 
    console.log(&quot;controller 2 activated&quot;);
  },
  deactivate: function(){ 
    console.log(&quot;controller 2 deactivated&quot;);
  }
};
 
var con3= {
  activate: function(){ 
    console.log(&quot;controller 3 activated&quot;);
  },
  deactivate: function(){ 
    console.log(&quot;controller 3 deactivated&quot;);
  }
};

var sm = new StateMachine;
sm.add(con1);
sm.add(con2);
sm.add(con3);

con2.active();

See the Pen ljyLq by buer (@buer) on CodePen.

URL 中的hash

定位当前页面所用的URL(基于URL)是不能更改的,如果改变则会引起页面的刷新,这是我们要避免发生的。幸好我们有一些解决办法。操作URL 的一种传统办法是改变它的hash。hash 不会发送给服务器,因此更改hash 不会造成页面的刷新。比如,这个
URL 是Twitter 的页面,它的hash 值就是#!/maccman:

http://twitter.com/#!/maccman

可以通过location 对象来读取或修改页面hash :

// 设置hash
window.location.hash = "foo";
assertEqual( window.location.hash , "#foo" );
// 去掉“#”
var hashValue = window.location.hash.slice(1);
assertEqual( hashValue, "foo" );

如果URL 中没有hash,location.hash 则返回空字符串。否则,location.hash 和URL的hash 部分相等,带有# 前缀。
太过频繁地设置hash 也会影响性能,特别是在移动终端里的浏览器中。因此,如果你对hash 的改动太频繁,就要注意限制这种改动,否则在移动终端里可能会造成页面的频繁滚动。

检测hash 的变化

以往检测hash 变化的方法是通过轮询的计时器来监听,这种方法非常原始。现在情形有所改观,现代浏览器都支持hashchange 事件。这是一个window 的事件,如果想检测hash 的改变就需要绑定这个监听:

window.addEventListener("hashchange", function(){ /* ... */ }, false);

使用jQuery 的代码:

$(window).bind("hashchange", function(event){
// hash 发生改变,更改状态
});

当触发hashchange 事件时,我们需要确定应用的当前状态。这个事件的浏览器兼容性非
常不错,主流浏览器的最新版本都支持这个事件:

  • IE >= 8。
  • Firefox >= 3.6。
  • Chrome。
  • Safari >= 5。
  • Opera >= 10.6。