Vue 常見業務場景以及細節心得

陳永森發表於2018-05-02

前言

上一年末,公司需要開啟新的業務線,並且是需要以新的前端框架技術 Vue,去完成這項工作。我很開心能做為其中的開拓者之一,在此之前,我並沒有多少 Vue 的開發經驗,也僅僅是停留在學習階段,而且公司一直採用的前端框架還是 jQuery,所以公司也沒 Vue 框架開發經驗沉澱。外面的世界日新月異,對於新技術也是一直感興趣,所以一直希望有機會能用上新的框架去開發專案。很開心能有這個機會,雖然任重而道遠,一切從零開始摸索。  

但是,何為“開拓者”!這就是!


Vue 

Vue 是當今最火的前端框架之一,這就不用多說了。這裡就只留兩點建議給剛上手的童鞋: 

  1. 怎麼去學習: 
    我建議可以直接去看 Vue 官方文件,官方文件寫得挺好的,而且學習曲線還是挺平滑的。這也是 Vue 的一個優點。 
  2. 要注意什麼:
    要理解其框架思想。上一代的以 jQuery 為代表的框架的思想是直接操作 DOM,更新檢視。現在主流以 Vue、React 為代表的前端框架,最大的特點就是資料驅動,運算元據,檢視更新的工作留給框架去做。兩種思想是截然不同的,如果之前是接觸 jQuery 比較多,這裡就需要注意,不要被 jQuery 的開發思維給影響到,必要時可以將之前的思維拋棄掉,以新的思維方式去接受並且開發。 這裡就不多說如何上手 Vue 了,直接去官方文件開啟 Vue 的學習旅程吧。在這主要分享一下,在這個專案過程中,所遇到的典型業務場景,解決方案,還有一些心得,希望能對你們有所幫助。


常見的業務場景

1、全域性註冊與區域性註冊的選擇

基於 Vue 的開發,接觸最多的便是元件化開發了,這也是框架解決了的前端開發痛點之一。在 Vue 中,元件註冊分為全域性註冊區域性註冊,兩者的區別以及常見的業務場景有哪些呢:

  • 全域性註冊

    Vue.component('component-name', {
      // ... 選項 ...
    })複製程式碼

    一個網頁,一般由基礎功能塊,再由盒子模型合理佈局之後,便組成了我們常見的網頁。這些基礎性功能塊,再由一些基礎元素組成,比如 button 標籤、input 標籤等等。這些大家應該都瞭解。 在這基礎上,整個頁面基本遵守頁面設計統一的原則,基礎元素一般要設計統一,但又要在多個地方複用,所以此時就是全域性元件的發揮之地。我們將多處需要複用的元素,封裝成全域性元件,功能與設計統一維護,而需要用到的地方,直接呼叫就好了,無需二次開發。這就是全域性元件的便利之處。

  • 區域性註冊

    var ComponentA = { /* ... */ }
    new Vue({
      el: '#app',
      components: {
        'component-a': ComponentA
      }
    })
    複製程式碼

    一張網頁,一般對應著一個 Vue 例項,也就是根元件,最外層的元件。在這基礎上,內部我們一般會以功能劃分為若干個模組,比如頭部,內容區等等。這些模組,都是一個個獨立的元件。這就是區域性註冊的業務場景,雖然犧牲一定複用性(但其在內部元件一樣可以多處複用),但是可以獨立維護內部功能以及獨立的業務邏輯。這樣剝離出來,程式碼以及業務邏輯都很清晰,而不是全部都冗雜在外層元件中。

所以在兩者的選擇上,全域性元件更偏向於搭建基礎元件,而區域性元件偏向於業務模組,根據業務劃分不同的元件。

總結:

  • 全域性元件最大的特點就是複用,要發揮這個特點才註冊成全域性,不然會造成一定的浪費。最常見的業務場景就是基礎元件,比如 buttoninput 等等。市面上也有很多不錯的元件庫,比如 iView
  • 區域性元件最大的特點就是相關業務邏輯獨立維護,降低耦合度以及提高業務的清晰度。


