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
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
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
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
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
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
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
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 参考链接
# 函数式组件
函数式组件,即无状态,无法实例化,内部没有任何生命周期处理方法,非常轻量,因而渲染性能高,特别适合用来只依赖外部数据传递而变化的组件。
# 示例:
- 单文件组件:
<!-- 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
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
2
3
4
5
6
7
8
9
10
11
12
# 参考链接
# @hook:
监听子组件生命周期
比如有父组件 Parent
和子组件 Child
,如果父组件监听到子组件挂载 mounted
就做一些逻辑处理,可使用以下写法:
<Child @hook:mounted="doSomething"/>
1
这里不仅仅是可以监听 mounted
,其它的生命周期事件,如created
,updated
等也都可以
# 父组件监听子组件加载完成事件
有些时候需要在父组件mounted
的时候获取子组件的dom
,但是这个时候是不一定获取得到的,因为子组件不一定能够加载完成。
# 解决方法
(不推荐❌)父组件使用定时器 setInterval,里面不断判断是否获取到子组件dom,获取到则清除定时器。
子组件
mounted
的时候,抛出自定义事件,父组件监听该事件,然后再获取子组件。
// parent.vue
<child @has-mounted="getDom" />
// child.vue
mounted() {
this.$emit('has-mounted');
}
1
2
3
4
5
6
7
2
3
4
5
6
7
- (推荐✔)父组件引用子组件时,使用 @hook:mounted="getDom" 父组件监听getDom事件即可。
// 子组件
<child @hook:mounted="getDom" />
// 父组件
methods:{
getDom() {
// 获取子元素等操作
}
}
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
当然,这里不仅仅可以监听mounted
,其他子组件的生命周期事件,比如:created
,updated
等都可以使用,是不是超级方便!
# 参考链接
编辑 (opens new window)