0%

如何从零开始封装一个Vue组件

组件系统是Vue中非常重要的一个特性,通过组件的封装,我们可以实现代码的复用以及功能的解耦。组件可以扩展HTML元素,封装可重用的代码。在较高层面上,组件是自定义的元素,Vue.js的编译器为它添加特殊功能。在有些情况下,组件也可以是原生HTML元素的形式,以is特性扩展。

组件的注册

组件的注册有两种方式,第一种方式是全局注册,当我们注册全局组件后,我们可以在任何Vue根实例的模板中使用它,全局注册组件的语法如下:

    Vue.component('my-component-name', {
      // ... 选项 ...
    })
    

全局注册组件也存在一个问题,假如我们项目中的组件都进行了全局注册,webpack打包工具会将这些注册了的组件都进行打包,即使在项目中不再使用的组件也仍旧会被打包,这就会造成代码的冗余,所以我们还有一种注册组件的方式,就是局部注册,它的语法如下:

先使用普通JavaScript对象的方式来定义组件:

    var ComponentA = { /* ... */ }
    var ComponentB = { /* ... */ }
    var ComponentC = { /* ... */ }
    

然后在 components 选项中定义你想要使用的组件:

    new Vue({
      el: '#app',
      components: {
        'component-a': ComponentA,
        'component-b': ComponentB
      }
    })
    

components 对象中的每个属性的属性名就是自定义元素的名字,其属性值就是这个组件的选项对象。

上面简单回顾了Vue组件的注册,但通常我们在开发的过程中会使用到如Babel 和 webpack 的模块系统,通过mport/require语法导入组件,我们的项目中会有一个components目录用来存放封装的所有组件,每个组件都有各自的文件。举个简单的例子,在componments文件夹下,我们保存了两个封装好的组件ComponentA和ComponentB,然后我们在ComponentC中引入ComponentA和ComponentB:

    import ComponentA from './components/ComponentA'
    import ComponentB from './components/ComponentB'
    
    export default {
      components: {
        ComponentA,
        ComponentB
      },
      // ...
    }
    

这样就完成了组件在模块系统中的局部注册,开发中也是最常用的组件注册方式,接下来我就演示一下如何在使用vue-cli脚手架工具创建的项目中从零开始封装一些通用的组件,以封装一个Modal组件为例。

封装一个Modal组件

先使用vue-cli官方的脚手架工具创建一个vue项目:

vue create modal

然后在src目录下创建一个components文件夹,为了方便管理,封装组件的时候每个组件也拥有各自的文件夹,里面存放组件的源码以及组件注册的代码,创建一个modal文件夹,在modal文件夹下面创建一个index.vue文件,里面写组件相关的模板、逻辑和样式。封装一个组件的核心是父子组件之间的通讯。

首先在main.vue中写入我们的组件骨架以及样式:

    <template>
        <div class="vue-modal__wrapper" v-if="visible">
          <div class="vue-modal">
            <div class="vue-modal__header">
              <span class="vue-modal__title">我是Title</span>
              <span class="vue-modal__headerbtn">x</span>
            </div>
            <div class="vue-modal__body">
              我是Body
            </div>
            <div class="vue-modal__footer">
              <button class="vue-modal__button">取消</button>
              <button class="vue-modal__button primary">确定</button>
            </div>
          </div>
        </div>
    </template>
    
    <script>
    export default {
      name: 'VueModal',
      props: {
        visible: {
          type: Boolean,
          default: false
        }
      },
      data() {
        return {
    
        };
      },
      methods: {
    
      }
    }
    </script>
    
    <style>
    .vue-modal__wrapper {
        position: fixed;
        top: 0;
        right: 0;
        bottom: 0;
        left: 0;
        overflow: auto;
        margin: 0;
        z-index: 9999;
        background: rgba(0, 0, 0, 0.5);
    }
    .vue-modal {
        position: relative;
        margin: 0 auto 50px;
        border-radius: 2px;
        -webkit-box-shadow: 0 1px 3px rgba(0,0,0,.3);
        box-shadow: 0 1px 3px rgba(0,0,0,.3);
        -webkit-box-sizing: border-box;
        box-sizing: border-box;
        width: 50%;
        margin-top: 15vh;
    }
    .vue-modal {
        background: #fff;
        box-sizing: border-box;
    }
    .vue-modal__header {
        padding: 20px 20px 10px;
    }
    .vue-modal__title {
        line-height: 24px;
        font-size: 18px;
        color: #303133;
    }
    .vue-modal__headerbtn {
        position: absolute;
        top: 20px;
        right: 20px;
        padding: 0;
        background: 0 0;
        border: none;
        outline: 0;
        cursor: pointer;
        font-size: 16px;
    }
    .vue-modal__body {
        padding: 30px 20px;
        color: #606266;
        font-size: 14px;
    }
    .vue-modal__footer {
        padding: 10px 20px 20px;
        text-align: right;
        -webkit-box-sizing: border-box;
        box-sizing: border-box;
    }
    .vue-modal__button {
        display: inline-block;
        line-height: 1;
        white-space: nowrap;
        cursor: pointer;
        background: #fff;
        border: 1px solid #dcdfe6;
        color: #606266;
        -webkit-appearance: none;
        text-align: center;
        -webkit-box-sizing: border-box;
        box-sizing: border-box;
        outline: 0;
        margin: 0;
        -webkit-transition: .1s;
        transition: .1s;
        font-weight: 500;
        padding: 12px 20px;
        font-size: 14px;
        border-radius: 4px;
    }
    .vue-modal__button.primary {
      background: #3a8ee6;
      color: #fff;
    }
    .vue-modal__button+.vue-modal__button {
        margin-left: 10px;
    }
    </style>
    




