vue有着完整的組件化開發機制,但是官網只給了開發的方式,對於開發規范以及組件化開發的最佳實踐,還需要我們來摸索。本文就平時開發中的經驗來談談“把握邊界”和“狀態驅動”這兩個話題。
邊界把握
邊界把握其實很好理解。在模塊化編程中,我們通常要定義好一個模塊的功能邊界,做什么,不做什么,從外部接收什么,向外部提供什么。在vue的組件化系統之下,這些問題又更具體一些,需要我們細細把握。
划分業務邏輯
這個原則適用於任何模塊化開發,一個組件要負責哪些業務,在開始寫之初就應該非常明確,否則邊界就容易模糊了。舉個例子,頁面上有個彈出層,里面會顯示用戶名。那么在彈出層組件中,需要有username這樣一個數據嗎?
很顯然是不需要的。彈出層的任務就是:彈出、關閉、顯示內容。至於是什么內容,組件並不需要關心。所以我們頂多會定義一個通用的content字段,或者干脆用slot。
組件簡單了尚且容易把握,當業務較復雜的時候就需要好好斟酌了,這是個基本思維。
父子通信的注意點
這個話題想必大家不陌生,你甚至可以朗朗上口的背出來:父通過props傳遞數據給子,子通過emit發送消息給父。這有什么好說的呢?
props容易忽略的問題在於,當父組件傳遞一個對象給子組件時,這個傳遞就不再是“單向”的。因為子組件拿到的是一個引用,當子組件修改了該對象上的屬性值,父組件的數據也會相應變化。數據流就變成了雙向的,子組件是不應該直接修改父組件的數據的。所以我們要在props中只傳遞簡單值。對象、數組這樣的引用類型要避免傳遞。
為了保證props傳遞的數據類型,推薦在定義props的時候寫明類型和默認值:
props: { name: { type: string, default: '' } }
關於子組件emit消息,我之前也談到過一個原則,子組件需要對外通知的是“我發生了什么”,而不是“你去干什么”。這只是語義上的一個差別,往小里說只是一個命名的事。但從邏輯上來講,缺是一個邊界把握不清楚的行為。
這也是很容易想通的,如果讓子組件決定父組件的行為,那么他們在邏輯上便耦合了。舉個例子:點擊彈出層上的確定按鈕,父組件去請求商品列表。那么子組件發出的消息應該叫"confirm"或"ok",而不是叫"request-product"。
避免全局操作
我們在平時的編程中,通常會用一些BOM的方法如history,或者是使用document上的方法,這類訪問全局對象的行為,我也視之為“越界”行為。畢竟已經跨出了組件之外了。
一旦一個組件有操作全局對象的行為,那它就可以被認為有潛在威脅。所以通常應該注意以下方面:
-
用this.$el.querySelector代替document.querySelector,不要去查詢組件外的DOM
-
用到的BOM接口,統一封裝成模塊,在組件中引入使用
-
本地存儲也進行一次包裝,例如,把localStorage相關操作統一封到一個storage.js模塊中
-
子組件盡量避免監聽window的事件,可讓最外層組件監聽,然后傳遞數據
vuex的狀態管理
如果你使用了vuex,那么store中的數據管理也是需要留意的。vue完美集成了vuex這樣一個全局狀態管理工具,可以在任何組件中通過this.store訪問/提交狀態。
既然是全局狀態,我們擔心的又來了,組件內操作全局的東西,豈不是一次越界行為?而且各種commit散落在各個組件中,將來找起來豈不是很麻煩?
我的做法是這樣的,單獨定義一個模塊,姑且叫做storeMonitor吧,所有修改全局狀態的方法全部定義在這里面,組件借助這個storeMonitor去修改store中的數據,相當於是一個門面模式。這樣的好處是,組件間接地去修改全局狀態,相當於建立了一個隔離層。另一方面,所有的commit操作都集中在這個文件中,一目了然。
狀態驅動
何為狀態驅動
狀態驅動也可以說是數據驅動,只不過數據是具體存在的(比如一個js對象),“狀態”是抽象出來的一種描述。狀態驅動就是指代碼邏輯集中在數據操作, 而不是DOM操作以及樣式操作。
舉個例子,一個表單提交按鈕,不可點擊的時候要灰色背景,可點擊的時候要藍色背景。那么我們通過一個js變量disabled來控制,大致代碼如下:
<button :class="disabled ? 'bg-gray' : 'bg-blue'">提交</button>
這不就是mvvm雙向綁定的終極奧義嘛,說了半天廢話。
其實上面的代碼是有問題的。如果你隱隱覺得bg-gray、bg-blue這倆名字有點別扭,甚至那個disabled也看着不順眼,那么你有可能要理解我想說什么了。
問題在哪里呢?想想這段代碼表達了什么語義。“按鈕不可用的時候給灰色背景,可用的時候給藍色背景”,這,明明還是DOM世界的說法嘛。只是包上了雙向綁定的皮而已,根本不是狀態驅動。
而狀態驅動的精髓,是要保留業務邏輯,消滅和DOM、樣式有關的一切思維。而我們真正的業務邏輯可能是什么呢?“校驗通過的時候讓按鈕可用,不通過的時候失效”。所以,正確的代碼應該這么寫:
<button :class="validate ? 'enable' : 'disabled'">提交</button>
什么?別騙我!你只是改了命名而已。
我沒騙你,“命名即思維“,這是我一貫堅持的准則,胡亂給變量命名的人必然有一顆亂成麻團的腦袋。等你明白了舍生取義的道理,自然會回來和我一起念:「命名即思維」。
把頁面上的所有功能都完整的抽象成狀態,那就是狀態驅動了,而這狀態,不是樣式的狀態。那么,如何擁有正確的狀態驅動思維呢?答案就是:面向對象。
面向對象的思維
不看表象,看抽象。前端所要有的面向對象思維差不多就是這樣。
表象是啥呢?是輸入框,是彈出層,是列表,是表格,是花里胡哨的各種顏色。
抽象是啥呢?是用戶名,是密碼,是登陸狀態,是各種業務數據。我們把頁面的內容抽象成對象的屬性,把交互抽象成對象的方法。
還是舉個例子吧,看下面這個丑陋的原型圖:
那我們抽象出來的對象應該大致這樣:
{ businessOptions: [], currentIndex: 0, selectedList: [], select: function(index){ //選中操作 } remove: function(index){ //刪除操作 } }
我們的代碼邏輯應該是切換currentIndex,以及調用select方法來添加選項到selectedList數組。如果你想用active來表示當前激活的tab,或者是用left/right表示左邊/右邊兩欄,那就大大的犯了表象主義錯誤。
在寫小游戲的時候可能用到的面向對象思維較多,組件化開發中,也應當用這個思維去做整體設計。一個組件就是很具象的實體,所以要將之“物件化”。
css也要“狀態”
css作為樣式的描述語言,其命名方式以及組織方式有多種規則。在狀態驅動的開發思維下,我傾向讓css也具有“描述狀態”的能力。看下面的一段sass代碼:
.sidebar{ position: absolute; bottom: 0; width: 80%; &.show{ display: block; } &.hidden{ display: none; } .btn{ display: inline-block; width: 200px; height: 20px; } &.open{ left: 0; .btn{ background-image: url(left.png); } } &.close{ left: -80%; .btn{ background-image: url(right.png); } } }
光看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) => { instance = new MessageConstructor({ el: document.createElement('div') }); document.body.appendChild(instance.$el); Vue.nextTick(()=>{ instance.show = true; instance.content = config.content || ''; instance.type = config.type || 'danger'; instance.$on('close', function(){ this.show = false; document.body.removeChild(this.$el); }); instance.$on('confirm', config.onConfirm) }); } export default Message;
這樣的方式確實可以實現,但是其思想卻是和狀態驅動違背的,某個應用在某時某刻彈窗,這可以理解為這個應用的狀態,我們只需用一個變量來標記該狀態即可,犯不着手動創建節點、刪除節點這么大動干戈。事實上vue作者也推崇這樣來處理彈窗,節點始終掛載在頁面,需要彈的時候給顯示即可。
本篇結束,以上是筆者在實際開發者總結出的最佳實踐,當然這只是一個開發模式,並無對錯。大家可以參考,或引發其他思考。