vue的學習曲線不是很陡(相比其它框架,如anglarjs),官方文檔比較全面,分爲基礎篇和高級篇。我們剛開始學習的時候,肯定像引用jquery那樣,先把vue的js引進來,然後學習基礎內容。如果僅僅停留在基礎內容,沒有學習vue組件的話,我覺得也就沒有什麼意思了。vue的核心思想——組件,是一個很好的東西,它提供了功能複用。
1、單文件組件
所謂單文件組件,顧名思義,一個vue格式的文件就是一個組件。好比python和js的模塊,文件即模塊。vue組件帶有自己的模板,可以理解爲視圖,也帶有自己數據及邏輯。數據可以從外部來,通過Prop接收。用圖形表示:
由此可見,單文件組件就是一個完整而獨立的體系。注意,style有個屬性scope表示僅作用於當前組件。wpf當中的控件,有自己的xaml(視圖),邏輯,可以外部綁定數據源。我覺得vue組件類似wpf中的用戶控件。因爲用戶控件組合了基礎的控件,比如Button、TextBlock等等。用戶控件可直接當成一個獨立的組件使用。它和vue組件一樣,都是從外部獲取特定的信息,然後構建自己內部的數據以及邏輯。其實組件體現的是面向對象中的“封裝”思想。
2、動態組件&異步加載
有時候讀了官方的文檔,還是不明白,這時候就需要上網搜搜相關資料。好比《聖經》或者《道德經》中的經文是需要慢慢揣摩和體會的。當然vue的動態組件、以及組件的異步加載也是需要在實踐當中慢慢體會的。下來分享一個我在項目中使用的例子:
在後臺管理頁面中,編輯、增加一條信息,這時候需要在彈出頁面中操作。因此,我就封裝了一個彈出模態框(帶有遮罩效果)的組件。而編輯頁面是另外的一個組件。所以,需要把編輯頁面的組件“送到”彈出框的組件中呈現。有點像“裝飾器模式”,不要原生態地呈現編輯組件,而是把它包裝一番,再呈現,如下圖:
後臺管理中像這樣的編輯頁面非常多,所以彈出框組件的意義就在這,複用。我上面說了需要把“編輯軟件資源”的組件,送到彈出框組件顯示。如何送呢?其實也不難。我把這個組件作爲彈框組件的子組件。那麼這個彈框組件有很多個編輯組件。現在問題來了,如何控制它們顯示?當我點擊“編輯軟件資源”的時候,彈出對應的頁面,當我點擊編輯新聞的時候,它要彈出新聞的頁面,難道我要控制組件的顯示隱藏嗎?這個情況有點像“Tab”,任何時候,只能呈現一個TabItem,那麼其它的只能隱藏掉。好了,我們也可以這麼做。還有另外一個問題,我們如何導入這些組件,一次性import多個組件,貌似也沒有什麼問題。這會不會影響頁面加載的性能呢?我想肯定會。
我想到了秦腔中的“變臉”,對,這個很有意思,一個人通過變臉可以扮演多個人。和演員一樣,比如最近的一個電視劇中景甜扮演了“奉劍”和“千湄”兩個角色。vue裏面的動態組件就是如此,一個組件總是“扮演”各種組件。異步加載,當我需要用你的時候,再去import,這顯然是合理的。說了這麼多,我們看看代碼:
1 <template> 2 <transition name="modal"> 3 <div class="modal-mask"> 4 <div class="modal-wrapper"> 5 <div class="modal-container" :style="{width:width,height:height}"> 6 7 <div class="modal-header"> 8 <slot name="header"> 9 {{title}} 10 </slot> 11 <button class="modal-default-button" @click="close"> 12 X 13 </button> 14 </div> 15 16 <div class="modal-body" :style="{height:bodyHeight,width:bodyWidth}"> 17 <slot name="body"> 18 <component :is="currentComponent" @close="close" :id="id"></component> 19 </slot> 20 </div> 21 22 </div> 23 </div> 24 </div> 25 </transition> 26 </template> 27 28 <style scoped> 29 .modal-mask { 30 position: fixed; 31 z-index: 9998; 32 top: 50%; 33 left: 50%; 34 width: 100%; 35 height: 100%; 36 background-color: rgba(0, 0, 0, .5); 37 display: table; 38 transform: translateX(-50%) translateY(-50%); 39 transition: opacity .3s ease; 40 } 41 42 .modal-wrapper { 43 display: table-cell; 44 vertical-align: middle; 45 } 46 47 .modal-container { 48 margin: 0px auto; 49 padding: 20px 30px; 50 background-color: #fff; 51 border-radius: 2px; 52 box-shadow: 0 2px 8px rgba(0, 0, 0, .33); 53 transition: all .3s ease; 54 font-family: Helvetica, Arial, sans-serif; 55 } 56 57 .modal-header h3 { 58 margin-top: 0; 59 color: #42b983; 60 } 61 62 .modal-body { 63 margin: 10px 0; 64 overflow-y: auto 65 } 66 67 .modal-default-button { 68 float: right; 69 background: none; 70 border: none; 71 cursor: pointer; 72 } 73 74 /* 75 * The following styles are auto-applied to elements with 76 * transition="modal" when their visibility is toggled 77 * by Vue.js. 78 * 79 * You can easily play with the modal transition by editing 80 * these styles. 81 */ 82 83 .modal-enter { 84 opacity: 0; 85 } 86 87 .modal-leave-active { 88 opacity: 0; 89 } 90 91 .modal-enter .modal-container, 92 .modal-leave-active .modal-container { 93 -webkit-transform: scale(1.1); 94 transform: scale(1.1); 95 } 96 </style> 97 98 <script> 99 export default { 100 props: { 101 title: { 102 type: String 103 }, 104 width: { 105 type: String, 106 required: false, 107 default: '30%' 108 }, 109 height: { 110 type: String, 111 required: false, 112 default: '65%' 113 }, 114 115 currentComponent: { 116 type: String, 117 required: true 118 }, 119 id: { 120 type: Number, 121 default: 0 122 } 123 }, 124 components: { 125 newsItem(resolve) { 126 require(['../Admin/newsItem'], resolve) 127 }, 128 softwareItem(resolve) { 129 require(['../Admin/softwareItem'], resolve) 130 } 131 }, 132 data() { 133 return { 134 bodyHeight: '98%', 135 bodyWidth: '100%' 136 } 137 }, 138 methods: { 139 close(type) { 140 if (type) 141 this.$emit('close', type); 142 else 143 this.$emit('close'); 144 } 145 } 146 } 147 </script>
Props中接收 currentComponent,要呈現哪個組件,交給調用方,誰調用我,誰就必須告訴我,該顯示哪個子組件。
3、組件通信
組件間通信問題,是一個普遍問題。組件再獨立也得和其它組件協同完成任務吧。沒有一個組件能完成所有事情。常見的那就父子之間的通信以及兄弟之間的通信。有沒有父組件引發了一個事件,由子組件來處理呢?貌似沒有。如果有的話,就是父組件更改了Props中屬性的值。如果子組件非要在更改值
的時候,作出某些處理的話,那麼就用Watch了。
watch: { total(val, oldVal) { if (val != oldVal) { this.render(); } }, }
這個watch監視的是total(總頁數),是分頁組件監視Props中的total,一旦total改變,那麼分頁組件需要render,調用render方法重新渲染自己。
子組件觸發事件,父組件監聽,這是非常常見的。比如彈出框組件中的關閉事件,分頁組件中的 pageHandler 分頁事件,這些都要父組件來處理,子組件通過 $emit,這是vue全局的方法,哪個組件都可以用。父組件必須監聽pageHandler事件:
<ym-pager v-if="total" :page-index="pageIndex" :page-size='pageSize' :total='total' :groups="5" @pageHandler="loadData"></ym-pager>
兄弟之間的通信,如何解決呢?網上一搜,基本上都是給一個總線級別的組件,這個組件就是用來通訊的,誰需要發佈事件,就往這裏發,誰需要處理,那麼就監聽相關事件。理論上可以實現,但是我在實踐的過程中,始終沒有成功,不知道爲什麼。還有一種思路,
通過vuex實現,事件發佈方,更改vuex中的某個狀態值,那麼監控方發現這個狀態有變化的時候,就去處理事件。vuex是一個集中式的狀態管理器。“天下有變,則命一上將將荊州之軍以向宛、洛。。。。。。”,《隆中對》反映了蜀漢對天下大勢要密切監視,一旦
發生了變化,就要採取行動了。兄弟之間的通信,我們項目還真沒有用到過,如果需要的同學,可進一步查閱資料,這裏僅探討思路。
4、slot
這個特別有用,也有意思。插槽,它反映了一種IOC(控制反轉)的思想。本來子組件的呈現由自己做決定,可是某些情況下,子組件的某一部分變數很大,需要抽象出來,就用了slot先佔着,等父組件調用的時候,再告訴該如何渲染。比如我們有一個table組件,這個組件實現了分頁等功能。可是table的表頭和表的內容充滿着變數,若是由父組件通過Props傳遞,也可以,就是特別麻煩,傳遞的東西太多了,而且子組件這邊也需要很多處理。大道至簡,用slot,簡潔。table組件不用那麼費勁。調用table的父組件也不用想着如何更好地傳遞數據了。
<table class="ym-table table-hover"> <slot name="thead"></slot> <slot name="tbody"></slot> </table>
table組件中定義了兩個命名slot,看看如何調用:
<YmTable :page-title="pageTitle" :total="totalCount" :page-size="pageSize" @pager="pager" @newItem="newItem"> <thead slot="thead"> <tr> <th>序號</th> <th>軟件名稱</th> <th>簡介</th> </tr> </thead> <tbody slot="tbody"> <tr v-for="(item,index) in items" :key="item.id"> <td v-text="getIndex(index)"></td> <td> {{item.name}} </td> <td> {{item.summary}} </td> </tr> </tbody> </YmTable>
5、vue生命週期
生命週期是個老生常談的問題。是個對象,那就總有個生命週期吧。比如.net中Page對象,頁面的生命週期,而且這個還是主考官特別愛考的問題。Android的中Activity的生命週期。Page和Activity對象的功能有點像,提供用戶操作的界面,可以簡單地理解爲UI。
網上最著名的就是這張圖:
這個圖,我們大致理解一下,它核心就是如何把VM(虛擬的dom)轉換爲實際dom,而且在什麼時候轉換。這裏有一點記住就行了,Created的時候,dom還沒有被渲染出來,此時不宜操作dom相關的事情。Mounted的時候,做的事情就多了。比如,在mounted的時候,通過layui綁定form的提交事件。
mounted() { let that = this; var form = layui.form; //綁定form提交事件 layui.form.on('submit(*)', function (data) { that.summit(); return false; }); },
再例如,封裝了一個Select的組件,在updated的時候,執行select的render:
updated() { layui.form.render('select'); },
總之,vue生命週期中,都會留有鉤子函數,通過這些才能把我們的業務邏輯注入到Vue對象中,而且得到執行。我們做一件事情,要看準時機,如果時機不對,事倍功半,甚至一敗塗地。諸葛亮出山的時機不對啊。
6、實例變量 && $nextTick
文檔中是這麼說的:將回調延遲到下次DOM更新循環之後執行。在修改數據之後立即使用這個方法,獲取更新後的DOM。很抽象啊,不理解。但是我需要它。我封裝了一個YmRichText組件,這個組件裏是調用了kindeditor,富文本框。
mounted() { let that = this; this.$nextTick(function () { that.kedit('textarea[name="content"]'); }); }, methods: { getConent() { return editor.html(); }, kedit(k) { let that = this; window.editor = KindEditor.create(k, { width: '98%', height: that.height + 'px', uploadJson: that.uploadFileUrl, allowFileManager: false }); } }
當在mounted的時候,不管怎麼樣創建的editor對象都爲空。所以使用了$nextTick。按理說,不應該啊,模板中有textarea,kindeditor的js和css也加載上了,而且也在mounted的時候調用的。但是反過來想,在$nextTick調用成功,說明在當前週期內,是不會調用kindeditor的方法的。我們的分頁組件中,也使用了 $nextTick,這個倒好理解,因爲在created時候,調用render,render方法中會操作dom,所以只能等下一個週期執行了。
created() { this.render(); }, watch: { total(val, oldVal) { if (val != oldVal) { this.render(); } }, pageIndex(val) { this.cindex = val; if (val == 1) { this.render(); } } }, methods: { render() { let self = this; this.$nextTick(function () { layui.laypage.render({ elem: self.pagerId, skin: self.cskin, count: self.total, //總數數 limit: self.pageSize, //每頁顯示條數 groups: self.cgroups, //連續顯示分頁數 curr: this.cindex, //當前頁 jump: function (obj, first) { if (!first) self.$emit("pageHandler", obj.curr); } }); }); } }
實例變量多了,比如引用父組件的$parent,引用子組件的$refs,$refs特別有用,比如要執行子組件裏的方法或者獲取子組件的數據。
<ym-company-select :oldCompanyId="oldCompanyId" ref="company"></ym-company-select>
this.data.companyId = this.$refs.company.companyId;
以上,就是我探討的vue組件開發的一些問題。