前端vue2 element ui高效配置化省時又省力
前言
這篇文章是筆者曾經全盤負責瞭接近一年的廣告投放系統沉淀下來的開發經驗,大傢各取所需,不喜勿噴~當然啦,有自己的見解、更好的建議的大牛朋友們,我們評論區見~hhh,大🔥多提點高見,讓筆者繼續學習繼續進步!
本文從 場景介紹 、 設計&實現 、 性能優化 三個部分進行講解。筆者當時的技術棧是 vue2
+ element-ui
,文章案例也是(其實大傢不必糾結於技術棧,掌握設計的思路和理念,什麼框架都是一樣的)。
主要能解決的問題就是 提高代碼復用能力、提升開發效率,特別是需要開發多個大型表單系統的,配置化可以極大的提升效率,讓你上班摸魚不再是夢想!為瞭早點下班,我們接著往下看吧!
一、場景介紹
1. 業務場景
如何定義「巨型」表單,這個因人而異。但如果隻是一些:收貨人信息、登陸、註冊的這種比較簡單的表單,那肯定算不上巨型,直接常規開發寫模版就好瞭,沒有必要為瞭配置化而配置化~
從筆者的理解出發,表單項非常多,比如筆者曾經負責的「投放系統」,隨隨便便提交時都會涉及幾十甚至上百個字段,這樣整個表單會有幾十、上百個表單項組成,這就算得上是巨型表單瞭。
先給大傢看看成品的其中的一小塊截圖~
別看到截圖好像表單項也就那樣,根據右欄數起來共40+個,但是這個隻是初期版的,還有很多字段是沒接進來的;
而且很多表單項之間有聯動、可增刪,還有很多表單項是隱藏的相信你很難想象,其實你隻要進行簡單的配置,就能實現上圖的界面。比如下圖的 js對象 就是上圖的其中幾個表單項的配置:
大傢已經不難看出,配置化思路其實就是對表單項進行瞭抽象,制定瞭一份協議去描述每個表單項。具體對象中的每個屬性有什麼用,這個筆者稍後講自己的設計思路時再詳細介紹~
這時候你一定會有疑問,為什麼要抽象、為什麼配置化的方案更好,我們接著往下看~
2. 配置化想法萌生
高復用、好維護。是的,筆者用配置化方式開發表單,完完全全就是為瞭高復用、可維護性,然後提升開發效率,解放生產力。
- 高復用:相似的業務邏輯進行統一處理,復用在相似領域的業務場景。如果說投放系統隻需要接入一個渠道,那真的寫
template
一把梭就完瞭。但事實上卻不是這樣的,當你接入瞭第一個facebook
,你發現後面還有tiktok
、 巨量引擎 、 廣點通 等各種媒體渠道… - 可維護:實現配置代替開發。即使把配置抽離,交到非技術人員處,其根據協議一樣能實現表單項的增刪,完成業務。並不是把東西做出來就完事瞭。首先,渠道方會有新配置功能推出,這個是不可控的。其次,系統開發時並不是全字段接入,而是先接入業務方所需要的核心配置,所以後期會有很多接入新的字段需求。
接下來舉兩個例子來說說,高復用、好維護體現在哪裡
- 表單1。代碼如下:
<el-form ref="form" :model="form"> <!-- 當活動區域的值為 “area1” 時, 活動名稱才展示 --> <el-form-item label="活動名稱" v-if="form.area === 'area1'"> <el-input v-model="form.name"></el-input> </el-form-item> <el-form-item label="活動區域"> <el-select v-model="form.area" multiple>...</el-input> </el-form-item> </el-form>
- 表單2。雖然跟 表單1 很相似,但又存在不同。比如 表單2 的活動區域不叫 “活動區域” ,且 表單項 之間的聯動關系有所不同,我們接著使用 copy大法 來做,代碼如下:
<el-form ref="form" :model="form"> <el-form-item label="活動名稱"> <el-input v-model="form.name"></el-input> </el-form-item> <!-- label變成 “活動2”,且需要填寫name後才能操作,且是多選 --> <el-form-item label="活動2"> <el-select v-model="form.area" :disable="form.name" multiple >...</el-input> </el-form-item> </el-form>
copy大法雖然好使,但是我們的復用能力基本就沒瞭,所有功能都近乎是重新開發,這使得非常的被動。別看上面舉例好像很輕松就能實現,筆者說過瞭,我們將要開發的是一個上百項表單項的系統,當模版的量堆積到一定程度時,你會想吐血。好不容寫瞭上千行模版,以為完事瞭,結果再接一個新的媒體,又是從一個新的開始……並且,你要再寫一個上千行的 template
和各種表單項之間的聯動邏輯,也是很痛苦的…
所以,怎麼提升復用能力 , 怎麼讓復雜的表單 變得清晰好維護,就是筆者的出發點的~
二、設計 & 實現
1. 設計協議
首先我們思考下我們的每個表單項目需要一些什麼:
- type 類型。比如
input
、select
、radio
等等 - label 表單項的名稱/描述。
- formKey 字段名。我們提交數據到後段的字段名,比如
form.name
的'name'
- value 存放表單值。表單上
v-model
所綁定的值 - options 配置項。比如配置
multiple
、disabled
、 是否顯示 等等
好瞭,有瞭以上這些點,我們試著把案例中的 表單1 用協議表達出來:
<el-form-item label="活動名稱" v-if="form.area === 'area1'"> <el-input v-model="form.name"></el-input> </el-form-item> <el-form-item label="活動區域"> <el-select v-model="form.area">...</el-input> </el-form-item>
我們可以用協議這樣去描述它
[ { type: 'el-input', label: '活動名稱', formKey: 'name', value: '', // 默認值為空字符串 options: { vIf: [ // 表示:當 form.area === 'area1',才顯示 { relationKey: 'area', value: 'area1' } ] } }, { type: 'el-select', label: '活動區域', formKey: 'area', value: 'area1', options: { multiple: true } } ]
是不是有點內意思瞭?如果把 開發巨型表單系統 轉換成 編寫JSON ,是不是很爽?
2. 實現渲染器
配置是有瞭,但是怎麼把配置轉換成我們真實的表單呢?如果直接開幹,我想大部分可能會先這樣下手,比如:
<template> <el-form-item :label="props.label"> <el-input v-if="props.type === 'el-input' && ...業務聯動邏輯" :disabled="props.disabled" v-model="props.value" ... /> <el-select v-if="props.type === 'el-select' && ...業務聯動邏輯" :disabled="props.disabled" multiple="props.multiple" v-model="props.value" ... >...</el-select> </el-form-item> </template>
好瞭,大傢觀察一下上面的 template
中,有沒發現很多冗餘的代碼。如果我們需要給組件傳入 props
比如例子中的 disabled
、 multiple
;控制 v-if
等等。。我們有多少個組件,這些重復的代碼就要寫多少次。如果以後有需要給所有組件傳多一個 props
,我們就要編輯n次~記住!我們配置化就是要提高效率的,所以這樣是不行的~
在此,筆者就建議編寫 render函數。render函數 的場景 & 對應的好處,大傢可以看看 官方文檔 對其的講解~
- 這裡不會深入介紹 render函數 ,如果還不知道的,大傢隻需要記住:
Vue
隻認 render函數,平時我們.vue文件
寫的template
,經過編譯之後就是 render函數 - render函數 作用就是返回一個
vNode
。我們vue2
初始化項目時寫的:render (h) => h(App)
是不是就似曾相識瞭呢?
都說 React 寫 jsx
比 Vue 寫 template
更好寫邏輯,那我們也用 render函數 ,好寫邏輯~ 😝 (當然,如果你對render函數不是特別熟悉,那麼寫template也是可以的)
接下來,我們看看,如何通過render函數,把我們的表單項做出來,以上述案例其中一個為例子:
<el-form-item label="活動名稱"> <el-input v-model="form.name"></el-input> </el-form-item>
這一段要怎麼通過render函數表述出來?根據官方文檔,我們理清三個參數是什麼就可以瞭:
createElement( 'div', // {String | Object | Function},一個 HTML 標簽名、組件選項對象,或者... {}, // 一個與模板中 attribute 對應的數據對象 [] // {String | Array},可以理解成時 children 節點 )
接著,我們直接開幹:
<script> export default { name: "FormItemDemo", render(createElement) { return createElement('el-form-item', { props: { label: '活動名稱' } }, [ createElement('el-input') // input組件 ]) } } </script>
在 App組件 中引用這個 FormItemDemo組件,代碼如下
<template> <div> <el-form label-width="100px"> <FormItemDemo /> </el-form> </div> </template> <script> import FormItemDemo from "./components/FormItemDemo.vue"; export default { name: 'App', components: { FormItemDemo } } </script>
這時候,頁面上就出現瞭我們的 input表單項 瞭
初始工作已經做完瞭,接下來的就是讓我們把 render函數 的一些動態數據用變量代替,跟我們的 配置config 結合起來。
❗️❗️❗️註意,render函數很靈活,第一個參數可以是字符串、組件對象、function。大傢不要被demo所局限,很多場景是需要我們定制一些組件,然後應用到我們的配置化中,而不是直接使用element-ui的原生組件。筆者項目中用的組件都是經過業務場景封裝後export出來的組件對象,再配置到type中,最後render函數接收的是一個組件對象,具備業務能力的組件。因此,我們可以在自己實現的組件中定制自己的業務邏輯。
3. render函數 & 配置數據
要說 render函數 也不是真的完美,畢竟要自己去實現譬如 v-if
、 v-model
這種指令,但是沒問題,它帶給我的便利給大,所以我能接受。
正式演示配置化的實現時,筆者先聲明一點:這裡的隻是 demo 級別的,具體實戰到項目要根據業務場景。筆者做業務時,是對 select
、 cascader
等組件都封裝瞭一層。因為很多時候我們的下拉數據要去後端拿,封裝後組件可以通過傳入的 params
和 urlPath
去獲取數據。所以,大傢更要關註思路,然後根據業務場景自己去思考、實現即可。
首先配置數據如下:
export default [ { type: 'el-input', label: '活動名稱', formKey: 'name', value: '', // 默認值為空字符串 options: { vIf: [ // 表示:當 form.area === 'area1',才顯示 { relationKey: 'area', value: 'area1' } ] } }, { type: 'el-select', label: '活動區域', formKey: 'area', value: 'area1', options: { multiple: true }, optionData: [ // 這裡模擬去後端拉回數據 { label: '區域1', value: 'area1' }, { label: '區域2', value: 'area2' } ] } ]
我們把 render函數 改造後,變成這樣
<script> export default { name: "FormItemDemo", props: { itemConfig: Object // 接收配置,外部傳入 }, render(createElement) { return createElement('el-form-item', { props: { label: this.itemConfig.label // 表單項的label } }, [ // 表單組件 createElement(this.itemConfig.type, { props: { value: this.itemConfig.value // 這裡是自己實現一個 v-model }, on: { change: (nVal) => { // 這裡是自己實現一個 v-model this.itemConfig.value = nVal } } }, this.itemConfig.optionData && this.itemConfig.optionData.map(option => { // 這裡隻是 本demo 處理 el-select 的 option 數據,實際大傢根據具體業務來實現即可 return createElement('el-option', { props: { label: option.label, value: option.value } }) })) ]) } } </script>
接下來我們在 app組件 中同時應用我們的 配置 + FormItemDemo 組件:
<template> <div> <el-form label-width="100px"> <FormItemDemo v-for="item in config" :item-config="item" /> </el-form> </div> </template> <script> import FormItemDemo from "./components/FormItemDemo.vue"; import config from "./config"; export default { name: 'App', components: { FormItemDemo }, data () { return { config } } } </script>
這時候我們看下頁面長什麼樣?
ok!!!實現瞭,接下來,我們隻需要根據業務需求不斷豐富我們的 FormItemDemo
組件即可。這裡,筆者會帶著大傢一起實現一個 聯動顯示隱藏 、 下拉框多選 的功能~相信看完後,你一定有醍醐灌頂的感受,然後就可以自己根據業務去實現需求瞭。
4.豐富組件能力,實現業務
我們先來看第一個需求:
- 當活動區域的值為 “area1” 時, 活動名稱才展示
分析一下這個需求,我們的 input組件 跟 select組件 聯動,所以 input組件 要獲取 select組件 的值,這時候,我們可以在 app組件 中,將整個 config
傳入 FormItemDemo組件。
再回看一下我們的配置,我們把顯示隱藏的配置放在 options.vIf
中(這裡筆者設計成瞭一個數組,因為碰到的業務經常存在一個表單會受到好幾個表單值聯動的),所以 FormItemDemo組件 需要用這個來判斷是否執行本次 render
以此來實現 v-if
。如圖所示:
筆者用瞭一個 computed
去實現這個需求。大傢可以不用仔細深入,隻要知道 componentShow
的作用就是,找到聯動的 relationKey
的 config
中的 value
值,判斷是否跟配置的一致。
computed: { componentShow () { const vIfArr = this.itemConfig?.options.vIf if (!vIfArr) return true const relationArr = this.config.filter(config => vIfArr.find(vIf => vIf.relationKey === config.formKey)) for (const relationItem of relationArr) { const vIfItem = vIfArr.find(_ => _.relationKey === relationItem.formKey) // 這裡就是判斷 聯動的表單值 是否不滿足 可以顯示 的條件,不滿足則不顯示 if (relationItem.value !== vIfItem.value) return false } return true } }
模擬實現 v-if
,隻需要把上述計算屬性在 render 的開頭進行判斷即可
ok,直接看下結果!兩個表單項之間的聯動完成瞭
接下來的需求,大傢自行思考下怎麼實現即可。其實都是異曲同工的
- 控制 select 多選 、 單選
- 添加 filter 屬性 …
好瞭,這樣子,基本上就大功告成瞭,隻要我們把 FormItemDemo
的業務邏輯都實現瞭,後續不管開發N個表單系統,我們隻需要配置就完事瞭,摸魚也就是板上定釘的事情瞭~但是,一個優秀的前端,怎麼能這麼算瞭呢?我們好歹也要做一點優化是吧?
三、配置靜態化
細心的朋友可能已經發現瞭,我們上述實現配置化的時候,直接把整個 config 賦值給 data ,然後在 App組件 的 el-form
中 v-for
使用,那這樣避免不瞭就會出現一些尷尬的事情,比如我們看下圖:
沒錯,就如大傢所見,所有的屬性都帶上瞭 getter
和 setter
,這意味著,他們都被初始化成瞭響應式的。由於我們的業務是非常復雜的,所以當我們真的要用一個 config
去描述整個表單時,config
的規模遠不止以上這麼點,並且整個配置對象的層級可能還會比較深,如果這樣的話就可能會有性能問題瞭。
熟悉 Vue2
的同學都知道,初始化的時候,會對 data
做一個深度遍歷添加 get
、 set
變成響應時數據,並且在組件執行 render函數 時,會訪問到這些對象的屬性。一旦訪問到,就會觸發 data屬性 的依賴收集動作,如果無腦多的屬性時,這個 get方法
將被無腦執行。
這肯定不符合我們這種優秀的前端的作風的是吧?怎麼搞,優化唄。思路我們也不自己想瞭,直接拿 尤大 的處理來耍吧哈哈哈。😝
有深入看過 Vue2 源碼的同學,對 __ob__
這個屬性一定不陌生,上面截圖也有這個屬性,但是大傢發現沒,這個 ob屬性
卻沒有對應的 get
、set
。讓我們打開源碼,看看 尤大 做瞭什麼?
首先,在進行響應式處理之前,調用瞭一個 def
的方法,這裡 第四個參數 是沒傳的
看看 def 的具體實現,其實就是重新定義這個對象的屬性。由於沒傳 enumerable
,所以此時 __ob__
的 enumerable
為 false
這樣有什麼用?一句話概括就是無法遍歷到這個屬性,後續響應式初始化時也會跳過這個屬性。不清楚的夥伴可以看看筆者寫的一個 demo
來加深理解:
沒錯,我們這裡也是采用同樣的方式對我們的 config
進行 非響應式 優化。其實整個 config
數據,我們隻是需要保證 value
是響應式的即可,其他很多描述性數據都是大可不必的。那我們就把其他字段進行一個優化~
// 優化函數 function optimize (array) { return array.reduce((acc, cur) => { for (const key of Object.keys(cur)) { if (key === 'value') continue // 將不是 value 的屬性都進行非響應式優化 Object.defineProperty(cur, [key], { enumerable: false }) } acc.push(cur) return acc }, []) }
具體就不展開介紹函數實現瞭,大傢 get 到思路就 ok 瞭(有興趣的可以細看一下)~
此時,我們再打印 config 來看看變成什麼樣瞭:
ok,這下就舒服瞭~終於大功告成瞭!!!
寫在最後,其實這個是我差不多一年前的實踐瞭,一直在分享與不分享之間徘徊。因為這類型的文章,適用性一般般,需要有一定的業務場景應用才能比較有用,但是嘛,說不定有跟我之前遇到一樣境況的小夥伴,又或者是大傢有更好的見解建議能為我帶來提升,所以我還是決定分享出來。
平時更多的開發夥伴都會吐槽天天在業務中摸爬打滾,項目沒有亮點,這個是不可否認也不可避免的現狀吧~企業本就是為瞭盈利生產,不可能每時每刻都能有技術挑戰的活下來,更多時候我們可能是平庸的業務中度過。但是,我們能否在平庸的業務中再拓展出更高效、更便捷的方式,說不定這就是一種突破和亮點吧。
雖然你們可能不會認可,但是這的的確確是讓我開發效率提升大半以上的方案,並且出去面試我也是把這一條放在第一點去寫。它可能不是特別亮點,不是特別完善,但對我個人而言,我從 設計 – 實現 – 優化 的每一步都有自我的思考且落地,對我自己而言,它就是亮點瞭,也許很少人會註意到配置優化這一步,筆者個人覺得這一步算是點睛之筆,因為深入到瞭細節。最後,希望能幫助到有需要的大🔥,大傢可以早點下班,留給多的時間給自己去享受生活!沒有絕對完美的技術和方案,隻有合適與否,根據場景選擇方案哈,更多關於vue2 element-ui前端配置化的資料請關註WalkonNet其它相關文章!
推薦閱讀:
- 解決Antd中Form表單的onChange事件中執行setFieldsValue不生效
- antd vue v-decorator的取值與賦值問題
- v-html渲染組件問題
- element ui提交表單返回成功後自動清空表單的值的實現代碼
- 詳解vue3.2中setup語法糖<script lang="ts" setup>