然后我们修改一下App.vue:




    <template>
      <div id="app">
        <span class="button" @click="openModal">Open Modal</span>
        <Modal :visible="modalVisible">
        </Modal>
      </div>
    </template>
    
    <script>
    import Modal from './components/modal/index';
    
    export default {
      name: 'app',
      data() {
        return {
          modalVisible: false
        }
      },
      components: {
        Modal
      },
      methods: {
        openModal() {
          this.modalVisible = true;
        }
      }
    }
    </script>
    
    <style>
    #app {
      font-family: 'Avenir', Helvetica, Arial, sans-serif;
      -webkit-font-smoothing: antialiased;
      -moz-osx-font-smoothing: grayscale;
      text-align: center;
      color: #2c3e50;
      margin-top: 60px;
    }
    .button {
      cursor: pointer;
    }
    </style>
    

点击Open Modal按钮,展示Modal的雏形如下:

image

接下来进一步完善Modal组件,我们可以打开Modal了,但怎么让它关闭呢,很简单,监听Modal右上角关闭图标和取消按钮的点击事件,点击事件被触发时,使用$emit通知父组件关闭Modal,完善后的组件代码如下:

    <template>
        <div class="vue-modal__wrapper" v-if="visible">
          <div class="vue-modal">
            <div class="vue-modal__header">
              <span class="vue-modal__title">我是Title</span>
              <span class="vue-modal__headerbtn" @click="handleClose">x</span>
            </div>
            <div class="vue-modal__body">
              我是Body
            </div>
            <div class="vue-modal__footer">
              <button class="vue-modal__button" @click="handleClose">取消</button>
              <button class="vue-modal__button primary">确定</button>
            </div>
          </div>
        </div>
    </template>
    
    <script>
    export default {
      name: 'VueModal',
      props: {
        visible: {
          type: Boolean,
          default: false
        }
      },
      data() {
        return {
    
        };
      },
      methods: {
        handleClose() {
          this.$emit('onCancel');
        }
      }
    }
    </script>
    
    <style>
    .vue-modal__wrapper {
        position: fixed;
        top: 0;
        right: 0;
        bottom: 0;
        left: 0;
        overflow: auto;
        margin: 0;
        z-index: 9999;
        background: rgba(0, 0, 0, 0.5);
    }
    .vue-modal {
        position: relative;
        margin: 0 auto 50px;
        border-radius: 2px;
        -webkit-box-shadow: 0 1px 3px rgba(0,0,0,.3);
        box-shadow: 0 1px 3px rgba(0,0,0,.3);
        -webkit-box-sizing: border-box;
        box-sizing: border-box;
        width: 50%;
        margin-top: 15vh;
    }
    .vue-modal {
        background: #fff;
        box-sizing: border-box;
    }
    .vue-modal__header {
        padding: 20px 20px 10px;
    }
    .vue-modal__title {
        line-height: 24px;
        font-size: 18px;
        color: #303133;
    }
    .vue-modal__headerbtn {
        position: absolute;
        top: 20px;
        right: 20px;
        padding: 0;
        background: 0 0;
        border: none;
        outline: 0;
        cursor: pointer;
        font-size: 16px;
    }
    .vue-modal__body {
        padding: 30px 20px;
        color: #606266;
        font-size: 14px;
    }
    .vue-modal__footer {
        padding: 10px 20px 20px;
        text-align: right;
        -webkit-box-sizing: border-box;
        box-sizing: border-box;
    }
    .vue-modal__button {
        display: inline-block;
        line-height: 1;
        white-space: nowrap;
        cursor: pointer;
        background: #fff;
        border: 1px solid #dcdfe6;
        color: #606266;
        -webkit-appearance: none;
        text-align: center;
        -webkit-box-sizing: border-box;
        box-sizing: border-box;
        outline: 0;
        margin: 0;
        -webkit-transition: .1s;
        transition: .1s;
        font-weight: 500;
        padding: 12px 20px;
        font-size: 14px;
        border-radius: 4px;
    }
    .vue-modal__button.primary {
      background: #3a8ee6;
      color: #fff;
    }
    .vue-modal__button+.vue-modal__button {
        margin-left: 10px;
    }
    </style>
    

App.vue的代码如下:

    <template>
      <div id="app">
        <span class="button" @click="openModal">Open Modal</span>
        <Modal 
          :visible="modalVisible"
          @onCancel="handleCancel">
        </Modal>
      </div>
    </template>
    
    <script>
    import Modal from './components/modal/index';
    
    export default {
      name: 'app',
      data() {
        return {
          modalVisible: false
        }
      },
      components: {
        Modal
      },
      methods: {
        openModal() {
          this.modalVisible = true;
        },
        handleCancel() {
          this.modalVisible = false;
        }
      }
    }
    </script>
    
    <style>
    #app {
      font-family: 'Avenir', Helvetica, Arial, sans-serif;
      -webkit-font-smoothing: antialiased;
      -moz-osx-font-smoothing: grayscale;
      text-align: center;
      color: #2c3e50;
      margin-top: 60px;
    }
    .button {
      cursor: pointer;
    }
    </style>
    

