vue組件之高內聚、低耦合(一次組件自定義v-model的封裝經歷)


  前段時間公司開通了積分機制,關乎到升級大計。看着自己博客里的兩篇隨筆,我哭了。三年了。。只寫了兩篇博客。哎,平常實在是不想寫,甚至連引用別人的文章都不願意。現如今沒辦法了,寫吧。

  本來想寫一個關於vue插槽和動態組件的博客着。寫了一個星期了,還沒寫完(上班沒時間,下班不想動)。前兩天新調到人資寫H5遇到了一個組件傳值的小問題。沒改動,還是讓前輩給改的。趕腳臉很疼。先看一下這個項目的交互圖。

 

  可以看到的是這里有一個選擇組件,有單選和多選的功能。通過點擊觸發選項變化,單選的話點擊標簽如果已經選中沒變化,如果沒選中,狀態改為選中,其他已選中的取消選中;多選的話點擊不會對其他選項造成影響,只對自己的狀態進行取反操作。下面是組件封裝之前的代碼

 1 /** queryTab組件*/
 2 
 3 <template>
 4     <div id="query_tab" @click.stop :class="activeValue == 'postIdList' ? 'query_tab_padding': ''">
 5         <div class="queyr_tab_title">
 6             {{title}}
 7             <span 
 8                 v-if="title=='崗位'||title=='狀態'"
 9                 class='able-more-query'>(可多選)
10             </span>
11         </div>
12         <van-row gutter="20" class='query_tabs_btn'>
13             <van-col span="8"  
14                     v-for="(item,index) in tabs"
15                     :key="index">
16                     <van-button 
17                         @click="selsctQuery(item)"
18                         :class="active(item) ? 'active':''"
19                         size="small"
20                         type="text">
21                         {{item.dictValue}}
22                     </van-button>
23             </van-col>
24         </van-row>
25         <slot></slot>
26     </div>
27 </template>
28 <script>
29 export default {
30     props:{
31         // 字典名稱
32         title:{
33             type:String,
34             default:''
35         },
36         // 字典列表
37         tabs:{
38             type:Array,
39             default: ()=>[]
40         },
41         // 多選
42         multi:{
43             type:Boolean,
44             default:false
45         },
46         // 當前選中
47         activeValue: {
48             type:String,
49             default:''
50         },
51         // 表單
52         form:{
53             type:Object,
54             default:()=>({})
55         },
56         // 對比字段
57         valueKey: {
58             type:[Array,String],
59             default:"dictId"
60         }
61     },
62     computed:{
63         /**  @information 當前高亮 */
64         active() {
65             let {form,multi,activeValue,tabs} = this;
66             return data=>{
67                 if(!multi){
68                     // 單選判斷選中的那個
69                     return form[activeValue] === data.dictId;
70                 }else {
71                     // 多選
72                     let index = form[activeValue].findIndex(item=>item === data.dictId);
73                     return index !== -1;
74                 }
75             }
76         }
77     },
78     methods:{
79         /** @information 選擇查詢條件  */
80         selsctQuery({dictId}){
81             let { multi,form,activeValue:key } = this
82             let current = dictId
83             if(multi){
84                 let value = form[key]?[...form[key]]:[]
85                 let i = form[key].findIndex(el => el===dictId)
86                 if(i===-1){
87                     value.push(dictId)
88                 }else{
89                     value.splice(i,1)
90                 }
91                 current=value
92             }
93             this.$emit('selectQuery',{dictId:current,key})
94         }
95     }
96 }
queryTab.vue

從代碼可以看出,它接收了 共6個變量

  字典名稱 title,

  字典列表 tabs,

  是否多選 multi,

  當前選擇器對應form表單的字段 activeValue,

  查詢表單對象 form,

  對比字段 valueKey ,(這個字段我估計之前設計的應該是字典名稱和字典值的字符串描述或者數組描述,從 default:"dictId" 看出來的,不過后來沒有用。)

  1個計算屬性方法遍歷更新標簽點擊(高亮)狀態,

  和一個標簽點擊事件 selsctQuery根據是單選還是多選判斷去判斷current的值,然后將結果通過$emit 傳給外部selectQuery事件,那么我們在看看這個組件在頁面中是怎么應用的。

 1 /** queryTab組件的調用*/
 2 <template>
 3   .
 4   .
 5   .
 6 <QueryTab 
 7   :title="selectQueryTab.label"
 8   v-if="selectQueryTab.value !== 'other' && selectQueryTab.value" 
 9   :tabs="tabDict"
