单页应用

单页应用（single-page application，缩写SPA）是一种网络应用程序或网站的模型，它通过动态重写当前页面来与用户交互，而非传统的从服务器重新加载整个新页面。这种方法避免了页面之间切换打断用户体验，使应用程序更像一个桌面应用程序。在单页应用中，所有必要的代码（HTML、JavaScript和CSS）都通过单个页面的加载而检索，或者依需（通常是为响应用户操作）动态装载适当的资源并添加到页面。尽管可以用位置散列或HTML5来提供应用程序中单独逻辑页面的感知和导航能力，但页面在过程中的任何时间点都不会重新加载，也不会将控制转移到其他页面。与单页应用的交互通常涉及到与网页服务器后端的动态通信。

## 历史

尽管早在2003年就讨论过这个概念，但单页应用（single-page application）一词的起源并不明晰。Stuart (stunix) Morris在2002年4月编写的自主网站slashdotslash.com有着相同的目标和功能；同年稍晚，Lucas Birdeau、Kevin Hakman、Michael Peachey和Evan Yeh在美国专利8,136,109中描述了一个单页应用程序的实现。

在网页浏览器中可以使用JavaScript显示用户界面（UI）、运行应用程序逻辑，以及与Web服务器通信。成熟的开源程式库支持构建一个单页应用，从而减少开发人员要编写的JavaScript代码量。

## 技术举措

现今已有各种技术可以使网页浏览器停留在单个页面，并同时支持应用程序与服务器通信。

### JavaScript框架

诸如Angular、Ember.js、ExtJS和React等面向网页浏览器的JavaScript框架采纳了单页应用（SPA）原则。

- Angular是一个全面的客户端侧框架。其模板基于双向UI数据绑定。数据绑定是一种自动方法，在模型改变时更新视图，以及在视图改变时更新模型。其HTML模板在浏览器中编译。编译步骤创建纯HTML，浏览器将其重新渲染到实时视图。该步骤会在随后的页面浏览中重复。在传统的服务器端HTML编程中，控制器和模型等概念在服务器进程中进行交互以产生新的HTML视图。在Angular框架中，控制器和模型状态在客户端的浏览器中维护，从而使生成新页面不依赖与服务器的交互。
- Ember.js是基于模型-视图-控制器（MVC）软件架构模型的客户端侧JavaScript Web应用程序框架。它允许开发人员在一个框架中通过常用的习惯用语和最佳实践来创建可伸缩的单页面应用程序。该框架提供丰富的对象模型、声明性双向数据绑定、计算属性，Handlebars.js提供的自动更新模板，以及一个路由器管理应用程序状态。
- Meteor.js是一个专门为单页应用设计的全栈（客户端-服务器）JavaScript框架。它具有比Angular、Ember或ReactJS更简单的数据绑定特性，并且使用Distributed Data Protocol和一个发布/订阅来自动将数据更改传播到客户端，无需开发人员编写任何同步代码。全栈反应确保从数据库到模板的所有层都可以在必要时自动更新。诸如服务器端渲染等生态系统包则解决搜索引擎优化（SEO）等问题。
- Aurelia 是一个适用于移动设备、桌面和网页的JavaScript客户端框架。它类似AngularJS，但更新、更符合标准，并采用模块化举措。Aurelia使用下一代ECMAScript编写。
- Vue.js（通常称为Vue）是一个用于构建用户界面的开源渐进式JavaScript框架。
- React（通常写为React.js或ReactJS）是一个构建用户界面的JavaScript函式库。它由Facebook、Instagram和个人开发者以及企业社区维护。React最大的优势是易于使用——基本上任何熟悉HTML的开发人员都可以创建React应用程序。另一个所称的优势是可能使用相同的技术堆栈来同时创建Web与移动应用程序。有多家公司使用React和Redux库来让开发人员创建复杂但可扩展的Web应用程序。
- Fulcro是一个全栈库，它采用Netflix的Falcor，Facebook的Relay和Om Next对反应性，功能性，数据驱动软件进行改编的数据驱动原则。

### Ajax