现在我们可以打开Modal,也可以关闭Modal了,下一步我们要做的是监听确定按钮的点击事件,点击后也是通过$emit通知父组件做相应的处理,Modal最常用的创建是里面有一个表单,我们点击确定按钮的时候提交表单到后台,进一步修改main.vue代码如下:

    <template>
        <div class="vue-modal__wrapper" v-if="visible">
          <div class="vue-modal">
            <div class="vue-modal__header">
              <span class="vue-modal__title">我是Title</span>
              <span class="vue-modal__headerbtn" @click="handleClose">x</span>
            </div>
            <div class="vue-modal__body">
              我是Body
            </div>
            <div class="vue-modal__footer">
              <button class="vue-modal__button" @click="handleClose">取消</button>
              <button class="vue-modal__button primary"  @click="handleOk">确定</button>
            </div>
          </div>
        </div>
    </template>
    
    <script>
    export default {
      name: 'VueModal',
      props: {
        visible: {
          type: Boolean,
          default: false
        }
      },
      data() {
        return {
    
        };
      },
      methods: {
        handleClose() {
          this.$emit('onCancel');
        },
        handleOk() {
          this.$emit('onOk'); 
        }
      }
    }
    </script>
    
    <style>
    .vue-modal__wrapper {
        position: fixed;
        top: 0;
        right: 0;
        bottom: 0;
        left: 0;
        overflow: auto;
        margin: 0;
        z-index: 9999;
        background: rgba(0, 0, 0, 0.5);
    }
    .vue-modal {
        position: relative;
        margin: 0 auto 50px;
        border-radius: 2px;
        -webkit-box-shadow: 0 1px 3px rgba(0,0,0,.3);
        box-shadow: 0 1px 3px rgba(0,0,0,.3);
        -webkit-box-sizing: border-box;
        box-sizing: border-box;
        width: 50%;
        margin-top: 15vh;
    }
    .vue-modal {
        background: #fff;
        box-sizing: border-box;
    }
    .vue-modal__header {
        padding: 20px 20px 10px;
    }
    .vue-modal__title {
        line-height: 24px;
        font-size: 18px;
        color: #303133;
    }
    .vue-modal__headerbtn {
        position: absolute;
        top: 20px;
        right: 20px;
        padding: 0;
        background: 0 0;
        border: none;
        outline: 0;
        cursor: pointer;
        font-size: 16px;
    }
    .vue-modal__body {
        padding: 30px 20px;
        color: #606266;
        font-size: 14px;
    }
    .vue-modal__footer {
        padding: 10px 20px 20px;
        text-align: right;
        -webkit-box-sizing: border-box;
        box-sizing: border-box;
    }
    .vue-modal__button {
        display: inline-block;
        line-height: 1;
        white-space: nowrap;
        cursor: pointer;
        background: #fff;
        border: 1px solid #dcdfe6;
        color: #606266;
        -webkit-appearance: none;
        text-align: center;
        -webkit-box-sizing: border-box;
        box-sizing: border-box;
        outline: 0;
        margin: 0;
        -webkit-transition: .1s;
        transition: .1s;
        font-weight: 500;
        padding: 12px 20px;
        font-size: 14px;
        border-radius: 4px;
    }
    .vue-modal__button.primary {
      background: #3a8ee6;
      color: #fff;
    }
    .vue-modal__button+.vue-modal__button {
        margin-left: 10px;
    }
    </style>
    

App.vue的代码如下:

    <template>
      <div id="app">
        <span class="button" @click="openModal">Open Modal</span>
        <Modal 
          :visible="modalVisible"
          @onCancel="handleCancel"
          @onOk="handleOk">
        </Modal>
      </div>
    </template>
    
    <script>
    import Modal from './components/modal/index';
    
    export default {
      name: 'app',
      data() {
        return {
          modalVisible: false
        }
      },
      components: {
        Modal
      },
      methods: {
        openModal() {
          this.modalVisible = true;
        },
        handleCancel() {
          this.modalVisible = false;
        },
        handleOk() {
          this.modalVisible = false;
        }
      }
    }
    </script>
    
    <style>
    #app {
      font-family: 'Avenir', Helvetica, Arial, sans-serif;
      -webkit-font-smoothing: antialiased;
      -moz-osx-font-smoothing: grayscale;
      text-align: center;
      color: #2c3e50;
      margin-top: 60px;
    }
    .button {
      cursor: pointer;
    }
    </style>
    

现在,作为一个Modal最基本的功能已经做好了,我需要进一步拓展Modal的功能。

