全是乾貨 ! 封裝Vue組件的一些技巧 你知道幾個? ( 文末有彩蛋!)


全是乾貨 ! 封裝Vue組件的一些技巧 你知道幾個? ( 文末有彩蛋!)



前言

寫Vue有很長一段時間了,除了常規的業務開發之外,也應該思考和反思一下封裝組件的正確方式。以彈窗組件為例,一種實現是在需要模板中引入需要彈窗展示的組件,然後通過一個flag變量來控制彈窗的組件,在業務代碼裡面會充斥著冗餘的彈窗組件邏輯,十分不優雅。

本文整理了開發Vue組件的一些技巧,包含大量代碼示例。

開發環境

vue-cli3提供了非常方便的功能,可以快速編寫一些測試demo,是開發組件必備的環境。下面是安裝使用步驟

// 全局安裝vue-cli3
npm install -g @vue/cli
vue -V // 查看版本是否為3.x
// 安裝擴展,此後可以快速啟動單個vue文件
npm install -g @vue/cli-service-global
// 快速啟動demo文件
vue serve demo.vue

如果需要scss,則還需要在目錄下安裝sass-loader等。

下面是使用vue-cli3可能會遇見的幾個問題。

自定義入口文件

如果需要(比如需要開發移動端的組件),可以在使用vue serve時自定義html入口文件,在根目錄下編寫index.html,並確保頁面包含#app的dom即可。

引入公共混合文件

通過style-resources-loader在每個文件引入公共樣式混合等,參考自動化導入

需要訪問Vue全局對象

在某些時候需要放問全局Vue對象,如開發全局指令、插件時

import Vue from "vue"
import somePlugin from "../src/somePlugin"
Vue.use(somePlugin)

上面這種寫法並不會生效,這是因為vue serve xxx.vue僅僅只能作為快速原型開發的方案,使用的Vue與 import引入的Vue不是同一個對象。一種解決辦法是手動指定vue serve的入口文件

// index.js
import Vue from "../node_modules/vue/dist/vue.min"
import placeholder from "../src/placeholder/placeholder"
Vue.use(placeholder)
new Vue({
el: "#app",
template: ``,
created(){},
})


Vue的組件系統

Vue組件的API主要包含三部分:prop、event、slot

  • props 表示組件接收的參數,最好用對象的寫法,這樣可以針對每個屬性設置類型、默認值或自定義校驗屬性的值,此外還可以通過type、validator等方式對輸入進行驗證
  • slot可以給組件動態插入一些內容或組件,是實現高階組件的重要途徑;當需要多個插槽時,可以使用具名slot
  • event是子組件向父組件傳遞消息的重要途徑

單向數據流

參考:單向數據流-官方文檔。

父級 prop 的更新會向下流動到子組件中,但是反過來則不行

單向數據流是Vue組件一個非常明顯的特徵,不應該在子組件中直接修改props的值

  • 如果傳遞的prop僅僅用作展示,不涉及修改,則在模板中直接使用即可
  • 如果需要對prop的值進行轉化然後展示,則應該使用computed計算屬性
  • 如果prop的值用作初始化,應該定義一個子組件的data屬性並將prop作為其初始值

從源碼/src/core/vdom/create-component

.js和/src/core/vdom/helpers/extract-props.js裡可以看見,在處理props的取值時,首先從

function extractPropsFromVNodeData(){
const res = {}
const { attrs, props } = data
// 執行淺拷貝
checkProp(res, props, key, altKey, true) || checkProp(res, attrs, key, altKey, false)
return res
}

在子組件修改props,卻不會修改父組件,這是因為extractPropsFromVNodeData中是通過淺複製將attrs傳遞給props的。

淺複製意味著在子組件中對對象和數組的props進行修改還是會影響父組件,這就違背了單向數據流的設計。因此需要避免這種情況出現。

組件之間的通信

這裡可以參考:vue組件通信全揭秘,寫的比較全面

  • 父子組件的關係可以總結為 prop 向下傳遞,事件event向上傳遞
  • 祖先組件和後代組件(跨多代)的數據傳遞,可以使用provide和reject來實現

此外,如果需要跨組件或者兄弟組件之間的通信,可以通過eventBus或者vuex等方式來實現。

“繞開”單向數據流

考慮下面場景:父組件將數據通過prop形式傳遞給子組件,子組件進行相關操作並修改數據,需要修改父組件的prop值(一個典型的例子是:購物車的商品數量counter組件)。

根據組件單向數據流和和事件通信機制,需要由子組件通過事件通知父組件,並在父組件中修改原始的prop數據,完成狀態的更新。在子組件中修改父組件的數據的場景在業務中也是比較常見的,那麼有什麼辦法可以“繞開”單向數據流的限制呢?

