單頁面應用的核心

這幾年裏,單頁面應用的框架令人應接不暇,各種新的概念也層出不窮。從過去的 jQuery Mobie、Backbone 到今天的 Angular 2、React、Vue 2,除了版本號不同,他們還有很多的相同之處。

剛開始寫商業代碼的時候,我使用的是 jQuery。使用 jQuery 來實現功能很容易,找到一個相應的 jQuery 插件,再編寫相應的功能即可。對於單頁面應用亦是如此,尋找一個相輔助的插件就可以了,如 jQuery Mobile。

儘管在今天看來,jQuery Mobile 已經不適合於今天的多數場景了。這個主要原因是,當時的用戶對於移動 Web 應用的理解和今天是不同的。他們覺得移動 Web 應用就是針對移動設備而訂製的,移動設備的 UI、更快的加載速度等等。而在今天,多數的移動 Web 應用,幾乎都是單頁面應用了。

過去,即使我們想創建一個單頁面應用,可能也沒有一個合適的方案。而在今天,可選擇的方案就多了(PS:參見《第四章:學習前端只需要三個月【框架篇】》)。每個人在不同類型的項目上,也會有不同的方案,沒有一個框架能解決所有的問題

  • 對於工作來說,我更希望的是一個完整的解決方案。
  • 對於編程體驗來說,我喜歡一點點的去創造一些輪子。

當我們會用的框架越多的時候, 所花費的時間抉擇也就越多。而單頁面應用的都有一些相同的元素,對於這些基本元素的理解,可以讓我們更快的適合其他框架。

單頁面應用的演進

我接觸到單頁面應用的時候,它看起來就像是將所有的內容放在一個頁面上麼。只需要在一個 HTML 寫好所需要的各個模板,並在不同的頁面上 data-role 表明這是個頁面(基於 jQuery Mobile)——每個定義的頁面都和今天的移動應用的模式相似,有 header、content、footer 三件套。再用 id 來定義好相應的路由。

<div data-role="page" id="foo"> 
...
</div>
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

這樣我們就在一個 HTML 裏返回了所有的頁面了。隨後,只需要在在入口處的 href 裏,寫好相應的 ID 即可。

<a href="#foo">跳轉到foo</a>
  • 1
  • 1

當我們點擊相應的鏈接時,就會切換到 HTML 中相應的 ID。這種簡單的單頁面應用基本上就是一個離線應用了,只適合於簡單的場景,可是它帶有單頁面應用的基本特性。而複雜的應用,則需要從服務器獲取數據。然而早期受限於移動瀏覽器性能的影響,只能從服務器獲取相應的 HTML,並替換當前的頁面。

在這樣的應用中,我們可以看到單頁面應用的基本元素: 頁面路由,通過某種方式,如 URL hash 來說明表明當前所在的頁面,並擁有從一個頁面跳轉到另外一個頁面的入口。

當移動設備的性能越來越好時,開發者們開始在瀏覽器裏渲染頁面:

  • 使用 jQuery 來做頁面交互
  • 使用 jQuery Ajax 來從服務端獲取數據
  • 使用 Backbone 來負責路由及 Model
  • 使用 Mustache 作爲模板引擎來渲染頁面
  • 使用 Require.js 來管理不同的模板
  • 使用 LocalStorage 來存儲用戶的數據

通過結合這一系列的工具,我們終於可以實現一個複雜的單頁面應用。而這些,也就是今天我們看到的單頁面應用的基本元素。我們可以在 Angular 應用、React 應用、Vue.js 應用 看到這些基本要素的影子,如:Vue Router、React Router、Angular 2 RouterModule 都是負責路由(頁面跳轉及模塊關係)的。在 Vue 和 React 裏,它們都是由輔助模塊來實現的。因爲 React 只是層 UI 層,而 Vue.js 也是用於構建用戶界面的框架。

路由:頁面跳轉與模塊關係