10   :activeValue='selectQueryTab.value'
11   :form='form'
12   @selectQuery='selectQuery'
13   :multi="selectQueryTab['multi']">
14   <div :class="[selectQueryTab.value == 'postIdList' ? 'btn_abs': 'bottom_btn']" v-if="selectQueryTab.value !== 'source'">
15     <van-button type="default" @click="resetCurrent">重置</van-button>
16     <van-button type="primary" @click="query">查詢</van-button>
17   </div>
18 </QueryTab>
19 <OtherQuery  v-if="selectQueryTab.value == 'other'" 
20   :form='form'
21   :salaryDictId="salaryDictId"
22   @selectOtherQuery='selectOtherQuery'  
23   @reset='resetOther'
24   @query='query'
25   @setSalaryDict="salaryDictId = $event"
26   @changePicker='changePicker'>
27 </OtherQuery>
28   .
29   .
30   .
31 
32 </template>
33 <script>
34 export default {
35     data() {
36         return {
37         form:{
38                 postIdList:[], // 崗位                                               
39                 memberStatus:[], // 狀態
40                 source:'', // 來源
41           /** 只有以上三項是外邊的,其他都是更多里的*/
42                 memberType:'', // 類型
43                 memberSex:'', // 性別
44                 education:'', // 簡歷學歷
45                 memberId:'', // 人員id
46                 type:'', // 采集類型
47                 collectionTime:[], // 采集時間
48                 collectionEducation:"",   // 采集學歷
49                 age:[], // 年齡
50                 recruitmentChannel: '', //招聘渠道
51                 registrationTime:[], // 注冊時間
52                 expectationPlace:[],   // 期望工作地點
53                 expectationPost:"",   // 期望職位
54                 expectationSalary:"",   // 期望薪資
55                 salaryDictId:null,
56                 isdelete:"",   // 刪除狀態
57                 workExperience:"",  // 工作經歷
58                 schoolName:""   // 學校名稱
59             },
60             otherKey: [
61               'memberSex',
62               'age',
63               'collectionTime',
64               'registrationTime',
65               'expectationPlace',
66               'expectationSalary',
67               "expectationPost",
68               "collectionEducation",
69               "education",
70               "workExperience",
71               "schoolName",
72               "type",
73               "isdelete",
74               "recruitmentChannel"], // 更多選項的key
75         }
76     },
77     
78     methods:{
79         /**  選擇查詢條件 */
80         selectQuery({dictId:val,key}) {
81             let {multi,value} = this.selectQueryTab;
82             // 判斷是單選多選
83             if(!multi){
84                 // 單選替換
85                 this.form[key] = this.form[key] ===  val ? '' : val;
86                 value == 'source' && (this.show = false)
87                 this.queryList();
88             }else {
89                 this.form[key] = val;
90             }
91         }
92      /**  選擇其他 */
93         selectOtherQuery({dictId,key}) {
94             this.form[key] = dictId;
95         }
96   }
97 }
98 </script>
queryTab組件的調用

  在外邊調用queryTab組件就是把該傳的值傳進去然后用selectQuery事件接收值變化然后改變form的屬性。(這里沒有傳valueKey,我最后發現是在請求字典的接口里統一做了處理,轉成dictId、dictValue)。

  單看的話,這么寫也沒什么。其實我以前在項目里也是這么干的。可是這里有一個問題就是他有3個(崗位、狀態、來源)在外邊一層,而更多地值(更多里的)卻在下一層組件里如果都放到一個組件太亂,如果將組件分開。就會有子組件傳值給父組件,然后父組件再傳值給爺爺組件的麻煩,而且還是每一個值改變一次,就傳一次。再來看一下otherQuery里面的代碼

 1 /** otherQuery組件*/
 2 <template>
 3     <div  @click.stop id="person_manage_other_query" :style="'maxHeight:'+height+'px'">
 4         <!-- 性別 -->
 5         <QueryTab
 6                 title="性別"
 7                 :tabs='sexDictionary'
 8                 :form='form'
 9                 activeValue='memberSex'
10                 @selectQuery="$emit('selectOtherQuery',$event)">
11         </QueryTab>
12         <!-- 期望地點 -->
13         <QueryTab
14                 title="期望地點"
15                 :tabs='expectationPlace'
16                 :form='form'
17                 :multi="true"
18                 activeValue='expectationPlace'
19                 @selectQuery="$emit('selectOtherQuery',$event)">
20         </QueryTab>
21       .
22       .
23       .
24         <div class="bottom_btn">
25             <van-button type="default" @click="$emit('reset')">重置</van-button>
26             <van-button type="primary" @click="$emit('query')">查詢</van-button>
27         </div>
28     </div>
29 </template>
30 <script>
31 export default {
32     props:{
33         form:{
34             type:Object,
35             default:()=>({})
36         }
37     },
38 }
39 </script>
otherQuery.vue組件

   好像otherQuery組件沒有干什么事情只是通過下面這句代碼將值再次傳給了外邊去處理。

