vue二次封裝一個高頻可復用組件的全過程
前言
在我們的業(yè)務(wù)里,我們通常會二次封裝一些高頻業(yè)務(wù)組件,比如彈框,抽屜,表單等這些業(yè)務(wù)組件,為什么要二次封裝?我們所有人心里的答案肯定是,同樣類似的代碼太多了,我想復用組件,或者原有組件可能達不到我想要的效果,我想基于原有組件自定義一些自己的接口,那么此時就需要二次封裝了。二次封裝雖好,但同時也會帶來一定的心智負擔,因為二次封裝的組件可能會變得不那么純粹。
本文是一篇筆者關(guān)于二次封裝組件的思考,希望看完在項目中有所思考和幫助。
正文開始...
在內(nèi)容開始之前,本文主要從以下幾個方向去思考:
1、二次組件必須繼承原有組件的所有特性
2、二次組件名必須見名知意
3、自定義暴露出來的接口越簡單越好
4、留有自定義插槽,讓用戶可以自己選擇
5、封裝二次的組件,能根據(jù)schame
數(shù)據(jù)配置,讓組件更通用
繼承原有組件接口
在之前的項目例子中,我們以一個彈框組件為例
我們看下在業(yè)務(wù)中一般是怎么寫的
<template> <div class="list-app"> <div><a href="javascript:void(0)" rel="external nofollow" @click="handleToHello">to hello</a></div> ... <list-modal title="編輯" width="50%" v-model="formParams" :visible.sync="dialogVisible" @refresh="featchList" ></list-modal> </div> </template> <script> import { sourceDataMock } from '@/mock'; import ListModal from './ListModal'; export default { name: 'list', components: { ListModal, }, ... }; </script>
我們再繼續(xù)看下list-modal
這個組件
<!--ListModal.vue--> <template> <el-dialog :visible.sync="currentVisible" width="30%" v-bind="$attrs" > <el-form label-position="left" label-width="80px" :model="formParams"> <el-form-item label="日期"> <el-input v-model="formParams.date"></el-input> </el-form-item> <el-form-item label="名稱"> <el-input v-model="formParams.name"></el-input> </el-form-item> <el-form-item label="地址"> <el-input v-model="formParams.address"></el-input> </el-form-item> </el-form> <span slot="footer" class="dialog-footer"> <el-button @click="closeModal">取 消</el-button> <el-button type="primary" @click="handleSure">確 定</el-button> </span> </el-dialog> </template>
我們會發(fā)現(xiàn),這個list-modal
業(yè)務(wù)組件只是包了一層,當我們使用v-bind="$attrs"
時,vue
提供的這個api
會將父組件所有的props
繼承,官方給了一大段解釋
- $attrs
包含了父作用域中不作為 prop 被識別 (且獲取) 的 attribute 綁定 (class 和 style 除外)。當一個組件沒有聲明任何 prop 時,這里會包含所有父作用域的綁定 (class 和 style 除外),并且可以通過 v-bind="$attrs" 傳入內(nèi)部組件——在創(chuàng)建高級別的組件時非常有用。
首先我們思考為什么要用這個$attrs
?上面一段話的意思是,父組件class
與style
會排除
我們從頁面上可以看出title
與width
都是父組件傳過來的,但是我們發(fā)現(xiàn),實際上這兩個外部看似自己傳入的props
也是el-dialog
的props
,所以說我們必須要保持自己二次封裝的組件也有el-dialog
所有能力,所以此時v-bind='$attrs'
就可以做到了
- $listeners
包含了父作用域中的 (不含 .native 修飾器的) v-on 事件監(jiān)聽器。它可以通過 v-on="$listeners" 傳入內(nèi)部組件——在創(chuàng)建更高層次的組件時非常有用。
在以上的$attrs
我們是將父級的所有的props
都拿到了,但是自定義事件呢,所以才有的了$listeners
所以你在父組件寫了一個el-dialog
的自定義事件想要生效,那么必須在子組件綁定$listeners
<!--list/ListModal.vue--> <el-dialog :visible.sync="currentVisible" width="30%" v-bind="$attrs" v-on="$listeners" > ... </el-dialog>
正常來說一個高階二次組件必須要有v-bind="$attrs"
與v-on="$listeners"
另外我們自己封裝的二次組件里有v-model='formParams'
這個formParams
就是我們彈框內(nèi)部表單的使用內(nèi)容
v-model
關(guān)于v-model
實際上官方解釋就是用在組件或者表單上創(chuàng)建雙向綁定,如果把v-model
看成是一個內(nèi)部提供的一個語法糖,那么它可以拆解成:value="value"
與:input=“handleInput”
,v-model
不僅僅是可以作用在表單元素上,并且還可以作用在組件上,同時也提供了一個model
的接口,提供自定義修改事件名稱
<script> export default { name: 'list-modal', model: { prop: 'formParams', event: 'change', }, props: { visible: { type: Boolean, default: false, }, formParams: { type: Object, }, }, data() { return { currentVisible: false, }; }, watch: { visible(bool) { this.currentVisible = bool; }, currentVisible(bool) { this.$emit('update:visible', bool); }, } }; </script>
以上代碼就自定義了model
的event
,prop
就是formParams
,同時props
上必須有引入formParams
不知道你有沒有好奇,為啥我data
中定義了一個currentVisible
,而且watch
了visible
與currentVisible
,使用currentVisible
時,這里是有一個坑,因為彈框的icon
關(guān)閉操作不會觸發(fā)最外層事件,也就是你點擊右上角的關(guān)閉操作后,當你再次打開時,此時,就打不開了,所以就沒直接用visible
了,我們需要另一個變量,然后去watch
最終達到我們需要的效果。
在這里有人會奇怪,傳入子組件的formParams
直接在表單上使用了,嘿,這樣不是直接修改props
嗎,但實際上控制臺并不會報錯,如果你父組件傳入的是一個基礎(chǔ)數(shù)據(jù)類型,你在子組件里修改是會直接警告你不能修改的,但是你傳入的是一個對象,你此時修改的是對象屬性值,并沒有修改原對象,所以一個非基礎(chǔ)數(shù)據(jù)類型數(shù)據(jù),修改內(nèi)部值時,是不會警告的,這樣做也是ok的。
插槽
在這個彈框中的確認和取消操作是用插槽slot="footer"
去顯示的,如果你想自定義插槽,那么你可以通過具名插槽進行兼容處理
<el-dialog :visible.sync="currentVisible" width="30%" v-bind="$attrs" v-on="$listeners" > ... <template v-if="$slots.footer"> <slot name="footer" /> </template> <span v-else slot="footer" class="dialog-footer"> <el-button @click="closeModal">取 消</el-button> <el-button type="primary" @click="handleSure">確 定</el-button> </span> </el-dialog>
在我們的業(yè)務(wù)中有大量這樣的XXXModal
彈框,如果我們只是這樣包了一層,那么我們只是完成了組件的基本使用,也是符合我們常規(guī)業(yè)務(wù)需求,但是你會發(fā)現(xiàn),我們絕大部份業(yè)務(wù)里的彈框內(nèi)容都是表單,所以我能不能通過可配置的schame
數(shù)據(jù)去配置出來呢?
組件更抽象
我們在components
下新建了一個form-modal
組件,并注冊成全局組件,我的目標是把彈框的內(nèi)容區(qū)域做成可配置化,這樣我只需要用配置數(shù)據(jù)就可以渲染出對應(yīng)的內(nèi)容
<!--src/components/form-modal/view/index.vue--> <template> <div class="form-modal"> <el-dialog :visible.sync="currentVisible" v-bind="$attrs" v-on="$listeners"> <el-form v-bind="formConfig.formAttrs" :model="formParams"> <div v-for="(item, index) in formConfig.fields" :key="index"> <el-form-item :label="item.label"> <!--自定義插槽--> <template v-if="item.slot"> <slot :name="item.slot" :row="{ ...item, formParams, index }" /> </template> <!--文本or文本域--> <template v-else-if="['text', 'textarea'].includes(item.type)"> <el-input :type="item.type" v-bind="item.attrs || {}" v-model="formParams[item.key]" ></el-input> </template> <!--下拉框--> <template v-else-if="item.type === 'select'"> <el-select v-bind="item.attrs" v-model="formParams[item.key]"> <el-option v-for="(sitem, index) in item.options.data" :key="index" :label="sitem[item.options.extraProps.label]" :value="sitem[item.options.extraProps.value]" > </el-option> </el-select> </template> </el-form-item> </div> </el-form> <span slot="footer" class="dialog-footer"> <el-button @click="closeModal">取 消</el-button> <el-button type="primary" @click="handleSure">確 定</el-button> </span> </el-dialog> </div> </template>
全局注冊
// src/components/index.js import Vue from 'vue'; import FormModal from './form-modal'; const custCompoment = { FormModal, }; export const installCustComponent = () => { Object.keys(custCompoment).forEach((key) => { Vue.component(key, custCompoment[key]); }); };
main.js
// main.js import { installCustComponent } from '@/components'; installCustComponent(); ...
我們發(fā)現(xiàn)在模版里面有不少添加條件,實際上,這些條件主要根據(jù)你業(yè)務(wù)需要而定,除了模版方式,插槽,我們也可以預留一個自定義formater
的接口,像下面這樣
<!--src/components/form-modal/view/index.vue--> <div v-for="(item, index) in formConfig.fields" :key="index"> <el-form-item :label="item.label"> <!--自定義render--> <template v-if="item.formater"> <component :is="'renderComponent'" :value="formParams[item.key]" :input="e => formParams[item.key] = e" v-bind="{ ...item }" ></component> </template> <!--自定義插槽--> <template v-else-if="item.slot"> <slot :name="item.slot" :row="{ ...item, formParams, index }" /> </template> <!--文本or文本域--> ... </el-form-item> </div>
那么此時你會發(fā)現(xiàn)有一個renderComponent
這樣的自定義組件,我們必須引入進來
/* src/components/form-modal/view/render.js*/ export default { functional: true, props: ['value'], render(h, ctx) { const { formater, attrs, input: handleInput } = ctx.data.attrs; return formater(h, { attrs: { ...attrs, value: ctx.props.value, }, on: { input(e) { handleInput(e); }, }, }); }, };
在form-modal/view/index.vue
中我們必須引入,所以模版中就可以使用了
<script> // src/components/form-modal/view/index.vue import renderComponent from './render'; export default { name: 'form-modal', model: { prop: 'formParams', event: 'change', }, components: { renderComponent, }, props: { visible: { type: Boolean, default: false, }, formParams: { type: Object, }, formConfig: { type: Object, }, }, ... </script>
我們再看下我們之前業(yè)務(wù)彈框與schame
再次抽象后的兩個組件,其實第二個全局組件就多了一個formConfig
屬性,我們統(tǒng)一把內(nèi)容抽離了出去,我們的form-modal
就變得更加通用,我們只需要關(guān)注formConfig
這份配置數(shù)據(jù)就行
/* eslint-disable func-names */ <template> <div class="list-app"> ... <list-modal title="編輯" width="50%" class="list-modal" style="border: 1px solid transparent" v-model="formParams" :visible.sync="dialogVisible" @refresh="featchList" @close="handleClose" > <div slot="footer">確定</div> </list-modal> <form-modal title="編輯" width="50%" class="list-modal" style="border: 1px solid transparent" v-model="formParams" :formConfig="formConfig" :visible.sync="dialogVisible2" @refresh="featchList" @close="handleClose" > <template slot-scope="{ row }" slot="number"> <el-input :type="row.type" v-bind="row.attrs || {}" v-model="row.formParams[row.slot]" ></el-input> </template> </form-modal> </div> </template> <script> import { sourceDataMock } from '@/mock'; import ListModal from './ListModal'; export default { name: 'list', components: { ListModal, }, data() { return { ... tableData: [], dialogVisible: false, dialogVisible2: false, formParams: { date: '', name: '', address: '', number: '1', scholl: '公眾號:Web技術(shù)學苑', }, }; }, computed: { formConfig() { return { formAttrs: { labelWidth: '80px', labelPosition: 'left', }, fields: [ { type: 'text', key: 'date', label: '日期', attrs: { placeholder: '請?zhí)顚懭掌?, }, }, { type: 'text', key: 'name', label: '名稱', attrs: { placeholder: '請?zhí)顚懨Q', }, }, { type: 'select', key: 'address', label: '地址', attrs: { placeholder: '請選擇地址', style: { width: '100%', }, }, options: { data: this.tableData, extraProps: { value: 'address', label: 'address', }, }, }, { type: 'text', slot: 'number', label: '編號', attrs: { placeholder: '請輸入編號', }, }, { type: 'text', key: 'scholl', label: '畢業(yè)學校', attrs: { placeholder: '請輸入畢業(yè)學校', }, formater: (h, props) => h('el-input', { ...props, }), }, ], }; }, }, }; </script> <style scoped> .list-app .el-form { text-align: left; } </style>
看下最終的結(jié)果
在我們自定義一個formater
的接口,我們注意到,實際上這里有用vue
的純函數(shù)組件,我們注意到在render.js
中我們是申明了functional: true
,這里會有巨坑,如果是一個函數(shù)組件,在render
函數(shù)中是獲取不到this
的,只能通過第二個ctx
參數(shù)獲取父組件傳入的props
信息
/* eslint-disable no-param-reassign */ export default { functional: true, props: ['value'], render(h, ctx) { // console.log(this, '---'); // 會是null,只能通過第二個參數(shù)ctx拿對應(yīng)參數(shù) const { formater, attrs, input: handleInput } = ctx.data.attrs; return formater(h, { attrs: { ...attrs, value: ctx.props.value, }, on: { input(e) { handleInput(e); }, }, }); }, };
并且我們修改數(shù)據(jù),我們發(fā)現(xiàn)我們用了一個父組件傳入的一個回調(diào)函數(shù)去修改,這在react
很常見,這里我們也是通過回調(diào)方式修改數(shù)據(jù),因為vue
數(shù)據(jù)流是單向的,所以只能這種方式去修改了
因此在業(yè)務(wù)中我們的form-modal
就變得更通用,更高頻了,這樣會減少你重復勞動的時間,你只需要關(guān)注配置接口信息就行。
但是這樣帶來的負擔是有的,如果這個form-modal
耦合了太多業(yè)務(wù)邏輯,那么帶來的心智負擔是有的,當你二次封裝的一個高頻組件,你組內(nèi)小伙伴不能像使用第三方組件庫那么快捷時,說明組件的接口設(shè)計還有提高的空間,判斷一個組件好不好用的標準就是,零負擔,而且人人能改,人人都能改動,如果因為業(yè)務(wù)特殊,當我們考慮二次封裝一個組件參雜很多業(yè)務(wù)邏輯判斷時,那我的觀點是,還是不要進行二次封裝了。
總結(jié)
以一個彈框組件為例,我們二次封裝組件到底需要注意哪些問題,以及我們必須注意些什么,核心思想就是繼承原有組件的特性,
v-bind='$attrs'
與v-on="$listeners"
是核心當我們二次封裝一個組件時,我們自定義的一些接口能少就少,組件名必須見名知意
二次封裝的組件不僅僅只是包一層,我們可以嘗試用數(shù)據(jù)配置方式讓組件更通用,預留一些接口插槽,或者自定義
formater
函數(shù),不強制約束,讓組件靈活性拓展性更強些組件的
props
名字盡量不要帶來負擔,最好與原有組件props
保持一致本文 code example
到此這篇關(guān)于vue二次封裝一個高頻可復用組件的文章就介紹到這了,更多相關(guān)vue二次封裝高頻可復用組件內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Vue通過echarts實現(xiàn)數(shù)據(jù)圖表化顯示
Echarts,它是一個與框架無關(guān)的 JS 圖表庫,但是它基于Js,這樣很多框架都能使用它,例如Vue,估計IONIC也能用,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2022-08-08vue+element 模態(tài)框表格形式的可編輯表單實現(xiàn)
這篇文章主要介紹了vue+element 模態(tài)框表格形式的可編輯表單實現(xiàn),小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2019-06-06Vue3?中的?readonly?特性及函數(shù)使用詳解
readonly是Vue3中提供的一個新特性,用于將一個響應(yīng)式對象變成只讀對象,這篇文章主要介紹了Vue3?中的?readonly?特性詳解,需要的朋友可以參考下2023-04-04vue調(diào)用swiper插件步驟教程(最易理解且詳細)
有時候我們需要在vue中使用輪播組件,如果是在vue組件中引入第三方組件的話,最好通過npm安裝,從而進行統(tǒng)一安裝包管理,下面這篇文章主要給大家介紹了關(guān)于vue調(diào)用swiper插件的相關(guān)資料,需要的朋友可以參考下2023-04-04Vuejs學習筆記之使用指令v-model完成表單的數(shù)據(jù)雙向綁定
表單類控件承載了一個網(wǎng)頁數(shù)據(jù)的錄入與交互,本章將介紹如何使用指令v-model完成表單的數(shù)據(jù)雙向綁定功能,本文通過實例代碼給大家介紹的非常詳細,具有一定的參考借鑒價值。感興趣的朋友跟隨小編一起看看吧2019-04-04vue實現(xiàn)動態(tài)給data函數(shù)中的屬性賦值
這篇文章主要介紹了vue實現(xiàn)動態(tài)給data函數(shù)中的屬性賦值,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-09-09vue3+el-select實現(xiàn)觸底加載更多功能(ts版)
這篇文章主要給大家介紹了基于vue3和el-select實現(xiàn)觸底加載更多功能,文中有詳細的代碼示例,感興趣的同學可以借鑒參考下2023-07-07