要說起路由,那可是有很長的故事。當我們在瀏覽器上輸入網址的時候,我們就已經開始了各種路由的旅途了。

  1. 瀏覽器會檢查有沒有相應的域名緩存,沒有的話就會一層層的去向 DNS服務器 尋向,最後返回對應的服務器的 IP 地址。
  2. 接着,我們請求的網站將會將由對應 IP 的 HTTP 服務器處理,HTTP 服務器會根據請求來交給對應的應用容器來處理。
  3. 隨後,我們的應用將根據用戶請求的路徑,將請求交給相應的函數來處理。最後,返回相應的 HTML 和資源文化

當我們做後臺應用的時候,我們只需要關心上述過程中的最後一步。即,將對應的路由交給對應的函數來處理。這一點,在不同的後臺框架的表現形式都是相似的。

Python 語言裏的 Web 開發框架 Django 的 URLConf,使用正規表達式來表正

url(r'^articles/2003/$', views.special_case_2003),
  • 1
  • 1

而在 Laravel 裏,則是通過參數的形式來呈現

Route::get('posts/{post}/comments/{comment}', function ($postId, $commentId) {
    //
});
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

雖然表現形式有一些差別,但是總體來說也是差不多的。而對於前端應用來說,也是如此,將對應的 URL 的邏輯交由對應的函數來處理

React Router 使用了類似形式來處理路由,代碼如下所示:

 <Route path="blog" component={BlogList} />
 <Route path="blog/:id" component={BlogDetail} />
  • 1
  • 2
  • 1
  • 2

當頁面跳轉到 blog 的時候,會將控制權將給 BlogList 組件來處理。

當頁面跳轉到 blog/fasfasf-asdfsafd 的時候,將匹配到這二個路由,並交給 BlogDetail 組件 來處理。而路由中的 id 值,也將作爲參數 BlogDetail 組件來處理。

相似的,而 Angular 2 的形式則是:

{ path: 'blog',      component: BlogListComponent },
{ path: 'blog/:id',      component: BlogDetailComponent },
  • 1
  • 2
  • 1
  • 2

相似的,這裏的 BlogDetailComponent 是一個組件,path 中的 id 值將會傳遞給 BlogDetailComponent 組件。

從上面來看,儘管表現形式上有所差異,但是其行爲是一致的:使用規則引擎來處理路由與函數的關係。稍有不同的是,後臺的路由完全交由服務器端來控制,而前端的請求則都是在本地改變其狀態。

