關于 Vue App 開發的一些思考

我來 TalkingData 實習已經六個月了。在這六個月期間,我獨立完成了三個前端 SPA 項目,從 Vue 1 & Vuex 1 到 Vue 2 & Vuex 2 都有使用。從最先開始的四個模塊、八個功能,到最后多模塊嵌套、數十個功能,項目的難度越來越大,復雜度越來越高,坑也越踩越多。第三個項目完成后,我仔細回顧了這三個項目的開發歷程,重新整理了項目代碼,發現了很多問題,也產生了很多思考。

個人愚見,還望大家多批評指正。

關于前端模塊化

模塊化是一個源于后端的概念。在我的理解中,模塊化的目的在于提升代碼的可復用度與可維護性,加速開發效率。同時模塊化又意味著大量的解耦,將不同的模塊盡可能的分開(因為這樣才能彼此獨立開發)。

傳統的前端開發,每一個鏈接都對應一個實體頁面,既沒有軟路由的概念,也沒有單頁應用的概念。我們訪問不同的鏈接,得到的就是不同的頁面。這樣的設計有兩個問題:由于頁面彼此獨立,頁面元素的復用性較差;由于每次頁面切換都要對整個頁面進行刷新,頁面加載的性能也相對較差。為了解決生產環境下的復用問題,SSI 技術應運而生了。后來,為了解決研發階段代碼復用的問題,像 Gulp 這樣的構建工具誕生了。這應該可以算是很早的前端模塊化的嘗試。

進入 React 時代之后,前端模塊化開始變得很常見了。React 作為視圖層框架,將視圖分割成了一個個組件,我們可以分別開發各個組件,再將這些組件組裝起來,形成最終的頁面。這時候,前端開發的過程就很像曾經的后端開發了:我們先把產品分割成組件,然后依次開發各個組件,再把各個組件組裝起來,形成最終的產品。模塊化的出現,也使得前端可以引入后端常見的單元測試,在開發早期就能避免很多 BUG 。

前端模塊化是一件很有意思,又有點讓人頭疼的問題。SPA 的開發方式,更像是在搭積木:我們先按照圖紙,把每一塊積木( Component )的樣子構建出來,再按照圖紙把一塊塊積木搭建起來,組成我們想要的結構( View ),而有的積木是一扇窗戶,可以打開、關閉,有的積木是一組輪子,可以轉動。這些行為都是精確在積木上的,在設計和制作積木的過程中,就已經完成了。在搭積木的過程中,我們不再關心這些行為,而是更專注于怎么實現圖紙要求的結構。這樣的設計使得整個開發過程更加的清爽,但也伴隨著一些問題:我們必須將各個組件盡可能的解耦,然后再將相關的組件通過額外的部件連接起來(比如用皮帶連接方向盤和轉向軸)。

搭積木的例子就舉到這里,我們回歸到問題本身:組件解耦之后,有些需要組件間相互配合完成的業務邏輯(換言之,組件間通信 & 狀態共享)有些難以組織。在相同組件樹上的組件,我們可以添加父組件進行管理,如果不在同一個組件樹上呢?同時,為了進行組件間通信,我們不得不為組件多添加一些結構,或是父組件,或是其它的方式。這也是前端模塊化讓人比較頭疼的地方。

關于組件間通信

因為我是從 React 轉到 Vue 的,所以先聊聊 React 的組件間通信。React 有一個典型特征:單向數據流。所有的數據都只能由父組件傳遞給子組件,不能回傳,即便是進行修改,也是子組件通過父組件傳遞進來的函數修改父組件的狀態,再由父組件傳遞給子組件。React 是如何進行組件間通信的呢?它們通過操作父組件進行組件間通信。乍一看蠻合理,但是如果組件不在相同層級上,就很麻煩了:它們要一直向上回溯,找到共有的父組件,再通過 Props 逐級傳遞,將修改父組件的函數傳遞下去,才能實現組件間通信。

Vue 的處理方式很像,只不過它不需要傳遞函數,而是通過觸發事件( Vue 1 的時候更簡單,只需要使用 .sync 進行雙向綁定就好了。Vue 2 也有 .sync,但是其本質還是通過觸發事件完成數據修改)。

這樣的做法問題很明顯:為了完成組件間通信,我們必須要在至少一個父組件上,甚至整個組件樹上下發處理函數 / 分發事件(Vue 的事件是不冒泡的)。如果組件通信的跨度很大,那我們的代碼會變得非常難以維護。而且,如果這兩個組件屬于完全不同的組件樹(比如屬于完全獨立的兩個 Module 上),我們幾乎沒有辦法妥善處理組件間通信(雖說根 View 肯定是所有組件的父組件,但是我們并不想在 View 層添加任何業務邏輯)。

