聊聊vue元件開發的「邊界把握」和「狀態驅動」

2022-01-16 10:46:53 字數 3801 閱讀 4471

vue有著完整的元件化開發機制,但是官網只給了開發的方式,對於開發規範以及元件化開發的最佳實踐,還需要我們來摸索。本文就平時開發中的經驗來談談「把握邊界」和「狀態驅動」這兩個話題。

邊界把握其實很好理解。在模組化程式設計中,我們通常要定義好乙個模組的功能邊界,做什麼,不做什麼,從外部接收什麼,向外部提供什麼。在vue的元件化系統之下,這些問題又更具體一些,需要我們細細把握。

這個原則適用於任何模組化開發,乙個元件要負責哪些業務,在開始寫之初就應該非常明確,否則邊界就容易模糊了。舉個例子,頁面上有個彈出層,裡面會顯示使用者名稱。那麼在彈出層元件中,需要有username這樣乙個資料嗎?

很顯然是不需要的。彈出層的任務就是:彈出、關閉、顯示內容。至於是什麼內容,元件並不需要關心。所以我們頂多會定義乙個通用的content欄位,或者乾脆用slot。

元件簡單了尚且容易把握,當業務較複雜的時候就需要好好斟酌了,這是個基本思維。

props容易忽略的問題在於,當父元件傳遞乙個物件給子元件時,這個傳遞就不再是「單向」的。因為子元件拿到的是乙個引用,當子元件修改了該物件上的屬性值,父元件的資料也會相應變化。資料流就變成了雙向的,子元件是不應該直接修改父元件的資料的。所以我們要在props中只傳遞簡單值。物件、陣列這樣的引用型別要避免傳遞。

props: 

}

關於子元件emit訊息,我之前也談到過乙個原則,子元件需要對外通知的是「我發生了什麼」,而不是「你去幹什麼」。這只是語義上的乙個差別,往小裡說只是乙個命名的事。但從邏輯上來講,缺是乙個邊界把握不清楚的行為。

這也是很容易想通的,如果讓子元件決定父元件的行為,那麼他們在邏輯上便耦合了。舉個例子:點選彈出層上的確定按鈕,父元件去請求商品列表。那麼子元件發出的訊息應該叫"confirm"或"ok",而不是叫"request-product"。

我們在平時的程式設計中,通常會用一些bom的方法如history,或者是使用document上的方法,這類訪問全域性物件的行為,我也視之為「越界」行為。畢竟已經跨出了元件之外了。

一旦乙個元件有操作全域性物件的行為,那它就可以被認為有潛在威脅。所以通常應該注意以下方面:

用this.$el.queryselector代替document.queryselector,不要去查詢元件外的dom

用到的bom介面,統一封裝成模組,在元件中引入使用

本地儲存也進行一次包裝,例如,把localstorage相關操作統一封到乙個storage.js模組中

子元件盡量避免監聽window的事件,可讓最外層元件監聽,然後傳遞資料

如果你使用了vuex,那麼store中的資料管理也是需要留意的。vue完美整合了vuex這樣乙個全域性狀態管理工具,可以在任何元件中通過this.store訪問/提交狀態。

既然是全域性狀態,我們擔心的又來了,元件內操作全域性的東西,豈不是一次越界行為?而且各種commit散落在各個元件中,將來找起來豈不是很麻煩?

我的做法是這樣的,單獨定義乙個模組,姑且叫做storemonitor吧,所有修改全域性狀態的方法全部定義在這裡面,元件借助這個storemonitor去修改store中的資料,相當於是乙個門面模式。這樣的好處是,元件間接地去修改全域性狀態,相當於建立了乙個隔離層。另一方面,所有的commit操作都集中在這個檔案中,一目了然。

狀態驅動也可以說是資料驅動,只不過資料是具體存在的(比如乙個js物件),「狀態」是抽象出來的一種描述。狀態驅動就是指**邏輯集中在資料操作, 而不是dom操作以及樣式操作。

舉個例子,乙個表單提交按鈕,不可點選的時候要灰色背景,可點選的時候要藍色背景。那麼我們通過乙個js變數disabled來控制,大致**如下:

提交
這不就是mvvm雙向繫結的終極奧義嘛,說了半天廢話。

其實上面的**是有問題的。如果你隱隱覺得bg-gray、bg-blue這倆名字有點彆扭,甚至那個disabled也看著不順眼,那麼你有可能要理解我想說什麼了。

問題在**呢?想想這段**表達了什麼語義。「按鈕不可用的時候給灰色背景,可用的時候給藍色背景」,這,明明還是dom世界的說法嘛。只是包上了雙向繫結的皮而已,根本不是狀態驅動。

