关于vue组件绑定在this上的实现方法

548 阅读3分钟

关于vue组件,平时开发过程中更多的是通过模板进行渲染,然后通过import引用,局部注册组件的方式来调用的;因为项目迁移并且要摒弃一些组件库的原因,需要自己封装一些组件,以下是相应的四个组件的实现过程,许多博客也有相关组件的封装方式,通过学习在这里记录一笔,方便日后温故。

$alert组件

实现目标: [通过this.$alert()调用该组件,并且显示相关内容]
首先我们需要写一个alert组件的模板文件,如下

<template>
  <div :class="`${prefixCls}_wrap`">
    <div :class="`${prefixCls}_mask`" @touchmove.prevent></div>
    <div :class="`${prefixCls}_container`">
      <div :class="`${prefixCls}_header`">
        <div :class="`${prefixCls}_header_title`">{{ title }}</div>
      </div>
      <div :class="`${prefixCls}_main`">
        <div :class="`${prefixCls}_main_content`">{{ content }}</div>
      </div>
      <div :class="`${prefixCls}_footer`">
        <a href="javascript:;" :class="`${prefixCls}_footer_text`" @click.prevent="handleConfirm">{{ confirmButtonText }}</a>
      </div>
    </div>
  </div>
</template>
<script>
const prefixCls = 'app_alert'
export default {
  name: 'app-alert',
  data() {
    return {
      prefixCls,
      visible: false,
      title: '',
      content: '',
      confirmButtonText: '',
    }
  },
  methods: {
    handleConfirm() {}
  }
}
</script>
<style lang="scss" scoped>
  $app_alert: '.app_alert';
  #{$app_alert} {
    /* 遮罩层样式 */
    &_mask {
      position: fixed;
      z-index: 999;
      left: 0;
      top: 0;
      right: 0;
      bottom: 0;
      background: rgba(0, 0, 0, 0.5);
    }

    /* 内容外层样式 */
    &_container {
      position: fixed;
      z-index: 1000;
      width: 75%;
      left: 50%;
      top: 50%;
      transform: translate(-50%, -50%);
      background: #fff;
      font-family: PingFangSC-Medium;
      font-weight: 500;
      border-radius: 4px;
      overflow: hidden;
    }

    /* 头部样式 */
    &_header {
      text-align: center;
      margin: 25px 0 10px;
      &_title {
        font-size: 16px;
        color: rgba(0,0,0,.85);
        line-height: 1.4;
        font-family: PingFangSC-Medium;
        font-weight: 500;
      }
    }

    /* 主体内容样式 */
    &_main {
      text-align: center;
      margin: 0 20px 25px;
      &_content {
        line-height: 1.4;
        font-family: PingFangSC-Regular;
        font-weight: 300;
        font-size: 15px;
        color: rgba(0,0,0,.45);
      }
    }

    /* 底部样式 */
    &_footer {
      position: relative;
      padding: 12px 0 11px;
      &_text {
        display: block;
        line-height: 1.4;
        font-size: 16px;
        font-family: PingFangSC-Regular;
        font-weight: 300;
        color: #E62E34;
        text-align: center;
        &::before {
          content: '';
          position: absolute;
          left: 0;
          top: 0;
          right: 0;
          height: 1px;
          border-top: 1px solid #eee;
          transform-origin: 0 0;
          transform: scaleY(.5);
        }
      }
    }
  }
</style>

以上文件中可以做任何我们想定义的或者想要展示的内容,重要的是下面的js文件
通过上面文件不难发现,该组件中没有props,所以目前无法给该组件进行传值,我们需要在当前目录中新建一个js,如下:

import Vue from 'vue'
import Alert from './index.vue'

const AppAlertConstructor = Vue.extend(Alert)

// 关闭alert窗口vm表示当前实例
const closeAlert = function (vm) {
  vm.visible = false
  vm.$nextTick(() => {
    vm.$destroy(true)
    vm.$el.parentNode.removeChild(vm.$el)
  })
}

const AppAlert = (param) => {
  return new Promise((resolve) => {
    // 默认按钮文案为‘确定’
    const { title, content, confirmButtonText = '确定' } = param
    const AlertInstance = new AppAlertConstructor({
      data: {
        title,
        content,
        confirmButtonText
      }
    })

    AlertInstance.vm = AlertInstance.$mount()
    AlertInstance.vm.visible = true
    AlertInstance.dom = AlertInstance.vm.$el
    document.body.appendChild(AlertInstance.dom)
    AlertInstance.dom.style.zIndex = 99999

    AlertInstance.handleAlert = () => {
      resolve()
      closeAlert(AlertInstance)
    }
  })
}
export default {
  install: Vue => {
    Vue.prototype.$alert = AppAlert
  }
}

