本文同步在我的博客shymean.com上,歡迎關注javascript
寫Vue有很長一段時間了,除了常規的業務開發以外,也應該思考和反思一下封裝組件的正確方式。以彈窗組件爲例,一種實現是在須要模板中引入須要彈窗展現的組件,而後經過一個flag變量來控制彈窗的組件,在業務代碼裏面會充斥着冗餘的彈窗組件邏輯,十分不優雅。css
本文整理了開發Vue組件的一些技巧,包含大量代碼示例。html
vue-cli3提供了很是方便的功能,能夠快速編寫一些測試demo,是開發組件必備的環境。下面是安裝使用步驟vue
// 全局安裝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等。java
下面是使用vue-cli3可能會碰見的幾個問題,更多使用教程能夠參考:一份超級詳細的Vue-cli3.0使用教程[趕忙來試試!]node
自定義入口文件git
若是須要(好比須要開發移動端的組件),能夠在使用vue serve
時自定義html入口文件,在根目錄下編寫index.html
,並確保頁面包含#app
的dom便可。github
引入公共混合文件web
經過style-resources-loader
在每一個文件引入公共樣式混合等,參考自動化導入vuex
須要訪問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組件的API主要包含三部分:prop、event、slot
參考:單向數據流-官方文檔。
父級 prop 的更新會向下流動到子組件中,可是反過來則不行
單向數據流是Vue組件一個很是明顯的特徵,不該該在子組件中直接修改props的值
從源碼/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組件通訊全揭祕,寫的比較全面
此外,若是須要跨組件或者兄弟組件之間的通訊,能夠經過eventBus或者vuex等方式來實現。
考慮下面場景:父組件將數據經過prop形式傳遞給子組件,子組件進行相關操做並修改數據,須要修改父組件的prop值(一個典型的例子是:購物車的商品數量counter組件)。
根據組件單向數據流和和事件通訊機制,須要由子組件經過事件通知父組件,並在父組件中修改原始的prop數據,完成狀態的更新。在子組件中修改父組件的數據的場景在業務中也是比較常見的,那麼有什麼辦法能夠「繞開」單向數據流的限制呢?
能夠參考React的狀態提高,直接經過props將父元素的數據處理邏輯傳入子組件,子組件只作數據展現和事件掛載便可
<template>
<div class="counter">
<div class="counter_btn" @click="onMinus">-</div>
<div class="counter_val">{{value}}</div>
<div class="counter_btn" @click="onPlus">+</div>
</div>
</template>
<script> export default { props: { value: { type: Number, default: 0 }, onMinus: Function, onPlus: Function }, }; </script>
複製代碼
而後在調用時傳入事件處理函數
<template>
<div>
<counter :value="counter2Val" :on-minus="minusVal" :on-plus="plusVal"></counter>
</div>
</template>
<script> export default { data() { return { counter2Val: 0, } }, methods: { minusVal(){ this.counter2Val-- }, plusVal(){ this.counter2Val++ } } } </script>
複製代碼
很明顯,因爲在每一個父組件中都須要實現on-minus
和on-plus
,所以狀態提高並無從根本上解決問題。
Vue內置了v-model
指令,v-model 是一個語法糖,能夠拆解爲 props: value 和 events: input。就是說組件只要提供一個名爲 value 的 prop,以及名爲 input 的自定義事件,知足這兩個條件,使用者就能在自定義組件上使用 v-model
<template>
<div>
<button @click="changeValue(-1)">-1</button>
<span>{{currentVal}}</span>
<button @click="changeValue(1)">+1</button>
</div>
</template>
<script> export default { props: { value: { type: Number // 定義value屬性 } }, data() { return { currentVal: this.value }; }, methods: { changeVal(val) { this.currentVal += parseInt(val); this.$emit("input", this.currentVal); // 定義input事件 } } }; </script>
複製代碼
而後調用的時候只須要傳入v-model指令便可
<counter v-model="counerVal"/>
複製代碼
使用v-model,能夠很方便地在子組件中同步父組件的數據。在2.2以後的版本中,能夠定製v-model
指令的prop和event名稱,參考model配置項
export default {
model: {
prop: 'value',
event: 'input'
},
// ...
}
複製代碼
在開發組件中,獲取組件實例是一個很是有用的方法。組件能夠經過$refs
、$parents
、$children
等方式得到vm實例引用
$refs
在組件(或者dom上)增長ref屬性便可
$parents
獲取子組件掛載的父組件節點
$children
,獲取組件的全部子節點
這些接口返回的都是vnode,能夠經過vnode.componentInstance
得到對應的組件實例,而後直接調用組件的方法或訪問數據。雖然這種方式多多少少有些違背組件的設計理念,增長了組件之間的耦合成本,但代碼實現會更加簡潔。
一般狀況下,表單驗證是表單提交前一個十分常見的應用場景。那麼,如何把表單驗證的功能封裝在組件內部呢?
下面是一個表單組件的示例,展現了經過得到組件的引用來實現表單驗證功能。
首先定義組件的使用方式,
xm-form
接收model
和rule
兩個prop
model
表示表單綁定的數據對象,最後表單提交的就是這個對象rule
表示驗證規則策略,表單驗證可使用async-validator插件xm-form-item
接收的prop
屬性,對應form組件的model和rule的某個key值,根據該key從model上取表單數據,從rule上取驗證規則下面是使用示例代碼
<template>
<div class="page">
<xm-form :model="form" :rule="rule" ref="baseForm">
<xm-form-item label="姓名" prop="name">
<input v-model="form.name"/>
</xm-form-item>
<xm-form-item label="郵箱" prop="email">
<input v-model="form.email"/>
</xm-form-item>
<xm-form-item>
<button @click="submit">提交</button>
</xm-form-item>
</xm-form>
</div>
</template>
<script> import xmForm from "../src/form/form" import xmFormItem from "../src/form/form-item" export default { components: { xmForm, xmFormItem, }, data() { return { form: { name: "", email: "" }, rule: { name: [ {required: true, message: '用戶名不能爲空', trigger: 'blur'} ], email: [ {required: true, message: '郵箱不能爲空', trigger: 'blur'}, {type: 'email', message: '郵箱格式不正確', trigger: 'blur'} ], } } }, methods: { submit() { // 調用form組件的validate方法 this.$refs.baseForm.validate().then(res => { console.log(res) }).catch(e => { console.log(e) }) } } } </script>
複製代碼
接下來讓咱們實現form-item組件,其主要做用是放置表單元素,及展現錯誤信息
<template>
<label class="form-item">
<div class="form-item_label">{{label}}</div>
<div class="form-item_mn">
<slot></slot>
</div>
<div class="form-item_error" v-if="errorMsg">{{errorMsg}}</div>
</label>
</template>
<script> export default { name: "form-item", props: { label: String, prop: String }, data() { return { errorMsg: "" } }, methods: { showError(msg) { this.errorMsg = msg } } } </script>
複製代碼
而後讓咱們來實現form組件
calcFormItems
獲取每一個xm-form-item
的引用,保存在formItems中prop
屬性,處理對應的error信息<template>
<div class="form">
<slot></slot>
</div>
</template>
<script> import AsyncValidator from 'async-validator'; export default { name: "xm-form", props: { model: { type: Object }, rule: { type: Object, default: {} } }, data() { return { formItems: [] } }, mounted() { this.calcFormItems() }, updated() { this.calcFormItems() }, methods: { calcFormItems() { // 獲取form-item的引用 if (this.$slots.default) { let children = this.$slots.default.filter(vnode => { return vnode.tag && vnode.componentOptions && vnode.componentOptions.Ctor.options.name === 'form-item' }).map(({componentInstance}) => componentInstance) if (!(children.length === this.formItems.length && children.every((pane, index) => pane === this.formItems[index]))) { this.formItems = children } } }, validate() { let validator = new AsyncValidator(this.rule); let isSuccess = true let findErrorByProp = (errors, prop) => { return errors.find((error) => { return error.field === prop }) || "" } validator.validate(this.model, (errors, fields) => { this.formItems.forEach(formItem => { let prop = formItem.prop let error = findErrorByProp(errors || [], prop) if (error) { isSuccess = false } formItem.showError(error && error.message || "") }) }); return Promise.resolve(isSuccess) } } } </script>
複製代碼
這樣咱們就完成了一個通用的表單驗證組件。從這個例子中能夠看出獲取組件引用,在組件開發中是一個很是有用的方法。
一些組件如提示框、彈出框等,更適合單獨的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>
<div class="alert">
<div class="alert-main" v-for="item in notices" :key="item.name">
<div class="alert-content">{{ item.content }}</div>
</div>
</div>
</template>
<script> let seed = 0; function getUuid() { return 'alert_' + (seed++); } export default { data() { return { notices: [] } }, methods: { add(notice) { const name = getUuid(); let _notice = Object.assign({ name: name }, notice); this.notices.push(_notice); // 定時移除,單位:秒 const duration = notice.duration; setTimeout(() => { this.remove(name); }, duration * 1000); }, remove(name) { const notices = this.notices; for (let i = 0; i < notices.length; i++) { if (notices[i].name === name) { this.notices.splice(i, 1); break; } } } } } </script>
複製代碼
下面來實現消息組件掛載到頁面的邏輯,並對外暴露展現消息的接口
// 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,就能夠實現高階組件。
Vue內置了一個高階組件keep-alive
,查看源碼能夠發現其實現原理,就是經過維護一個cache,並在render函數中根據key返回緩存的vnode,來實現組件的持久化。
節流是web開發中處理事件比較常見的需求。常見的場景有及時搜索框避免頻繁觸發搜索接口、表單按鈕防止在短暫時間誤重複提交等
首先來看看Throttle組件的使用方式,接收兩個props
time
表示節流的時間間隔events
表示須要處理的事件名,多個事件用逗號分隔在下面的例子中,經過Throttle組件來控制其內部button的點擊事件,此時連續點擊屢次,觸發clickBtn的次數要比點擊的次數小(節流函數經過一個定時器進行處理)。
<template>
<div>
<Throttle :time="1000" events="click">
<button @click="clickBtn">click {{count}}</button>
</Throttle>
</div>
</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組件的技巧
在瞭解Vue的API以後,理解上面的概念都比較輕鬆,封裝組件,除了對於API的熟練度以外,更多地是考察JavaScript基礎。Vue入門十分輕鬆,可是要寫好優雅的Vue代碼,也是一份不小的學問。