Modal的标题有时我们可能需要让它呈现一些自定义的样式或者文字,所以我们可以借助slot插槽实现,这一步完善后的man.vue代码如下:

    <template>
        <div class="vue-modal__wrapper" v-if="visible">
          <div class="vue-modal">
            <div class="vue-modal__header">
              <div class="vue-modal__title" v-if="$slots.title">
                <slot name="title"></slot>
              </div>
              <div class="vue-modal__title" v-else>{{ title }}</div>
              <span class="vue-modal__headerbtn" @click="handleClose">x</span>
            </div>
            <div class="vue-modal__body">
              我是Body
            </div>
            <div class="vue-modal__footer">
              <button class="vue-modal__button" @click="handleClose">取消</button>
              <button class="vue-modal__button primary"  @click="handleOk">确定</button>
            </div>
          </div>
        </div>
    </template>
    
    <script>
    export default {
      name: 'VueModal',
      props: {
        visible: {
          type: Boolean,
          default: false
        },
        title: {
          type: String,
          default: ''
        },
      },
      data() {
        return {
    
        };
      },
      methods: {
        handleClose() {
          this.$emit('onCancel');
        },
        handleOk() {
          this.$emit('onOk'); 
        }
      }
    }
    </script>
    
    <style>
    .vue-modal__wrapper {
        position: fixed;
        top: 0;
        right: 0;
        bottom: 0;
        left: 0;
        overflow: auto;
        margin: 0;
        z-index: 9999;
        background: rgba(0, 0, 0, 0.5);
    }
    .vue-modal {
        position: relative;
        margin: 0 auto 50px;
        border-radius: 2px;
        -webkit-box-shadow: 0 1px 3px rgba(0,0,0,.3);
        box-shadow: 0 1px 3px rgba(0,0,0,.3);
        -webkit-box-sizing: border-box;
        box-sizing: border-box;
        width: 50%;
        margin-top: 15vh;
    }
    .vue-modal {
        background: #fff;
        box-sizing: border-box;
    }
    .vue-modal__header {
        padding: 20px 20px 10px;
    }
    .vue-modal__title {
        line-height: 24px;
        font-size: 18px;
        color: #303133;
    }
    .vue-modal__headerbtn {
        position: absolute;
        top: 20px;
        right: 20px;
        padding: 0;
        background: 0 0;
        border: none;
        outline: 0;
        cursor: pointer;
        font-size: 16px;
    }
    .vue-modal__body {
        padding: 30px 20px;
        color: #606266;
        font-size: 14px;
    }
    .vue-modal__footer {
        padding: 10px 20px 20px;
        text-align: right;
        -webkit-box-sizing: border-box;
        box-sizing: border-box;
    }
    .vue-modal__button {
        display: inline-block;
        line-height: 1;
        white-space: nowrap;
        cursor: pointer;
        background: #fff;
        border: 1px solid #dcdfe6;
        color: #606266;
        -webkit-appearance: none;
        text-align: center;
        -webkit-box-sizing: border-box;
        box-sizing: border-box;
        outline: 0;
        margin: 0;
        -webkit-transition: .1s;
        transition: .1s;
        font-weight: 500;
        padding: 12px 20px;
        font-size: 14px;
        border-radius: 4px;
    }
    .vue-modal__button.primary {
      background: #3a8ee6;
      color: #fff;
    }
    .vue-modal__button+.vue-modal__button {
        margin-left: 10px;
    }
    </style>
    

App.vue的代码如下:

    <template>
      <div id="app">
        <span class="button" @click="openModal">Open Modal</span>
        <Modal 
          :visible="modalVisible"
          @onCancel="handleCancel"
          @onOk="handleOk">
          <span slot="title">这里是自定义的标题</span>
        </Modal>
      </div>
    </template>
    
    <script>
    import Modal from './components/modal/index';
    
    export default {
      name: 'app',
      data() {
        return {
          modalVisible: false
        }
      },
      components: {
        Modal
      },
      methods: {
        openModal() {
          this.modalVisible = true;
        },
        handleCancel() {
          this.modalVisible = false;
        },
        handleOk() {
          this.modalVisible = false;
        }
      }
    }
    </script>
    
    <style>
    #app {
      font-family: 'Avenir', Helvetica, Arial, sans-serif;
      -webkit-font-smoothing: antialiased;
      -moz-osx-font-smoothing: grayscale;
      text-align: center;
      color: #2c3e50;
      margin-top: 60px;
    }
    .button {
      cursor: pointer;
    }
    </style>
    

同样地,body也是用户在使用Modal组件的时候才能决定的,所以同样借助slot插槽,进一步完善main.vue:

    <template>
        <div class="vue-modal__wrapper" v-if="visible">
          <div class="vue-modal">
            <div class="vue-modal__header">
              <div class="vue-modal__title" v-if="$slots.title">
                <slot name="title"></slot>
              </div>
              <div class="vue-modal__title" v-else>{{ title }}</div>
              <span class="vue-modal__headerbtn" @click="handleClose">x</span>
            </div>
            <div class="vue-modal__body">
              <slot name="body"></slot>
            </div>
            <div class="vue-modal__footer">
              <button class="vue-modal__button" @click="handleClose">取消</button>
              <button class="vue-modal__button primary"  @click="handleOk">确定</button>
            </div>
          </div>
        </div>
    </template>
    
    <script>
    export default {
      name: 'VueModal',
      props: {
        visible: {
          type: Boolean,
          default: false
        },
        title: {
          type: String,
          default: ''
        },
      },
      data() {
        return {
    
        };
      },
      methods: {
        handleClose() {
          this.$emit('onCancel');
        },
        handleOk() {
          this.$emit('onOk'); 
        }
      }
    }
    </script>
    
    <style>
    .vue-modal__wrapper {
        position: fixed;
        top: 0;
        right: 0;
        bottom: 0;
        left: 0;
        overflow: auto;
        margin: 0;
        z-index: 9999;
        background: rgba(0, 0, 0, 0.5);
    }
    .vue-modal {
        position: relative;
        margin: 0 auto 50px;
        border-radius: 2px;
        -webkit-box-shadow: 0 1px 3px rgba(0,0,0,.3);
        box-shadow: 0 1px 3px rgba(0,0,0,.3);
        -webkit-box-sizing: border-box;
        box-sizing: border-box;
        width: 50%;
        margin-top: 15vh;
    }
    .vue-modal {
        background: #fff;
        box-sizing: border-box;
    }
    .vue-modal__header {
        padding: 20px 20px 10px;
    }
    .vue-modal__title {
        line-height: 24px;
        font-size: 18px;
        color: #303133;
    }
    .vue-modal__headerbtn {
        position: absolute;
        top: 20px;
        right: 20px;
        padding: 0;
        background: 0 0;
        border: none;
        outline: 0;
        cursor: pointer;
        font-size: 16px;
    }
    .vue-modal__body {
        padding: 30px 20px;
        color: #606266;
        font-size: 14px;
    }
    .vue-modal__footer {
        padding: 10px 20px 20px;
        text-align: right;
        -webkit-box-sizing: border-box;
        box-sizing: border-box;
    }
    .vue-modal__button {
        display: inline-block;
        line-height: 1;
        white-space: nowrap;
        cursor: pointer;
        background: #fff;
        border: 1px solid #dcdfe6;
        color: #606266;
        -webkit-appearance: none;
        text-align: center;
        -webkit-box-sizing: border-box;
        box-sizing: border-box;
        outline: 0;
        margin: 0;
        -webkit-transition: .1s;
        transition: .1s;
        font-weight: 500;
        padding: 12px 20px;
        font-size: 14px;
        border-radius: 4px;
    }
    .vue-modal__button.primary {
      background: #3a8ee6;
      color: #fff;
    }
    .vue-modal__button+.vue-modal__button {
        margin-left: 10px;
    }
    </style>
    

