前端框架有哪些典型問題?

想整理下有哪些問題是需要前端框架解決的, 開發當中比較容易碰到, 所以每個前端框架都需要考慮的,

舉幾個我比較熟悉的例子:

* Dropdown 組件, 用到很典型的組件化和跨組件通信

* Modal 實現, 需要在正常數據流里維護掛載到根節點的 DOM

* 數據流, 比如 Backbone.Collection 和 Redux

* 路由問題

其他有沒有想到的...?


我的職業是前端工程師【九】:你應該知道的單頁面應用的五要素 - 知乎專欄

這幾年裡,單頁面應用的框架令人應接不暇,各種新的概念也層出不窮。從過去的 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 來定義好相應的路由。

&
...
&

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

&跳轉到foo&

當我們點擊相應的鏈接時,就會切換到 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),

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

Route::get("posts/{post}/comments/{comment}", function ($postId, $commentId) {
//
});

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

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

&
&

當頁面跳轉到 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 應用來說,我們可以做兩件事來保證數據的安全:

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

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

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

數據展示:模板引擎

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

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

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

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

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

&&

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

交互:事件與狀態管理

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

組件交互:狀態管理

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

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

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

用戶交互:事件

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

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


一個框架,就是一個半成品的應用,也即是把很多應用通用的功能都做了,剩下來只要補上特定應用的代碼,就構建起引用了。

前端框架的範圍可以無限擴展,只從最簡單的方面來說。

1. 路由問題

什麼樣的路徑(path)對應到什麼樣的網頁(page),這是必須要有的,因為一個使用的應用通常都不會只有一個頁面吧,解決這類為題的比如react-router。

2. 數據管理問題

驅動應用功能的還是數據,所以,怎麼管理應用中的數據,是一個重要問題,解決這類問題的有Flux和Redux。

3. 模塊管理

模塊化思想已經廣泛被接受,如果模塊管理得好,各個功能就可以獨立開發,然後壘磚一樣搭在一起就好了。

先寫這麼多,回頭再補充。


框架會承諾很多東西,承諾做某件事會變得特別容易,承諾做一個todo會變得非常容易,承諾開發效率會有很大提升,這些通常都是真的。不過框架不會承認讓一個團隊適應自己的設計和開發模式需要非常多的時間,也就是所謂學習曲線,因為我們的應用不是todo;也不會承認tutorial里沒有展示的細節,要做另外一些事情不得不去繞圈子,真正簡潔的方案會被說成是「不符合框架哲學」,甚至被當成不專業的表現。框架承諾一種把開發當填空的遊戲,用自己的規則替代開發棧的規則,但實際的開發並不是填空,問題空間往往比框架的規則範圍大。框架都很自大,自成一派,一不小心就變成宗教,框架的信徒都以為這就是銀彈,這就是X開發的未來。


徐飛叔曾經提過的, 端到端組件和視圖層組件的問題。現在流行的視圖層組件雖然靈活方便,但是破壞了抽象。比如有一個複雜的查詢選擇並返回用戶的dialog框需要在很多業務下用到,早期的Ext/JQuery派的框架會把視圖層和邏輯都封裝在內,調用非常方便。但是react下卻要把邏輯重複寫在各個上下文環境里,雖然有了redux,但還是會非常彆扭。不知道有沒有更好的使用姿勢。


model層的工作,即統一的狀態管理,每個組件都可直接或間接獲取用於渲染ui或參與邏輯處理的數據,如redux,vuex。

view層的工作,用來渲染取自model層或組件內部的狀態,一般單項或雙向綁定。

router層的工作,用來切換組件容器內組建的展示和渲染,切換時可重新渲染也可進行復用。

這只是簡單的大致方向,具體不同框架還會做額外工作,比如model層還有計算屬性,如vue的getters,view層自帶遍歷,如vue的v-for,router層的鉤子函數等等。

最後,感謝諸多團隊和個人開發者的優秀框架作品,前端有你們更精彩,筆芯


