MVC的JavaScript Web富应用开发(完整版)-笔记

当使用JavaScript 开发应用时,那些经典的设计模式和最佳实践被抛在了脑后。开发者往往忽略架构模型,比如 MVC 模型,而常将应用中的 HTML 和 JavaScript 混杂在一起,看着像一个大杂烩。

什么是 MVC

MVC 是一种设计模式,它将应用划分为 3 个部分 : 数据(模型) 、展现层(视图)和用户交互层(控制器)。
换句话说,一个事件的发生是这样的过程:
1. 用户和应用产生交互。
2. 控制器的事件处理器被触发。
3. 控制器从模型中请求数据,并将其交给视图。
4. 视图将数据呈现给用户。

如何发送新的聊天消息:
1. 用户提交一个新的聊天消息。
2. 控制器的事件处理器被触发。
3. 控制器创建了一个新的聊天模型(Chat Model)记录。
4. 然后控制器更新视图。

我们可以不用类库或框架就实现这种 MVC 架构模式。
关键是要将 MVC 的每部分按照职责进行划分,将代码清晰地分割为若干部分,并保持良好的解耦。这样可以对每个部分进行独立开发、测试和维护。

组成:

模型M用来存放应用的所有数据对象。
比如,可能有一个 User 模型,用以存放用户列表、他们的属性及所有与模型有关的逻辑。
模型不必知晓视图和控制器的细节,模型只需包含数据及直接和这些数据相关的逻辑。
任何事件处理代码、视图模板,以及那些和模型无关的逻辑都应当隔离在模型之外。将模型和视图的代码混在一起,是违反 MVC 架构原则的。
模型是最应该从你的应用中解耦出来的部分。
当控制器从服务器抓取数据或创建新的记录时,它就将数据包装成模型实例。也就是说,我们的数据是面向对象的(object oriented) ,任何定义在这个数据模型上的函数或逻辑都可以直接被调用。

视图层是呈现给用户的,用户与之产生交互。
在 JavaScript 应用中,视图大都是由HTML、CSS 和 JavaScript 模板组成的。
除了模板中简单的条件语句之外,视图不应当包含任何其他逻辑。
实际上,和模型类似,视图也应当从应用的其他部分中解耦出来。
视图不必知晓模型和控制器中的细节,它们是相互独立的。将逻辑混入视图之中是编程的大忌。

这并不是说 MVC 不允许包含视觉呈现相关的逻辑,只要这部分逻辑没有定义在视图之内即可。
我们将视觉呈现逻辑归类为“视图助手” (helper) : 和视图有关的独立的小型工具函数

控制器C是模型和视图之间的纽带。
控制器从视图获得事件和输入,对它们进行处理(很可能包含模型),并相应地更新视图。
当页面加载时,控制器会给视图添加事件监听,比如监听表单提交或按钮点击。然后,当用户和你的应用产生交互时,控制器中的事件触发器就开始工作了。

JavaScript 是基于原型的编程语言,并没有包含内置类的实现。
JavaScript 中并没有真正的类,但 JavaScript 中有构造函数和 new 运算符。
构造函数用来给实例对象初始化属性和值。
任何 JavaScript 函数都可以用做构造函数,构造函数必须使用 new 运算符作为前缀来创建新的实例。
new 运算符改变了函数的执行上下文,同时改变了 return 语句的行为。
当使用 new 关键字来调用构造函数时,执行上下文从全局对象(window)变成一个空的上下文,这个上下文代表了新生成的实例。因此, this 关键字指向当前创建的实例。

模型和数据

引入 MVC 模式,数据管理则归入模型(MVC 中的 M) 。
模型应当从视图和控制器中解耦出来。
与数据操作和行为相关的逻辑都应当放入模型中,通过命名空间进行管理。

对象关系映射(Ojbect-relational mapper,简称 ORM)是在除 JavaScript 以外的编程语言中常见的一种数据结构。
在 JavaScript 应用中,对象关系映射也是一种非常有用的技术,它可以用来做数据管理及用做模型。
本质上讲,ORM 是一个包装了一些数据的对象层。
这里 ORM 只是用于抽象 JavaScript 数据类型
这个额外的层有一个好处,我们可以通过给它添加自定义的函数和属性来增强基础数据的功能。比如添加数据的合法性验证、监听、数据持久化及服务器端的回调处理等,这样会增加代码的重用率。