App.vue同步更新如下:

    <template>
      <div id="app">
        <span class="button" @click="openModal">Open Modal</span>
        <Modal 
          :visible="modalVisible"
          @onCancel="handleCancel"
          @onOk="handleOk">
          <span slot="title">这里是自定义的标题</span>
          <div slot="body">
            这里是自定义的内容
          </div>
        </Modal>
      </div>
    </template>
    
    <script>
    import Modal from './components/modal/index';
    
    export default {
      name: 'app',
      data() {
        return {
          modalVisible: false
        }
      },
      components: {
        Modal
      },
      methods: {
        openModal() {
          this.modalVisible = true;
        },
        handleCancel() {
          this.modalVisible = false;
        },
        handleOk() {
          this.modalVisible = false;
        }
      }
    }
    </script>
    
    <style>
    #app {
      font-family: 'Avenir', Helvetica, Arial, sans-serif;
      -webkit-font-smoothing: antialiased;
      -moz-osx-font-smoothing: grayscale;
      text-align: center;
      color: #2c3e50;
      margin-top: 60px;
    }
    .button {
      cursor: pointer;
    }
    </style>
    

接下来我们对Modal底部的按钮进行完善,有时候的需求可能需要自定义按钮的文本,取消按钮和确定按钮的文本我们分别通过cancelText和okText从父组件接收,修改main.vue,如下:

    <template>
        <div class="vue-modal__wrapper" v-if="visible">
          <div class="vue-modal">
            <div class="vue-modal__header">
              <div class="vue-modal__title" v-if="$slots.title">
                <slot name="title"></slot>
              </div>
              <div class="vue-modal__title" v-else>{{ title }}</div>
              <span class="vue-modal__headerbtn" @click="handleClose">x</span>
            </div>
            <div class="vue-modal__body">
              <slot name="body"></slot>
            </div>
            <div class="vue-modal__footer">
              <button class="vue-modal__button" @click="handleClose">{{ cancelText ? cancelText : '取消'}}</button>
              <button class="vue-modal__button primary"  @click="handleOk">{{ okText ? okText : '确定'}}</button>
            </div>
          </div>
        </div>
    </template>
    
    <script>
    export default {
      name: 'VueModal',
      props: {
        visible: {
          type: Boolean,
          default: false
        },
        title: {
          type: String,
          default: ''
        },
        okText: {
          type: String,
          default: ''
        },
        cancelText: {
          type: String,
          default: ''
        },
      },
      data() {
        return {
    
        };
      },
      methods: {
        handleClose() {
          this.$emit('onCancel');
        },
        handleOk() {
          this.$emit('onOk'); 
        }
      }
    }
    </script>
    
    <style>
    .vue-modal__wrapper {
        position: fixed;
        top: 0;
        right: 0;
        bottom: 0;
        left: 0;
        overflow: auto;
        margin: 0;
        z-index: 9999;
        background: rgba(0, 0, 0, 0.5);
    }
    .vue-modal {
        position: relative;
        margin: 0 auto 50px;
        border-radius: 2px;
        -webkit-box-shadow: 0 1px 3px rgba(0,0,0,.3);
        box-shadow: 0 1px 3px rgba(0,0,0,.3);
        -webkit-box-sizing: border-box;
        box-sizing: border-box;
        width: 50%;
        margin-top: 15vh;
    }
    .vue-modal {
        background: #fff;
        box-sizing: border-box;
    }
    .vue-modal__header {
        padding: 20px 20px 10px;
    }
    .vue-modal__title {
        line-height: 24px;
        font-size: 18px;
        color: #303133;
    }
    .vue-modal__headerbtn {
        position: absolute;
        top: 20px;
        right: 20px;
        padding: 0;
        background: 0 0;
        border: none;
        outline: 0;
        cursor: pointer;
        font-size: 16px;
    }
    .vue-modal__body {
        padding: 30px 20px;
        color: #606266;
        font-size: 14px;
    }
    .vue-modal__footer {
        padding: 10px 20px 20px;
        text-align: right;
        -webkit-box-sizing: border-box;
        box-sizing: border-box;
    }
    .vue-modal__button {
        display: inline-block;
        line-height: 1;
        white-space: nowrap;
        cursor: pointer;
        background: #fff;
        border: 1px solid #dcdfe6;
        color: #606266;
        -webkit-appearance: none;
        text-align: center;
        -webkit-box-sizing: border-box;
        box-sizing: border-box;
        outline: 0;
        margin: 0;
        -webkit-transition: .1s;
        transition: .1s;
        font-weight: 500;
        padding: 12px 20px;
        font-size: 14px;
        border-radius: 4px;
    }
    .vue-modal__button.primary {
      background: #3a8ee6;
      color: #fff;
    }
    .vue-modal__button+.vue-modal__button {
        margin-left: 10px;
    }
    </style>
    