文件結構用模塊組織不利於review,用功能組織不利於開發……


亂花漸欲迷人眼,淺草才能沒馬蹄


長期以來,前端所處的位置是比較偏應用層,而且是很薄的一層,而架構又要求深度和廣度,所以之前在前端裡面做架構,好比在小水塘里游泳,稍微撲騰兩下就到處碰壁。但最近這幾年來,前端的範圍被大大拓展了,所以這一層逐漸變得大有可為。

怎樣去理解架構呢?在早期的文字MUD遊戲里,有這麼一句話:「你感覺哪裡不對,但是又說不上來。」在我們開發和使用軟體系統的過程中,或多或少會遇到這樣的感覺,有這種感覺就說明架構方面可能有些問題。

在狹義的前端領域,架構要處理的很重要的事情是組件的集成。由於JavaScript本身缺乏命名空間這樣的機制,多數框架都傾向於自己搞一套,所以這方面的碎片化是很嚴重的。如果一個公司的實力不足以自研所有用到的組件,就會一直面臨這方面的問題。

比如說,在做某個功能的過程中,發現需要一個組件,時間來不及做,就到網上搜了個,加到代碼裡面,先運行起來再說。一不小心,等功能做完的時候,已經引入了無數種組件了,有很多代碼是重疊的,可能有的還有衝突,外觀也不一致。

環顧四周的大型互聯網公司,基本上都有自己的前端框架,比如阿里的Kissy和Arale,騰訊的JX,百度的Tangram,360的QWrap等,為什麼?因為要整合別的框架,並且在此基礎上發展適合自己的組件庫,代價非常大,初期沒辦法的時候只能湊合,長期來說,所有代碼都可控的意義非常重要。

那麼,是不是一套框架可以包打天下呢,這個真的很難。對於不同的產品形態,如果想要用一套框架去適應,有的會偏輕,有的又偏重,有的要兼容低端瀏覽器,有的又不要,很難取捨。

常見的前端產品形態包括:

  • 內容型Web站點 側重渲染方面的優化,前端邏輯比重小
  • 操作型B/S系統 以數據和邏輯為中心,界面較規整
  • 內嵌Web的本地應用 要處理緩存和一些本地介面,包括PC客戶端和移動端

另外有Web遊戲,因為跟我們的企業形態關係不大,而且也比較獨特,所以不包含在內。這三種產品的前端框架要處理的事情顯然是不太一樣的,所以可以細分成2-3種項目模板,整理出對應的種子項目,供同類產品初始化用。

最近我們經常在前端領域聽說兩個詞:全端、全棧。

全端的意思是,原來的只做在瀏覽器中運行的Web程序不夠,還要做各種終端,包括iOS,Android等本地應用,甚至PC桌面應用。

為什麼廣義的前端應當包含本地應用呢?因為現在的本地應用,基於很多考慮,都變成了混合應用,也就是說,開發這個應用的技術,既包含原生的代碼,也包含了嵌入的HTML5代碼。這麼一來,就造成了開發本地應用的人技能要求較廣,能夠根據產品的場景,合理選擇每個功能應當使用的技術。

現在有一些PC端的混合應用開發技術,比如node-webkit和hex,前者的典型應用是Intel? XDK,後者的典型應用是有道詞典,此外,豌豆莢的PC客戶端也是採用類似技術的,也有一些產品是用的qt-webkit。這類技術可以方便做跨平台,極大減少開發工作量。

所以,我們可以看到,在很多公司,開發安卓、iOS應用的人員跟Web前端的處於同一個團隊中,這很大程度上就是考慮到這種情況。

全棧的意思是,除了只做在瀏覽器中運行的代碼,還寫一些服務端的代碼,這個需求又是從哪裡來的呢?

