vue2 元件庫開發記錄-開發技巧
- 前言
- install
- props 屬性
- provide 和 inject 屬性
- $children 和 $parent
- EventBus 事件總線
- $attrs
- $scopedSlots 作用域插槽
- Vue.extends 實作 js 調用元件
- 指令
- mixin
- 總結
前言
本文主要是記錄我在開發元件庫時的一些開發技巧。并且會講解一些比較特殊的元件。
install
我們在使用
element-ui
的時候,可以通過
Vue.use(Button)
注冊元件。
use
函數内部實際上就是調用了傳入的對象的
install
函數,同時
install
函數會接受到一個
vue
參數。
import Alert from "./src/alert.vue";
Alert.install = Vue => Vue.component(Alert.name, Alert);
export default Alert;
props 屬性
props 是用來做父子元件之間的通信,并且 props 的寫法有很多種,相信做 vue 開發的同學應該都知道的。是以我這裡主要說 2 點
- 當預設值是一個數組或者是對象時,必須從一個工廠函數中傳回,否則所有元件執行個體都會共用一個值
export default {
props: {
obj: {
type: Object,
// 簡寫的時候需要注意傳回的{}外層需要套一個(),否則就是一個空函數,沒有傳回值
default: () => ({})
}
}
};
- 自定義校驗。這個在項目開發中可能會很少會用到。但是在元件開發中卻經常用到。比如一個
元件的Button
屬性是type
類型,但是隻接受String
,default
,primary
,開發者可能傳入的字元串不符合我們的預期,是以需要用到自定義校驗,給開發者一個适當的提示。success
export default {
props: {
type: {
type: String,
default: 'default',
validator: function (value) {
// 這個值必須比對下列字元串中的一個
return ['default', 'primary', 'success'].indexOf(value) !== -1;
}
}
}
};
注意:元件的 this,在
default
和
validator
字段中是不可用的。
provide 和 inject 屬性
這兩個屬性我相信很多同學都是沒有使用過的,甚至有的人可能都沒聽過。這兩個屬性可以用來做跨元件層級通訊,實作父子或者祖孫元件之間的通信。可能會有人說,我使用
props
屬性将資料一層一層的傳遞下去不也可以實作
provide
和
inject
的效果嗎,這個說的也沒錯,但是有些場景卻實作不了。我們看一下
checkbox
元件的使用方式
<checkbox-group v-model="value">
<checkbox label="抽煙"></checkbox>
<checkbox label="喝酒"></checkbox>
<checkbox label="探頭"></checkbox>
</checkbox-group>
上面可以看見
checkbox-group
元件無法通過
props
屬性給
checkbox
元件傳遞資料,因為
checkbox
元件并不是直接寫在
checkbox-group
元件内部的,而是通過
slot
插槽放置到
checkbox-group
元件中的,進而實作了父子關系的元件。此時,
provide
和
inject
屬性就可以解決這種組合元件的寫法之間的通訊問題。
provide
和
inject
的特點如下:
-
可以是一個對象或者是傳回一個對象的函數(推薦使用這種寫法)。provide
-
可以是一個數組或者是一個對象(推薦這種寫法)。inject
- 如果
傳入的是一個響應式的對象(元件開發中一般直接傳入provide
),那麼this
接收的值也是個響應式資料inject
代碼示例:
// checkbox-group元件
export default {
props: {
// 綁定值
value: {
type: Array
},
},
provide() {
return {
// 直接把元件執行個體傳遞給所有子孫元件
CheckboxGroup: this
};
}
};
// checkbox元件
export default {
inject: {
// 接收checkbox-group元件通過provide傳遞過來的資料
CheckboxGroup: {
default: ""
}
},
mounted(){
// checkbox-group元件的value值
console.log(this.CheckboxGroup?.value)
}
};
provide
和
inject
的使用場景有兩個,分别如下:
- 具有組合關系的元件。比方說上面的
元件和checkbox-group
元件,他們就是組合關系。checkbox
元件可以單個進行使用,也可以和checkbox
元件在一起組合使用。checkbox-group
元件通過判斷checkbox
是否存在來判斷開發者是單個使用還是結合this.CheckboxGroup
元件一起使用,進而實作不同的邏輯。checkbox-group
- 跨層級元件傳遞資料。當你的元件層級很深的時候,比如
。如果A->B->C->D
想要跟A
進行通訊,就必須通過D
和B
的C
屬性一層一層的傳遞下去,這樣會造成資料的混亂的,而且如果傳遞的資料非常多,寫起來也很麻煩。是以這個時候可以使用props
和provide
來進行通信,資料的流向就不用經過inject
和B
,你隻需要專注于C
和A
之間的資料處理即可。D
$children 和 $parent
$children
是用來擷取目前元件的所有子元件,
$parent
是用來擷取目前元件的父元件。元件的子元件可能會有多個,但是父元件隻能有一個(根元件沒有父元件)。是以我們可以通過遞歸的方式擷取該元件的子孫元件和父級元件,并實作廣播和派發的功能,實作具有上下級元件關系的通訊。
注意:一般查找父級元件或者子孫元件都是通過元件的
name
字段進行查找的,是以每個元件内部最好有一個
name
字段,這樣才能有效過濾出你想要查找的元件。
代碼示例:
// 查找所有子孫元件
function findChildren(context, componentName) {
return context.$children.reduce((components, child) => {
if (child.$options.name === componentName) {
components.push(child);
}
const children = findChildren(child, componentName);
return components.concat(children);
}, []);
}
// 查找所有父級元件
function findParents(context, componentName) {
const parents = [];
const parent = context.$parent;
if (parent) {
if (parent.$options.name === componentName) {
parents.push(parent);
}
return parents.concat(findParents(parent, componentName));
} else {
return [];
}
}
派發與廣播
// 向下通知
function broadcast(options) {
const { eventName, params, componentName } = options;
// 擷取目前元件下的所有的孩子
const broad = (children) => {
children.forEach((child) => {
if (componentName) {
if (child.$options.name === componentName) {
child.$emit(eventName, params);
}
} else {
child.$emit(eventName, params);
}
if (child.$children) {
broad(child.$children);
}
});
};
broad(this.$children);
}
// 向上通知
function dispatch(options) {
const { eventName, params, componentName } = options;
let parent = this.$parent || this.$root;
let name = parent.$options.name;
while (parent) {
if (componentName) {
if (name === componentName) {
parent.$emit(eventName, params);
}
} else {
parent.$emit(eventName, params);
}
parent = parent.$parent;
name = parent?.$options.name;
}
}
廣播通信例子
const parent = {
name:'parent'
template:`
<div>
<div @click='onBroadcast'>broadcast</div>
<child/>
</div>
`,
methods:{
onBroadcast(){
broadcast.call(this,{
name:'custom',
params:'hello world',
componentName:'child'
})
}
}
}
const child = {
name:'child',
created(){
this.$on('custom',event=>{
console.log(event) // hello world
})
}
}
在上面例子中,child 元件需要在 parent 元件進行廣播前使用
$on
注冊事件,否則是接收不到 parent 元件的廣播的
廣播與派發的應用場景可用于
Form
和
FormItem
元件的表單校驗:
-
,input
等表單元件值發生變化的時候,通過checkbox
向上通知dispatch
元件進行校驗FormItem
-
元件需要校驗整個表單的時候,通過Form
查找到所有findChildren
元件,調用FormItem
元件内部的方法進行校驗,獲得校驗結果,進而回報給使用者FormItem
EventBus 事件總線
EventBus 可實作任意元件之間的通信,借助 EventBus 的
$emit
派發事件,
$on
監聽事件就可以實作任意元件之間的通信。 EventBus 實際上是通過釋出/訂閱方法來實作的,通過導出一個
new Vue()
執行個體(單例),所有元件都是用該執行個體進行事件的派發和監聽。
代碼示例:
// event-bus.js
import Vue from 'vue';
const EventBus = new Vue();
// 發送事件
EventBus.$emit('custom', { age: 1 });
// 接收事件
EventBus.$on('custom', (event) => {});
// 監聽一次事件
EventBus.$once('custom', (event) => {});
// 移除事件
EventBus.$off('custom');
EventBus 在元件庫開發中使用的場景比較少,但也是一種跨元件的通信方式,對比于
props
,
provide 和 inject
,
$children 和 $parent
這些通信方式(隻能在具有上下級關系的元件中進行通信,不能再兄弟元件之間進行通信),優點在于可以在任意元件之間進行通信,缺點就是一旦事件多了,就變得很難管理。
$attrs
$attrs
包含哪些沒有在元件的
props
字段中聲明的屬性(class 和 style 除外)。
例子
const A = {
props:['name']
}
<A name='張三' age='17' sex='男' />
從上面的例子中,我們可以看見
props
屬性中隻聲明了
name
字段,是以
age
和
sex
字段包含在了
$attrs
中,
name
并不在
$attrs
中
常用于那些有許多原生屬性的元件中,比如
input
元件,原生的
input
标簽包含了很多字段,如果我們将
input
标簽的所有原生屬性都在
props
中都聲明一邊,那就會非常麻煩。我們一般會将一些非原生屬性或者需要在元件内部使用到的原生屬性聲明在
props
中,其餘的字段通過
v-bind="$attrs"
挂在到
input
标簽上面
代碼示例:
const LinInput = {
template:'<input :disabled="disabled" :value="value" v-bind="$attrs" />',
props:['disabled','value']
}
<lin-input disabled value='1' name='age' placeholder='請輸入' />
$scopedSlots 作用域插槽
$scopedSlots
作用域插槽。這個東西我相信很多同學都沒接觸過。我也是在開發
Table
元件時第一次使用到它。不得不說這個東西真的很強大很巧妙。文字說明可能不夠透徹,是以下面我們通過
Table
元件的代碼來講解。
首先看一下使用方式:
<template>
<lin-table value-key="id" :dataSource="tableData">
<lin-table-column prop="date" label="日期">
<template slot-scope="scope">
<div>{{ scope.row.date }}</div>
</template>
</lin-table-column>
<lin-table-column prop="name" label="姓名"></lin-table-column>
<lin-table-column prop="address" label="位址"></lin-table-column>
</lin-table>
</template>
export default {
data() {
return {
tableData: [
{
id: 1,
date: "2016-05-02",
name: "王小虎",
address: "上海市普陀區金沙江路 1518 弄",
}
],
};
},
};
lin-table-column
元件隻負責收集資料,并不會渲染任何東西。比如
prop
,
label
這些資料,然後将這些資料存放在
table
元件中。然後通過
table
元件來渲染這些東西。
lin-table-column.jsx
let columnId = 0;
export default {
name: 'LinTableColumn',
props: {
prop: String,
label: String
},
inject: {
// table元件的執行個體
table: {
default: null
}
},
watch: {
prop(val) {
this.column.prop = val;
},
label(val) {
this.column.label = val;
}
},
created() {
// 把該元件的props屬性都存儲起來
const column = {
...this.$props,
id: `col-${columnId++}`
};
// 預設提供一個渲染單元格的render函數,核心内容
// h是渲染函數,rowData是每一行的資料
column.renderCell = (h, rowData) => {
let render = (h, data) => {
return data.row[column.prop];
};
// 判斷是不是使用了插槽
if (this.$scopedSlots.default) {
// 通過this.$scopedSlots.default擷取預設插槽的VNode,也就是這個東西
// <template slot-scope="scope">
// <div>{{ scope.row.date }}</div>
// </template>
render = (h, data) => this.$scopedSlots.default(data);
}
return render(h, rowData);
};
this.column = column;
},
mounted() {
if (this.table) {
// 把該元件收集到的資料存儲在table元件中。
this.table.columns.push(this.column);
}
},
destroyed() {
if (this.table) {
// 銷毀的時候需要把對應的列移除掉
const index = this.table.columns.findIndex(
(column) => column.id === this.column.id
);
if (index > -1) {
this.table.columns.splice(index, 1);
}
}
},
render() {
// 不做實際的渲染
return null;
}
};
table.vue
<template>
<div>
<div class="lin-table-slot">
<!-- lin-table-column元件 -->
<slot></slot>
</div>
<table class="lin-table">
<!-- 渲染頭部相關的東西 -->
<lin-table-header ref="linTableHeaderComp"></lin-table-header>
<!-- 渲染表格内容 -->
<lin-table-body ref="linTableBodyComp"></lin-table-body>
</table>
</div>
</template>
<script>
import LinTableHeader from './TableHeader.jsx';
import LinTableBody from './TableBody.jsx';
export default {
name: 'LinTable',
components: {
LinTableHeader,
LinTableBody
},
props: {
// 資料源
dataSource: {
type: Array,
default: () => [],
require: true
},
// 每一行資料的唯一辨別key
valueKey: {
type: String,
require: true
}
},
provide() {
return {
// 往子元件中注入table執行個體,以便子元件可以通路到table元件的資料
table: this
};
},
data() {
return {
// 存儲lin-table-column元件收集到的資訊
columns: []
};
}
};
</script>
TableHeader 元件的内容還是很簡單的,就是根據使用
v-for
将
columns
字段的 label 字段渲染出來。是以這裡不講解 TableHeader 元件,直接講解
TableBody
元件
TableBody.jsx
export default {
name: 'LinTableBody',
computed: {
// 資料源
dataSource() {
if (this.table) {
return this.table.dataSource;
}
return [];
},
// 列數組
columns() {
if (this.table) {
return this.table.columns;
}
return [];
},
// 每一行資料的唯一辨別 key
valueKey() {
if (this.table) {
return this.table.valueKey;
}
return '';
}
},
inject: {
table: {
default: null
}
},
render(h) {
const { dataSource, columns, valueKey } = this;
return (
<tbody class="lin-table-tbody">
{dataSource.map((row, rowIndex) => {
const rowKey = row[valueKey] || rowIndex;
return (
<tr key={rowKey}>
{columns.map((column, idx) => (
<td key={`${rowKey}-${idx}`}>
// lin-table-column中的renderCell渲染函數
{column.renderCell(h, {
row,
column,
rowIndex
})}
</td>
))}
</tr>
);
})}
</tbody>
);
}
};
從上面可以看出
TableBody
元件的核心就是調用
renderCell
函數,而
renderCell
這個函數就是在
lin-table-column
元件收集到的每個單元格的渲染函數。
Vue.extends 實作 js 調用元件
像
element-ui
的
message
,
message-box
等元件都通過 js 的方式進行調用。在實際項目開發中有時候也需要根據需求實作一個 js 調用的元件,比如,我點選一個按鈕,使用者沒有權限的時候需要彈框顯示暫無權限,遊客則需要彈出登入框。是以,下面以
message
元件為例,講解一下怎麼通過 Vue.extends 實作 js 調用元件。
首先建立一個
message.vue
,并實作你需要的功能
<template>
<transition name="message" @after-leave="afterLeave">
<div :class="`lin-message-${type}`" v-show="show">
<p class="lin-message-content">{{ message }}</p>
</div>
</transition>
</template>
<script>
export default {
name: 'LinMessage',
props: {
// 類型主題
type: {
type: String,
default: 'info'
},
// 消息文字
message: {
type: String
}
},
data() {
return {
// 控制是否顯示
show: false
};
},
methods: {
// vue動畫結束後回調函數
afterLeave() {
this.$emit('closed');
}
}
};
</script>
使用 Vue.extends 繼承一個 vue 元件
import Vue from 'vue';
import Message from './message.vue';
// 建立一個子類構造器
const MessageConstruct = Vue.extend(Message);
class LinMessage {
// 參數
options = null;
// message執行個體對象
instance = null;
// message元件參數
propsData = {};
// 自動關閉定時器
timer = null;
constructor(options) {
this.options = options || {};
this.initProps(options);
this.init();
}
// 初始化message元件參數
initProps() {
const props = ['type', 'message'];
const propsData = {};
props.forEach((prop) => {
if (prop in this.options) {
propsData[prop] = this.options[prop];
}
});
this.propsData = propsData;
}
// 初始化
init() {
// 建立一個vue執行個體,實際上跟new Vue()差不多
this.instance = new MessageConstruct({
// propsData會跟元件中的props進行合并
propsData: {
...this.propsData
}
// 這個選項會直接覆寫掉元件的props,是以一般這個是不會使用的
// props:{}
// 這個選項會跟元件中的data進行合并,寫法上可以是一個對象,也可以是傳回一個對象的函數
// data:{}
});
// 渲染
this.instance.$mount();
// 将渲染出來的dom挂在到body上面
document.body.appendChild(this.instance.$el);
// 顯示出來,this.instance相當于在元件中的this
this.instance.show = true;
// 設定定時器,用于定時關掉message元件
this.setTimer();
// 監聽事件
this.instance.$once('closed', () => {
// 銷毀元件
this.destory();
});
}
setTimer() {
const { duration } = this.options;
// 等于0不會自動消失
if (duration !== 0) {
this.timer = setTimeout(() => {
this.close();
}, duration || 3000);
}
}
// 隐藏message元件
close() {
if (this.instance && this.instance.show) {
this.instance.show = false;
}
}
// 銷毀message元件
destory() {
if (this.instance) {
document.body.removeChild(this.instance.$el);
this.instance.$destroy();
}
if (this.timer) {
clearTimeout(this.timer);
}
}
}
// 建立執行個體,options可傳入字元串或者一個對象
function createInstance(options) {
const toString = Object.prototype.toString;
if (toString.call(options).includes('Object')) {
return new LinMessage(options);
}
return new LinMessage({
message: options.toString()
});
}
// 建立不同類型type的message元件
function createInstanceByType(options, type) {
const toString = Object.prototype.toString;
if (toString.call(options).includes('Object')) {
return new LinMessage({
...options,
type
});
}
return new LinMessage({
message: options.toString(),
type
});
}
createInstance.success = function success(options) {
return createInstanceByType(options, 'success');
};
createInstance.info = function info(options) {
return createInstanceByType(options, 'info');
};
export default createInstance;
指令
指令在元件庫或者在實際項目開發中使用到的場景比較少。雖然比較少,但是還是有必要講一下。比如現在有個需求需要根據使用者的權限去顯示或者隐藏某個按鈕,你可能會在頁面上先判斷使用者是否有權限,然後再通過
v-show
去顯示或者隐藏。這樣做是可以的,但是如果頁面上需要控制的按鈕比較多,那樣就會顯得很麻煩了。這個時候我們可以使用指令。用法如下:
當使用者擁有
p1
和
p2
權限的時候,就會顯示按鈕。
指令中也提供了 5 個鈎子函數,我們可以再不同的鈎子函數中處理不同的事情:
- bind:隻調用一次。在這裡可進行初始化
- inserted:元素插入到父節點
- update:指令所在的元件發生更新調用
- componentUpdated:指令所在的元件和
全部更新完成後調用其子元件
- unbind:調用一次。指令更元素解綁,在這裡可進行一些銷毀工作
常用到的鈎子函數有三個,分别是:
bind
,
update
,
unbind
,其餘的我基本上沒用過。
指令的結構如下:
Vue.directive('permission', {
bind(el, binding, vnode) {
// el是指令綁定的元素,你可以在el上面綁定一些資訊,用于在其他鈎子函數中使用,比如 el.message='你好'
// binding是包含了指令的相關資訊,比如 v-permission:foo="['p1','p2]" ,foo和['p1','p2]都可以在binding中拿到,詳情可以列印出來看一下
// vnode這個比較有意思了。他可以拿到指令所在的元件的上下文執行個體,你可以拿着這個上下文執行個體去通路元件中的方法和屬性
},
update(el, binding, vnode) {},
unbind(el, binding, vnode) {}
});
v-permission
實作
Vue.directive('permission', function (el, binding, vnode) {
const permissionList = vnode.context.$store.state.permissionList;
const value = binding.value;
if (!permissionList.includes(value)) {
el.style.display = 'none';
}
});
上面的寫法等同于
Vue.directive('permission', {
bind(el, binding, vnode) {
const permissionList = vnode.context.$store.state.permissionList;
const value = binding.value;
if (!permissionList.includes(value)) {
el.style.display = 'none';
}
},
update(el, binding, vnode) {
const permissionList = vnode.context.$store.state.permissionList;
const value = binding.value;
if (!permissionList.includes(value)) {
el.style.display = 'none';
}
}
});
mixin
通常我們會将一些具有相同邏輯功能的東西封裝成一個
mixin
,方面其他元件或者頁面使用。比如,
select
這個元件需要在使用者點選元件外的地方時,把下拉框隐藏起來,而且其他元件也需要用到這個功能。是以我們可以把使用者點選元件外的地方的邏輯抽離出來,封裝成一個
mixin
。下面講解一下
meta-info
這個
mixin
,主要功能就是根據頁面中的
metaInfo
字段修改網頁的 meta 資訊。
使用方式如下:
export default {
metaInfo: {
title: "metaInfo", 設定title
meta: [
{
// 設定meta
name: "keyWords",
content: "metaInfo",
},
],
link: [
{
// 設定 link
rel: "asstes",
href: "https://github.com/c10342/lin-view-ui",
},
],
},
};
代碼示例:
import { VUE_META_KEY_NAME } from './src/common/constants.js';
import updateMetaInfo from './src/metaOperate/updateMetaInfo.js';
import { isUndefined, isFunction } from '@lin-view-ui/utils';
const VueMetaInfo = {};
VueMetaInfo.install = function install(Vue) {
Vue.mixin({
beforeCreate() {
// 擷取頁面中的 metaInfo 字段資訊
const metaInfo = this.$options[VUE_META_KEY_NAME];
// metaInfo 存在
if (!isUndefined(metaInfo)) {
// 标記該頁面存在 metaInfo 字段資訊
this._hasMetaInfo = true;
// 判斷元件内是否存在computed對象
if (isUndefined(this.$options.computed)) {
// 沒有需要先初始化一下
this.$options.computed = {};
}
// 為元件添加computed對象并傳回meta資訊
// metaInfo 寫法上可以是一個對象,也可以是一個傳回一個對象的函數
if (isFunction(metaInfo)) {
this.$options.computed.$metaInfo = metaInfo;
} else {
// 如果是一個對象,則需要改寫成函數的形式
this.$options.computed.$metaInfo = () => metaInfo;
}
}
},
beforeMount() {
// 在頁面挂在到dom之前更新meta資訊
if (this._hasMetaInfo) {
updateMetaInfo(this.$metaInfo);
}
},
mounted() {
// dom挂載之後,繼續監聽meta資訊。如果發生變化,繼續更新
if (this._hasMetaInfo) {
this.$watch('$metaInfo', () => {
updateMetaInfo(this.$metaInfo);
});
}
},
activated() {
if (this._hasMetaInfo) {
// keep-alive元件激活時調用
updateMetaInfo(this.$metaInfo);
}
},
deactivated() {
if (this._hasMetaInfo) {
// keep-alive 元件停用時調用。
updateMetaInfo(this.$metaInfo);
}
}
});
};
export default VueMetaInfo;
總結
在元件庫開發中,會遇見很多平時實際項目中用不到的東西,比如
$attrs
,
provide
,
injected
,
$scopedSlots
這些東西。在開發的過程中可以多參考一下其他元件庫的源碼,然後在對比一下自己的實作思路,你會看見很多你所不知道的知識點。最後,如果這篇文章對你有幫助的話,希望可以幫我點個贊。去點贊