修改App.vue:

    <template>
      <div id="app">
        <span class="button" @click="openModal">Open Modal</span>
        <Modal 
          :visible="modalVisible"
          @onCancel="handleCancel"
          @onOk="handleOk"
          cancelText="cancel"
          okText="ok">
          <span slot="title">这里是自定义的标题</span>
          <div slot="body">
            这里是自定义的内容
          </div>
        </Modal>
      </div>
    </template>
    
    <script>
    import Modal from './components/modal/index';
    
    export default {
      name: 'app',
      data() {
        return {
          modalVisible: false
        }
      },
      components: {
        Modal
      },
      methods: {
        openModal() {
          this.modalVisible = true;
        },
        handleCancel() {
          this.modalVisible = false;
        },
        handleOk() {
          this.modalVisible = false;
        }
      }
    }
    </script>
    
    <style>
    #app {
      font-family: 'Avenir', Helvetica, Arial, sans-serif;
      -webkit-font-smoothing: antialiased;
      -moz-osx-font-smoothing: grayscale;
      text-align: center;
      color: #2c3e50;
      margin-top: 60px;
    }
    .button {
      cursor: pointer;
    }
    </style>
    

到了这里,Modal已经是一个比较完善的组件了,但还有进一步优化的空间,有时候我们的需求还可能是对底部的html和样式进行自定义,同样引入slot插槽,当使用Modal的时候,如果用户没有对Modal的底部进行自定义,则默认显示取消和关闭按钮,如果有进行自定义则显示自定义的内容,完善后的main.vue代码如下:

    <template>
        <div class="vue-modal__wrapper" v-if="visible">
          <div class="vue-modal">
            <div class="vue-modal__header">
              <div class="vue-modal__title" v-if="$slots.title">
                <slot name="title"></slot>
              </div>
              <div class="vue-modal__title" v-else>{{ title }}</div>
              <span class="vue-modal__headerbtn" @click="handleClose">x</span>
            </div>
            <div class="vue-modal__body">
              <slot name="body"></slot>
            </div>
            <div class="vue-modal__footer" v-if="$slots.footer">
              <slot name="footer"></slot>
            </div>
            <div class="vue-modal__footer" v-else>
              <button class="vue-modal__button" @click="handleCancel">{{ cancelText ? cancelText : '取消'}}</button>
              <button class="vue-modal__button primary" @click="handleOk">{{ okText ? okText : '确定'}}</button>
            </div>
          </div>
        </div>
    </template>
    
    <script>
    export default {
      name: 'VueModal',
      props: {
        visible: {
          type: Boolean,
          default: false
        },
        title: {
          type: String,
          default: ''
        },
        okText: {
          type: String,
          default: ''
        },
        cancelText: {
          type: String,
          default: ''
        },
      },
      data() {
        return {
    
        };
      },
      methods: {
        handleClose() {
          this.$emit('onCancel');
        },
        handleOk() {
          this.$emit('onOk'); 
        }
      }
    }
    </script>
    
    <style>
    .vue-modal__wrapper {
        position: fixed;
        top: 0;
        right: 0;
        bottom: 0;
        left: 0;
        overflow: auto;
        margin: 0;
        z-index: 9999;
        background: rgba(0, 0, 0, 0.5);
    }
    .vue-modal {
        position: relative;
        margin: 0 auto 50px;
        border-radius: 2px;
        -webkit-box-shadow: 0 1px 3px rgba(0,0,0,.3);
        box-shadow: 0 1px 3px rgba(0,0,0,.3);
        -webkit-box-sizing: border-box;
        box-sizing: border-box;
        width: 50%;
        margin-top: 15vh;
    }
    .vue-modal {
        background: #fff;
        box-sizing: border-box;
    }
    .vue-modal__header {
        padding: 20px 20px 10px;
    }
    .vue-modal__title {
        line-height: 24px;
        font-size: 18px;
        color: #303133;
    }
    .vue-modal__headerbtn {
        position: absolute;
        top: 20px;
        right: 20px;
        padding: 0;
        background: 0 0;
        border: none;
        outline: 0;
        cursor: pointer;
        font-size: 16px;
    }
    .vue-modal__body {
        padding: 30px 20px;
        color: #606266;
        font-size: 14px;
    }
    .vue-modal__footer {
        padding: 10px 20px 20px;
        text-align: right;
        -webkit-box-sizing: border-box;
        box-sizing: border-box;
    }
    .vue-modal__button {
        display: inline-block;
        line-height: 1;
        white-space: nowrap;
        cursor: pointer;
        background: #fff;
        border: 1px solid #dcdfe6;
        color: #606266;
        -webkit-appearance: none;
        text-align: center;
        -webkit-box-sizing: border-box;
        box-sizing: border-box;
        outline: 0;
        margin: 0;
        -webkit-transition: .1s;
        transition: .1s;
        font-weight: 500;
        padding: 12px 20px;
        font-size: 14px;
        border-radius: 4px;
    }
    .vue-modal__button.primary {
      background: #3a8ee6;
      color: #fff;
    }
    .vue-modal__button+.vue-modal__button {
        margin-left: 10px;
    }
    </style>
    

