-
Notifications
You must be signed in to change notification settings - Fork 8
Expand file tree
/
Copy pathv-img.vue
More file actions
236 lines (219 loc) · 6.06 KB
/
v-img.vue
File metadata and controls
236 lines (219 loc) · 6.06 KB
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
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
<template>
<img
class="v-img"
:class="classname"
:style="style"
:height="height"
ref="vImg"
:width="width"
:data-src="imageSrc.$src"
:data-uncropped-src="imageSrc.$uncroppedSrc"
:src="transparentImg"
v-bind="$attrs"
referrerpolicy="no-referrer"
v-on="$listeners"
@load="onLoad"
@error="onError"
@click="onClick"
/>
</template>
<script>
import {providerConfig, default as getSrc} from './provider-config'
import ua from './ua'
/**
* TODO:
* [ ] 可以考虑在check完isSupportWebp后再动态引入lazySizes
* [x] 只watch src的改动
* WHY: 现有一个竞态条件:
* 1. 组件beforeMount时,会异步去获取浏览器是否支持webp,如果支持则按规则转换data-src属性
* 2. lazySizes检测到图片出现在屏幕时,会将data-src的值更新到src属性
* 如果顺序是1 => 2,则是理想的
* 如果顺序是2 => 1,则图片会被请求两次:一是原始src;二是转换为webp的src
*/
const STATUS_IDLE = 0
// 目前没有必要区分 idle 和 loading,暂且保留标识符
const STATUS_LOADING = 1
const STATUS_LOADED = 2
const STATUS_ERROR = 3
export default {
name: 'VImg',
props: {
/** 图片地址 */
src: {
type: String,
default: ''
},
/** 图片宽度, 值为数字, 该属性会与懒加载有关(宽度、高度设置一个即可) */
width: {
type: [String, Number]
},
/** 图片高度, 值为数字, 该属性会与懒加载有关(宽度、高度设置一个即可) */
height: {
type: [String, Number]
},
/** 是否需要 loading 效果 */
hasLoading: {
type: Boolean,
default: true
},
/** 服务提供商 */
provider: {
default: 'alibaba',
validator(v) {
return Object.keys(providerConfig).indexOf(v) > -1
}
},
/**
* 当provider=alibaba时,v-img默认开启oss图片处理服务
* 具体使用方式可参考组件示例和alibaba文档
* @see https://help.aliyun.com/document_detail/44686.html?spm=a2c4g.11186623.6.1236.5aa3e849edfZdj
*/
extraQuery: {
type: String,
default: ''
},
/**
* loading 时的占位图
*/
placeholder: {
type: String,
default: ''
},
/**
* 图片加载失败时的占位图
*/
error: {
type: String,
default: ''
},
/**
* 是否开启自动裁剪
*/
autocrop: {
type: Boolean,
default: true
}
},
data() {
return {
isSupportWebp: null,
status: STATUS_IDLE,
transparentImg:
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII='
}
},
computed: {
classname() {
switch (this.status) {
case STATUS_IDLE:
return 'lazyload'
case STATUS_LOADING:
return 'lazyloading'
case STATUS_ERROR:
return 'lazyload-error'
default:
return ''
}
},
style() {
const baseStyle = {
backgroundSize: 'auto 22px',
backgroundPosition: 'center center',
backgroundRepeat: 'no-repeat',
backgroundColor: '#f0f2f5'
}
switch (this.status) {
case STATUS_LOADING:
if (!this.hasLoading) return {}
return {
...baseStyle,
backgroundImage: `url(${this.loadingImage})`
}
case STATUS_ERROR:
if (!this.hasLoading) return {}
return {
...baseStyle,
backgroundImage: `url(${this.reloadImage})`,
backgroundSize: 'auto 40px',
cursor: 'pointer'
}
default:
return {}
}
},
imageSrc() {
return getSrc(this)
},
loadingImage() {
return this.placeholder || this.$vImg.placeholder
},
reloadImage() {
return this.error || this.$vImg.error
}
},
watch: {
src() {
/**
* 当元素的classList已经不包含lazyload,说明lazySizes已经开始更新当前元素
* 更新之后,lazySizes就不会检查data-src属性的变更;此时要手动更新
*/
if (!this.$el.classList.contains('lazyload')) this.forceUpdateSrc()
}
},
beforeMount() {
this.checkLayout()
this.checkSupportWebp()
},
mounted() {
document.addEventListener('lazybeforeunveil', this.onLoading)
},
beforeDestroy() {
document.removeEventListener('lazybeforeunveil', this.onLoading)
},
methods: {
checkLayout() {
if (!this.width && !this.height) {
console.warn(
'You better set image width or height attribute, otherwise v-img lazyload may not work properly'
)
}
},
async checkSupportWebp() {
// use sync check first
this.isSupportWeb = ua.isSupportWebp(navigator.userAgent)
if (this.isSupportWebp) return
this.isSupportWebp = JSON.parse(localStorage.getItem('isSupportWebp'))
if (this.isSupportWebp !== null) return
this.isSupportWebp = await new Promise(resolve => {
const emptyWebp =
'data:image/webp;base64,UklGRloAAABXRUJQVlA4WAoAAAAQAAAAAAAAAAAAQUxQSAIAAAAAf1ZQOCAyAAAA0AEAnQEqAQABAAEAHCWgAnS6AfgAA7AA/vQ1H/6TZ4mzxNnySP/3UV+6iv3UV/7kqAA='
const image = new Image()
image.onload = () => resolve(!!(image.width && image.height))
image.onerror = () => resolve(false)
image.src = emptyWebp
})
localStorage.setItem('isSupportWebp', this.isSupportWebp)
},
forceUpdateSrc() {
this.$el.setAttribute('src', this.imageSrc.$src)
},
onLoading(e) {
if (this.$refs.vImg == e.target) {
this.status = STATUS_LOADING
document.removeEventListener('lazybeforeunveil', this.onLoading)
}
},
onLoad() {
if (this.$el.getAttribute('src') === this.imageSrc.$src)
this.status = STATUS_LOADED
},
onError() {
this.status = STATUS_ERROR
this.$el.setAttribute('src', this.transparentImg)
},
onClick() {
if (this.status === STATUS_ERROR) this.forceUpdateSrc()
}
}
}
</script>