装载数据:
数据可以直接嵌套显示在初始页面中,或者通过 Ajax 或 JSONP 的方式使用单独的HTTP 请求加载数据。
推荐使用后两种技术,因为直接在初始页面中嵌套数据会增加页面体积,而并行加载会更快一些。
AJAX 和 JSON 同样允许你将HTML 页面缓存住,而不是每次渲染都会动态发起请求。

控制器和状态

通常是用服务器的 session cookies 来管理状态。因此当用户页面跳转之后,上一个页面的状态就丢失了,只有 cookies 保存了下来。然而JavaScript 应用往往被限制在单页面,这也意味着可以将状态保存在客户端的内存中。

将状态保存在客户端其中一个主要好处是带来更快速的界面响应。用户和页面产生交互时可以立即得到反馈。
应当避免将状态或数据保存在 DOM 中,会导致程序逻辑变得更加错综复杂且混乱不堪。
状态应该是保存在应用的控制器里的。
控制器是模块化的且非常独立

模块模式
用来封装逻辑并避免全局命名空间污染的好方法
通常是创建一个匿名函数并立即执行它。
在匿名函数里的逻辑都在闭包里运行,为应用中的变量提供了局部作用域和私有的运行环境 :

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

全局导入
模块为我们提供了一种简单的方法来解决这些问题。
将全局对象作为参数传入匿名函数,可以将它们导入我们的代码中,这种实现方法比隐式的全局对象更加简洁而且速度更快 :

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

将全局变量 jQuery 导入我们的模块里,并将其重命名为 $ 。它清晰
地表明这个模块中所用到的全局变量是什么,并且对这个全局对象的读取速度很快,用这种模式可以安全地使用 jQuery 的别名 $ ,而且你的代码不会和其他的类库产生冲突。

全局导出
添加少量上下文
如果想自定义作用域的上下文,则需要将函数添加至一个对象中,比如 :

(function(){
    var mod = {};
    mod.contextFunction = function(){
        assertEqual( this, mod );
    };
    mod.contextFunction();
})();

在 contextFunction() 中的上下文不是全局的,而是 mod 对象。这时使用 this 就不必担
心会创建全局变量了。
抽象出库
路由选择

视图和模板

视图是应用的接口,它们为用户提供视觉呈现并与用户产生交互。

动态渲染视图
通过JavaScript程序创建视图有多种方式:
1、使用document.createElement()创建DOM元素,设置它们的内容并将它们追加至页面中。

$("#views").empty();
var container = $("<div />").attr({id: "user"});
var name = $("<span />").text(data.name);
$("#views").append(container.append(name));

2、将静态 HTML 包含在页面中,在必要的时候显示或隐藏。

<div id="views">
    <div class="groups"> ... </div>
    <div class="user">
        <span></span>
    </div>
</div>

$("#views div").hide();
var container = $("#views .user");
container.find("span").text(data.name);
container.show();

模板

jQuery.tmpl

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

  • 在 JavaScript 中以行内形式存储。
  • 在自定义 script 标签里以行内形式存储。
  • 远程加载。
  • 在 HTML 中以行内形式存储。

依赖管理

依赖管理系统除了能解决实际的编程复杂度和可维护性的问题,还能解决性能方面的问题。浏览器需要针对每个 JavaScript 文件都发起一个 HTTP 请求,尽管可以将这些请求放入异步队列,但大量的 HTTP 连接总会造成性能的下降,每个连接都包含额外的HTTP 头信息、Cookie,并都要做 TCP 的三次握手 。当你的应用是基于 SSL提供服务的话,情况会更加糟糕。

CommonJS

模块加载器
Yabble
RequrieJS

包装模块
模块的按需加载
Sprockets
LABjs

无交互行为内容的闪烁(FUBC)

使用文件

部署

性能

提高性能,最简单的也是最显著的方法就是 : 减少 HTTP 请求的数量。

