单页应用

**單頁式應用程式**（英語：single-page application，縮寫 **SPA**）是一種網路應用程式或網站的模型，它透過動態重寫當前頁面來與使用者互動，而非傳統的從伺服器重新載入整個新頁面。這種方法避免了頁面之間切換打斷使用者體驗，使應用程式更像一個桌面應用程式。在單頁式應用程式中，所有必要的代碼（HTML、JavaScript 和 CSS）都透過單個頁面的載入而檢索，或者依需（通常是為回應使用者操作）動態載入適當的資源並添加到頁面。儘管可以用位置雜湊或 HTML5 歷史 API 來提供應用程式中單獨邏輯頁面的感知和導覽能力，但頁面在過程中的任何時間點都不會重新載入，也不會將控制轉移到其他頁面。與單頁式應用程式的互動通常涉及到與網頁伺服器後端的動態通訊。

## 歷史

儘管早在 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 框架

諸如 AngularJS、Ember.js、Meteor.js、ExtJS 和 React 等網頁瀏覽器端的 JavaScript 框架採納了單頁式應用程式（SPA）原則。

* AngularJS 是一個全面的客戶端側框架。其模板基於雙向 UI 資料繫結。資料繫結是一種自動方法，在模型改變時更新視圖，以及在視圖改變時更新模型。其 HTML 模板在瀏覽器中編譯。編譯步驟建立純 HTML，瀏覽器將其重新彩現到即時視圖。該步驟會在隨後的頁面瀏覽中重複。在傳統的伺服器端 HTML 編程中，控制器和模型等概念在伺服器行程中進行互動以產生新的 HTML 視圖。在 AngularJS 框架中，控制器和模型狀態在客戶端的瀏覽器中維護，從而使生成新頁面不依賴與伺服器的互動。
* Ember.js 是基於模型 - 視圖 - 控制器（MVC）軟體架構模型的客戶端側 JavaScript Web 應用程式框架。它允許開發人員在一個框架中透過常用的習慣用語和最佳實踐來建立可伸縮的單頁面應用程式。該框架提供豐富的物件模型、宣告性雙向資料繫結、計算屬性，Handlebars.js 提供的自動更新模板，以及一個路由器管理應用程式狀態。
* Meteor.js 是一個專門為單頁式應用程式設計的 Full Stack（客戶端 - 伺服器）JavaScript 框架。它具有比 Angular、Ember 或 ReactJS 更簡單的資料繫結特性，並且使用 Distributed Data Protocol 和一個發布 / 訂閱來自動將資料更改傳播到客戶端，無需開發人員編寫任何同步代碼。Full Stack 反應確保從資料庫到模板的所有層都可以在必要時自動更新。諸如伺服器端彩現等生態系統包則解決搜尋引擎最佳化（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 是一個 Full Stack 程式庫，它採用 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 和 Ajax）

對伺服器的請求通常會帶來原始資料（如 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）可能因此對排名降級。

### 客戶端 / 伺服器代碼分割區

增加伺服器與客戶端之間所共享代碼量的一種方法是使用如 Mustache、Handlebars 等無邏輯模板語言。這種模板可以用不同的代管語言來呈現，如伺服器上的 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 （頁面存檔備份，存於網際網路檔案館）