# Vue 常用技巧

# 自定义指令

# 仅输入数字

点此展开
// <input v-only-number>
Vue.directive('onlyNumber', {
  inserted: function (targetDom) {
    targetDom.addEventListener('keypress', function (event) {
      event = event || window.event;
      let charcode = typeof event.charCode === 'number' ? event.charCode : event.keyCode;
      if (!/\d/.test(String.fromCharCode(charcode)) && charcode > 9 && !event.ctrlKey) {
        if (event.preventDefault) {
          event.preventDefault();
        } else {
          event.returnValue = false;
        }
      }
    });
  }
});

# 自定聚焦

点此展开
// <input v-focus>
Vue.directive('focus', {
  inserted: function (el) {
    el.focus()
  }
})

# 点击复制到剪切板

点此展开
// 复制
// <div v-copy="number">copy</div>
// <div v-copy="{ value: '111', onSuccess: () => {}, onError: () => {} }">copy</div>
Vue.directive('copy', {
  bind: (targetDom, binding) => {
    let onSuccess = () => {}
    let onError = () => {}
    if (binding.value && typeof binding.value === 'object') {
      targetDom.dataset.copyValue = binding.value.value
      // eslint-disable-next-line prefer-destructuring
      onSuccess = binding.value.onSuccess
      // eslint-disable-next-line prefer-destructuring
      onError = binding.value.onError
    } else {
      targetDom.dataset.copyValue = binding.value
    }
    targetDom.addEventListener('click', async() => {
      try {
        // 使用最新clipboard API
        await navigator.clipboard.writeText(targetDom.dataset.copyValue)
        onSuccess()
      } catch (err) {
        let $input = document.createElement('textarea')
        $input.style.opacity = '0'
        $input.value = targetDom.dataset.copyValue
        document.body.appendChild($input)
        $input.select()

        const isSuccess = document.execCommand('copy')
        isSuccess ? onSuccess() : onError()
        document.body.removeChild($input)
        $input = null
      }
    })
  },

  // 更新存储的值,存储在 dom 的 dataset 中
  update: (el, binding) => {
    el.dataset.copyValue = binding.value
  }
})

# 快捷键映射

点此展开
// <div v-shortcut="{'27': key1}">copy</div>
Vue.directive('shortcut', {
  bind: function (targetDom, binding) {
    // 往 dom 对象中挂载函数,以便卸载时,消除消息监听,keyCode 编码映射表:https://www.bejson.com/othertools/keycodes/
    targetDom.shortcutFun = function (event) {
      Object.keys(binding.value).forEach((key) => {
        event.keyCode.toString() === key && binding.value[key]()
      })
    }
    window.addEventListener('keyup', targetDom.shortcutFun)
  },

  // 指令被卸载,消除消息监听
  unbind: function (targetDom) {
    window.removeEventListener('keyup', targetDom.shortcutFun)
  }
})

# 点击元素外部进行回调

点此展开
// <div v-click-outside="fun1">
//   <div>copy</div>
//   <div>copy2</div>
// </div>
Vue.directive('clickOutside', {
  bind: function (targetDom, binding) {
    targetDom.clickOutsideFun = function (event) {
      if (event.target !== targetDom && !targetDom.contains(event.target)) {
        binding.value && binding.value()
      }
    }
    window.addEventListener('click', targetDom.clickOutsideFun)
    window.addEventListener('touchend', targetDom.clickOutsideFun)
  },

  // 指令被卸载,消除消息监听
  unbind: function (targetDom) {
    window.removeEventListener('click', targetDom.clickOutsideFun)
    window.removeEventListener('touchend', targetDom.clickOutsideFun)
  }
})

# 点击约束