2、怎麼去把握業務邊界

上面提到,我們在元件化開發的時候,需要根據功能去劃分模組。模組的劃分,過細或者不夠具體,都會在開發過程中造成一定的影響。所以,如何去劃分模組,或者說如何把握好一個模組的功能邊界,明確其業務範圍,也是需要我們去注意的。通常,我們可以從以下幾個問題去思考:  

  • 劃分出來的模組,它需要做什麼?不需要做什麼?
  • 它向外暴露什麼?同時向外接受什麼?  

考慮好這幾個問題之後,模組結構基本也清晰了。

  1. 這個模組需要做什麼 ,不需要做什麼
    首先,我們從這兩個問題入手思考,這兩個問題也是為了讓我們確定這個模組的功能邊界。比如,我們設計一個彈窗元件,我們從這兩個問題出發思考,它需要做什麼,彈出、顯示內容、關閉,沒了。顯示什麼內容呢,它不需要關心,這就是它不需要做的事情。從這兩個問題出發,去把握好模組的功能邊界,劃分出來的模組結構也會比較清晰。  
  2. 模組向外暴露什麼,向外接受什麼
    這其實是考慮如何進行資料通訊了。在 Vue 中,最常見的就是父子之間的通訊了,父元件通過 props 傳遞資料給子元件,子元件通過 emit 觸發自定義事件傳送資訊給父元件。這就是父子之間的通訊過程。 
    但是有一個點容易被忽略,當 props 是一個引用型別的時候,這個時候傳遞就不是單向資料流了。因為子元件拿到的是一個引用,它可以修改這個引用物件上的值,父元件的資料也會隨之相應地變化了,所以這就變成了雙向資料流。
    這是不應該的,子元件不應該直接修改父元件的資料的,而且當你這麼做的時候,Vue 也會提示你不要這樣操作。所以在 props 中一般只傳遞基本型別的值,避免傳遞引用型別的值,同時最好在定義 props 的時候,寫好型別和預設值,校驗一次。
    但有些場景下,傳遞引用型別會方便一些。我個人分為兩種情況:
    1. 當這個元件是全域性複用時,比如 input 、button 等等這些全域性複用的自定義元件,就只傳遞基本型別。
    2. 當這個元件是某個功能模組內部使用,比如區域性註冊的元件,因為它侷限於某個模組下,並且資料量大的時候,傳遞引用型別就方便一點。但還是要記得不能直接修改引用物件的值。
      若需要有對這個引用某個值進行操作,可以將這個值賦予 data 或者 computed 屬性,再去相對應的操作。(這也是為了將資料修改約束在頂層元件,這樣做的好處是資料流清晰,只有一處修改資料的地方。出問題追溯資料也比較好追。因為框架是基於資料驅動,要面對的問題都是因為資料,所以要對資料的結構、來源,去向以及修改做好約束,或者管理好資料。) 

這是向外接受要思考的方向。 向外暴露呢,或者說向外提供什麼呢?

元件化開發,內部高度自治,所以一個邏輯到了某個元件,元件內部處理完之後,需要向外部告知。此時需要注意一個原則:

應該向外告知,我發生了什麼,而不能是,你去幹什麼? 

可能有點拗口,但是你仔細思考一下就理解了,後者其實是決定了外部行為,這樣就存在了耦合,這樣的設計是不合理的。前者只是告知外部它自己發生了什麼,它不管外部是根據這個資訊,進行什麼響應,這樣的設計才是合理的。雖然這句話聽起來,看似很相似。


3、資料配置

前面也提到,對資料的結構、來源,去向以及修改做好約束,或者管理好資料。因為資料驅動,我們要高度關注資料,合理設計資料結構以及管理資料。資料配置是我在專案中用得最多的一種資料管理方式,其實在很多框架細節上,隨處可見。

什麼是資料配置

舉個栗子,最常見的業務場景,後臺管理。

Vue 常見業務場景以及細節心得

大致的程式碼結構如下:

<div id="manage" class="manage">
    <div class="header"></div>
    <div class="main">
        <div class="sidebar">
	    <ul class="menu" v-for="menu in menuDef" :key="menu.type">
	        <div class="menu-title">{{ menu.name }}</div>
		<li class="sub-menu" v-for="submenu in menu.submenu" :key="submenu.type">{{ submenu.name }}</li>
	    </ul>
	</div>
	<div class="content">內容區</div>
    </div>
</div>

<script>
    // 二級選單
    var subServer = [
        { type: 'manage', name: '管理服務', extClass: 'server-manage' },
        { type: 'list', name: '服務列表', extClass: 'server-list' },
    ];
    // 二級選單
    var subNews = [
        { type: 'internation', name: '國際新聞', extClass: 'i-news' },
        { type: 'home', name: '國內新聞', extClass: 'home-news' },
    ];
    // 一級選單
    var menu = [
        { type: 'server', name: '服務', extClass: 'server', submenu: subServer },
        { type: 'news', name: '新聞', extClass: 'news', submenu: subNews }
    ];
    // 根例項
    var manage = new Vue({
        el: '#manage',
        data: {
            menuDef: menu
        }
    });
</script>
複製程式碼

這是一個簡單的後臺管理頁面,左側皮膚有一系列的選單項,右側內容區基於左側選單切換而變化。左側選單結構一致,只不過是資料不同而已。在這個例子也可以看出:

  • 頁面結構與資料的關係。頁面結構只是資料的外層表現而已,並且是基於資料結構去變化的
  • 所以再抽象一點,將資料從頁面結構抽象出來,便就是這個頁面所需要的資料結構了。
  • 如果我們將剝離出來的資料結構,單獨維護,這便是我所說的資料配置了。

資料配置有什麼好處呢?

  • 只關注資料結構,不用關注頁面結構,即使增刪選單項、或者修改結構層級,我也只需在資料這一層,進行調整就好了,不需要再進入邏輯層或者頁面層進行變動。 
  • 這種方式對於後期維護相當友好。不管是擴充套件還是維護,都是相當的便利的。記得在我這個專案開發過程中,後臺管理頁面有過一定的變動,在 PM 跟我講完需要變動的地方的時候,我也基本都改好了...因為我只需更換資料層就好了,就比方我修改一個配置文件而已,只要文件標註寫清楚了,誰來改都是一樣的便利。 

適合的場景:

  • 相似的結構,只是資料有所不同。 
  • 這適合的場景也是比較廣泛的,基本也可以搭配元件複用去實現的。  

上面說到,右側內容區是基於左側選單切換而變化的。這個實現是基於路由實現的,Vue 也提供了 vue-router 官方路由庫,具體的文件也是可以直接去看官方文件。在這裡想是,對於路由的管理,同樣也可以運用資料配置的方式,統一在一處管理路由配置項,還可以與其他資料結構巢狀使用。比如接上上面的例子:

// 路由配置
// compons.serviceManage、compons.serviceList 就是內容區的內容元件
var serviceRoutes = {
    serviceManage: { path: '/serviceManage', name: 'serviceManage', component: compons.serviceManage },
    serviceList: { path: '/serviceList', name: 'serviceList', component: compons.serviceList}
};
// ...

// 選單項配置
// 二級選單
var subServer = [
    { type: 'manage', name: '管理服務', extClass: 'server-manage', router: serviceRoutes.serviceManage },
    { type: 'list', name: '服務列表', extClass: 'server-list', router: serviceRoutes.serviceList },
];
// ....

// 如何使用
// 上面配置的路由配置物件全部放進來,下面會迴圈建立路由物件。
var routerList = [serviceRoutes, articleRoutes, ...];
// 路由配置,迴圈路由配置
var router = [];
routerList.forEach(router => {
   router.push(router); 
});

// 根例項
var manage = new Vue({
    router,				// 將配置好的路由物件傳進來
    el: '#manage',
    data: {
        menuDef: menu
    }
});
複製程式碼

後期在維護這一塊的時候,只需要對資料結構的配置以及路由的配置維護就好了。


4、如何優雅地搭配 jQuery 使用

在 Vue 的開發過程中,大多時候並不需要你去親自去操作 DOM,你只要關注資料層,通過資料驅動,由框架去完成檢視的更新。

但在某些特殊場景下,你切確需要去進行一些底層的 DOM 操作,比如 hover 某個模組出現 tip,或者 hover 某個結構出現工具欄。這種業務場景,你可能想到的是,在模組內部處理 mouseenter 和 mouseleave 事件的時候,進行這塊邏輯的處理。這是比較典型的 JQuery 思維去處理這個問題,這樣處理方案有很多缺點:

  • 無法複用,你所做的處理,只是限定在某個模組下的,別的地方無法直接複用。 
  • 耦合高,因為這個業務本身就可以剝離出去的,成為一個單獨的元件維護了。如果還夾雜在其他模組內部,會造成模組內部邏輯以及結構不清晰。 
  • 後期維護難。

如果是用 Vue 的方式去實現呢?

Vue 提供了一個自定義指令 (directive) 的介面給我們,這種方式就很好地解決了這個業務場景下的問題。在 Vue 裡面,它不建議你直接在例項內部中進行一些 DOM 操作,如果你需要進行一些底層 DOM 操作,你可以將它們抽離到自定義指令中來。而在需要用到的時候,我直接使用就好了,完美解決上面的處理方案存在的問題。

// jQuery 的方式,錯誤的方式
<div id='app' @mouseenter='handleMouseEnter' @mouseleave='handleMouseLeave'></div>
<div id='tip'></div>
var app = new Vue({
    el: '#app',
    data: {},
    methods: {
        handleMouseEnter: function () {
            document.querySelector('#tip').style.display = 'block';
        },
        handleMouseLeave: function () {
            document.querySelector('#tip').style.display = 'none';
        }
    }
});

// Vue 的方式,正確的選擇
<div id='app' v-tip='{ text: tipText }'></div>

Vue.directive('tip', {
    inserted: function(el, binding) {
	var tipDom = document.querySelector('#tip');
	if(tipDom) {
            // 如果已經存在,不需要建立 DOM
            // do something
	} else {
	    // 如果不存在,建立 DOM
            tipDom = document.createElement('div');
            tipDom.setAttribute('id', 'tip');
            document.body.appendChild(tip);
            // do something
	}
	// 為繫結該指令的元素註冊事件
	el.addEventListener('mouseenter', function() {
            // do something
	});
	el.addEventListener('mouseleave', function() {
            // do something
	});
    }
});

var app = new Vue({
    el: '#app',
    data: {
        tipText: 'Hello, world!'
    },
});
複製程式碼

這便是這種業務場景下,Vue 的優雅實現。很好地將業務邏輯剝離出來成自定義指令,一處實現,多處複用。程式碼邏輯清晰,耦合度低,後期好維護等等優點鋪面而來。簡直如浴春風啊~

具體相關的自定義指令的知識點以及運用,大家可以去檢視自定義指令的官方文件。


結尾

在開發過程中,還有很多細節一點的場景,比如如何結合 Vue 優雅地實現動畫,父子通訊,兄弟通訊需要注意的細節等等。這些細節就相對瑣碎了一點,一一講起來篇幅也會太長了。後期有讀者遇到相關問題,也可以再另起一篇幅,再給大家嘮嗑嘮嗑~

這篇就到這了。

Hello,world!


知乎:陳永森



相關文章