1 @selectQuery="$emit('selectOtherQuery',$event)"

  好吧,現在看來組件這么寫也沒什么太大的問題,用起來也不錯,最少現在一直健壯的運行着。我就是趕腳在改bug的時候要看好多的地方以為關聯性太強了。而且值改變還要去觸發外邊改變值,最少在我剛接這個項目的時候我改起來不是很順手,里邊外邊都需要關注。(嗯,我就是在找理由,要不我的博客寫什么呀。為了積分只能說這么用不好)還是將組件重新封裝一下,進行一下解耦才對得起‘高內聚,低耦合’這句話。

  那么有沒有一個方法讓組件之間的關聯性不這么緊密呢,這個時候我想起了vue的v-model我把組件進行了一把封裝,下面看一下代碼

 1 /**新queryTab組件*/
 2 <template>
 3     <div id="query_tab" @click.stop >
 4         <div class="queyr_tab_title">
 5             {{title}}
 6             <span  v-if="mult" class='able-more-query'>(可多選) </span>
 7         </div>
 8         <van-row gutter="20" class='query_tabs_btn'>
 9             <van-col span="8"  
10                   v-for="(item,index) in tabs"
11                   :key="index">
12                   <van-button 
13                       @click="selectQuery(index)"
14                       :class="active(item) ? 'active' : ''"
15                       size="small"
16                       type="text">
17                       {{item[lable]}}
18                   </van-button>
19             </van-col>
20         </van-row>
21         <slot></slot>
22     </div>
23 </template>
24 <script>
25 export default {
26     name: 'QueryTabCom',
27     model: {
28         prop: 'select',
29         event: 'up'
30     },
31     props:{
32         // 字典名稱
33         title:{ type: String, default: '' },
34         // 字典列表
35         tabs:{ type:Array, default: () => ([]) },
36         // 多選
37         mult:{ type:Boolean, default: false },
38         // 顯示標簽
39         lable: {type: String, default: 'dictName'},
40         // 標簽對應值
41         value: {type: String, default: 'dictId'},
42         // model綁定值再組件內的引用
43         select: {type: [String , Number , Array], default: ''}
44     },
45 
46     data() {
47         return {
48             // 當前選中列表
49             actives: {},
50         }
51     },
52     methods: {
53         /**標簽點擊事件 */
54         selectQuery(ind) {
55             let {actives, tabs, value, mult} = this
56             let select= null
57             if(!mult) {
58           //如果是單選點擊的是已選的選項就清空,不是已經選擇的選項就切換
59                 select = tabs[ind].dictId === this.select ? null : tabs[ind].dictId 
60             }else{
61           //如果是多選的話現將選項本身取反然后過濾返回正確的列表
62                 actives[ind] = !actives[ind]
63                 let list = Object.keys(actives).filter( key => actives[key])
64                 select = list.map( ind => tabs[ind][value])
65             }
66        //更新值
67             this.$emit('up', select)
68         }
69     },
70     computed: {
71      /** 計算顯示高亮狀態*/
72         active() {
73             let { mult, tabs, select } = this;
74             return obj => {
75                 if(!mult){
76                     // 單選判斷選中的那個
77                     return (select || select === 0 || select === '0') && select.toString() === obj.dictId.toString() ;
78                 }else {
79                     // 多選
80                     let has = select.find( item => item.toString() === obj.dictId.toString());                    
81                     return Boolean(has || has===0);
82                 }
83             }
84         }
85     }
86 }
87 </script>
重新封裝的queryTab組件

  好像和原來的組件也沒什么區別是吧,還是有點區別的。看這段代碼

