關(guān)于 Vue App 開發(fā)的一些思考

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

個(gè)人愚見,還望大家多批評(píng)指正。

關(guān)于前端模塊化

模塊化是一個(gè)源于后端的概念。在我的理解中,模塊化的目的在于提升代碼的可復(fù)用度與可維護(hù)性,加速開發(fā)效率。同時(shí)模塊化又意味著大量的解耦,將不同的模塊盡可能的分開(因?yàn)檫@樣才能彼此獨(dú)立開發(fā))。

傳統(tǒng)的前端開發(fā),每一個(gè)鏈接都對(duì)應(yīng)一個(gè)實(shí)體頁(yè)面,既沒有軟路由的概念,也沒有單頁(yè)應(yīng)用的概念。我們?cè)L問不同的鏈接,得到的就是不同的頁(yè)面。這樣的設(shè)計(jì)有兩個(gè)問題:由于頁(yè)面彼此獨(dú)立,頁(yè)面元素的復(fù)用性較差;由于每次頁(yè)面切換都要對(duì)整個(gè)頁(yè)面進(jìn)行刷新,頁(yè)面加載的性能也相對(duì)較差。為了解決生產(chǎn)環(huán)境下的復(fù)用問題,SSI 技術(shù)應(yīng)運(yùn)而生了。后來,為了解決研發(fā)階段代碼復(fù)用的問題,像 Gulp 這樣的構(gòu)建工具誕生了。這應(yīng)該可以算是很早的前端模塊化的嘗試。

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

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

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

關(guān)于組件間通信

因?yàn)槲沂菑?React 轉(zhuǎn)到 Vue 的,所以先聊聊 React 的組件間通信。React 有一個(gè)典型特征:?jiǎn)蜗驍?shù)據(jù)流。所有的數(shù)據(jù)都只能由父組件傳遞給子組件,不能回傳,即便是進(jìn)行修改,也是子組件通過父組件傳遞進(jìn)來的函數(shù)修改父組件的狀態(tài),再由父組件傳遞給子組件。React 是如何進(jìn)行組件間通信的呢?它們通過操作父組件進(jìn)行組件間通信。乍一看蠻合理,但是如果組件不在相同層級(jí)上,就很麻煩了:它們要一直向上回溯,找到共有的父組件,再通過 Props 逐級(jí)傳遞,將修改父組件的函數(shù)傳遞下去,才能實(shí)現(xiàn)組件間通信。

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

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

Vue 提供了一個(gè)狀態(tài)管理框架:Vuex,用來將狀態(tài)(或者說,數(shù)據(jù))從組件中剝離出來,外掛在整個(gè)應(yīng)用上,以此來增強(qiáng)對(duì)應(yīng)用狀態(tài)結(jié)構(gòu)的管理與狀態(tài)共享(組件間通信)的支持。

Vuex

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

Event Bus

關(guān)于 Event Bus 和 Vuex

Event Bus 托管數(shù)據(jù)

在我見到的 Vue 項(xiàng)目中,有的人會(huì)用 Event Bus 托管一部分?jǐn)?shù)據(jù)。這樣做本身沒什么問題,但是我覺得違背了 Event Bus 的初衷。同時(shí),這些數(shù)據(jù)既不屬于組件樹,又不在整個(gè)應(yīng)用的數(shù)據(jù)結(jié)構(gòu)上,無法妥善管理。

同時(shí)使用 Event Bus 和 Vuex

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

謹(jǐn)慎使用 Vuex

很多文章都提到了,我們也許并不需要使用 Vuex,除非我們的項(xiàng)目真的“大”到需要一個(gè)狀態(tài)管理機(jī)制來管理整個(gè)應(yīng)用的狀態(tài)。在我看來,評(píng)估一個(gè)項(xiàng)目是否需要 Vuex 的方式很簡(jiǎn)單:應(yīng)用對(duì)數(shù)據(jù)共享的需求有多重,以及應(yīng)用對(duì)數(shù)據(jù)緩存的依賴有多重:

  • 如果應(yīng)用中包含大量需要數(shù)據(jù)共享的組件,無論是局部的還是全局的,我們都可以考慮啟用 Vuex。
  • 如果頁(yè)面在初始化時(shí)需要通過 Ajax 從服務(wù)器端請(qǐng)求大量的數(shù)據(jù)以完成頁(yè)面渲染(尤其是不需要頻繁更新的數(shù)據(jù),比如一個(gè)日程表),我們可以考慮啟用 Vuex(因?yàn)槿绻覀儾煌鈷爝@些數(shù)據(jù),組件銷毀后就需要重新請(qǐng)求這些數(shù)據(jù)再重新渲染)。

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

如何擺脫 Vuex

