单页应用优势
单页应用有几个优势:
- 可以像桌面应用一样渲染,只需要重绘界面上需要变化的部分。
- 可以拥有和桌面一样的响应速度,尽可能把工作数据和处理过程从服务器端转移到客户端,把响应时间缩减至最小。只有数据验证、授权和持久存储必须放在服务器端。
- 可以把它的状态通知给用户,当单页应用需要等待服务器响应的时候,可以动态显示进度条或者繁忙指示器。
Pjax
Pjax是一个优秀的单页应用路由解决方案,Pjax的原理十分简单。
- 拦截a标签的默认跳转动作。
- 使用Ajax请求新页面。
- 将返回的Html替换到页面中。
- 使用HTML5的History API或者Url的Hash修改Url。
我们来看看HTML5在History里增加了什么:
history.pushState(state, title, url)
pushState方法会将当前的url添加到历史记录中,然后修改当前url为新url。请注意,这个方法只会修改地址栏的Url显示,但并不会发出任何请求。我们正是基于此特性来实现Pjax。它有3个参数:
- state: 可以放任意你想放的数据,它将附加到新url上,作为该页面信息的一个补充。
- title: 顾名思义,就是document.title。不过这个参数目前并无作用,浏览器目前会选择忽略它。
- url: 新url,也就是你要显示在地址栏上的url。
history.replaceState(state, title, url)
replaceState方法与pushState大同小异,区别只在于pushState会将当前url添加到历史记录,之后再修改url,而replaceState只是修改url,不添加历史记录。
window.onpopstate 事件
一般来说,每当url变动时,popstate事件都会被触发。但若是调用pushState来修改url,该事件则不会触发,因此,我们可以把它用作浏览器的前进后退事件。该事件有一个参数,就是上文pushState方法的第一个参数state。
IE6到IE9是不支持pushState的,要修改Url,只能利用Url的Hash,也即是#号。那么接下来我们简单实现一下#号的路由跳转:
//default page, which will be loaded initially
var home = {};
home.partial = 'lib/home.html';
home.init = function(){
}
//404 page
var notfound = {};
notfound.partial = "lib/404.html";
notfound.init = function(){
alert('URL does not exist. please check your code.');
}
所有的公用方法打包放到一个对象miniSPA中
var miniSPA = {};
然后是 changeUrl 方法,对应在index.html中有如下触发定义:
<body onhashchange="miniSPA.changeUrl();">
onhashchange是在location.hash发生改变的时候触发的事件,能够通过它获取局部 url 的改变。在index.html中定义了如下的链接:
<a href="#home">Home (Default)</a>
<a href="#postMD">POST request</a>
<a href="#getEmoji">GET request</a>
<a href="#wrong">Invalid url</a>
<!-- html 片段嵌入的位置 -->
<div id="demo"></div>
每个 url 都以#号开头,这样就能被onhashchange事件抓取到。最后的 div 就是局部刷新的 html 片段嵌入的位置。
miniSPA.changeUrl = function(){
var url = location.hash.replace('#', '');
if(url === '') url = 'home';
//刚才定义的路由对象都是全局变量
if(!window[url]){
url = 'notfound';
}
//请求相应页面的数据
miniSPA.ajaxRequest(window[url].partial, 'GET', '', function(status, page){
if(status === '404'){
//html片段嵌入id为demo的div中,这里setting的具体实现就不定义了,其实是通过ajax数据改变前端的model,然后填充视图模板改变视图
settings.divDemo.innerHTML = page404;
} else {
settings.divDemo.innerHTML = page;
}
miniSPA.initFunc(url);
})
}
initFunc方法的作用是解析片段对应的初始化方法,判断其类型是否为函数,并执行它。
//execute the controller function responsible for current template
miniSPA.initFunc = function(partial) {
var fn = window[partial].init;
if(typeof fn === 'function') {
fn();
}
}
当然,点击浏览器的后退、前进,就要通过window.onpopstate 事件来处理了。
MVC
上面谈到的Pjax,承担着应用范围的任务,像是管理URL锚或者cookie,把特定功能的任务调度隔离的功能模块。接下来我们来回忆一下MVC模式,MVC的部件包括以下三个:
- 模型,应用的数据和业务规则
- 视图,模型数据的感官(通常是视觉的)表现
- 控制器,将用户的请求转化成命令,更新应用的模型和视图。
我们的单页面应用架构在多个层级上采用重复的MVC模式,所以我们把它叫做分形MVC。我们理解的分形有多深,和看问题的角度有关,从远处看我们的web应用的时候,看到的是单一的MVC模式,控制器处理URI和用户输入,与模型进行交互,在浏览器中提供视图。
当放大一点时,应用被分割成两部分,服务器端采用MVC模式向客户端提供数据,采用MVC的单页应用允许用户查看浏览器端的模型,并与之交互。服务器端的模型是从数据库获取的数据,而视图是要发送给浏览器的数据表现,控制器是协调数据管理和同浏览器通信的代码。在客户端,模型包括从服务器接收到的数据,视图是用户界面,控制器是协调客户端数据和界面的逻辑。
再放大得近一点,我们看到了更多的MVC模式。比如,服务器端应该采用MVC模式来提供HTTP数据API。服务器端使用的数据库采用它自己的MVC模式,在客户端,客户端应用使用MVC模式,客户端组件调用的子功能模块本身也使用MVC模式。
构建Model
Model把所有的业务逻辑和数据整合到一个名字空间里面,Pjax或者功能模块并不直接和web服务器通信,而是通过和Model交互,Model通过自己使用Data模块,从web服务器分离出来。假设我们实现一个聊天的chat功能和登入登出,那么会遇到以下问题:
- 为了管理登入和登出的问题,Pjax需要知道当前用户,它需要确定“当前用户是谁”的方法,在需要时更改用户。
- chat功能也需要查看当前用户,以此判断他是否授权发送或接收消息。它需要确定正在和用户聊天的人,如果有的话。它需要查询在线人员的列表,这样就可以把他们显示在聊天模块的左边。
模块很多必须的业务逻辑和数据是重叠的。比如,Pjax和chat功能都需要知道当前用户对象,我们想到了一些策略,如何设法管理这种重叠:
- 在每个功能模块中构建必须的逻辑和数据
- 在不同的功能模块中构建部分逻辑和数据,然后在模块中互相调用,以便共享信息。
- 构建中央Model,合并逻辑和数据。
第一种方法显然不妥,在不同的模块中维护并行的数据和方法,这容易产生冗余代码和错误。第二种选择的效果好一点,不过是暂时的,一旦逻辑和数据达到了适度水平的复杂度。第三个选择是使用Model,这是目前为止最好的选择。
Model是Pjax和所有功能模块访问单页应用的数据(准确说是共享数据)和业务逻辑的地方。如果需要登入,我们调用Model提供的方法,如果想获取人员列表,就从Model获取。虽然所有的业务逻辑和数据都是通过Model访问的,但并不意味着必须只能使用一个js文件来存放Model,可以通过命名空间把Model分成多个容易管理的小文件。
Model不需要浏览器,这意味着Model不可以假定存在document对象,让Pjax和功能模块来表示Model的数据,是“干净的”MVC。通过避免DOM,我们可以测试除了UI之外的所有东西,无需运行浏览器。