Skip to content

Commit

Permalink
fix(ui/lazy): 增加1px像素占位 增加图片URL缓存
Browse files Browse the repository at this point in the history
affects: @varlet/ui
  • Loading branch information
haoziqaq committed Dec 23, 2020
1 parent f8fcf78 commit 865a10c
Show file tree
Hide file tree
Showing 3 changed files with 92 additions and 44 deletions.
19 changes: 8 additions & 11 deletions packages/varlet-ui/src/lazy/example/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,10 @@
<button @click="move">move</button>

<img
style="width: 200px; height: 200px"
style="width: 200px; height: 200px; object-fit: cover"
v-for="i in images"
:key="i.id"
v-lazy="i.url"
lazy-loading="https://himg.bdimg.com/sys/portraitn/item/47a3bac4d7d3befd5141515061"
lazy-error="https://himg.bdimg.com/sys/portraitn/item/47a3bac4d7d3befd5141515061"
lazy-attempt="1"
>

<button @click="fix">reset</button>
Expand All @@ -28,34 +25,34 @@ export default defineComponent({
const images = reactive([
{
id: 1,
url: 'https://cn.vuejs.org/images/logo.png',
url: 'https://ddtask.oss-cn-shanghai.aliyuncs.com/ddtask/201208ysl/images/1606982343202.jpg?Expires=1608703112&OSSAccessKeyId=TMP.3Kj8QoTDYVpdcwERRbtMRUh7wkoZvgG25VLcqMaqzYFdcqECUoupXbaFYu36z8tcVvCgwHxUo2RF1xdB46MJpNesxSzKsv&Signature=tmLF4Y65KgbR%2FeUXdZTGSMArxIo%3D&versionId=CAEQFBiBgMDNormesRciIGU5YmQ5MWQwZjZhZTQxNGY5Nzk5YjM1ZDRhY2Q0NzYw&response-content-type=application%2Foctet-stream',
},
{
id: 2,
url: 'https://cn.vuejs.org/imagessss/logo.png',
url: 'https://ddtask.oss-cn-shanghai.aliyuncs.com/ddtask/201208ysl/images/1606982343202.jpg?Expires=1608703112&OSSAccessKeyId=TMP.3Kj8QoTDYVpdcwERRbtMRUh7wkoZvgG25VLcqMaqzYFdcqECUoupXbaFYu36z8tcVvCgwHxUo2RF1xdB46MJpNesxSzKsv&Signature=tmLF4Y65KgbR%2FeUXdZTGSMArxIo%3D&versionId=CAEQFBiBgMDNormesRciIGU5YmQ5MWQwZjZhZTQxNGY5Nzk5YjM1ZDRhY2Q0NzYw&response-content-type=application%2Foctet-stream',
},
{
id: 3,
url: 'https://cn.vuejs.org/images/logo.png',
url: 'https://ddtask.oss-cn-shanghai.aliyuncs.com/ddtask/201208ysl/images/1606982343202.jpg?Expires=1608703112&OSSAccessKeyId=TMP.3Kj8QoTDYVpdcwERRbtMRUh7wkoZvgG25VLcqMaqzYFdcqECUoupXbaFYu36z8tcVvCgwHxUo2RF1xdB46MJpNesxSzKsv&Signature=tmLF4Y65KgbR%2FeUXdZTGSMArxIo%3D&versionId=CAEQFBiBgMDNormesRciIGU5YmQ5MWQwZjZhZTQxNGY5Nzk5YjM1ZDRhY2Q0NzYw&response-content-type=application%2Foctet-stream',
},
{
id: 4,
url: 'https://cn.vuejs.org/images/logo.png',
url: 'https://ddtask.oss-cn-shanghai.aliyuncs.com/ddtask/201208ysl/images/1606982343202.jpg?Expires=1608703112&OSSAccessKeyId=TMP.3Kj8QoTDYVpdcwERRbtMRUh7wkoZvgG25VLcqMaqzYFdcqECUoupXbaFYu36z8tcVvCgwHxUo2RF1xdB46MJpNesxSzKsv&Signature=tmLF4Y65KgbR%2FeUXdZTGSMArxIo%3D&versionId=CAEQFBiBgMDNormesRciIGU5YmQ5MWQwZjZhZTQxNGY5Nzk5YjM1ZDRhY2Q0NzYw&response-content-type=application%2Foctet-stream',
},{
id: 5,
url: 'https://cn.vuejs.org/images/logo.png',
url: 'https://ddtask.oss-cn-shanghai.aliyuncs.com/ddtask/201208ysl/images/1606982343202.jpg?Expires=1608703112&OSSAccessKeyId=TMP.3Kj8QoTDYVpdcwERRbtMRUh7wkoZvgG25VLcqMaqzYFdcqECUoupXbaFYu36z8tcVvCgwHxUo2RF1xdB46MJpNesxSzKsv&Signature=tmLF4Y65KgbR%2FeUXdZTGSMArxIo%3D&versionId=CAEQFBiBgMDNormesRciIGU5YmQ5MWQwZjZhZTQxNGY5Nzk5YjM1ZDRhY2Q0NzYw&response-content-type=application%2Foctet-stream',
}
])
return {
images,
fix() {
images[1].url = 'https://cn.vuejs.org/images/logo.png'
images[1].url = 'https://ddtask.oss-cn-shanghai.aliyuncs.com/ddtask/201208ysl/images/1606982343202.jpg?Expires=1608703112&OSSAccessKeyId=TMP.3Kj8QoTDYVpdcwERRbtMRUh7wkoZvgG25VLcqMaqzYFdcqECUoupXbaFYu36z8tcVvCgwHxUo2RF1xdB46MJpNesxSzKsv&Signature=tmLF4Y65KgbR%2FeUXdZTGSMArxIo%3D&versionId=CAEQFBiBgMDNormesRciIGU5YmQ5MWQwZjZhZTQxNGY5Nzk5YjM1ZDRhY2Q0NzYw&response-content-type=application%2Foctet-stream'
},
push() {
images.push({
id: Date.now(),
url: 'https://cn.vuejs.org/images/logo.png'
url: 'https://ddtask.oss-cn-shanghai.aliyuncs.com/ddtask/201208ysl/images/1606982343202.jpg?Expires=1608703112&OSSAccessKeyId=TMP.3Kj8QoTDYVpdcwERRbtMRUh7wkoZvgG25VLcqMaqzYFdcqECUoupXbaFYu36z8tcVvCgwHxUo2RF1xdB46MJpNesxSzKsv&Signature=tmLF4Y65KgbR%2FeUXdZTGSMArxIo%3D&versionId=CAEQFBiBgMDNormesRciIGU5YmQ5MWQwZjZhZTQxNGY5Nzk5YjM1ZDRhY2Q0NzYw&response-content-type=application%2Foctet-stream'
})
},
pop() {
Expand Down
88 changes: 55 additions & 33 deletions packages/varlet-ui/src/lazy/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import { App, Directive, Plugin } from 'vue'
import { DirectiveBinding } from '@vue/runtime-core'
import { inViewport } from '../utils/elements'
import { checkIntersectionObserverAPI, createInViewportObserver, removeItem, throttle } from '../utils/shared'
import {
CacheInstance,
checkIntersectionObserverAPI, createCache,
createInViewportObserver,
removeItem,
throttle
} from '../utils/shared'

interface LazyOptions {
loading?: string
Expand All @@ -18,19 +24,22 @@ type Lazy = LazyOptions & {
attemptLock: boolean
}

type LazyHTMLElement = HTMLElement & { lazy: Lazy }
type LazyHTMLElement = HTMLElement & { _lazy: Lazy }

const BACKGROUND_IMAGE_ARG_NAME = 'background-image'
const LAZY_LOADING = 'lazy-loading'
const LAZY_ERROR = 'lazy-error'
const LAZY_ATTEMPT = 'lazy-attempt'
const EVENTS = ['resize', 'animationend', 'transitionend', 'touchmove', 'scroll']
const PIXEL = 'data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw=='

const lazyElements: LazyHTMLElement[] = []

const imageCache: CacheInstance<string> = createCache<string>(100)

let defaultLazyOptions: LazyOptions = {
loading: '',
error: '',
loading: PIXEL,
error: PIXEL,
attempt: 3,
throttleWait: 300,
}
Expand All @@ -42,21 +51,21 @@ let observer: IntersectionObserver | null = null
const useIntersectionObserverAPI: boolean = checkIntersectionObserverAPI()

function setLoading(el: LazyHTMLElement) {
el.lazy.loading
? el.lazy.arg === BACKGROUND_IMAGE_ARG_NAME
? el.style.backgroundImage = `url(${el.lazy.loading})`
: el.setAttribute('src', el.lazy.loading)
el._lazy.loading
? el._lazy.arg === BACKGROUND_IMAGE_ARG_NAME
? el.style.backgroundImage = `url(${el._lazy.loading})`
: el.setAttribute('src', el._lazy.loading)
: null

!useIntersectionObserverAPI && checkAll()
}

function setError(el: LazyHTMLElement) {
if (el.lazy.error) {
if (el.lazy.arg === BACKGROUND_IMAGE_ARG_NAME) {
el.style.backgroundImage = `url(${el.lazy.error})`
if (el._lazy.error) {
if (el._lazy.arg === BACKGROUND_IMAGE_ARG_NAME) {
el.style.backgroundImage = `url(${el._lazy.error})`
} else {
el.setAttribute('src', el.lazy.error)
el.setAttribute('src', el._lazy.error)
}
}

Expand All @@ -65,7 +74,7 @@ function setError(el: LazyHTMLElement) {
}

function setSuccess(el: LazyHTMLElement, attemptSRC: string) {
el.lazy.arg === BACKGROUND_IMAGE_ARG_NAME
el._lazy.arg === BACKGROUND_IMAGE_ARG_NAME
? el.style.backgroundImage = `url(${attemptSRC})`
: el.setAttribute('src', attemptSRC)

Expand All @@ -86,10 +95,10 @@ function unbindEvents() {
}

function buildAttemptSRC(el: LazyHTMLElement): string {
const { src } = el.lazy
const { src } = el._lazy

if (el.lazy.currentAttempt === 0) {
el.lazy.currentAttempt++
if (el._lazy.currentAttempt === 0) {
el._lazy.currentAttempt++
return src
}

Expand All @@ -98,7 +107,7 @@ function buildAttemptSRC(el: LazyHTMLElement): string {
const params: string = index > -1 ? src.slice(src.indexOf('?')) : ''

const searchParams = new URLSearchParams(params)
searchParams.set('lazyAttempt', String(++el.lazy.currentAttempt))
searchParams.set('lazyAttempt', String(++el._lazy.currentAttempt))

return `${url}?${searchParams.toString()}`
}
Expand All @@ -110,41 +119,52 @@ function mergeLazyOptions(el: LazyHTMLElement, binding: DirectiveBinding<string>
attempt: el.getAttribute(LAZY_ATTEMPT) ? Number(el.getAttribute(LAZY_ATTEMPT)) : defaultLazyOptions.attempt
}

el.lazy = {
el._lazy = {
src: binding.value,
arg: binding.arg,
currentAttempt: 0,
attemptLock: false,
...lazyInnerOptions
}

defaultLazyOptions.filter?.(el.lazy)
defaultLazyOptions.filter?.(el._lazy)
}

function attemptLoad(el: LazyHTMLElement) {
if (el.lazy.attemptLock === true) {
return
}
el.lazy.attemptLock = true

setLoading(el)

const attemptSRC: string = buildAttemptSRC(el)

function createImage(el: LazyHTMLElement, attemptSRC: string) {
const image: HTMLImageElement = new Image()
image.src = attemptSRC
image.addEventListener('load', () => {
el.lazy.attemptLock = false
el._lazy.attemptLock = false

imageCache.add(attemptSRC)
setSuccess(el, attemptSRC)
})
image.addEventListener('error', () => {
el.lazy.attemptLock = false
;(el.lazy.currentAttempt as number) >= (el.lazy.attempt as number)
el._lazy.attemptLock = false

;(el._lazy.currentAttempt as number) >= (el._lazy.attempt as number)
? setError(el)
: attemptLoad(el)
})
}

function attemptLoad(el: LazyHTMLElement) {
if (el._lazy.attemptLock === true) {
return
}
el._lazy.attemptLock = true

const attemptSRC: string = buildAttemptSRC(el)
if (imageCache.has(attemptSRC)) {
setSuccess(el, attemptSRC)
el._lazy.attemptLock = false
return
}

setLoading(el)
createImage(el, attemptSRC)
}

function check(el: LazyHTMLElement) {
inViewport(el) && attemptLoad(el)
}
Expand Down Expand Up @@ -172,6 +192,8 @@ function observe(el: LazyHTMLElement) {
}

function mounted(el: LazyHTMLElement, binding: DirectiveBinding<string>) {
!el.getAttribute('src') && el.setAttribute('src', PIXEL)

mergeLazyOptions(el, binding)

if (useIntersectionObserverAPI) {
Expand All @@ -187,7 +209,7 @@ function unmounted(el: LazyHTMLElement) {
}

function diff(el: LazyHTMLElement, binding: DirectiveBinding<string>): boolean {
const { src, arg, attempt, loading, error } = el.lazy
const { src, arg, attempt, loading, error } = el._lazy
return src !== binding.value ||
arg !== binding.arg ||
attempt !== Number(el.getAttribute(LAZY_ATTEMPT)) ||
Expand Down
29 changes: 29 additions & 0 deletions packages/varlet-ui/src/utils/shared.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
export interface CacheInstance<T> {
cache: T[]
has(key: T)
add(key: T)
remove(key: T)
}

export const isString = (val: unknown): val is string => typeof val === 'string'

export const isBaseObject = (val: unknown) => Object.prototype.toString.call(val) === '[object Object]'
Expand Down Expand Up @@ -44,3 +51,25 @@ export const createInViewportObserver = <T>(handler: (el: T) => void): Intersect
})
})
}

export const createCache = <T>(max: number): CacheInstance<T> => {
const cache: T[] = []

return {
cache,
has(key: T) {
return cache.includes(key)
},
add(key: T) {
if (this.has(key)) {
return
}

this.cache.length === max && cache.shift()
this.cache.push(key)
},
remove(key: T) {
this.has(key) && removeItem(this.cache, key)
}
}
}

0 comments on commit 865a10c

Please sign in to comment.