而狀態驅動的精髓,是要保留業務邏輯,消滅和dom、樣式有關的一切思維。而我們真正的業務邏輯可能是什麼呢?「校驗通過的時候讓按鈕可用,不通過的時候失效」。所以,正確的**應該這麼寫:

提交
什麼?別騙我!你只是改了命名而已。

我沒騙你,「命名即思維「,這是我一貫堅持的準則,胡亂給變數命名的人必然有一顆亂成麻團的腦袋。等你明白了捨生取義的道理,自然會回來和我一起念:「命名即思維」。

把頁面上的所有功能都完整的抽象成狀態,那就是狀態驅動了,而這狀態,不是樣式的狀態。那麼,如何擁有正確的狀態驅動思維呢?答案就是:物件導向。

不看表象,看抽象。前端所要有的物件導向思維差不多就是這樣。

表象是啥呢?是輸入框,是彈出層,是列表,是**,是花裡胡哨的各種顏色。

抽象是啥呢?是使用者名稱,是密碼,是登陸狀態,是各種業務資料。我們把頁面的內容抽象成物件的屬性,把互動抽象成物件的方法。

還是舉個例子吧,看下面這個醜陋的原型圖:

那我們抽象出來的物件應該大致這樣:

remove: function(index)

}

我們的**邏輯應該是切換currentindex,以及呼叫select方法來新增選項到selectedlist陣列。如果你想用active來表示當前啟用的tab,或者是用left/right表示左邊/右邊兩欄,那就大大的犯了表象主義錯誤。

在寫小遊戲的時候可能用到的物件導向思維較多,元件化開發中,也應當用這個思維去做整體設計。乙個元件就是很具象的實體,所以要將之「物件化」。

css作為樣式的描述語言,其命名方式以及組織方式有多種規則。在狀態驅動的開發思維下,我傾向讓css也具有「描述狀態」的能力。看下面的一段sass**:

.sidebar

&.hidden

.btn

&.open

}&.close}}

光看css,不看js**的情況下,我們已經可以得知介面的展示邏輯了:有乙個名為sidebar的側邊欄,它有四種狀態,分別是:show、hidden、open、close。sidebar下有乙個按鈕btn,它在sidebar開啟的時候是向左的背景圖,在sidebar關閉的時候是向右的背景圖。

這樣一套結構清晰,語義明確的css規則,能夠幫助我們很快理清頁面邏輯,別人在看你的**的時候一目了然。上面只是乙個簡單的例子,實踐的時候會有複雜的場景,可根據具體功能劃分出各自的作用域(巢狀語法),稍微需要花時間去設計,換來的是清晰的**。

用mvvm框架去寫彈框元件的時候,往往會有這樣乙個困惑:在jquery時代,我們通過$.msg('內容')這樣的方式呼叫彈框,此時在頁面上動態建立乙個節點,關閉彈框的時候再把節點移除。習慣於此,我們很希望能用同樣的方式來處理彈框。

當然這在vue中也是可以做到的,方式就是動態建立標籤,並且動態new乙個元件例項去渲染它,在監聽到close訊息時,把這個節點手動刪掉。大體**如下:

const messageconstructor = vue.extend(alert);

const message = (config) => );

vue.nexttick(()=>);

instance.$on('confirm', config.onconfirm)

});}export default message;

這樣的方式確實可以實現,但是其思想卻是和狀態驅動違背的,某個應用在某時某刻彈窗,這可以理解為這個應用的狀態,我們只需用乙個變數來標記該狀態即可,犯不著手動建立節點、刪除節點這麼大動干戈。事實上vue作者也推崇這樣來處理彈窗,節點始終掛載在頁面,需要彈的時候給顯示即可。

Vue全域性元件的開發

把一些使用頻率較高的元件封裝為全域性元件,防止頻繁引入。例如 element中的各個元件 實現方法,通過借助webpack的require.context 方法來封裝自己常用的全域性元件,開發步驟如下 第一步,封裝importall.js importall.js檔案 import vue from...

vue開發 vue全域性元件的方法

在vue專案中,可以自定義元件像vue resource一樣使用vue.use 方法來使用,具體實現方法 1 首先建乙個自定義元件的資料夾,比如叫loading,裡面有乙個index.js,還有乙個自定義元件loading.vue,在這個loading.vue裡面就是這個元件的具體的內容,比如 lo...

vue的元件化開發(元件通訊,插槽,遞迴元件)

1.vue元件的通訊 包括子通父,父通子,兄弟通訊,祖代與後代通訊,vuex全域性狀態管理。1.1 父向子通訊 props屬性和refs屬性。1.1.1 props屬性 在子元件中 props age1 number,string 存在多種型別。在父元件中 1.1.2 refs屬性 通過this.r...