点此展开
/*** 监听 ajax,自动为匹配到的 dom 元素添加点击约束  ***/
// <div id="1" v-waiting="['get::waiting::/test/users?pageIndex=2', 'get::/test/users?pageIndex=1']" @click="test"></div>
// <div id="2" v-waiting="'get::loading::http://yapi.luckly-mjw.cn/mock/50/test/users?pageIndex=2'" @click="test">copy</div>
// <div id="3" v-waiting="'get::disable::http://yapi.luckly-mjw.cn/mock/50/test/users?pageIndex=2'" @click="test">copy</div>
Vue.directive('waiting', {
  bind: function (targetDom, binding) {
    // 注入全局方法
    (function () {
      if (window._hadResetAjaxForWaiting) { // 如果已经重置过,则不再进入。解决开发时局部刷新导致重新加载问题
        return
      }
      window._hadResetAjaxForWaiting = true
      window._ajaxMap = {} // 接口映射 'get::http://yapi.luckly-mjw.cn/mock/50/test/users?pageIndex=1': dom

      let originXHR = window.XMLHttpRequest
      let originOpen = originXHR.prototype.open

      // 重置 XMLHttpRequest
      window.XMLHttpRequest = function () {
        let targetDomList = [] // 存储本 ajax 请求,影响到的 dom 元素
        let realXHR = new originXHR() // 重置操作函数,获取请求数据

        realXHR.open = function (method, url, asyn) {
          Object.keys(window._ajaxMap).forEach((key) => {
            let [targetMethod, type, targetUrl] = key.split('::')
            if (!targetUrl) { // 设置默认类型
              targetUrl = type
              type = 'v-waiting-waiting'
            } else { // 指定类型
              type = `v-waiting-${type}`
            }
            if (targetMethod.toLocaleLowerCase() === method.toLocaleLowerCase() && url.indexOf(targetUrl) > -1) {
              targetDomList = [...window._ajaxMap[key], ...targetDomList]
              window._ajaxMap[key].forEach(dom => {
                if (!dom.classList.contains(type)) {
                  dom.classList.add('v-waiting', type)
                  if (window.getComputedStyle(dom).position === 'static') { // 如果是 static 定位,则修改为 relative,为伪类的绝对定位做准备
                    dom.style.position = 'relative'
                  }
                }
                dom.waitingAjaxNum = dom.waitingAjaxNum || 0 // 不使用 dataset,是应为 dataset 并不实时,在同一个时间内,上一次存储的值不能被保存
                dom.waitingAjaxNum++
              })
            }
          })
          originOpen.call(realXHR, method, url, asyn)
        }

        // 监听加载完成,清除 waiting
        realXHR.addEventListener('loadend', function () {
          targetDomList.forEach(dom => {
            // dom.waitingAjaxNum--
            dom.waitingAjaxNum === 0 && dom.classList.remove(
              'v-waiting',
              'v-waiting-loading',
              'v-waiting-waiting',
              'v-waiting-disable',
            )
          })
        }, false)
        return realXHR
      }
    })();

    // 注入全局 css
    (function () {
      if (!document.getElementById('v-waiting')) {
        let code = `
      .v-waiting {
    pointer-events: none;
    /*cursor: not-allowed; 与 pointer-events: none 互斥,设置 pointer-events: none 后,设置鼠标样式无效 */
  }
  .v-waiting::before {
    position: absolute;
    content: '';
    left: 0;
    top: 0;
    width: 100%;
    height: 100%;
    opacity: 0.4;
    z-index: 9999;
    background-color: #ffffff;
  }
  .v-waiting-loading::after {
    position: absolute;
    content: '数据加载中...';
    top: 50%;
    left: 0;
    width: 100%;
    color: #666666;
    font-size: 20px;
    text-align: center;

    transform: translateY(-50%);
    z-index: 9999;
  }
  .v-waiting-waiting::after {
    position: absolute;
    content: '';
    left: 50%;
    top: 50%;
    width: 30px;
    height: 30px;
    z-index: 9999;
    cursor: not-allowed;
    animation: v-waiting-v-waiting-keyframes 1.1s infinite linear;
    background-size: 30px 30px;
    background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAMAAABg3Am1AAAAe1BMVEUAAAAmJDYnJDQlIzUmJTYmJTYnJDYkJDcnJTYlJTMnJTYmJTUmJTYnJTYnJDYnIzYmJTYnJTYmJTYnJTYmJTYlJTUkJDMkJEAnJTUmJTUnJTUmJDYnJDYnJDYmJDYnJTYnJTYoJjYoJTYnJDUgIDAmJTUmJTYnJTYnJTb3WaDKAAAAKHRSTlMAgD8V1OSuKrIM9OzNiWFH3saeZTYvIge/l450VUwcuKSHbSYQp6aIG32lvwAAAU5JREFUSMflleeOgzAQhE0xndBLSEi7tu//hHfgkU8HxjJS/pzy/bLxTHa9Ggh7EvYQlNYOvU8T3FjfuTQzmBoeBBxDg0eg1sqcusXKIvCm0wdElDTzMoOedxq9K7v+7emwLZddeGJb5UTBKA+TJF0aPgi840EzLUBEFDmrGwB/Xb1V3f8LendjzMVqpjCcmIo6U8RnHtOZmdM9vLRh/5fQ3yVvAyL3ZK73ZdL0ZFb2530ksdXG0xJJAXf96yXTbxPodXrI7J/VhYD+2mFMRHE4LQuhjzS5QNhcTN+V1fQ17FAOoIhvPntFxjIOKnM5vmRX5VlVlqto1rSd2fN0cFs8PMKQq2O3/qD0BOyN2JGlrhBvVhgX94LhKLaXgffyJ1Nlr1dhgF5spIPzdF35kET5HWtPtm0Ix7/LXsOnsSHY2RJGn4TM3FFE3HPYM/gG2m01vLvk6YQAAAAASUVORK5CYII=);
  }
  @-webkit-keyframes v-waiting-v-waiting-keyframes {
    from {
      transform: translate(-50%, -50%) rotate(0deg);
    }
    to {
      transform: translate(-50%, -50%) rotate(360deg);
    }
  }      `
        let style = document.createElement('style')
        style.id = 'v-waiting'
        style.type = 'text/css'
        style.rel = 'stylesheet'
        style.appendChild(document.createTextNode(code))
        let head = document.getElementsByTagName('head')[0]
        head.appendChild(style)
      }
    })()

    // 添加需要监听的接口,注入对应的 dom
    const targetUrlList = Array.isArray(binding.value) ? binding.value : [binding.value]
    targetUrlList.forEach(targetUrl => window._ajaxMap[targetUrl] = [targetDom, ...(window._ajaxMap[targetUrl] || [])])
  },

  // 指令被卸载,消除消息监听
  unbind: function (targetDom, binding) {
    const targetUrlList = typeof binding.value === Array ? binding.value : [binding.value]
    targetUrlList.forEach(targetUrl => {
      const index = window._ajaxMap[targetUrl].indexOf(targetDom)
      index > -1 && window._ajaxMap[targetUrl].splice(index, 1)
      if (window._ajaxMap[targetUrl].length === 0) {
        delete window._ajaxMap[targetUrl]
      }
    })
  }
})