App.vue的代码:

    <template>
      <div id="app">
        <span class="button" @click="openModal">Open Modal</span>
        <Modal 
          :visible="modalVisible"
          @onCancel="handleCancel"
          @onOk="handleOk"
          cancelText="cancel"
          okText="ok">
          <span slot="title">这里是自定义的标题</span>
          <div slot="body">
            这里是自定义的内容
          </div>
          <div slot="footer">
            <button class="slot-button" @click="closeModal">自定义关闭按钮</button>
          </div>
        </Modal>
      </div>
    </template>
    
    <script>
    import Modal from './components/modal/index';
    
    export default {
      name: 'app',
      data() {
        return {
          modalVisible: false
        }
      },
      components: {
        Modal
      },
      methods: {
        openModal() {
          this.modalVisible = true;
        },
        handleCancel() {
          this.modalVisible = false;
        },
        closeModal() {
          this.handleCancel();
        },
        handleOk() {
          this.modalVisible = false;
        }
      }
    }
    </script>
    
    <style>
    #app {
      font-family: 'Avenir', Helvetica, Arial, sans-serif;
      -webkit-font-smoothing: antialiased;
      -moz-osx-font-smoothing: grayscale;
      text-align: center;
      color: #2c3e50;
      margin-top: 60px;
    }
    .button {
      cursor: pointer;
    }
    .slot-button {
      display: inline-block;
        line-height: 1;
        white-space: nowrap;
        cursor: pointer;
        background: #fff;
        border: 1px solid #dcdfe6;
        color: #606266;
        -webkit-appearance: none;
        text-align: center;
        -webkit-box-sizing: border-box;
        box-sizing: border-box;
        outline: 0;
        margin: 0;
        -webkit-transition: .1s;
        transition: .1s;
        font-weight: 500;
        padding: 12px 20px;
        font-size: 14px;
        border-radius: 4px;
        background: #3a8ee6;
        color: #fff;
    }
    </style>
    

现在,我们的Modal算是比较完美了,但是加点转场动画会不会更好一点呢,是的,Modal显示或者关闭的时候给它加些动画,修改main.vue:

        <template>
          <transition name="vue-modal-fade">
            <div class="vue-modal__wrapper" v-if="visible">
              <div class="vue-modal">
                <div class="vue-modal__header">
                  <div class="vue-modal__title" v-if="$slots.title">
                    <slot name="title"></slot>
                  </div>
                  <div class="vue-modal__title" v-else>{{ title }}</div>
                  <span class="vue-modal__headerbtn" @click="handleClose">x</span>
                </div>
                <div class="vue-modal__body">
                  <slot name="body"></slot>
                </div>
                <div class="vue-modal__footer" v-if="$slots.footer">
                  <slot name="footer"></slot>
                </div>
                <div class="vue-modal__footer" v-else>
                  <button class="vue-modal__button" @click="handleCancel">{{ cancelText ? cancelText : '取消'}}</button>
              <button class="vue-modal__button primary" @click="handleOk">{{ okText ? okText : '确定'}}</button>
            </div>
          </div>
        </div>
      </transition>
    </template>
    
    <script>
    export default {
      name: 'VueModal',
      props: {
        visible: {
          type: Boolean,
          default: false
        },
        title: {
          type: String,
          default: ''
        },
        okText: {
          type: String,
          default: ''
        },
        cancelText: {
          type: String,
          default: ''
        },
      },
      data() {
        return {
    
        };
      },
      methods: {
        handleClose() {
          this.$emit('onCancel');
        },
        handleOk() {
          this.$emit('onOk'); 
        }
      }
    }
    </script>
    
    <style>
    .vue-modal__wrapper {
        position: fixed;
        top: 0;
        right: 0;
        bottom: 0;
        left: 0;
        overflow: auto;
        margin: 0;
        z-index: 9999;
        background: rgba(0, 0, 0, 0.5);
    }
    .vue-modal {
        position: relative;
        margin: 0 auto 50px;
        border-radius: 2px;
        -webkit-box-shadow: 0 1px 3px rgba(0,0,0,.3);
        box-shadow: 0 1px 3px rgba(0,0,0,.3);
        -webkit-box-sizing: border-box;
        box-sizing: border-box;
        width: 50%;
        margin-top: 15vh;
    }
    .vue-modal {
        background: #fff;
        box-sizing: border-box;
    }
    .vue-modal__header {
        padding: 20px 20px 10px;
    }
    .vue-modal__title {
        line-height: 24px;
        font-size: 18px;
        color: #303133;
    }
    .vue-modal__headerbtn {
        position: absolute;
        top: 20px;
        right: 20px;
        padding: 0;
        background: 0 0;
        border: none;
        outline: 0;
        cursor: pointer;
        font-size: 16px;
    }
    .vue-modal__body {
        padding: 30px 20px;
        color: #606266;
        font-size: 14px;
    }
    .vue-modal__footer {
        padding: 10px 20px 20px;
        text-align: right;
        -webkit-box-sizing: border-box;
        box-sizing: border-box;
    }
    .vue-modal__button {
        display: inline-block;
        line-height: 1;
        white-space: nowrap;
        cursor: pointer;
        background: #fff;
        border: 1px solid #dcdfe6;
        color: #606266;
        -webkit-appearance: none;
        text-align: center;
        -webkit-box-sizing: border-box;
        box-sizing: border-box;
        outline: 0;
        margin: 0;
        -webkit-transition: .1s;
        transition: .1s;
        font-weight: 500;
        padding: 12px 20px;
        font-size: 14px;
        border-radius: 4px;
    }
    .vue-modal__button.primary {
      background: #3a8ee6;
      color: #fff;
    }
    .vue-modal__button+.vue-modal__button {
        margin-left: 10px;
    }
    .vue-modal-fade-enter-active {
      animation: vue-modal-fade-in .3s;
    }
    .vue-modal-fade-leave-active {
      animation: vue-modal-fade-out .3s;
    }
    
    @keyframes vue-modal-fade-in {
      0% {
        opacity: 0;
      }
      100% {
        opacity: 1;
      }
    }
    
    @keyframes vue-modal-fade-out {
      0% {
        opacity: 1;
      }
      100% {
        opacity: 0;
      }
    }
    </style>
    

