vue前端开发那些事——vue组件开发
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组件开发的一些问题。