Wang's blog Wang's blog
首页
  • 前端文章

    • HTML教程
    • CSS
    • JavaScript
  • 前端框架

    • Vue
    • React
    • VuePress
    • Electron
  • 后端技术

    • Npm
    • Node
    • TypeScript
  • 编程规范

    • 规范
  • 我的笔记
  • Git
  • GitHub
  • VSCode
  • Mac工具
  • 数据库
  • Google
  • 服务器
  • Python爬虫
  • 前端教程
更多
收藏
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

Wang Mings

跟随大神,成为大神!
首页
  • 前端文章

    • HTML教程
    • CSS
    • JavaScript
  • 前端框架

    • Vue
    • React
    • VuePress
    • Electron
  • 后端技术

    • Npm
    • Node
    • TypeScript
  • 编程规范

    • 规范
  • 我的笔记
  • Git
  • GitHub
  • VSCode
  • Mac工具
  • 数据库
  • Google
  • 服务器
  • Python爬虫
  • 前端教程
更多
收藏
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • CSS

  • Npm

  • Vue

    • Vue简介
    • 路由守卫
    • tsvue的写法
    • Vue常用技巧
      • 自定义指令
        • 仅输入数字
        • 自定聚焦
        • 点击复制到剪切板
        • 快捷键映射
        • 点击元素外部进行回调
        • 点击约束
      • 开发技巧
        • Vue.observable 状态共享
        • 参考链接
        • 函数式组件
        • 示例:
        • 参考链接
        • @hook: 监听子组件生命周期
        • 父组件监听子组件加载完成事件
        • 解决方法
        • 参考链接
    • vue的JSX写法
    • vue防抖组件
    • 动态热更新设置
    • keepActive缓存路由
    • 自动生成面包屑VUE组件
    • vue生命周期对比生命周期
    • eslint编译时警告或错误配置
    • Vue中封装axios的取消请求事情
    • vue+element递归生成无限菜单组件
    • Vue框架dist目录下各个文件的区别
    • 超详细Vue的种和Vue的种组件间通信方式
    • Vue项目中出现Loadingchunk{n}failed问题的解决方法
    • Vuex

    • 其他

    • 基础

    • 工具

    • 组件

    • 规模化

    • 过渡&动画

    • 可复用性&组合

  • HTML

  • Node

  • Yaml

  • React

  • 框架

  • 规范

  • Electron

  • JS演示

  • VuePress

  • JavaScript

  • TypeScript

  • 微信小程序

  • TypeScript-axios

  • 前端
  • Vue
wangmings
2022-07-19
目录

Vue常用技巧

# 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;
        }
      }
    });
  }
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 自定聚焦

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

# 点击复制到剪切板

点此展开
// 复制
// <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
  }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

# 快捷键映射

点此展开
// <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)
  }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 点击元素外部进行回调

点此展开
// <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)
  }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 点击约束

点此展开
/*** 监听 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]
      }
    })
  }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144

# 开发技巧

# Vue.observable 状态共享

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

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

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

// store.js
import Vue from "vue"

export const store = Vue.observable({ count: 0 })
export const mutations = {
  setCount(count) {
    store.count = count
  }
};
1
2
3
4
5
6
7
8
9

然后在 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>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# 参考链接

  • API — Vue.js (opens new window)
  • 7个有用的Vue开发技巧 (opens new window)

# 函数式组件

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

# 示例:

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

# 参考链接

  • 渲染函数 & JSX — Vue.js (opens new window)
  • 7个有用的Vue开发技巧 (opens new window)

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

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

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

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

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

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

# 解决方法

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

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

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

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

// 父组件
methods:{
    getDom() {
       // 获取子元素等操作
    }
}
1
2
3
4
5
6
7
8
9

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

# 参考链接

  • Vue@hook那些事 - 知乎 (opens new window)
  • 7个有用的Vue开发技巧 (opens new window)
编辑 (opens new window)
tsvue的写法
vue的JSX写法

← tsvue的写法 vue的JSX写法→

最近更新
01
theme-vdoing-blog博客静态编译问题
09-16
02
搜索引擎
07-19
03
友情链接
07-19
更多文章>
Theme by Vdoing | Copyright © 2019-2022 Evan Xu | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式