狀態提升

可以參考React的狀態提升,直接通過props將父元素的數據處理邏輯傳入子組件,子組件只做數據展示和事件掛載即可

<template>

-

{{value}}

+


/<template>

然後在調用時傳入事件處理函數

<<template>

<counter>

/<template>

很明顯,由於在每個父組件中都需要實現on-minus和on-plus,因此狀態提升並沒有從根本上解決問題。

v-model語法糖

Vue內置了v-model指令,v-model 是一個語法糖,可以拆解為 props: value 和 events: input。就是說組件只要提供一個名為 value 的 prop,以及名為 input 的自定義事件,滿足這兩個條件,使用者就能在自定義組件上使用 v-model

<template>

<button>-1/<button>
{{currentVal}}
<button>+1/<button>

/<template>

然後調用的時候只需要傳入v-model指令即可

<counter>

使用v-model,可以很方便地在子組件中同步父組件的數據。在2.2之後的版本中,可以定製v-model指令的prop和event名稱,參考model配置項

export default {
model: {
prop: 'value',
event: 'input'
},
// ...
}


獲得組件實例的引用

在開發組件中,獲取組件實例是一個非常有用的方法。組件可以通過$refs、$parents、$childrend等方式獲得vm實例引用

  • $refs在組件(或者dom上)增加ref屬性即可
  • $parents獲取子組件掛載的父組件節點
  • $childrend,獲取組件的所有子節點

這些接口返回的都是vnode,可以通過vnode.componentInstance獲得對應的組件實例,然後直接調用組件的方法或訪問數據。雖然這種方式多多少少有些違背組件的設計理念,增加了組件之間的耦合成本,但代碼實現會更加簡潔。

表單驗證組件

通常情況下,表單驗證是表單提交前一個十分常見的應用場景。那麼,如何把表單驗證的功能封裝在組件內部呢?

下面是一個表單組件的示例,展示了通過獲得組件的引用來實現表單驗證功能。

首先定義組件的使用方式,

  • xm-form接收model和rule兩個prop
  • model表示表單綁定的數據對象,最後表單提交的就是這個對象
  • rule表示驗證規則策略,表單驗證可以使用async-validator插件
  • xm-form-item接收的prop屬性,對應form組件的model和rule的某個key值,根據該key從model上取表單數據,從rule上取驗證規則

下面是使用示例代碼

<template>

<xm-form>
<xm-form-item>

/<xm-form-item>
<xm-form-item>

/<xm-form-item>
<xm-form-item>
<button>提交/<button>
/<xm-form-item>
/<xm-form>

/<template>

接下來讓我們實現form-item組件,其主要作用是放置表單元素,及展示錯誤信息

<template>
<label>
{{label}}


<slot>

{{errorMsg}}

/<label>
/<template>

然後讓我們來實現form組件

  • 通過calcFormItems獲取每個xm-form-item的引用,保存在formItems中
  • 暴露validate接口,內部調用AsyncValidator,並根據結果遍歷formItems中每個表單元素的prop屬性,處理對應的error信息
<template>

<slot>

/<template>

這樣我們就完成了一個通用的表單驗證組件。從這個例子中可以看出獲取組件引用,在組件開發中是一個非常有用的方法。

封裝API組件

一些組件如提示框、彈出框等,更適合單獨的API調用方式,如