這個需求其實來自優化。我們要優化一個系統的前端部分,有這麼一些事情可以做:

  • HTML結構的優化,減少DOM樹的層次等等
  • CSS渲染性能的優化,批量寫入DOM變更之類
  • 資源文件的優化,比如小圖片的合併,圖像格式的處理,圖標字體的使用等
  • JavaScript邏輯的優化,模塊化,非同步載入,性能優化
  • 載入位元組量的優化,主要是分攤的策略
  • HTTP請求的優化

這裡面,除了前三條,其他都可能跟後端有些關係,尤其是最後一條。但是前端的人沒法去優化後端的東西,這是不同的協作環節,所以就很麻煩。

那麼,如果有了全棧,這個問題可以怎麼解決呢?

這種簡單的事情可以在靜態代理伺服器上配置出來,更複雜的就比較難了,需要一定的服務端邏輯。比如說,我們有多個ajax請求,請求不同的服務,每個請求的數據量都非常少,但因為請求數很多,可能會影響載入性能,如果能把它們在服務端就合併成一個就好了。但這個優化是前端發起的,傳統模式下,他的職責範圍有限,優化不到服務端去,而這多個服務很可能是跨產品模塊的,想要合併,放在哪個後端團隊都很怪異。

這可真難辦,就像老虎追猴子,猴子上了樹,老虎只能在下面乾瞪眼。但是如果我們能讓老虎上樹,這就不是個問題了。如果有這麼一層NodeJS,這一層完全由前端程序員控制,他就可以在這個地方做這種合併,非常的合理。

除此之外,我們常常會用到HTML模板,但使用它的最佳位置是隨著產品的場景而不同的,可能某個地方在前端更好,可能某個地方在後端好些。到底放在哪合適,只有前端開發人員才會知道,如果前端開發人員不能參與一部分後端代碼的開發,優化工作也還是做不徹底。有NodeJS之後會怎樣呢,因為不管前端模板還是後端模板,都是JavaScript的,可以使用同一套庫,這樣在做調整的時候不會有代碼遷移的煩惱,直接把模板換地方即可。

現在,也有很多業務場景有實時通信的需求,目前來說最合適的方案是http://Socket.io,它默認使用NodeJS來當服務端,這也是NodeJS的一個重要使用場景。

這樣,前端開發人員也部分參與了運行在服務端的代碼,他的工作範圍從原先客戶端瀏覽器,向後拓展了一個薄層,所以就有了全棧的稱呼。至於說這個稱呼還繼續擴展,一個前端開發人員從視覺到交互到靜態HTML到JavaScript包辦的情況,這個就有些過頭了。

以上這些,主要解決的都是代碼層面的事情。另外有一個方面,也是需要關注,但卻常常不能引起重視的,那就是前端的工程化問題。

早期為什麼沒有這些問題?因為那時候前端很簡單,複雜度不高,現在整個很複雜了,就帶來了很多管理問題。比如說整個系統的前端都組件化了之後,HTML會拆分成各種模板,JavaScript會拆分成各種模塊,而CSS也通過LESS或者SASS這種方式,變成了一種編譯式的語言。

這時候,我們考慮一個所謂的組件,它就比較麻煩了。它可能是一個或者多個HTML模板,加上一個或者多個JavaScript模塊,再包含CSS中的一部分構成的,而前兩者都可能有依賴項,三個部分也都要避免與其他組件的衝突。

這些東西都需要管理,並且提供一種比較好的方案去維護。在JavaScript被模塊化之後,也可以通過單元測試來控制它們的質量,並且把這個過程自動化,每次版本有變更之前,保證它們最基本的正確性。最終,需要有一種自動化的發布機制,把這幾類代碼提取,打包合併,壓縮,發布。


推薦閱讀:

很迷茫,不知道自己現在是要繼續學習 React.js 還是系統地學習 JS?
公司團隊有自己專職的UI設計師,但是前端團隊成員js能力薄弱,是否需要用bootstrap?
2018年一個合格的前端應該是什麼樣的?
2016 年的今天,Angular 1 還有什麼使用價值?
如何評價阿里巴巴最近開源的ANT DESIGN PRO?

TAG:前端開發 | JavaScript | 前端框架 |