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了。

  props: ["pageIndex", "pageSize", "total", "groups", "skin"],
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组件开发的一些问题。

posted @ 2018-12-06 16:41  micDavid  阅读(4087)  评论(0编辑  收藏  举报