import MessageBox from '@/components/MessageBox.vue'
MessageBox.toast('hello)

如何實現制這種不需要手動嵌入模板裡面的組件呢?原來,除了在通過在模板中嵌入組件到children掛載組件,Vue還為組件提供了手動掛載的方法$mount

let component = new MessageBox().$mount()
document.getElementById('app').appendChild(component.$el)

通過這種方式,我們就是可以封裝API形式調用組件,下面是一個alert消息提示的接口封裝

消息彈窗組件

一個消息組件就是在頁面指定繪製展示提示消息的組件,下面是簡單實現

<template>


{{ item.content }}



/<template>

下面來實現消息組件掛載到頁面的邏輯,並對外暴露展示消息的接口

// alert.js
import Vue from 'vue';
// 具體的組件
import Alert from './alert.vue';
Alert.newInstance = properties => {
const props = properties || {};
// 實例化一個組件,然後掛載到body上
const Instance = new Vue({
data: props,
render (h) {
return h(Alert, {
props: props
});
}
});
const component = Instance.$mount();
document.body.appendChild(component.$el);
// 通過閉包維護alert組件的引用
const alert = Instance.$children[0];
return {
// Alert組件對外暴露的兩個方法
add (noticeProps) {
alert.add(noticeProps);
},
remove (name) {
alert.remove(name);
}
}
};
// 提示單例
let messageInstance;
function getMessageInstance () {
messageInstance = messageInstance || Alert.newInstance();
return messageInstance;
}
function notice({ duration = 1.5, content = '' }) {
// 等待接口調用的時候再實例化組件,避免進入頁面就直接掛載到body上

let instance = getMessageInstance();
instance.add({
content: content,
duration: duration
});
}
// 對外暴露的方法
export default {
info (options) {
return notice(options);
}
}

然後就可以使用API的方式來調用彈窗組件了

import alert from './alert.js'
// 直接使用
alert.info({content: '消息提示', duration: 2})
// 或者掛載到Vue原型上
Vue.prototype.$Alert = alert
// 然後在組件中使用
this.$Alert.info({content: '消息提示', duration: 2})


高階組件

高階組件可以看做是函數式編程中的組合。可以把高階組件看做是一個函數,他接收一個組件作為參數,並返回一個功能增強的組件。

高階組件是一個接替Mixin實現抽象組件公共功能的方法,不會因為組件的使用而汙染DOM(添加並不想要的div標籤等)、可以包裹任意的單一子元素等等

在React中高階組件是比較常用的組件封裝形式,在Vue中如何實現高階組件呢?

在組件的render函數中,只需要返回一個vNode數據類型即可,如果在render函數中提前做一些處理,並返回this.$slots.default[0]對應的vnode,就可以實現高階組件。

內置的keep-alive

Vue內置了一個高階組件keep-alive,查看源碼可以發現其實現原理,就是通過維護一個cache,並在render函數中根據key返回緩存的vnode,來實現組件的持久化。

throttle

節流是web開發中處理事件比較常見的需求。常見的場景有及時搜索框避免頻繁觸發搜索接口、表單按鈕防止在短暫時間誤重複提交等

首先來看看Throttle組件的使用方式,接收兩個props

  • time表示節流的時間間隔
  • events表示需要處理的事件名,多個事件用逗號分隔

在下面的例子中,通過Throttle組件來控制其內部button的點擊事件,此時連續點擊多次,觸發clickBtn的次數要比點擊的次數小(節流函數通過一個定時器進行處理)。

 <template>

<throttle>
<button>click {{count}}/<button>
/<throttle>

/<template>

下面是具體實現,實現高階組件的主要功能是在render函數中對當前插槽中的vnode進行處理

const throttle = function (fn, wait = 50, ctx) {
let timer
let lastCall = 0
return function (...params) {
const now = new Date().getTime()
if (now - lastCall < wait) return
lastCall = now
fn.apply(ctx, params)
}
}
export default {
name: 'throttle',
abstract: true,
props: {
time: Number,
events: String,
},
created() {
this.eventKeys = this.events.split(',')
this.originMap = {}
this.throttledMap = {}
},
// render函數直接返回slot的vnode,避免外層添加包裹元素
render(h) {
const vnode = this.$slots.default[0]
this.eventKeys.forEach((key) => {
const target = vnode.data.on[key]
if (target === this.originMap[key] && this.throttledMap[key]) {
vnode.data.on[key] = this.throttledMap[key]
} else if (target) {
// 將原本的事件處理函數替換成throttle節流後的處理函數

this.originMap[key] = target
this.throttledMap[key] = throttle(target, this.time, vnode)
vnode.data.on[key] = this.throttledMap[key]
}
})
return vnode
},
}

我們還可以進一步封裝,通過debounce函數來實現Debounce組件,可見高階組件的作用,就是為了增強某個組件而存在的。關於高階組件的其他應用,可以參考HOC(高階組件)在vue中的應用。

小結

本文整理了幾種實現Vue組件的技巧

  • 以counter計數器組件為例,展示了通過v-model語法糖同步父子組件的方式
  • 以表單驗證組件為例,展示了通過獲取子組件的實例來封裝組件的方法
  • 以全局彈窗組件為例,展示了手動mount掛載組件封裝API組件的方式
  • 以throttle節流組件為例,展示了在vue中一種實現高階組件的方式

在瞭解Vue的API之後,理解上面的概念都比較輕鬆,封裝組件,除了對於API的熟練度之外,更多地是考察JavaScript基礎。Vue入門十分輕鬆,但是要寫好優雅的Vue代碼,也是一份不小的學問。

最後 小編親自整理2019年4月最新Web前端angualr,html5,Javascript,nodejs,react,vue以及web前端交互的電子書和視頻合集 關注私信666 即可獲取! 僅限前50位!


分享到:


相關文章: