前言
上一年末,公司需要開啟新的業務線,並且是需要以新的前端框架技術 Vue,去完成這項工作。我很開心能做為其中的開拓者之一,在此之前,我並沒有多少 Vue 的開發經驗,也僅僅是停留在學習階段,而且公司一直採用的前端框架還是 jQuery,所以公司也沒 Vue 框架開發經驗沉澱。外面的世界日新月異,對於新技術也是一直感興趣,所以一直希望有機會能用上新的框架去開發專案。很開心能有這個機會,雖然任重而道遠,一切從零開始摸索。
但是,何為“開拓者”!這就是!
Vue
Vue 是當今最火的前端框架之一,這就不用多說了。這裡就只留兩點建議給剛上手的童鞋:
- 怎麼去學習:
我建議可以直接去看 Vue 官方文件,官方文件寫得挺好的,而且學習曲線還是挺平滑的。這也是 Vue 的一個優點。 - 要注意什麼:
要理解其框架思想。上一代的以 jQuery 為代表的框架的思想是直接操作 DOM,更新檢視。現在主流以 Vue、React 為代表的前端框架,最大的特點就是資料驅動,運算元據,檢視更新的工作留給框架去做。兩種思想是截然不同的,如果之前是接觸 jQuery 比較多,這裡就需要注意,不要被 jQuery 的開發思維給影響到,必要時可以將之前的思維拋棄掉,以新的思維方式去接受並且開發。 這裡就不多說如何上手 Vue 了,直接去官方文件開啟 Vue 的學習旅程吧。在這主要分享一下,在這個專案過程中,所遇到的典型業務場景,解決方案,還有一些心得,希望能對你們有所幫助。
常見的業務場景
1、全域性註冊與區域性註冊的選擇
基於 Vue 的開發,接觸最多的便是元件化開發了,這也是框架解決了的前端開發痛點之一。在 Vue 中,元件註冊分為全域性註冊和區域性註冊,兩者的區別以及常見的業務場景有哪些呢:
- 全域性註冊
一個網頁,一般由基礎功能塊,再由盒子模型合理佈局之後,便組成了我們常見的網頁。這些基礎性功能塊,再由一些基礎元素組成,比如Vue.component('component-name', { // ... 選項 ... })複製程式碼
button
標籤、input
標籤等等。這些大家應該都瞭解。 在這基礎上,整個頁面基本遵守頁面設計統一的原則,基礎元素一般要設計統一,但又要在多個地方複用,所以此時就是全域性元件的發揮之地。我們將多處需要複用的元素,封裝成全域性元件,功能與設計統一維護,而需要用到的地方,直接呼叫就好了,無需二次開發。這就是全域性元件的便利之處。 - 區域性註冊
一張網頁,一般對應著一個 Vue 例項,也就是根元件,最外層的元件。在這基礎上,內部我們一般會以功能劃分為若干個模組,比如頭部,內容區等等。這些模組,都是一個個獨立的元件。這就是區域性註冊的業務場景,雖然犧牲一定複用性(但其在內部元件一樣可以多處複用),但是可以獨立維護內部功能以及獨立的業務邏輯。這樣剝離出來,程式碼以及業務邏輯都很清晰,而不是全部都冗雜在外層元件中。var ComponentA = { /* ... */ } new Vue({ el: '#app', components: { 'component-a': ComponentA } }) 複製程式碼
所以在兩者的選擇上,全域性元件更偏向於搭建基礎元件,而區域性元件偏向於業務模組,根據業務劃分不同的元件。
總結:
- 全域性元件最大的特點就是複用,要發揮這個特點才註冊成全域性,不然會造成一定的浪費。最常見的業務場景就是基礎元件,比如
button
、input
等等。市面上也有很多不錯的元件庫,比如 iView 。 - 區域性元件最大的特點就是相關業務邏輯獨立維護,降低耦合度以及提高業務的清晰度。
2、怎麼去把握業務邊界
上面提到,我們在元件化開發的時候,需要根據功能去劃分模組。模組的劃分,過細或者不夠具體,都會在開發過程中造成一定的影響。所以,如何去劃分模組,或者說如何把握好一個模組的功能邊界,明確其業務範圍,也是需要我們去注意的。通常,我們可以從以下幾個問題去思考:
- 劃分出來的模組,它需要做什麼?不需要做什麼?
- 它向外暴露什麼?同時向外接受什麼?
考慮好這幾個問題之後,模組結構基本也清晰了。
- 這個模組需要做什麼 ,不需要做什麼
首先,我們從這兩個問題入手思考,這兩個問題也是為了讓我們確定這個模組的功能邊界。比如,我們設計一個彈窗元件,我們從這兩個問題出發思考,它需要做什麼,彈出、顯示內容、關閉,沒了。顯示什麼內容呢,它不需要關心,這就是它不需要做的事情。從這兩個問題出發,去把握好模組的功能邊界,劃分出來的模組結構也會比較清晰。 - 模組向外暴露什麼,向外接受什麼
這其實是考慮如何進行資料通訊了。在 Vue 中,最常見的就是父子之間的通訊了,父元件通過 props 傳遞資料給子元件,子元件通過 emit 觸發自定義事件傳送資訊給父元件。這就是父子之間的通訊過程。
但是有一個點容易被忽略,當 props 是一個引用型別的時候,這個時候傳遞就不是單向資料流了。因為子元件拿到的是一個引用,它可以修改這個引用物件上的值,父元件的資料也會隨之相應地變化了,所以這就變成了雙向資料流。
這是不應該的,子元件不應該直接修改父元件的資料的,而且當你這麼做的時候,Vue 也會提示你不要這樣操作。所以在 props 中一般只傳遞基本型別的值,避免傳遞引用型別的值,同時最好在定義 props 的時候,寫好型別和預設值,校驗一次。
但有些場景下,傳遞引用型別會方便一些。我個人分為兩種情況: - 當這個元件是全域性複用時,比如 input 、button 等等這些全域性複用的自定義元件,就只傳遞基本型別。
- 當這個元件是某個功能模組內部使用,比如區域性註冊的元件,因為它侷限於某個模組下,並且資料量大的時候,傳遞引用型別就方便一點。但還是要記得不能直接修改引用物件的值。
若需要有對這個引用某個值進行操作,可以將這個值賦予 data 或者 computed 屬性,再去相對應的操作。(這也是為了將資料修改約束在頂層元件,這樣做的好處是資料流清晰,只有一處修改資料的地方。出問題追溯資料也比較好追。因為框架是基於資料驅動,要面對的問題都是因為資料,所以要對資料的結構、來源,去向以及修改做好約束,或者管理好資料。)
這是向外接受要思考的方向。 向外暴露呢,或者說向外提供什麼呢?
元件化開發,內部高度自治,所以一個邏輯到了某個元件,元件內部處理完之後,需要向外部告知。此時需要注意一個原則:
應該向外告知,我發生了什麼,而不能是,你去幹什麼?
可能有點拗口,但是你仔細思考一下就理解了,後者其實是決定了外部行為,這樣就存在了耦合,這樣的設計是不合理的。前者只是告知外部它自己發生了什麼,它不管外部是根據這個資訊,進行什麼響應,這樣的設計才是合理的。雖然這句話聽起來,看似很相似。
3、資料配置
前面也提到,對資料的結構、來源,去向以及修改做好約束,或者管理好資料。因為資料驅動,我們要高度關注資料,合理設計資料結構以及管理資料。資料配置是我在專案中用得最多的一種資料管理方式,其實在很多框架細節上,隨處可見。
什麼是資料配置?
舉個栗子,最常見的業務場景,後臺管理。
大致的程式碼結構如下:
<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!
知乎:陳永森