仔细思考一下,还有几个简单的细节可以进行优化,一个是点击蒙层的时候我们可能希望能关闭modal, 还有就是我们可能不需要Modal右上角的关闭按钮,很简单,分别使用showClose和maskClosable两个props从父组件接收值,showClose为false的时候不显示icon,默认为true,maskClosable为true时表示点击蒙层可以关闭Modal,默认为false,还有一点也是比较常见的需求,有时候我们希望Modal的宽度由我们自己定义,修改一下main.vue:

    <template>
      <transition name="vue-modal-fade">
        <div class="vue-modal__wrapper" v-if="visible">
          <div class="vue-modal" :style="{width}">
            <div class="vue-modal__header">
              <div class="vue-modal__title" v-if="$slots.title">
                <slot name="title"></slot>
              </div>
              <div class="vue-modal__title" v-else>{{ title }}</div>
              <span class="vue-modal__headerbtn" @click="handleClose">x</span>
            </div>
            <div class="vue-modal__body">
              <slot name="body"></slot>
            </div>
            <div class="vue-modal__footer" v-if="$slots.footer">
              <slot name="footer"></slot>
            </div>
            <div class="vue-modal__footer" v-else>
              <button class="vue-modal__button" @click="handleCancel">{{ cancelText ? cancelText : '取消'}}</button>
              <button class="vue-modal__button primary" @click="handleOk">{{ okText ? okText : '确定'}}</button>
            </div>
          </div>
        </div>
      </transition>
    </template>
    
    <script>
    export default {
      name: 'VueModal',
      props: {
        visible: {
          type: Boolean,
          default: false
        },
        title: {
          type: String,
          default: ''
        },
        okText: {
          type: String,
          default: ''
        },
        cancelText: {
          type: String,
          default: ''
        },
        showClose: {
          type: Boolean,
          default: true
        },
        maskClosable: {
          type: Boolean,
          default: false,
        },
        width: {
          type: String
        }
      },
      data() {
        return {
    
        };
      },
      methods: {
        handleClose() {
          this.$emit('onCancel');
        },
        handleOk() {
          this.$emit('onOk'); 
        }
      }
    }
    </script>
    
    <style>
    .vue-modal__wrapper {
        position: fixed;
        top: 0;
        right: 0;
        bottom: 0;
        left: 0;
        overflow: auto;
        margin: 0;
        z-index: 9999;
        background: rgba(0, 0, 0, 0.5);
    }
    .vue-modal {
        position: relative;
        margin: 0 auto 50px;
        border-radius: 2px;
        -webkit-box-shadow: 0 1px 3px rgba(0,0,0,.3);
        box-shadow: 0 1px 3px rgba(0,0,0,.3);
        -webkit-box-sizing: border-box;
        box-sizing: border-box;
        width: 50%;
        margin-top: 15vh;
    }
    .vue-modal {
        background: #fff;
        box-sizing: border-box;
    }
    .vue-modal__header {
        padding: 20px 20px 10px;
    }
    .vue-modal__title {
        line-height: 24px;
        font-size: 18px;
        color: #303133;
    }
    .vue-modal__headerbtn {
        position: absolute;
        top: 20px;
        right: 20px;
        padding: 0;
        background: 0 0;
        border: none;
        outline: 0;
        cursor: pointer;
        font-size: 16px;
    }
    .vue-modal__body {
        padding: 30px 20px;
        color: #606266;
        font-size: 14px;
    }
    .vue-modal__footer {
        padding: 10px 20px 20px;
        text-align: right;
        -webkit-box-sizing: border-box;
        box-sizing: border-box;
    }
    .vue-modal__button {
        display: inline-block;
        line-height: 1;
        white-space: nowrap;
        cursor: pointer;
        background: #fff;
        border: 1px solid #dcdfe6;
        color: #606266;
        -webkit-appearance: none;
        text-align: center;
        -webkit-box-sizing: border-box;
        box-sizing: border-box;
        outline: 0;
        margin: 0;
        -webkit-transition: .1s;
        transition: .1s;
        font-weight: 500;
        padding: 12px 20px;
        font-size: 14px;
        border-radius: 4px;
    }
    .vue-modal__button.primary {
      background: #3a8ee6;
      color: #fff;
    }
    .vue-modal__button+.vue-modal__button {
        margin-left: 10px;
    }
    .vue-modal-fade-enter-active {
      animation: vue-modal-fade-in .3s;
    }
    .vue-modal-fade-leave-active {
      animation: vue-modal-fade-out .3s;
    }
    
    @keyframes vue-modal-fade-in {
      0% {
        opacity: 0;
      }
      100% {
        opacity: 1;
      }
    }
    
    @keyframes vue-modal-fade-out {
      0% {
        opacity: 1;
      }
      100% {
        opacity: 0;
      }
    }
    </style>
    

现在我们的Modal组件就封装好了,也可以说是一个比较完美的Modal组件了。

以上也是封装Vue组件的时候比较通用的思路。