每一个 HTTP 请求除了有 TCP 开销以外,还包含了大量的头信息。保持最小的独立连接数可以保证用户的页面加载速度更快。这显然涉及到了服务器需要传输的数据量的问题。让页面和其资源文件保持较小的体积将减少网络用时——对任何互联网上的应用而言,这才是真正的瓶颈。

将多个脚本文件合并成一个脚本文件,或将多个 CSS 合并成一个样式表,能减少页面渲染所需的 HTTP 连接的数量。可以在部署或运行时这样做。如果是后者,务必保证生成的文件在生产环境中可以被缓存。

使用 CSS Sprites 技术合并多张小图为一张大图,然后使用 CSS 的 background-image 和background-position 属性在页面中显示对应的图片。只需要设定图片要显示的尺寸和背景位置的偏移坐标。

避免重定向也是减少 HTTP 请求的数量的方法。你也许认为这很少见,其实 URL 结尾缺少斜线(/)是一个最常见的重定向场景,而这个斜线不应当被丢掉。例如,当前访问http://facebook.com 时,会被重定向到 http://facebook.com/。如果使用了 Apache,可以使用 Alias 或mod_rewrite 来修正这个问题。

浏览器如何下载资源:
为了加速页面渲染,现代浏览器并行下载所需的资源。
但是,在所有的样式表和脚本下载完成之前,页面是不会开始渲染的。有些浏览器更是变本加厉,在处理任何 JavaScript 文件时,都会阻塞其他资源的下载。
尽管如此,大多数脚本需要访问 DOM,并且增加一些诸如事件句柄之类的东西,它们会在页面加载完成后执行。
换言之,浏览器没有必要在一切都下载完成之前限制页面的渲染,因为这样做降低了性能。

通过设置脚本的 defer 属性可以解决这个问题——告诉浏览器该脚本不会在页面加载完成之前操作 DOM :

defer 属性设置为“defer”的脚本将和其他资源一起并行下载,它们不会阻塞页面的渲染。

HTML5 还引入了一个新的脚本下载和执行的模式,称作 async 。通过设置 async 属性,脚本将在完成下载后等待合适的时机执行代码。这意味着有可能(很有可能)异步不会按照它们在页面中出现的顺序执行代码,这又可能导致脚本执行如有依赖顺序时出错。

如果脚本没有依赖关系,async 则是很有用的。例如 Google Analytics 默认利用了该特性:

缓存

缓存就是将最近请求的资源存储到本地,以便接下来的请求能从磁盘中使用这些资源,而不用再次去下载。
明确地告诉浏览器什么是可以被缓存的是很重要的。

针对静态资源,可通过添加一个表示“在很远的将来才过期”的 Expires 头, 让缓存“永不”失效。这将保证浏览器只会下载该资源一次。
所有的静态资源文件都应该这样设置,包括脚本、样式表和图片。
建议相对于当前日期设置一个表示“很远的将来”的失效日期。

Expires: Thu, 20 March 2015 00:00:00 GMT

如上例告诉浏览器该缓存在 2015 年 3 月 20 日之前不会过期。
如果使用了 Apache,用 ExpiresDefault 可以方便地设置一个相对的失效日期 :

ExpiresDefault "access plus 5 years"  

但是如果想在那个时间之前让资源过期,则很有用的技术就是——在引用资源文件的 URL 查询参数中添加文件的修改时间(或 mtime)。例如,Rails 默认采用这种方式。这样一来,任何时候文件被修改,资源文件的 URL 都会改变,也即清除了缓存。

<link rel="stylesheet" href="master.css?1296085785" type="text/css">

HTTP 1.1 引入了一类新的头, Cache-Control 。它带给开发者更高级的缓存,同时还弥补了 Expires 的不足。 Cache-Control 的控制头信息有很多选项,可用逗号隔开:

Cache-Control: max-age=3600, must-revalidate  