目前最常采用Ajax技术。主要从JavaScript使用XMLHttpRequest/ActiveX对象（已不建议使用），其他Ajax方法包括使用IFRAME或script HTML元素。jQuery等流行的库可以为不同厂商的浏览器的Ajax行为标准化，进一步推广了Ajax技术。

### Websocket

WebSocket是HTML5规范中的一个双向有状态实时客户端与服务器通信技术，在性能和简单性方面优于Ajax。

### 服务器发送事件

服务器发送事件（SSE）是一种服务器向浏览器客户端发起数据传输的技术。一旦建立了初始连接，事件流将保持打开状态，直到客户端关闭。该技术通过传统的HTTP发送，并具有WebSockets缺乏的各种功能，例如自动重新连接、事件ID以及发送任意事件的能力。

### 浏览器插件

可以使用浏览器插件技术（如Silverlight、Flash或Java applet）实现对服务器的异步调用。此种方法在业界已经过时。

### 数据传输（XML、JSON）

对服务器的请求通常会带来原始数据（如XML或JSON）或传回新的HTML。在服务器返回HTML时，客户端上的JavaScript将更新DOM的部分区域。在返回原始数据时，客户端侧的JavaScript通常将其转换为HTML，然后用它来更新DOM的部分区域。

### 服务器架构

#### 瘦服务器架构

SPA技术将逻辑从服务器转移到了客户端。这导致Web服务器发展为一个纯数据API或Web服务。这种架构的转变在一些圈子中被称为“瘦服务器架构”，以强调复杂性已从服务端转移到客户端，并认为这最终降低了系统的整体复杂性。

#### 胖的有状态服务器架构

这种设计是服务器在内存中保存必要的客户端所处状态。这种模式下，当任何请求到达服务器（通常因用户操作）时，服务器发送适当的HTML和/或JavaScript，以及具体的更改，以使客户端达到新的期望状态（如添加、删除或更新部分客户端的DOM）。与此同时更新服务器中的状态。这种设计下的大部分逻辑都在服务器上运行，HTML通常也在服务器上呈现。在某些方面，服务器是模拟Web浏览器，接收事件并执行服务器状态下的增量更改，将这些更改自动传播到客户端。

这种方法需要更多的服务器内存和处理能力，但优点是简化的开发模型，因为：1、应用程序通常完全在服务器中编写；2、服务器中的数据和UI状态在相同的内存空间中共享，不需要自定义客户端/服务器通信隧道。

#### 胖的无状态服务器架构

这是有状态服务器方法的变体。客户端页面通常通过Ajax请求将表示其当前状态的数据发送到服务器。服务器使用这些数据能够重新构建需要修改的页面部分，并生成必要的数据或代码（如作为JSON或JavaScript），将其返回给客户端使其它达到新的状态。

此方法需要将更多数据发送给服务器，并可能需要更多计算资源才能部分或完全重建在服务器中的客户端页面状态。不过，这种方法更容易扩展，因为服务器中不存在每个客户端的页面数据，因此可以将Ajax请求分派到不同的服务器节点，而无需会话数据共享或服务器关联。

## 本地运行

部分单页应用可以使用file URI方案依赖本地文件运行。这使用户可以从服务器上下载单页应用并在本地设备中运行，而不需要依赖与服务器的网络连接。这类单页应用如果想要存储或更新数据，则必须使用基于浏览器的网页存储。这些应用受益于HTML5提供的高级功能。

## SPA模式带来的挑战

由于单页应用偏离了最初为浏览器设计的无状态页面重绘模型，所以带来了一些新挑战。每个问题都有一个或一些有效的解决方案：

- 客户端JavaScript库解决各种问题。
- 专门针对单页应用模型研发的服务器侧Web框架。
- 浏览器的发展以及为单页应用模型设计的HTML5规范。

### 搜索引擎优化

由于一些流行的网络搜索引擎的爬虫缺乏JavaScript执行能力，搜索引擎最佳化（SEO）已成为面向公众的网站采用单页应用模型必须面对的一个问题。