然后在main.js文件中进行全局注册

import Alert from '@/components/app-alert/index.js'
Vue.use(Alert)

使用方式如下:

showAlert() {
    this.$alert({
      title: '提示',
      content: '页面过期,重新加载'
    })
  },

效果如下

$confirm组件

实现目标: [通过this.$confirm()调用该组件,并进行相关操作]
组件模板如下:

<template>
  <div :class="`${prefixCls}_wrap`">
    <!-- 遮罩层 -->
    <div :class="[`${prefixCls}_mask`, !visibleMask ? `${prefixCls}_mask_transition` : '']" @touchmove.prevent></div>
    <div :class="`${prefixCls}_position`">
      <transition name="confirm">
        <div :class="`${prefixCls}_container`" v-if="visible">
          <!-- 标题部分 -->
          <div :class="`${prefixCls}_header`">
            <strong :class="`${prefixCls}_header_title`">
              {{ title }}
              <div v-html="iconHtml"></div>
            </strong>
          </div>

          <!-- 中间主内容部分 -->
          <div :class="`${prefixCls}_main`">
            {{ message }}
            <input type="text" v-model="inputValue" v-if="type === 'input'" :placeholder="placeholderText">
          </div>

          <!-- 底部按钮部分 -->
          <div :class="`${prefixCls}_footer`">
            <a href="javascript:;" :class="[`${prefixCls}_footer_cancel`, `${prefixCls}_footer_btn`]" @click.prevent="handleCancel">{{ this.cancelText }}</a>
            <a href="javascript:;" :class="[`${prefixCls}_footer_confirm`, `${prefixCls}_footer_btn`]" @click.prevent="handleConfirm">{{ this.confirmText }}</a>
          </div>
        </div>
      </transition>
    </div>
  </div>
</template>
<script>
/**
 * @description confirm组件,调用方式this.$confirm({...}).then(() => {}).catch(() => {})
 * @param title 属性title设置弹窗标题,当该属性有值时则不会出现icon图标,包裹iconHtml设置的html片段也不会渲染
 * @param message 属性message设置主体文本内容
 * @param placeholderText 属性placeholderText设置input框的占位文本
 * @param cancelText 属性cancelText设置取消按钮的文本,默认‘取消’
 * @param confirmText 属性confirmText设置确定按钮的文本,默认‘确定’
 * @param iconHtml 属性iconHtml自定义一个html片段代替默认的icon图标或标题内容样式等,设置为空时不出现icon
 * @param type 属性type为input时会出现input输入框,目前只有这一种状态
 * @param inputValue 属性inputValue,可以设置输入框默认的值,在点击确定后可以获取当前的输入框值
 */