並且同時在不同的前端框架上,他們在行爲上還有一些區別。這取決於我們是否需要後臺渲染,即刷新當前頁面時的表現形式。

  • 使用 Hash (#)或者 Hash Bang (#!) 的形式。即 # 開頭的參數形式,諸如 ued.party/#/blog。當我們訪問 blog/12 時,URL 的就會變成 ued.party/#/blog/12
  • 使用新的 HTML 5 的 history API。用戶看到的 URL 和正常的 URL 是一樣的。當用戶點擊某個鏈接進入到新的頁面時,會通過 history 的 pushState 來填入新的地址。當我們訪問 blog/12 時,URL 的就會變成 ued.party/blog/12。當用戶刷新頁面的時候,請通過新的 URL 來向服務器請求內容。

幸運的是,大部分的最新 Router 組件都會判斷是否支持 history API,再來決定先用哪一個方案。

數據:獲取與鑑權

實現路由的時候,只是將對應的控制權交給控制器(或稱組件)來處理。而作爲一個單頁面應用的控制器,當執行到相應的控制器的時候,就可以根據對應的 blog/12 來獲取到用戶想要的 ID 是 12。這個時候,控制器將需要在頁面上設置一個 loading 的狀態,然後發送一個請求到後臺服務器。

對於數據獲取來說,我們可以通過封裝過 XMLHttpRequest 的 Ajax 來獲取數據,也可以通過新的、支持 Promise 的 Fetch API 來獲取數據,等等。Fetch API 與經過 Promise 封裝的 Ajax 並沒有太大的區別,我們仍然是寫類似於的形式:

fetch(url).then(response => response.json())
  .then(data => console.log(data))
  .catch(e => console.log("Oops, error", e))
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

對於複雜一點的數據交互來說,我們可以通過 RxJS 來解決類似的問題。整個過程中,比較複雜的地方是對數據的鑑權與模型(Model)的處理。

模型麻煩的地方在於:轉變成想要的形式。後臺返回的值是可變的,它有可能不返回,有可能是 null,又或者是與我們要顯示的值不一樣——想要展示的是 54%,而後臺返回的是 0.54。與此同時,我們可能還需要對數值進行簡單的計算,顯示一個範圍、區間,又或者是不同的兩種展示。

同時在必要的時候,我們還需要將這些值存儲在本地,或者內存裏。當我們重新進入這個頁面的時候,我們再去讀取這些值。

一旦談論到數據的時候,不可避免的我們就需要關心安全因素。對於普通的 Web 應用來說,我們可以做兩件事來保證數據的安全:

  1. 採用 HTTPS:在傳輸的過程中保證數據是加密的。
  2. 鑑權:確保指定的用戶只能可以訪問指定的數據。

目前,流行的前端鑑權方式是 Token 的形式,可以是普通的定製 Token,也可以是 JSON Web Token。獲取 Token 的形式,則是通過 Basic 認證——將用戶輸入的用戶名和密碼,經過 BASE64 加密發送給服務器。服務器解密後驗證是否是正常的用戶名和密碼,再返回一個帶有時期期限的 Token 給前端。

隨後,當用戶去獲取需要權限的數據時,需要在 Header 裏鑑定這個 Token 是否有限,再返回相應的數據。如果 Token 已經過期了,則返回 401 或者類似的標誌,客戶端就在這個時候清除 Token,並讓用戶重新登錄。

數據展示:模板引擎

現在,我們已經獲取到這些數據了,下一步所需要做的就是顯示這些數據。與其他內容相比,顯示數據就是一件簡單的事,無非就是:

  • 依據條件來顯示、隱藏某些數據
  • 在模板中對數據進行遍歷顯示
  • 在模板中執行方法來獲取相應的值,可以是函數,也可以是過濾器。
  • 依據不同的數值來動態獲取樣式
  • 等等

不同的框架會存在一些差異。並且現代的前端框架都可以支持單向或者雙向的數據綁定。當相應的數據發生變化時,它就可以自動地顯示在 UI 上。

最後,在相應需要處理的 UI 上,綁上相應的事件來處理。

只是在數據顯示的時候,又會涉及到另外一個問題,即組件化。對於一些需要重用的元素,我們會將其抽取爲一個通用的組件,以便於我們可以複用它們。

<my-sizer [(size)]="fontSizePx"></my-sizer>
  • 1
  • 1

並且在這些組件裏,也會涉及到相應的參數變化即狀態改變。

交互:事件與狀態管理

完成一步步的渲染之後,我們還需要做的事情是:交互。交互分爲兩部分:用戶交互、組件間的交互——共享狀態。

組件交互:狀態管理

用戶從 A 頁面跳轉到 B 頁面的時候,爲了解耦組件間的關係,我們不會使用組件的參數來傳入值。而是將這些值存儲在內存裏,在適當的時候調出這些值。當我們處理用戶是否登錄的時候,我們需要一個 isLogined 的方法來獲取用戶的狀態;在用戶登錄的時候,我們還需要一個 setLogin 的方法;用戶登出的時候,我們還需要更新一下用戶的登錄狀態。

在沒有 Redux 之前,我都會寫一個 service 來管理應用的狀態。在這個模塊裏寫上些 setter、getter 方法來存儲狀態的值,並根據業務功能寫上一些來操作這個值。然而,使用 service 時,我們很難跟蹤到狀態的變化情況,還需要做一些額外的代碼來特別處理。

有時候也會犯懶一下,直接寫一個全局變量。這個時候維護起代碼來就是一場噩夢,需要全局搜索相應的變量。如果是調用某個特定的 Service 就比較容易找到調用的地方。

用戶交互:事件

事實上,對於用戶交互來說也只是改變狀態的值,即對狀態進行操作。

舉一個例子,當用戶點擊登錄的時候,發送數據到後臺,由後臺返回這個值。由控制器一一的去修改這些狀態,最後確認這個用戶登錄,併發一個用戶已經登錄的廣播,又或者修改全局的用戶值。

節選自:我的職業是前端工程師



原文出自:http://blog.csdn.net/phodal/article/details/64110412

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章