1     model: {
2         prop: 'select',
3         event: 'up'
4      },

  這個是在v2.2.0新增的一個特性

  一個組件上的 v-model 默認會利用名為 value 的 prop 和名為 input 的事件,但是像我的自定義組件就用到了value屬性,而且組件里面也沒有input事件,幸好現在vue就有了這么一個新的東東專門用來做自定組件的v-model。其中的prop就是你自定義組件v-model綁定的值在組件內部的引用(可以是任何類型,但是在這里光聲明是不夠的,還要再porps里邊接收一下),event就是組件同步數據的時候要emit的事件名稱。這樣vulue屬性和input事件就解放出來了(以前做自定義的v-model還要考慮value屬性的沖突問題,現在model 選項可以用來避免這樣的沖突),這里我props里邊多接收了兩個變量lable,和value分別對應字典的key和value並且設置了默認值。

  下面再看看外邊引用這個組件的時候的新代碼是什么樣子的。

 1 <template>
 2   .
 3   .
 4   .
 5 <QueryTab 
 6   :title="selectQueryTab.label"
 7   v-if="selectQueryTab.value !== 'other' && selectQueryTab.value" 
 8   :tabs="tabDict"
 9   v-model='form[selectQueryTab.value]'
10   lable='dictValue'
11   value='dictId'
12   :multi="selectQueryTab['multi']">
13   <div :class="[selectQueryTab.value == 'postIdList' ? 'btn_abs': 'bottom_btn']" v-if="selectQueryTab.value !== 'source'">
14     <van-button type="default" @click="resetCurrent">重置</van-button>
15     <van-button type="primary" @click="query">查詢</van-button>
16   </div>
17 </QueryTab>
18 <OtherQuery  v-if="selectQueryTab.value == 'other'" 
19   :form='form'
20   :salaryDictId="salaryDictId"
21   @selectOtherQuery='selectOtherQuery'  
22   @reset='resetOther'
23   @query='query'
24   @setSalaryDict="salaryDictId = $event"
25   @changePicker='changePicker'>
26 </OtherQuery>
27   .
28   .
29   .
30 
31 </template>
32 <script>
33 export default {
34     data() {
35         return {
36         form:{
37                 postIdList:[], // 崗位                                               
38                 memberStatus:[], // 狀態
39                 source:'', // 來源
40           /** 只有以上三項是外邊的,其他都是更多里的*/
41                 memberType:'', // 類型
42                 memberSex:'', // 性別
43                 education:'', // 簡歷學歷
44                 memberId:'', // 人員id
45                 type:'', // 采集類型
46                 collectionTime:[], // 采集時間
47                 collectionEducation:"",   // 采集學歷
48                 age:[], // 年齡
49                 recruitmentChannel: '', //招聘渠道
50                 registrationTime:[], // 注冊時間
51                 expectationPlace:[],   // 期望工作地點
52                 expectationPost:"",   // 期望職位
53                 expectationSalary:"",   // 期望薪資
54                 salaryDictId:null,
55                 isdelete:"",   // 刪除狀態
56                 workExperience:"",  // 工作經歷
57                 schoolName:""   // 學校名稱
58             },
59             otherKey: [
60               'memberSex',
61               'age',
62               'collectionTime',
63               'registrationTime',
64               'expectationPlace',
65               'expectationSalary',
66               "expectationPost",
67               "collectionEducation",
68               "education",
69               "workExperience",
70               "schoolName",
71               "type",
72               "isdelete",
73               "recruitmentChannel"], // 更多選項的key
74         }
75     },       
76 }
77 </script>
封裝之后調用queryTab組件

  是不是感覺好多了,沒有了只改變之后的事件了,因為通過v-model綁定了嗎。
  這里再提一個新的問題如果某一個單選屬性后端要的是數組格式,而其他的單選屬性要的是字符串格式怎么辦。這里我又畫蛇添足的加入了一個type屬性用來接收組件希望的綁定值類型。
  看代碼。

 1 <template>
 2     <div id="query_tab" @click.stop >
 3         <div class="queyr_tab_title">
 4             {{title}}
 5             <span  v-if="mult" class='able-more-query'>(可多選) </span>
 6         </div>
 7         <van-row gutter="20" class='query_tabs_btn'>
 8             <van-col span="8"  
 9                   v-for="(item,index) in tabs"