Vue 提供了一個狀態管理框架:Vuex,用來將狀態(或者說,數據)從組件中剝離出來,外掛在整個應用上,以此來增強對應用狀態結構的管理與狀態共享(組件間通信)的支持。

Vuex

同時,Vue 也提供了另一種方式進行跨組件通信:Event Bus。它運用了觀察者模式,通過在 Bus(事件總線,本質是一個 Vue 實例)上觸發事件,再在 Bus 上捕獲事件,來完成組件間通信。這種方式下,狀態依然保存在各個組件內部。

Event Bus

關于 Event Bus 和 Vuex

Event Bus 托管數據

在我見到的 Vue 項目中,有的人會用 Event Bus 托管一部分數據。這樣做本身沒什么問題,但是我覺得違背了 Event Bus 的初衷。同時,這些數據既不屬于組件樹,又不在整個應用的數據結構上,無法妥善管理。

同時使用 Event Bus 和 Vuex

還有一個問題,我考慮了很久:一個項目究竟應不應該同時引入 Vuex 和 Event Bus 兩套邏輯。這個答案沒有正誤,但我更傾向于不這么做。通常這樣做的原因是:我只需要在局部進行組件間通信,而且我不想使用父組件管理子組件(可能邏輯比較復雜,可能新建父組件只為了完成這一個操作有點浪費),這時我會考慮使用 Event Bus 完成局部組件間通信的操作。舉個例子:一個集群管理系統,當我關閉某臺服務器的時候(關閉服務器的操作在對應服務器的卡片上),顯示開啟的服務器數量的組件會同時減去一,這個邏輯只有集群管理這一個頁面有。我的做法是,如果整個項目沒有使用 Vuex 進行狀態管理,我們可以使用 Event Bus,但是如果使用了 Vuex,我們應當將該邏輯整合到 Vuex 上,由 Vuex 觸發視圖更新(即完成組件間通信),而不是直接啟用一個 Event Bus。

謹慎使用 Vuex

很多文章都提到了,我們也許并不需要使用 Vuex,除非我們的項目真的“大”到需要一個狀態管理機制來管理整個應用的狀態。在我看來,評估一個項目是否需要 Vuex 的方式很簡單:應用對數據共享的需求有多重,以及應用對數據緩存的依賴有多重:

  • 如果應用中包含大量需要數據共享的組件,無論是局部的還是全局的,我們都可以考慮啟用 Vuex。
  • 如果頁面在初始化時需要通過 Ajax 從服務器端請求大量的數據以完成頁面渲染(尤其是不需要頻繁更新的數據,比如一個日程表),我們可以考慮啟用 Vuex(因為如果我們不外掛這些數據,組件銷毀后就需要重新請求這些數據再重新渲染)。

同時,如果啟用了 Vuex,我建議除了控制視圖的狀態屬性(比如控制 Switch 組件是開狀態還是關狀態)和表單數據外,其它狀態全部托管到 Vuex 上,在“關于業務邏輯”中我會解釋為什么。

如何擺脫 Vuex

之前提到,不是所有的應用都需要使用 Vuex 進行狀態管理,那我們該如何擺脫 Vuex?思路很清晰:全局狀態本地化,局部狀態局部化。

由于 Vue 對所謂“全局根組件”的概念比較淡薄,所以很多全局狀態就會有點無處安放,這也是我最先開始啟用 Vuex 的原因。其實仔細分析,一個應用的全局狀態其實很少,我能歸納到的大概只有兩點:登錄狀態 & 用戶信息(鑒權信息)。

  • 關于登錄狀態,請務必下放到 Vue Router 中,然后對所有的需要登錄后才能訪問的頁面,在路由層面進行統一控制。
  • 關于用戶信息,其實前端沒必要保存太多。用戶信息顯示最頻繁的地方,通常是在 Header 上。所以用戶信息可以直接作為 Header 的局部屬性存儲。鑒權信息可以利用 Session Storage 或者 Cookie 進行存儲。其它的全局狀態,也可以下放到 Cookie 和 Storage 中,在需要的組件中按需讀取。

局部狀態,我們可以通過添加父組件的形式進行局部的統一管理,也可以完全下放給組件本身,合理即可。但是,請不要使用 Event Bus 管理數據,同時也請有效控制 Event Bus 的數量,我認為一個就夠了。

關于項目結構

以下的討論,建立在一個使用了 Vuex 的 Vue SPA 項目上。

最先開始寫項目的時候,由于項目本身不是很復雜,我沒有在項目結構的設計上做任何的文章。在完成第三個項目的時候,面對頻繁更迭的需求,不斷添加的功能,整個項目的代碼越改越亂,以至于無法維護了。我用了很久的時間重新整理并構建了新的項目結構,以解決以下幾個問題:

  • 新功能添加頻繁、需求變化頻繁。
  • 項目依賴大量的 Ajax 請求,業務邏輯復雜。
  • 路由結構復雜,頁面嵌套非常多。

處理項目的時候,我選用了這樣的順序:從功能出發,按照實際的業務模塊構建頁面的路由結構,再從路由結構出發構建出視圖結構,再根據視圖對組件進行分類。這樣我們能得到一個很清晰的結構:一個項目被分成了不同的模塊,每個模塊有相互對應的視圖與組件。然后,我們再根據組件結構,按照模塊構建 Vuex 狀態樹,封裝 Ajax API 。

總結一下:一切對項目的分割與歸類都應當建立在 Module 上,Views,Components,Store,APIs 的結構一定要對應。

這樣,無論進行測試還是進行維護,我們都可以在不干擾任何其他組件的情況下進行操作,也盡可能降低了耦合。所有的組件間通信,全部放到 Vuex 中完成,這樣每一個 Module 中的組件都可以專注與自己本身(從 Vuex 中取用狀態 & 推送狀態到 Vuex 中),而不用考慮其他組件。

一個典型的項目結構是這樣的:

Vue SPA Template

有兩個一定要遵守的原則:

  • 視圖層( Views )不應處理任何的業務邏輯,只負責組裝組件( Components )。
  • 視圖層的文件邏輯結構應當與路由結構完全相符。

關于業務邏輯

前后端解耦之后,前端對 Ajax 的依賴變得非常的重。許多原本可以直接由服務器渲染的數據,都需要前端通過 Ajax 的方式從后端拉取。這新增了許多業務邏輯。如何安放這些業務邏輯,成了一個難題。

我的第三個項目中,后端交付給前端的接口有 60 多個,幾乎每一個頁面的渲染都依賴至少一個接口來獲取數據。我起先在組件中嵌入了大量的代碼來處理 Ajax 的 Response,導致組件中的函數被拉的很長,邏輯很多很混亂,可維護性非常差。

我沒有把對 Response 的處理封裝到 APIs 的原因是:我需要控制整個請求過程,比如在加載的時候給出友好提示。同時我認為,我們不應該把處理 Response 的過程放到 APIs 中,因為 Response 通常與數據掛鉤,我們應當將數據與請求隔離開,APIs 應該只專注于處理傳入的請求內容和生成請求,而不對應用狀態進行操控。

為了清理這些業務邏輯,我瞄準了 Vuex。除了發送表單的請求外,我將所有執行 Ajax 和處理 Response 的邏輯放到了 Vuex 的 Actions 內,而組件中只 Dispatch 請求數據的事件。這本身是合理的:Vuex 管理著整個應用的狀態,我們通過更新 Vuex 的狀態觸發視圖更新,渲染組件內容。同時,這樣整理過之后,我對 Ajax 的維護變得更加容易了,我不再需要精確到某一個組件進行維護,而是直接定位到它所屬的 Vuex Module 并維護對應的 Action 即可。同時,Vuex 的 Actions 支持 Promise,我們可以很容易的控制 Ajax 狀態提示信息的顯示。

我之所以建議除了控制視圖的狀態屬性和表單數據外,其它狀態全部托管到 Vuex 上,是因為通過 Ajax 拉取數據占用了整個項目數據來源的很大部分,同時 Ajax 操作會影響到其他本地狀態,比如會影響到分頁組件的分頁數量。為了方便管理和維護,全部托管到 Vuex 上付出的代價我認為是可以接受的。

關于組件復用

我們一直在重復一個概念:模塊化。模塊化就意味著復用。我們應該如何尋找可復用的組件,又該如何復用呢?

通常,可復用的組件不應該包含過多(甚至不應該包含任何)業務邏輯。這些組件接收外界傳遞給它的參數,返回固定的結果(有點像純函數,或者 React 中的 Dumb Component )。在項目開發中,我們很難一下子確定可復用的組件,我的建議是:首先不對組件進行復用,在開發完成后,對組件進行梳理,發現可復用的組件之后,再嘗試對組件進行抽象。切忌強行對組件進行復用,否則會使抽象組件變得臃腫,事半功倍。

還有就是,視圖層堅決不允許復用,不要試圖通過在視圖層對資源類型進行判斷或者其他方式控制視圖層的渲染結果。

關于項目管理

  • 一定要為每一次 Git Commit 留下清晰明確的日志,方便查閱和回滾。
  • 對每一個關鍵步驟都應該生成唯一的 Git Commit,而不是一口氣提交全部的修改。
  • 對每一次發布,都應當添加版本標記,方便進行版本控制和回溯。
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,283評論 6 530
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 97,947評論 3 413
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 175,094評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,485評論 1 308
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,268評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,817評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,906評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,039評論 0 285
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,551評論 1 331
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,502評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,662評論 1 366
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,188評論 5 356
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,907評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,304評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,563評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,255評論 3 389
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,637評論 2 370

推薦閱讀更多精彩內容