在2009至2015年间，Google网站管理员提出并推荐了一个“AJAX爬取方案”，其中使用起始为感叹号的片段标识符（`#!`）来标识有状态的AJAX页面。单页应用网站必须实现特殊行为才能允许搜索引擎的爬取工具提取相关的元数据。对于不支持此URL散列方案的搜索引擎，单页应用的散列网址会被视而不见。包括Jeni Tennison在内的许多作者认为这些“hash-bang”URI在W3C中被认为是存在问题的，因为它们使那些没有在浏览器中激活JavaScript的人无法访问页面。这也影响HTTP参照位址头，因为浏览器不允许在Referer头中发送片段标识符。2015年，Google不再建议使用散列AJAX爬取方案。

还有一些周边方法可以使网站表现为可抓取，它们都涉及创建反应单页应用内容的独立HTML页面。服务器可以创建基于HTML的网站版本并将此提供给爬取工具，也可以使用PhantomJS等无头(headless)浏览器运行JavaScript应用程序并输出生成的HTML。

这种方法需要付出不小的努力，最终可能给大型、复杂的网站带来很大的维护成本，并且也有潜在风险。如果服务器生成的HTML被认为与单页应用存在较大差异，则网站排名将受到处罚。运行PhantomJS来输出HTML可能会降低页面的响应速度，一些搜索引擎（尤其是Google）可能因此对排名降级。

### 客户端/服务器代码分区

增加服务器与客户端之间所共享代码量的一种方法是使用如、等无逻辑模板语言。这种模板可以用不同的托管语言来呈现，如服务器上的Ruby和客户端的JavaScript。但是，单纯共享模板通常需要重复使用业务逻辑来选择正确的模板，并使用数据填充它们。仅更新页面的一小部分（诸如大型模板中的一个文本的值）时，从模板呈现可能会产生负面的性能影响。替换整个模板也可能会干扰用户的选择或光标位置，只更新变更的值则可能不会。为避免这些问题，应用程序可以使用UI数据绑定或粒度DOM操作来更新页面的相应部分，而不是重新呈现整个模板。

### 浏览器历史记录

根据单页应用（SPA）模型的定义，它只有“单个页面”，因此这打破了浏览器为页面历史记录导航所设计的“前进/后退”功能。当用户按下后退按钮时，可能会遇到可用性障碍，页面可能返回真正的上一个页面，而非用户所期望的上一个页面。

传统的解决方案是不断更改浏览器网址（URL）的散列片段标识符以保持与当前的屏幕状态一致。这种方案可以通过JavaScript实现，并在浏览器中建立起网址历史事件。只要单页应用能根据网址散列所包含的信息重新生成相同的屏幕状态，就能实现预期的后退按钮行为。

而为进一步解决此问题，HTML5规范引入了 pushState 和 replaceState 来提供代码对实际网址和浏览器历史的访问。

### 分析

诸如Google分析等分析工具高度依赖在浏览器中加载全新的页面，而单页应用的工作方式不同于此。

### 初次加载的速度

单页应用的第一页加载会比基于服务器的应用慢。这是因为首次加载必须先拿到框架和应用程序的代码，再在浏览器中呈现所需的视图。基于服务器的应用程序只需将所需的HTML推送到浏览器，从而减少了延迟和下载用时。

#### 加快页面加载速度

有一些方法可以加快单页应用的初次加载速度，比如采用多项缓存措施、需要时再加载某些模块（懒加载）。但这依旧不可能摆脱需要下载框架的事实，某些部分必须在呈现前下载。这是一个“现在支付，或者稍后支付”的问题。性能与等待时间的权衡是开发者必须作出的权衡。

## 页面生命周期

单页应用在初始页面加载时被完全加载，然后页面区域被替换或更新为按需从服务器加载的新页面片段。为避免过度下载未使用的功能，单页应用通常会逐渐下载更多内容，如所需要的功能、页面的一小块，或者完整的一页。

单页应用（SPA）举措与原生桌面应用程序所使用的多文件界面（MDI）呈现技术类似。

## 外部链接

- Migrating Multi-page Web Applications to Single-page Ajax Interfaces (Delft University of Technology)
- The Single Page Interface Manifesto