之前提到,不是所有的應(yīng)用都需要使用 Vuex 進(jìn)行狀態(tài)管理,那我們?cè)撊绾螖[脫 Vuex?思路很清晰:全局狀態(tài)本地化,局部狀態(tài)局部化。

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

  • 關(guān)于登錄狀態(tài),請(qǐng)務(wù)必下放到 Vue Router 中,然后對(duì)所有的需要登錄后才能訪問的頁(yè)面,在路由層面進(jìn)行統(tǒng)一控制。
  • 關(guān)于用戶信息,其實(shí)前端沒必要保存太多。用戶信息顯示最頻繁的地方,通常是在 Header 上。所以用戶信息可以直接作為 Header 的局部屬性存儲(chǔ)。鑒權(quán)信息可以利用 Session Storage 或者 Cookie 進(jìn)行存儲(chǔ)。其它的全局狀態(tài),也可以下放到 Cookie 和 Storage 中,在需要的組件中按需讀取。

局部狀態(tài),我們可以通過添加父組件的形式進(jìn)行局部的統(tǒng)一管理,也可以完全下放給組件本身,合理即可。但是,請(qǐng)不要使用 Event Bus 管理數(shù)據(jù),同時(shí)也請(qǐng)有效控制 Event Bus 的數(shù)量,我認(rèn)為一個(gè)就夠了。

關(guān)于項(xiàng)目結(jié)構(gòu)

以下的討論,建立在一個(gè)使用了 Vuex 的 Vue SPA 項(xiàng)目上。

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

  • 新功能添加頻繁、需求變化頻繁。
  • 項(xiàng)目依賴大量的 Ajax 請(qǐng)求,業(yè)務(wù)邏輯復(fù)雜。
  • 路由結(jié)構(gòu)復(fù)雜,頁(yè)面嵌套非常多。

處理項(xiàng)目的時(shí)候,我選用了這樣的順序:從功能出發(fā),按照實(shí)際的業(yè)務(wù)模塊構(gòu)建頁(yè)面的路由結(jié)構(gòu),再?gòu)穆酚山Y(jié)構(gòu)出發(fā)構(gòu)建出視圖結(jié)構(gòu),再根據(jù)視圖對(duì)組件進(jìn)行分類。這樣我們能得到一個(gè)很清晰的結(jié)構(gòu):一個(gè)項(xiàng)目被分成了不同的模塊,每個(gè)模塊有相互對(duì)應(yīng)的視圖與組件。然后,我們?cè)俑鶕?jù)組件結(jié)構(gòu),按照模塊構(gòu)建 Vuex 狀態(tài)樹,封裝 Ajax API 。

總結(jié)一下:一切對(duì)項(xiàng)目的分割與歸類都應(yīng)當(dāng)建立在 Module 上,Views,Components,Store,APIs 的結(jié)構(gòu)一定要對(duì)應(yīng)。

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

一個(gè)典型的項(xiàng)目結(jié)構(gòu)是這樣的:

Vue SPA Template

有兩個(gè)一定要遵守的原則:

  • 視圖層( Views )不應(yīng)處理任何的業(yè)務(wù)邏輯,只負(fù)責(zé)組裝組件( Components )。
  • 視圖層的文件邏輯結(jié)構(gòu)應(yīng)當(dāng)與路由結(jié)構(gòu)完全相符。

關(guān)于業(yè)務(wù)邏輯

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

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

我沒有把對(duì) Response 的處理封裝到 APIs 的原因是:我需要控制整個(gè)請(qǐng)求過程,比如在加載的時(shí)候給出友好提示。同時(shí)我認(rèn)為,我們不應(yīng)該把處理 Response 的過程放到 APIs 中,因?yàn)?Response 通常與數(shù)據(jù)掛鉤,我們應(yīng)當(dāng)將數(shù)據(jù)與請(qǐng)求隔離開,APIs 應(yīng)該只專注于處理傳入的請(qǐng)求內(nèi)容和生成請(qǐng)求,而不對(duì)應(yīng)用狀態(tài)進(jìn)行操控。

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

我之所以建議除了控制視圖的狀態(tài)屬性和表單數(shù)據(jù)外,其它狀態(tài)全部托管到 Vuex 上,是因?yàn)橥ㄟ^ Ajax 拉取數(shù)據(jù)占用了整個(gè)項(xiàng)目數(shù)據(jù)來源的很大部分,同時(shí) Ajax 操作會(huì)影響到其他本地狀態(tài),比如會(huì)影響到分頁(yè)組件的分頁(yè)數(shù)量。為了方便管理和維護(hù),全部托管到 Vuex 上付出的代價(jià)我認(rèn)為是可以接受的。

關(guān)于組件復(fù)用

我們一直在重復(fù)一個(gè)概念:模塊化。模塊化就意味著復(fù)用。我們應(yīng)該如何尋找可復(fù)用的組件,又該如何復(fù)用呢?

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

還有就是,視圖層堅(jiān)決不允許復(fù)用,不要試圖通過在視圖層對(duì)資源類型進(jìn)行判斷或者其他方式控制視圖層的渲染結(jié)果。

關(guān)于項(xiàng)目管理

  • 一定要為每一次 Git Commit 留下清晰明確的日志,方便查閱和回滾。
  • 對(duì)每一個(gè)關(guān)鍵步驟都應(yīng)該生成唯一的 Git Commit,而不是一口氣提交全部的修改。
  • 對(duì)每一次發(fā)布,都應(yīng)當(dāng)添加版本標(biāo)記,方便進(jìn)行版本控制和回溯。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容