diff --git a/packages/varlet-vue2-ui/src/lazy/__tests__/__snapshots__/index.spec.js.snap b/packages/varlet-vue2-ui/src/lazy/__tests__/__snapshots__/index.spec.js.snap new file mode 100644 index 0000000..6c50cc2 --- /dev/null +++ b/packages/varlet-vue2-ui/src/lazy/__tests__/__snapshots__/index.spec.js.snap @@ -0,0 +1,31 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`test lazy background-image 1`] = `""`; + +exports[`test lazy background-image 2`] = `""`; + +exports[`test lazy error with attempt 1`] = `""`; + +exports[`test lazy error with attempt 2`] = `""`; + +exports[`test lazy error with attempt 3`] = `""`; + +exports[`test lazy error with attempt 4`] = `""`; + +exports[`test lazy example 1`] = ` +"
+
+
+
+
" +`; + +exports[`test lazy load 1`] = `""`; + +exports[`test lazy load 2`] = `""`; + +exports[`test lazy updated 1`] = `""`; + +exports[`test lazy updated 2`] = `""`; + +exports[`test lazy updated 3`] = `""`; diff --git a/packages/varlet-vue2-ui/src/lazy/__tests__/index.spec.js b/packages/varlet-vue2-ui/src/lazy/__tests__/index.spec.js new file mode 100644 index 0000000..56b8415 --- /dev/null +++ b/packages/varlet-vue2-ui/src/lazy/__tests__/index.spec.js @@ -0,0 +1,111 @@ +import example from '../example' +import Lazy, { imageCache } from '..' +import { mount } from '@vue/test-utils' +import Vue from 'vue' +import { delay, mockDoubleRaf, trigger } from '../../utils/jest' + +test('test lazy example', () => { + const wrapper = mount(example) + expect(wrapper.html()).toMatchSnapshot() + wrapper.destroy() +}) + +test('test lazy plugin', () => { + Vue.use(Lazy) + expect(Vue.directive('lazy')).toBeTruthy() +}) + +const Wrapper = { + directives: { Lazy }, + data: () => ({ + src: 'https://varlet.gitee.io/varlet-ui/cat.jpg', + error: 'https://varlet.gitee.io/varlet-ui/error.jpg', + }), + template: ` + + `, +} + +test('test lazy load', async () => { + const { mockRestore } = mockDoubleRaf() + const wrapper = mount(Wrapper) + await delay(80) + expect(wrapper.html()).toMatchSnapshot() + + await trigger(wrapper.element._lazy.preloadImage, 'load') + await delay(80) + expect(wrapper.html()).toMatchSnapshot() + + wrapper.destroy() + imageCache.clear() + mockRestore() +}) + +test('test lazy error with attempt', async () => { + const { mockRestore } = mockDoubleRaf() + const wrapper = mount(Wrapper) + expect(wrapper.html()).toMatchSnapshot() + + await delay(80) + + await trigger(wrapper.element._lazy.preloadImage, 'error') + await delay(80) + expect(wrapper.html()).toMatchSnapshot() + + await trigger(wrapper.element._lazy.preloadImage, 'error') + await delay(80) + expect(wrapper.html()).toMatchSnapshot() + + await trigger(wrapper.element._lazy.preloadImage, 'error') + await delay(80) + expect(wrapper.html()).toMatchSnapshot() + + wrapper.destroy() + imageCache.clear() + mockRestore() +}) + +test('test lazy updated', async () => { + const { mockRestore } = mockDoubleRaf() + const wrapper = mount(Wrapper) + await delay(80) + expect(wrapper.html()).toMatchSnapshot() + + await trigger(wrapper.element._lazy.preloadImage, 'load') + await delay(80) + expect(wrapper.html()).toMatchSnapshot() + + await wrapper.setData({ src: 'https://varlet.gitee.io/varlet-ui/dog.jpg' }) + await delay(80) + + await trigger(wrapper.element._lazy.preloadImage, 'load') + await delay(80) + expect(wrapper.html()).toMatchSnapshot() + + wrapper.destroy() + imageCache.clear() + mockRestore() +}) + +test('test lazy background-image', async () => { + const { mockRestore } = mockDoubleRaf() + const wrapper = mount({ + directives: { Lazy }, + template: ` + + `, + }) + await delay(80) + expect(wrapper.html()).toMatchSnapshot() + + await trigger(wrapper.element._lazy.preloadImage, 'load') + await delay(80) + expect(wrapper.html()).toMatchSnapshot() + + wrapper.destroy() + imageCache.clear() + mockRestore() +}) diff --git a/packages/varlet-vue2-ui/src/lazy/docs/en-US.md b/packages/varlet-vue2-ui/src/lazy/docs/en-US.md new file mode 100644 index 0000000..8a3910e --- /dev/null +++ b/packages/varlet-vue2-ui/src/lazy/docs/en-US.md @@ -0,0 +1,80 @@ +# Lazy + +### Intro + +Load when the image is visible + +### Install + +```js +import Vue from 'vue' +import { Lazy } from '@varlet-vue2/ui' + +Vue.use(Lazy) +``` + +### Basic Use + +```html + +``` + +### Background Image Lazy Load +```html +
+``` + +### Inline Attributes +You can modify the `loading`, `error` image, and `reload attempts` by using inline properties. + +```html + +``` + +### Plugin + +The option to set the default `Lazy` load option is provided, which is passed in at plugin registration. + +```js +import Vue from 'vue' +import { Lazy } from '@varlet-vue2/ui' + +Vue.use(Lazy, { + loading: 'https://xxx.cn/loading.png', + error: 'https://xxx.cn/error.png', + attempt: 3, + throttleWait: 300, + events: [ + 'scroll', + 'wheel', + 'mousewheel', + 'resize', + 'animationend', + 'transitionend', + 'touchmove' + ], + filter(lazy) { + // All properties of the context can be accessed, and some property interceptions can be performed. + // Such as simply modifying all image addresses http-> https + lazy.src.replace('http://', 'https://') + } +}) +``` + +## API + +### Plugin Options + +| Option | Description | Type | Default | +| --- | --- | --- | --- | +| `loading` | Loading images, if possible, select images that load quickly | _string_ | `Pixel transparent picture` | +| `error` | Load failed to display the picture | _string_ | `Pixel transparent picture` | +| `attempt` | The number of times a load failed to reload | _number_ | `3` | +| `throttleWait` | throttle wait time, The higher the value, the lower the trigger frequency | _number_ | `300` | +| `events` | A list of events that trigger visibility detection registration | _string[]_ | `['scroll'...]` | +| `filter` | Attribute interceptor function | _(lazy: Lazy) => void_ | `() => void` | \ No newline at end of file diff --git a/packages/varlet-vue2-ui/src/lazy/docs/zh-CN.md b/packages/varlet-vue2-ui/src/lazy/docs/zh-CN.md new file mode 100644 index 0000000..7326b9c --- /dev/null +++ b/packages/varlet-vue2-ui/src/lazy/docs/zh-CN.md @@ -0,0 +1,80 @@ +# 图片懒加载 + +### 介绍 + +在图片可见时进行加载 + +### 引入 + +```js +import Vue from 'vue' +import { Lazy } from '@varlet-vue2/ui' + +Vue.use(Lazy) +``` + +### 基本用法 + +```html + +``` + +### 背景图懒加载 +```html +
+``` + +### 内联属性 +可以通过内联属性修改 `loading`、 `error` 图片和`加载失败时尝试重新加载的次数`。 + +```html + +``` + +### 插件 + +`Lazy` 提供了在插件注册时传入的选项,可以设置默认的懒加载选项。 + +```js +import Vue from 'vue' +import { Lazy } from '@varlet-vue2/ui' + +Vue.use(Lazy, { + loading: 'https://xxx.cn/loading.png', + error: 'https://xxx.cn/error.png', + attempt: 3, + throttleWait: 300, + events: [ + 'scroll', + 'wheel', + 'mousewheel', + 'resize', + 'animationend', + 'transitionend', + 'touchmove' + ], + filter(lazy) { + // 可以访问 lazy 上下文的所有属性,执行一些属性拦截, + // 比如简单修改所有的图片地址 http -> https + lazy.src.replace('http://', 'https://') + } +}) +``` + +## API + +### 插件选项 + +| 参数 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| `loading` | 加载中的图片,尽可能选择加载速度很快的图片 | _string_ | `1像素透明图片` | +| `error` | 加载失败显示的图片 | _string_ | `1像素透明图片` | +| `attempt` | 加载失败时尝试重新加载的次数 | _number_ | `3` | +| `throttleWait` | 节流时间,数值越大事件触发频率越低 | _number_ | `300` | +| `events` | 触发可见性检测注册的事件列表 | _string[]_ | `['scroll'...]` | +| `filter` | 属性拦截函数 | _(lazy: Lazy) => void_ | `() => void` | diff --git a/packages/varlet-vue2-ui/src/lazy/example/index.vue b/packages/varlet-vue2-ui/src/lazy/example/index.vue new file mode 100644 index 0000000..93c8ff3 --- /dev/null +++ b/packages/varlet-vue2-ui/src/lazy/example/index.vue @@ -0,0 +1,47 @@ + + + + + diff --git a/packages/varlet-vue2-ui/src/lazy/example/locale/en-US.ts b/packages/varlet-vue2-ui/src/lazy/example/locale/en-US.ts new file mode 100644 index 0000000..13582cc --- /dev/null +++ b/packages/varlet-vue2-ui/src/lazy/example/locale/en-US.ts @@ -0,0 +1,4 @@ +export default { + basicUsage: 'Basic Usage', + backgroundImageLazyLoad: 'Background Image Lazy Load', +} diff --git a/packages/varlet-vue2-ui/src/lazy/example/locale/index.ts b/packages/varlet-vue2-ui/src/lazy/example/locale/index.ts new file mode 100644 index 0000000..d2e375e --- /dev/null +++ b/packages/varlet-vue2-ui/src/lazy/example/locale/index.ts @@ -0,0 +1,23 @@ +// lib +import _zhCN from '../../../locale/zh-CN' +import _enCN from '../../../locale/en-US' +// mobile example doc +import zhCN from './zh-CN' +import enUS from './en-US' +import { useLocale, add as _add, use as _use } from '../../../locale' + +const { add, use: exampleUse, pack, packs, merge } = useLocale() + +const use = (lang: string) => { + _use(lang) + exampleUse(lang) +} + +export { add, pack, packs, merge, use } + +// lib +_add('zh-CN', _zhCN) +_add('en-US', _enCN) +// mobile example doc +add('zh-CN', zhCN as any) +add('en-US', enUS as any) diff --git a/packages/varlet-vue2-ui/src/lazy/example/locale/zh-CN.ts b/packages/varlet-vue2-ui/src/lazy/example/locale/zh-CN.ts new file mode 100644 index 0000000..5b0210b --- /dev/null +++ b/packages/varlet-vue2-ui/src/lazy/example/locale/zh-CN.ts @@ -0,0 +1,4 @@ +export default { + basicUsage: '基本使用', + backgroundImageLazyLoad: '背景图懒加载', +} diff --git a/packages/varlet-vue2-ui/src/lazy/index.ts b/packages/varlet-vue2-ui/src/lazy/index.ts new file mode 100644 index 0000000..ebfa6a6 --- /dev/null +++ b/packages/varlet-vue2-ui/src/lazy/index.ts @@ -0,0 +1,226 @@ +import { getAllParentScroller, inViewport } from '../utils/elements' +import { createCache, removeItem, throttle } from '../utils/shared' +import type { VueConstructor, DirectiveOptions, PluginObject } from 'vue' +import type { DirectiveBinding } from 'vue/types/options' +import type { CacheInstance } from '../utils/shared' + +interface LazyOptions { + loading?: string; + error?: string; + attempt?: number; + throttleWait?: number; + filter?: (lazy: Lazy) => void; + events?: string[]; +} + +type LazyState = 'pending' | 'success' | 'error' + +type Lazy = LazyOptions & { + src: string; + arg: string | undefined; + currentAttempt: number; + attemptLock: boolean; + state: LazyState; + preloadImage?: HTMLImageElement; +} + +export interface LazyHTMLElement extends HTMLElement { + _lazy?: Lazy; +} +type ListenTarget = Window | HTMLElement + +const BACKGROUND_IMAGE_ARG_NAME = 'background-image' +const LAZY_LOADING = 'lazy-loading' +const LAZY_ERROR = 'lazy-error' +const LAZY_ATTEMPT = 'lazy-attempt' +const EVENTS = ['scroll', 'wheel', 'mousewheel', 'resize', 'animationend', 'transitionend', 'touchmove'] +export const PIXEL = 'data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==' +const lazyElements: LazyHTMLElement[] = [] +const listenTargets: ListenTarget[] = [] + +export const imageCache: CacheInstance = createCache(100) + +export const defaultLazyOptions: LazyOptions = { + loading: PIXEL, + error: PIXEL, + attempt: 3, + throttleWait: 300, + events: EVENTS, +} + +let checkAllWithThrottle = throttle(checkAll, defaultLazyOptions.throttleWait) + +function setSRC(el: LazyHTMLElement, src: string) { + if (el._lazy?.arg === BACKGROUND_IMAGE_ARG_NAME) { + el.style.backgroundImage = `url(${src})` + } else { + el.setAttribute('src', src) + } +} + +function setLoading(el: LazyHTMLElement) { + el._lazy?.loading && setSRC(el, el._lazy.loading) + + checkAll() +} + +function setError(el: LazyHTMLElement) { + el._lazy?.error && setSRC(el, el._lazy.error) + el._lazy!.state = 'error' + + clear(el) + checkAll() +} + +function setSuccess(el: LazyHTMLElement, attemptSRC: string) { + setSRC(el, attemptSRC) + el._lazy!.state = 'success' + + clear(el) + checkAll() +} + +function bindEvents(listenTarget: ListenTarget) { + if (listenTargets.includes(listenTarget)) { + return + } + listenTargets.push(listenTarget) + + defaultLazyOptions.events?.forEach((event: string) => { + listenTarget.addEventListener(event, checkAllWithThrottle, { passive: true }) + }) +} + +function unbindEvents() { + listenTargets.forEach((listenTarget: ListenTarget) => { + defaultLazyOptions.events?.forEach((event: string) => { + listenTarget.removeEventListener(event, checkAllWithThrottle) + }) + }) + + listenTargets.length = 0 +} + +function createLazy(el: LazyHTMLElement, binding: DirectiveBinding) { + const lazyOptions: LazyOptions = { + loading: el.getAttribute(LAZY_LOADING) ?? defaultLazyOptions.loading, + error: el.getAttribute(LAZY_ERROR) ?? defaultLazyOptions.error, + attempt: el.getAttribute(LAZY_ATTEMPT) ? Number(el.getAttribute(LAZY_ATTEMPT)) : defaultLazyOptions.attempt, + } + + el._lazy = { + src: binding.value, + arg: binding.arg, + currentAttempt: 0, + state: 'pending', + attemptLock: false, + ...lazyOptions, + } + + setSRC(el, PIXEL) + + defaultLazyOptions.filter?.(el._lazy) +} + +function createImage(el: LazyHTMLElement, attemptSRC: string) { + const image: HTMLImageElement = new Image() + image.src = attemptSRC + el._lazy!.preloadImage = image + + image.addEventListener('load', () => { + 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) ? setError(el) : attemptLoad(el) + }) +} + +function attemptLoad(el: LazyHTMLElement) { + if (el._lazy!.attemptLock) { + return + } + el._lazy!.attemptLock = true + el._lazy!.currentAttempt++ + + const { src: attemptSRC }: Lazy = el._lazy! + + if (imageCache.has(attemptSRC)) { + setSuccess(el, attemptSRC) + el._lazy!.attemptLock = false + return + } + + setLoading(el) + createImage(el, attemptSRC) +} + +async function check(el: LazyHTMLElement) { + ;(await inViewport(el)) && attemptLoad(el) +} + +function checkAll() { + lazyElements.forEach((el: LazyHTMLElement) => check(el)) +} + +async function add(el: LazyHTMLElement) { + !lazyElements.includes(el) && lazyElements.push(el) + getAllParentScroller(el).forEach(bindEvents) + await check(el) +} + +function clear(el: LazyHTMLElement) { + removeItem(lazyElements, el) + lazyElements.length === 0 && unbindEvents() +} + +function diff(el: LazyHTMLElement, binding: DirectiveBinding): boolean { + const { src, arg } = el._lazy! + + return src !== binding.value || arg !== binding.arg +} + +async function mounted(el: LazyHTMLElement, binding: DirectiveBinding) { + createLazy(el, binding) + await add(el) +} + +async function updated(el: LazyHTMLElement, binding: DirectiveBinding) { + if (!diff(el, binding)) { + lazyElements.includes(el) && (await check(el)) + return + } + + await mounted(el, binding) +} + +function mergeLazyOptions(lazyOptions: LazyOptions = {}) { + const { events, loading, error, attempt, throttleWait, filter } = lazyOptions + + defaultLazyOptions.events = events ?? defaultLazyOptions.events + defaultLazyOptions.loading = loading ?? defaultLazyOptions.loading + defaultLazyOptions.error = error ?? defaultLazyOptions.error + defaultLazyOptions.attempt = attempt ?? defaultLazyOptions.attempt + defaultLazyOptions.throttleWait = throttleWait ?? defaultLazyOptions.throttleWait + defaultLazyOptions.filter = filter +} + +const Lazy: DirectiveOptions & PluginObject = { + inserted: mounted, + unbind: clear, + updated, + install(app: VueConstructor, lazyOptions?: LazyOptions) { + mergeLazyOptions(lazyOptions) + + checkAllWithThrottle = throttle(checkAll, defaultLazyOptions.throttleWait) + + app.directive('lazy', this) + }, +} + +export const _LazyComponent = Lazy + +export default Lazy diff --git a/packages/varlet-vue2-ui/types/global.d.ts b/packages/varlet-vue2-ui/types/global.d.ts index ce75fc3..5e81c13 100644 --- a/packages/varlet-vue2-ui/types/global.d.ts +++ b/packages/varlet-vue2-ui/types/global.d.ts @@ -1,6 +1,7 @@ declare module 'vue' { export interface GlobalComponents { VarButton: typeof import('@varlet-vue2/ui')['_ButtonComponent'] + VarLazy: typeof import('@varlet-vue2/ui')['_LazyComponent'] VarLocale: typeof import('@varlet-vue2/ui')['_LocaleComponent'] VarSkeleton: typeof import('@varlet-vue2/ui')['_SkeletonComponent'] } diff --git a/packages/varlet-vue2-ui/types/index.d.ts b/packages/varlet-vue2-ui/types/index.d.ts index 70d8e71..c0645ba 100644 --- a/packages/varlet-vue2-ui/types/index.d.ts +++ b/packages/varlet-vue2-ui/types/index.d.ts @@ -6,6 +6,7 @@ export * from './button' export * from './locale' export * from './skeleton' export * from './cell' +export * from './lazy' export * from './progress' export * from './varComponent' export * from './varDirective' diff --git a/packages/varlet-vue2-ui/types/lazy.d.ts b/packages/varlet-vue2-ui/types/lazy.d.ts new file mode 100644 index 0000000..2255ce0 --- /dev/null +++ b/packages/varlet-vue2-ui/types/lazy.d.ts @@ -0,0 +1,5 @@ +import { VarDirective } from './varDirective' + +export class Lazy extends VarDirective {} + +export class _LazyComponent extends Lazy {}