经常用到的选项列举如下 :

  • max-age 以秒为单位,指定资源的最大有效时间。和 Expires 不一样的是,该指令是相对于该请求的时间。
  • public 标记资源是可被缓存的。默认情况下,通过 SSL 或使用 HTTP 认证后访问的资源,缓存是关闭的。
  • no-store 完全关闭缓存,动态内容才会更多地使用这个选项。
  • must-revalidate 告诉缓存它们必须遵循任何你给定的信息,并基于这些信息来判断资源的新旧程度。在某些条件下,HTTP 允许缓存针对它们自己的规则使用过期的资源。通过指定该选项,可告诉缓存要严格按照你的规则来决策。

给提供服务的资源增加 Last-Modified 头信息也有助于缓存。
浏览器在对该资源后续的请求中,就能指定 If-Modified-Since 头信息,这是一个时间戳。
如果该资源在最后一次访问之后未被修改,服务器只返回 304 状态码(未修改) 。
浏览器仍然可以请求,但服务器却不一定在响应中返回该资源的内容,这可以节省网络时间和带宽

Last-Modified 的替代方式是 ETag。比较 Etag 就像比较两个文件的哈希值——如果 ETag值不一致,缓存就过期了,必须重新验证。它的工作原理和 Last-Modified 头信息一样。服务器将用 ETag 头信息将该值附加到资源响应中,客户端将用 If-None-Match 头信息检查。

源码压缩(Minification)

JavaScript 源码压缩是从脚本文件中删除不必要的字符,它不改变功能。删除的字符包括空白、换行和注释。
更好的压缩工具应该能够翻译JavaScript,因此能安全地缩短变量和函数的名字,这样就进一步减少了字符。文件越小越好,因为在网络上传输的数据越少越好。
不仅仅可以对 JavaScript 文件进行压缩,样式表和 HTML 文件也可以被压缩。特别是样式表,通常包含了大量冗余的空白。压缩最好能在部署时再完成,因为开发时你不希望调试任何压缩过的代码。
如果在生产环境中有一个错误,首先应该尝试在开发环境中复现——你会发现这样更容易调试错误。
源码压缩带来额外的好处是让代码晦涩难读。
很多源码压缩的工具,建议选择一个带有 JavaScript 引擎、能够翻译代码的工具,如YUI Compressor智能优化源代码。

Gzip 压缩

在 Web 上 Gzip 是最流行并且支持最广泛的压缩方式。它是由 GNU 项目组开发的,在HTTP/1.1 中开始对其支持。
Web 客户端通过在发送请求时增加 Accept-Encoding 头信息来标识自己支持的压缩方式:
Accept-Encoding: gzip, deflate

如果 web 服务器看到该头信息,并且支持列出的压缩方式,它将会对响应结果进行压缩,并通过 Content-Encoding 头信息标识其压缩方式 :

Content-Encoding: gzip 

然后浏览器才能正确地解码, 得到解码后的响应。
显然,压缩数据可以减少网络传送时间,但这并没大范围地得以实现。Gzip 通常能减少响应 70% 的体积,巨大的体积缩减极大地加速了网站的加载速度。

使用 CDN

内容分发网络(或叫 CDN)为你的站点提供静态资源内容服务,以减少它们的加载时间。

用户和 web 服务器之间的距离对加载时间有直接的影响。CDN 将你的内容部署在跨越多个地理位置的服务器上,故当用户发起一个请求时,可从就近的服务器得到响应资源(理想情况是在同一个国家中) 。

Spine类库

Spine是一个轻量级 JavaScript 应用程序开发库.
Spine 将会让你在保证代码干净整洁和耦合松散的前提之下,构建功能全面、丰富的 JavaScript 应用程序.
Spine 包含了一个可以继承的类库 Spine.Class 、一个事件模块 Spine.Events 、一个ORM—— Spine.Model ,以及一个控制器类 Spine.Controller 。
你自己可以使用其他任何类库,像模板支持或 DOM 库等,用你最熟悉的就可以了。不仅如此,Spine 包含了对jQeury 和 Zepto.js 库特别的支持,它们是对 Spine 的非常好的补充。

Backbone类库

Backbone 是构建 JavaScript 应用程序的一个优秀的类库。它的优美之处在于其简洁。这是一个轻量类库,它覆盖了所有基础的功能,同时提供了最大的灵活性。