前端程序員必知:單頁面應用的核心
這幾年里,單頁面應用的框架令人應接不暇,各種新的概念也層出不窮。從過去的 jQuery Mobie、Backbone 到今天的 Angular 2、React、Vue 2,除了版本號不同,他們還有很多的相同之處。
剛開始寫商業代碼的時候,我使用的是 jQuery。使用 jQuery 來實現功能很容易,找到一個相應的 jQuery 插件,再編寫相應的功能即可。對于單頁面應用亦是如此,尋找一個相輔助的插件就可以了,如 jQuery Mobile。
盡管在今天看來,jQuery Mobile 已經不適合于今天的多數場景了。這個主要原因是,當時的用戶對于移動 Web 應用的理解和今天是不同的。他們覺得移動 Web 應用就是針對移動設備而訂制的,移動設備的 UI、更快的加載速度等等。而在今天,多數的移動 Web 應用,幾乎都是單頁面應用了。
過去,即使我們想創建一個單頁面應用,可能也沒有一個合適的方案。而在今天,可選擇的方案就多了。每個人在不同類型的項目上,也會有不同的方案,沒有一個框架能解決所有的問題
-
對于工作來說,我更希望的是一個完整的解決方案。
-
對于編程體驗來說,我喜歡一點點的去創造一些輪子。
當我們 會用的框架越多的時候, 所花費的時間抉擇也就越多 。而單頁面應用的都有一些相同的元素,對于這些基本元素的理解,可以讓我們更快的適合其他框架。
單頁面應用的演進
我接觸到單頁面應用的時候,它看起來就像是 將所有的內容放在一個頁面上么 。
只需要在一個 HTML 寫好所需要的各個模板,并在不同的頁面上 data-role 表明這是個頁面(基于 jQuery Mobile)——每個定義的頁面都和今天的移動應用的模式相似,有 header、content、footer 三件套。再用 id 來定義好相應的路由。
<div data-role="page" id="foo">
...
</div>
這樣我們就在一個 HTML 里返回了所有的頁面了。隨后,只需要在在入口處的 href 里,寫好相應的 ID 即可。
<a href="#foo">跳轉到foo</a>
當我們點擊相應的鏈接時,就會切換到 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 也是用于構建用戶界面的框架。
路由:頁面跳轉與模塊關系
要說起路由,那可是有很長的故事。當我們在瀏覽器上輸入網址的時候,我們就已經開始了各種路由的旅途了。
-
瀏覽器會檢查有沒有相應的域名緩存,沒有的話就會一層層的去向 DNS服務器 尋向,最后返回對應的服務器的 IP 地址。
-
接著,我們請求的網站將會將由對應 IP 的 HTTP 服務器處理,HTTP 服務器會根據請求來交給對應的應用容器來處理。
-
隨后,我們的應用將根據用戶請求的路徑,將請求交給相應的函數來處理。最后,返回相應的 HTML 和資源文化
當我們做后臺應用的時候,我們只需要關心上述過程中的最后一步。即,將對應的路由交給對應的函數來處理。這一點,在不同的后臺框架的表現形式都是相似的。
如 Python 語言里的 Web 開發框架 Django 的 URLConf,使用正規表達式來表正
url(r'^articles/2003/$', views.special_case_2003),
而在 Laravel 里,則是通過參數的形式來呈現
Route::get('posts/{post}/comments/{comment}', function ($postId, $commentId) {
//
});
雖然表現形式有一些差別,但是總體來說也是差不多的。而對于前端應用來說,也是如此, 將對應的 URL 的邏輯交由對應的函數來處理 。
React Router 使用了類似形式來處理路由,代碼如下所示:
<Route path="blog" component={BlogList} />
<Route path="blog/:id" component={BlogDetail} />
當頁面跳轉到 blog 的時候,會將控制權將給 BlogList 組件來處理。
當頁面跳轉到 blog/fasfasf-asdfsafd 的時候,將匹配到這二個路由,并交給 BlogDetail 組件 來處理。而路由中的 id 值,也將作為參數 BlogDetail 組件來處理。
相似的,而 Angular 2 的形式則是:
{ path: 'blog', component: BlogListComponent },
{ path: 'blog/:id', component: BlogDetailComponent },
相似的,這里的 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))
對于復雜一點的數據交互來說,我們可以通過 RxJS 來解決類似的問題。整個過程中,比較復雜的地方是對數據的鑒權與模型(Model)的處理。
模型麻煩的地方在于:轉變成想要的形式。后臺返回的值是可變的,它有可能不返回,有可能是 null,又或者是與我們要顯示的值不一樣——想要展示的是 54%,而后臺返回的是 0.54。與此同時,我們可能還需要對數值進行簡單的計算,顯示一個范圍、區間,又或者是不同的兩種展示。
同時在必要的時候,我們還需要將這些值存儲在本地,或者內存里。當我們重新進入這個頁面的時候,我們再去讀取這些值。
一旦談論到數據的時候,不可避免的我們就需要關心安全因素。對于普通的 Web 應用來說,我們可以做兩件事來保證數據的安全:
-
采用 HTTPS:在傳輸的過程中保證數據是加密的。
-
鑒權:確保指定的用戶只能可以訪問指定的數據。
目前,流行的前端鑒權方式是 Token 的形式,可以是普通的定制 Token,也可以是 JSON Web Token。獲取 Token 的形式,則是通過 Basic 認證——將用戶輸入的用戶名和密碼,經過 BASE64 加密發送給服務器。服務器解密后驗證是否是正常的用戶名和密碼,再返回一個帶有時期期限的 Token 給前端。
隨后,當用戶去獲取需要權限的數據時,需要在 Header 里鑒定這個 Token 是否有限,再返回相應的數據。如果 Token 已經過期了,則返回 401 或者類似的標志,客戶端就在這個時候清除 Token,并讓用戶重新登錄。
數據展示:模板引擎
現在,我們已經獲取到這些數據了,下一步所需要做的就是顯示這些數據。
與其他內容相比,顯示數據就是一件簡單的事,無非就是:
-
依據條件來顯示、隱藏某些數據
-
在模板中對數據進行遍歷顯示
-
在模板中執行方法來獲取相應的值,可以是函數,也可以是過濾器。
-
依據不同的數值來動態獲取樣式
-
等等
不同的框架會存在一些差異。并且現代的前端框架都可以支持單向或者雙向的數據綁定。當相應的數據發生變化時,它就可以自動地顯示在 UI 上。
最后,在相應需要處理的 UI 上,綁上相應的事件來處理。
只是在數據顯示的時候,又會涉及到另外一個問題,即組件化。對于一些需要重用的元素,我們會將其抽取為一個通用的組件,以便于我們可以復用它們。
<my-sizer [(size)]="fontSizePx"></my-sizer>
并且在這些組件里,也會涉及到相應的參數變化即狀態改變。
交互:事件與狀態管理
完成一步步的渲染之后,我們還需要做的事情是:交互。交互分為兩部分:用戶交互、組件間的交互——共享狀態。
組件交互:狀態管理
用戶從 A 頁面跳轉到 B 頁面的時候,為了解耦組件間的關系,我們不會使用組件的參數來傳入值。而是將這些值存儲在內存里,在適當的時候調出這些值。
當我們處理用戶是否登錄的時候,我們需要一個 isLogined 的方法來獲取用戶的狀態;在用戶登錄的時候,我們還需要一個 setLogin 的方法;用戶登出的時候,我們還需要更新一下用戶的登錄狀態。
在沒有 Redux 之前,我都會寫一個 service 來管理應用的狀態。在這個模塊里寫上些 setter、getter 方法來存儲狀態的值,并根據業務功能寫上一些來操作這個值。然而,使用 service 時,我們很難跟蹤到狀態的變化情況,還需要做一些額外的代碼來特別處理。
有時候也會犯懶一下,直接寫一個全局變量。這個時候維護起代碼來就是一場噩夢,需要全局搜索相應的變量。如果是調用某個特定的 Service 就比較容易找到調用的地方。
用戶交互:事件
事實上,對于用戶交互來說也只是改變狀態的值,即對狀態進行操作。
舉一個例子,當用戶點擊登錄的時候,發送數據到后臺,由后臺返回這個值。由控制器一一的去修改這些狀態,最后確認這個用戶登錄,并發一個用戶已經登錄的廣播,又或者修改全局的用戶值。
來自:https://mp.weixin.qq.com/s?__biz=MjM5Mjg4NDMwMA==&mid=2652974800&idx=1&sn=afc003d743f9e32224141a113c400742&chksm=bd4afdf38a3d74e5bdacb0ab97eb0de50dd71a378ac642fb38632d9590185e53ede6d2781198#rd