const prefixCls = 'app_confirm'
export default {
  name: 'app-confirm',
  data() {
    return {
      prefixCls,
      visible: false,
      visibleMask: true,
      title: '',             // 标题
      message: '',           // 主内容
      placeholderText: '',   // input输入框占位符
      cancelText: '',        // 取消按钮文案
      confirmText: '',       // 确定按钮文案
      iconHtml: '',
      type: '',              // 是否带有输入框
      inputValue: ''
    }
  },
  methods: {
    handleCancel() {},
    handleConfirm() {}
  }
}
</script>
<style lang="scss" scoped>
  $app_confirm: '.app_confirm';
  #{$app_confirm} {
    &_mask {
      position: fixed;
      z-index: 999;
      left: 0;
      top: 0;
      right: 0;
      bottom: 0;
      background: rgba(0, 0, 0, 0.6);
      transition: background .2s;
    }
    &_mask_transition {
      background: rgba(0, 0, 0, 0);
    }
    &_position {
      position: fixed;
      z-index: 1000;
      width: 80%;
      max-width: 300px;
      top: 50%;
      left: 50%;
      transform: translate(-50%, -50%);
    }
    &_container {
      background-color: #fff;
      text-align: center;
      border-radius: 3px;
      overflow: hidden;
      font-size: 16px;
    }
    &_header {
      padding: 1.8em 1.6em 0.5em;
      &_title {
        font-weight: 500;
        font-size: 18px;
      }
    }
    &_main {
      min-height: 40px;
      line-height: 1.3;
      word-wrap: break-word;
      word-break: break-all;
      font-size: 16px;
      color: rgba(0, 0, 0, 0.85);
      padding: 0 1.6em 1.5em;
      input {
        width: 4.6rem;
        height: .6rem;
        margin-top: .4rem;
        background-color: #F2F2F2;
        border-radius: .04rem;
        font-size: .28rem;
        padding-left: .2rem;
        border: 0 solid;
        outline: none;
        resize: none;
        text-transform: none;
        -webkit-appearance: button;
      }
    }
    &_footer {
      position: relative;
      line-height: 48px;
      font-size: 18px;
      display: flex;
      &::after {
        content: '';
        position: absolute;
        width: 200%;
        left: 0;
        top: 0;
        right: 0;
        height: 1px;
        border-top: 1px solid #D5D5D6;
        color: #D5D5D6;
        transform-origin: 0 0;
        transform: scale(0.5);
      }
      &_btn {
        display: block;
        flex: 1;
        text-decoration: none;
        -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
        position: relative;
      }
      &_cancel {
        color: #353535;
      }
      &_confirm {
        color: #E62E34;
        &::after {
          content: '';
          position: absolute;
          left: 0;
          top: 0;
          bottom: 0;
          width: 1px;
          height: 200%;
          border-left: 1px solid #D5D5D6;
          color: #D5D5D6;
          transform-origin: 0 0;
          transform: scale(0.5);
        }
      }
    }
  }
  /* ----------- transition动画样式 ----------- */
  .confirm-enter-active {
    transition: transform .2s, opacity .2s;
  }
  .confirm-leave-active {
    transition: transform .2s, opacity .2s;
  }
  .confirm-enter {
    transform: scale(1.1, 1.1);
    opacity: .75;
  }
  .confirm-leave-to {
    transform: scale(.75, .75);
    opacity: .75;
  }
</style>

组件通过vue的transition组件实现了相应的动画过渡效果,有兴趣的可以去了解一下; 其中添加了一些我自己需要的内容,如果不需要这些功能可以去除,比如input框。
下面是js文件

import Vue from 'vue'
import Confirm from './index.vue'

const AppConfirmConstructor = Vue.extend(Confirm)

// 关闭confirm窗口vm表示当前实例
const closeConfirm = function (vm) {
  vm.visible = false
  vm.visibleMask = false
  // 设置定时器为了配合css动画
  setTimeout(() => {
    vm.$destroy(true)
    vm.$el.parentNode.removeChild(vm.$el)
  }, 200);
  // vm.$nextTick(() => {
    
  // })
}

const AppConfirm = (param) => {
  return new Promise((resolve, reject) => {
    const defaultIconHtml = `<div style="background-image: url(${require('@/assets/img/success.png')});width:1.2rem;height:1.2rem;background-size:100%;margin:0 auto"></div>`
    const { title, message, inputValue, cancelText = '取消', confirmText = '确定', placeholderText, iconHtml = defaultIconHtml, type } = param
    const ConfirmInstance = new AppConfirmConstructor({
      data: {
        title,
        message,
        inputValue,
        cancelText,
        confirmText,
        placeholderText,
        iconHtml,
        type
      }
    })

    ConfirmInstance.vm = ConfirmInstance.$mount()
    ConfirmInstance.vm.visible = true
    ConfirmInstance.dom = ConfirmInstance.vm.$el
    document.body.appendChild(ConfirmInstance.dom)
    ConfirmInstance.dom.style.zIndex = 999999

    // 点击确定时走then
    ConfirmInstance.handleConfirm = () => {
      // 返回输入框的值
      const data = {
        inputValue: ConfirmInstance.inputValue
      }
      resolve(data)
      closeConfirm(ConfirmInstance)
    }
    // 点击取消时走catch
    ConfirmInstance.handleCancel = () => {
      reject()
      closeConfirm(ConfirmInstance)
    }
  })
}

export default {
  install: Vue => {
    Vue.prototype.$confirm = AppConfirm
  }
}

注册方式跟上面一样,使用方式如下:

showConfirm() {
    this.$confirm({
      // title: '这是标题',                 // 当title有值时不会出现icon图标
      inputValue: '默认输入的值',            // 可以设置输入框的默认值
      message: '这是信息主体内容',            // 主体文本内容
      // 当iconHtml没有设置时会出现默认的icon图标success.png
      // iconHtml: '',                     // 设置为空时不显示图标
      iconHtml: `<div style="background-image: url(${require('@/assets/img/WeChat.png')});width:1.2rem;height:1.2rem;background-size:100%;margin:0 auto"></div>`,
      // cancelText: '取消',                // 默认显示‘取消’
      // confirmText: '确定',               // 默认显示‘确定’
      placeholderText: '请输入内容',
      type: 'input',
    }).then((data) => {
      // 当type'input'时,点击确定按钮可以接收到输入框的值
      console.log('确定', data.inputValue)
    }).catch(() => {
      console.log('取消')
    })
  },

效果如下

$toast组件

实现目标: [通过this.$toast()调用该组件,并且提示相关信息]
模板文件

<template>
  <div :class="[`${prefixCls}_container`, !this.type ? `${prefixCls}_container_text` : '' ]">
    <div :class="[`${prefixCls}_icon`, alertType]" v-if="this.type"></div>
    <div :class="`${prefixCls}_content`">{{ content }}</div>
  </div>
</template>
<script>
const prefixCls = 'app_alert'
/**
 * @param type 'success'-成功,'fail'-失败,'warn'-警告
 * @param content 提示文本
 */
export default {
  name: 'app-alert',
  data() {
    return {
      prefixCls,
      visible: false,
      content: '',
      type: '',
      duration: 3000
    }
  },
  computed: {
    alertType() {
      return `${prefixCls}_${this.type}`
    }
  },
  methods: {
    setTimer() {
      setTimeout(() => {
        this.close()
      }, this.duration)
    },
    close() {
      this.visible = false
      setTimeout(() => {
        this.$destroy(true)
        this.$el.parentNode.removeChild(this.$el)
      }, 500)
    }
  },
  mounted() {
    this.setTimer()
  }
}
</script>
<style lang="scss" scoped>
  $alert: '.app_alert';
  #{$alert} {
    &_container {
      position: fixed;
      border-radius: 4px;
      text-align: center;
      min-width: 1.8rem;
      // max-width: 5.8rem;
      min-height: 1.8rem;
      padding: 0 8px 8px;
      top: 30%;
      left: 50%;
      transform: translateX(-50%);
      color: #fff;
      background-color: rgba(0, 0, 0, 0.7);
      &_text {
        display: flex;
        flex-direction: column;
        justify-content: center;
        padding-top: 8px;
      }
    }
    &_content {
      font-size: .3rem;
    }
    &_icon {
      display: inline-block;
      width: .85rem;
      height: .85rem;
      margin: .25rem .45rem .05rem .45rem;
      background-size: 100%;
    }
    &_success {
      background-image: url('~assets/img/success-toast.png');
    }
    &_fail {
      background-image: url('~assets/img/toast-fail.png');
    }
    &_warn {
      background-image: url('~assets/img/toast-warn.png');
    }
  }
</style>

接管组件的js文件

import Vue from 'vue'
import Toast from './index.vue'

// 直接将Vue组件作为Vue.extend的参数
const AppToastConstructor = Vue.extend(Toast)

let nId = 1
const AppToast = ({content = '成功', type, duration})=> {
  let id = `appalter${nId++}`
  const ToastInstance = new AppToastConstructor({
    data: {
      content,
      type,
      duration
    }
  })

  ToastInstance.id = id
  ToastInstance.vm = ToastInstance.$mount()
  ToastInstance.vm.visible = true
  ToastInstance.dom = ToastInstance.vm.$el
  document.body.appendChild(ToastInstance.dom)  // 将dom插入body
  ToastInstance.dom.style.zIndex = nId + 1001
  return ToastInstance.vm
}

export default {
  install: Vue => {
    Vue.prototype.$toast = AppToast
  }
}

注册方式跟上面的组件一样
使用方法如下:

this.$toast({
  content: '成功',
  type: 'success',
  duration: 2000         // 设置消失时间
})

效果如下:

loading组件

该组件实现思路与上面几个略有不同;
实现目标: [通过在元素上绑定相应指令调用该组件]
模板文件,写一个loading的组件,如下:

<template>
  <div :class="`${prefixCls}_wrap`" v-show="visible" @after-leave="handleAfterLeave">
    <div :class="`${prefixCls}_mask`"></div>
    <div :class="`${prefixCls}_container`">
      <div :class="`${prefixCls}_animation`">
        <span></span>
        <span></span>
        <span></span>
        <span></span>
        <span></span>
      </div>
      <div :class="`${prefixCls}_text`">
        {{ loadingText }}
      </div>
    </div>
  </div>
</template>
<script>
const prefixCls = 'app_loading'
export default {
  name: 'app-loading',
  data() {
    return {
      prefixCls,
      visible: false,
      loadingText: ''
    }
  },
  methods: {
    handleAfterLeave() {
      this.$emit('after-leave');
    }
  }
}
</script>
<style lang="scss" scoped>
  $app_loading: '.app_loading';
  #{$app_loading} {
    position: relative;
    &_mask {
      position: absolute;
      z-index: 999;
      top: 0;
      right: 0;
      bottom: 0;
      left: 0;
      // background-color: rgba(0,0,0, .8);
    }
    &_container {
      position: fixed;
      z-index: 1000;
      top: 50%;
      left: 50%;
      transform: translate(-50%, -50%);
    }
    &_text {
      margin-top: 5px;
      text-align: center;
      font-size: 13px;
      color: rgb(255, 114, 31);
    }
    &_animation {
      width: 150px;
      height: 10px;
      margin: 0 auto;
      text-align: center;
      span {
        display: inline-block;
        vertical-align: top;
        width: 10px;
        height: 100%;
        margin-right: 5px;
        border-radius: 50%;
        background: rgb(255, 114, 31);
        -webkit-animation: load 1.04s ease infinite;
        &:last-child {
          margin-right: 0;
        }
        &:nth-child(1) {
          -webkit-animation-delay: 0.13s;
        }
        &:nth-child(2) {
          -webkit-animation-delay: 0.26s;
        }
        &:nth-child(3) {
          -webkit-animation-delay: 0.39s;
        }
        &:nth-child(4) {
          -webkit-animation-delay: 0.52s;
        }
        &:nth-child(5) {
          -webkit-animation-delay: 0.65s;
        }
      }
      @-webkit-keyframes load {
        0% {
          opacity: 1;
        }
        100% {
          opacity: 0;
        }
      }
    }
  }
</style>

接管组件的js文件所做的事略有不同,如下:

import Vue from 'vue'
import Loading from './index.vue'

const AppLoadingConstructor = Vue.extend(Loading)

export default {
  install: Vue => {
    Vue.directive('loading', {
      bind: (el, binding) => {
        const loading = new AppLoadingConstructor({
          el: document.createElement('div'),
          data: {
            loadingText: el.getAttribute('loading-text'), // 通过app_loading_text属性获取loading的文字
          }
        })
        el.instance = loading    // el.instance是一个Vue的实例
        el.loading = loading.$el
        el.loadingStyle = {}
        toggleLoading(el, binding)
      },
      update: (el, binding) => {
        // el.instance.setText(el.getAttribute('loading-text'))
        if (binding.oldValue !== binding.value) {
          toggleLoading(el, binding)
        }
      },
      unbind: (el) => {
        if (el.domInserted) {
          document.body.removeChild(el.loading);
        }
      }
    })

    // 用于控制Loading的出现与消失
    const toggleLoading = (el, binding) => {
      if (binding.value) {
        Vue.nextTick(() => {
          el.originalPosition = document.body.style.position;
          el.originalOverflow = document.body.style.overflow;
          insertDom(document.body, el, binding); // 插入dom
        })
      } else {
        if (el.domVisible) {
          el.instance.$on('after-leave', () => {
            el.domVisible = false
            document.body.style.position = el.originalPosition
          })
          el.instance.visible = false
        }
      }
    }

    const insertDom = (parent, el) => {
      if (!el.domVisible) {
        Object.keys(el.loadingStyle).forEach(property => {
          el.loading.style[property] = el.loadingStyle[property]
        })
        if (el.originalPosition && el.originalPosition !== 'absolute') {
          parent.style.position = 'relative'
        }
        // if (!binding.modifiers.fullScreen) {
        //   parent.style.overflow = 'hidden'
        // }
      }
      el.domVisible = true
      parent.appendChild(el.loading)
      Vue.nextTick(() => {
        el.instance.visible = true
      })
      el.domInserted = true
    }
  }
}

注册方式与其他组件一样;
使用方式如下:

<template>
    <div v-loading="loading" loading-text="加载中了"></iv>
</template>
<script type="javascript">
    name: 'demo',
    data () {
        return {
            loading: false,    // 通过改变loading的值来展示/隐藏loading
        }
    }
</script>

实现效果如下:

点到为止~