# 开发技巧

# Vue.observable 状态共享

Vue.observable(object):让一个对象可响应。Vue 内部会用它来处理 data 函数返回的对象。

随着组件的细化,就会遇到多组件状态共享的情况,Vuex当然可以解决这类问题,不过就像Vuex官方文档所说的,如果应用不够大,为避免代码繁琐冗余,最好不要使用它。 通过使用 Vue 2.6 新增加的 Observable API 可以应对一些简单的跨组件数据状态共享的情况。

比如,我们在组件外创建一个 store.js 文件,然后在 App.vue 组件里面使用 store.js 提供的 storemutation 方法,同理其它组件也可以这样使用,从而实现多个组件共享数据状态。

// store.js
import Vue from "vue"

export const store = Vue.observable({ count: 0 })
export const mutations = {
  setCount(count) {
    store.count = count
  }
};

然后在 App.vue 里面引入 store.js,在组件中使用引入的数据和方法。

<!-- App.vue -->
<template>
  <div id="app">
    <p>count:{{count}}</p>
    <button @click="setCount(count+1)">+1</button>
    <button @click="setCount(count-1)">-1</button>
  </div>
</template>

<script>
import { store, mutations } from "./store"
export default {
  name: "App",
  computed: {
    count() {
      return store.count
    }
  },
  methods: {
    setCount: mutations.setCount
  }
}
</script>

# 参考链接

# 函数式组件

函数式组件,即无状态,无法实例化,内部没有任何生命周期处理方法,非常轻量,因而渲染性能高,特别适合用来只依赖外部数据传递而变化的组件。

# 示例:

  • 单文件组件:
<!-- List.vue 函数式组件 -->
<template functional>
  <div>
    <p v-for="item in props.items" @click="props.itemClick(item)">
      {{ item }}
    </p>
  </div>
</template>
  • 使用渲染函数:
Vue.component('my-component', {
  functional: true,
  // Props 是可选的
  props: {
    // ...
  },
  // 为了弥补缺少的实例
  // 提供第二个参数作为上下文
  render: function (createElement, context) {
    // ...
  }
})

# 参考链接

# @hook: 监听子组件生命周期

比如有父组件 Parent 和子组件 Child,如果父组件监听到子组件挂载 mounted 就做一些逻辑处理,可使用以下写法:

<Child @hook:mounted="doSomething"/>

这里不仅仅是可以监听 mounted,其它的生命周期事件,如createdupdated 等也都可以

# 父组件监听子组件加载完成事件

有些时候需要在父组件mounted的时候获取子组件的dom,但是这个时候是不一定获取得到的,因为子组件不一定能够加载完成。

# 解决方法

  1. (不推荐❌)父组件使用定时器 setInterval,里面不断判断是否获取到子组件dom,获取到则清除定时器。

  2. 子组件mounted的时候,抛出自定义事件,父组件监听该事件,然后再获取子组件。

// parent.vue
<child @has-mounted="getDom" />

// child.vue
mounted() {
    this.$emit('has-mounted');
}
  1. (推荐✔)父组件引用子组件时,使用 @hook:mounted="getDom" 父组件监听getDom事件即可。
// 子组件
<child @hook:mounted="getDom" />

// 父组件
methods:{
    getDom() {
       // 获取子元素等操作
    }
}

当然,这里不仅仅可以监听mounted,其他子组件的生命周期事件,比如:createdupdated等都可以使用,是不是超级方便!

# 参考链接

更新时间: 2021年12月4日星期六晚上7点08分