10                   :key="index">
11                   <van-button 
12                       @click="selectQuery(index)"
13                       :class="active(item) ? 'active' : ''"
14                       size="small"
15                       type="text">
16                       {{item[lable]}}
17                   </van-button>
18             </van-col>
19         </van-row>
20         <slot></slot>
21     </div>
22 </template>
23 <script>
24 export default {
25     name: 'QueryTabCom',
26     model: {
27         prop: 'select',
28         event: 'up'
29     },
30     props:{
31         // 字典名稱
32         title:{ type: String, default: '' },
33         // 字典列表
34         tabs:{ type:Array, default: () => ([]) },
35         // 多選
36         mult:{ type:Boolean, default: false },
37         // 顯示標簽
38         lable: {type: String, default: 'dictName'},
39         // 標簽對應值
40         value: {type: String, default: 'dictId'},
41         // model綁定值再組件內的引用
42         select: {type: [String , Number , Array], default: ''},
43      // model希望綁定值類型
44      type: {type: String, default: 'string'}
45 
46     },
47 
48     data() {
49         return {
50             // 當前選中列表
51             actives: {},
52         }
53     },
54     methods: {
55         /**標簽點擊事件 */
56         selectQuery(ind) {
57             let { actives, tabs, value, mult, type } = this
58             let select= null
59             if(!mult) {
60           //如果是單選點擊的是已選的選項就清空,不是已經選擇的選項就切換
61                 select = tabs[ind].dictId === this.select ? null : tabs[ind].dictId 
62           select = type === 'string' ? select.toString : [select]
63             }else{
64           //如果是多選的話現將選項本身取反然后過濾返回正確的列表
65                 actives[ind] = !actives[ind]
66                 let list = Object.keys(actives).filter( key => actives[key])
67                 select = list.map( ind => tabs[ind][value])
68           select = type === 'string' ? select.join(',') : select
69             }
70        //更新值
71             this.$emit('up', select)
72         }
73     },
74     computed: {
75      /** 計算顯示高亮狀態*/
76         active() {
77             let { mult, tabs, select } = this;
78             return obj => {
79                 if(!mult){
80                     // 單選判斷選中的那個
81                     return (select || select === 0 || select === '0') && select.toString() === obj.dictId.toString() ;
82                 }else {
83                     // 多選
84                     let has = select.find( item => item.toString() === obj.dictId.toString());                    
85                     return Boolean(has || has===0);
86                 }
87             }
88         }
89     }
90 }
91 </script>
最終的queryTab組件

  當然了,目前只有數組和字符串兩種,為什么沒有number呢(這里不得不吐槽一下,按說吧id這個東西作為鍵值為了加快搜索在數據庫里邊就都用自增int的,可是在咱們這里我愣是遇到了帶字母組合的id這種奇葩的情況,所以為了不再字符轉數字的時候出現NaN我只能放棄number這個類型了)。
  到這個時候是不是應該有掌聲了,本來我也以為大功告成了。可是現實給了我一個大嘴巴,好疼好疼的那種。看咱們最開始的交互圖,這里不是有一個來源嗎,它是單選,他觸發查詢是即時的,在只改變的時候就觸發了,因為沒有查詢。在源代碼里是這樣處理的。

 1 selectQuery({dictId:val,key}) {
 2     let {multi,value} = this.selectQueryTab;
 3     // 判斷是單選多選
 4     if(!multi){
 5         // 單選替換
 6          this.form[key] = this.form[key] ===  val ? '' : val;
 7         value == 'source' && (this.show = false)
 8         this.queryList();
 9     }else {
10          this.form[key] = val;
11     }
12 }    

  也就是說每一次是單選的時候就觸發查詢(不得不是真的很鬼的,因為在外部只有來源一個 單選,而otherQuery組件的事件又是通過selectOtherQuery方法做的更新)可是我總不能 在來源改變的時候單獨觸發一個事件來做查詢吧,那樣就有違我重新封裝這個組件的初衷了。而 在外邊又是所有屬性都放到form這個對象里邊了,就算是深度監聽也只是能知道form里有屬性 改變,而不知道具體哪個屬性改變。問了一下度娘,解決方式還挺多。
  一個是computer加watch就是聲明一個計算屬性返回‘來源’這個屬性,然后就可以監控新 計算的這個屬性區出發查詢事件了。還是貼一下代碼吧。

 1   computed: {
 2         source(){
 3             return this.form.source
 4         }
 5     },
 6     watch: {
 7         source(n,o){
 8             this.queryList()
 9         }
10     }

  另一個就是純利用計算屬性,代碼是這樣的

1 computed: {
2     source(){
3         this.queryList()
4         return this.form.source
5     }
6 }

  我用的是前者,如果是用后一種方法的話還要在頁面加一個dom節點的引用還要隱藏,因為計算出來的屬性並沒有用,只是用來監測值的改變用的。

1 <div v-show = 'false'>{{selectSource}}</div>

  當然實時監測對象屬性的方法還有很多,我只是隨便挑了兩個演示一把,畢竟這個不是今天的主題。

  至此我的改造都弄完了,自測了一把,很順滑。趕腳么么噠。寫博客沒經驗,文中如有不對的地方,歡迎大家指出。
  
  個人郵箱18231590815@163.com

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM