feat(components): [image] support native lazy loading (#7968)

This commit is contained in:
qiang 2022-06-01 13:21:05 +08:00 committed by GitHub
parent 19aa8ca424
commit 60cd22b890
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 92 additions and 62 deletions

12
breakings/2.2.3/image.yml Normal file
View File

@ -0,0 +1,12 @@
- scope: 'component'
name: 'el-image'
type: 'props'
version: '2.2.3'
commit_hash: '7a48556'
description: |
Per [HTMLImageElement.loading Request](https://github.com/element-plus/element-plus/issues/7841),
Add [native loading](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img#attr-loading) support for Image components.
props:
- api: 'loading'
before: ''
after: '"eager" | "lazy"'

View File

@ -33,6 +33,14 @@ image/load-failed
## Lazy Load ## Lazy Load
:::tip
Native `loading` has been supported since <VersionTag version="2.2.3" />, you can use `loading = "lazy"` to replace `lazy = true`.
If the current browser supports native lazy loading, the native lazy loading will be used first, otherwise will be implemented through scroll.
:::
:::demo Use lazy load by `lazy = true`. Image will load until scroll into view when set. You can indicate scroll container that adds scroll listener to by `scroll-container`. If undefined, will be the nearest parent container whose overflow property is auto or scroll. :::demo Use lazy load by `lazy = true`. Image will load until scroll into view when set. You can indicate scroll container that adds scroll listener to by `scroll-container`. If undefined, will be the nearest parent container whose overflow property is auto or scroll.
image/lazy-load image/lazy-load
@ -51,20 +59,21 @@ image/image-preview
### Image Attributes ### Image Attributes
| Name | Description | Type | Default | | Attribute | Description | Type | Default |
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------- | ----------------------------------------------------------------------- | | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------- | ----------------------------------------------------------------------- |
| `src` | image source, same as native. | `string` | — | | `src` | image source, same as native. | `string` | — |
| `fit` | indicate how the image should be resized to fit its container, same as [object-fit](https://developer.mozilla.org/en-US/docs/Web/CSS/object-fit). | `'fill' \| 'contain' \| 'cover' \| 'none' \| 'scale'-down'` | — | | `fit` | indicate how the image should be resized to fit its container, same as [object-fit](https://developer.mozilla.org/en-US/docs/Web/CSS/object-fit). | `'fill' \| 'contain' \| 'cover' \| 'none' \| 'scale'-down'` | — |
| `hide-on-click-modal` | when enabling preview, use this flag to control whether clicking on backdrop can exit preview mode. | `boolean` | `false` | | `hide-on-click-modal` | when enabling preview, use this flag to control whether clicking on backdrop can exit preview mode. | `boolean` | `false` |
| `lazy` | whether to use lazy load. | `boolean` | `false` | | `loading` <VersionTag version="2.2.3" /> | Indicates how the browser should load the image, same as [native](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img#attr-loading) | `'eager' \| 'lazy'` | — |
| `scroll-container` | the container to add scroll listener when using lazy load. | `string \| HTMLElement` | the nearest parent container whose overflow property is auto or scroll. | | `lazy` | whether to use lazy load. | `boolean` | `false` |
| `alt` | native attribute `alt`. | `string` | — | | `scroll-container` | the container to add scroll listener when using lazy load. | `string \| HTMLElement` | the nearest parent container whose overflow property is auto or scroll. |
| `referrer-policy` | native attribute `referrerPolicy`. | `string` | — | | `alt` | native attribute `alt`. | `string` | — |
| `preview-src-list` | allow big image preview. | `string[]` | — | | `referrer-policy` | native attribute `referrerPolicy`. | `string` | — |
| `z-index` | set image preview z-index. | `number` | — | | `preview-src-list` | allow big image preview. | `string[]` | — |
| `initial-index` | initial preview image index, less than the length of `url-list`. | `number` | `0` | | `z-index` | set image preview z-index. | `number` | — |
| `close-on-press-escape` | whether the image-viewer can be closed by pressing ESC | `boolean` | `true` | | `initial-index` | initial preview image index, less than the length of `url-list`. | `number` | `0` |
| `preview-teleported` | whether to append image-viewer to body. A nested parent element attribute transform should have this attribute set to `true`. | `boolean` | `false` | | `close-on-press-escape` | whether the image-viewer can be closed by pressing ESC | `boolean` | `true` |
| `preview-teleported` | whether to append image-viewer to body. A nested parent element attribute transform should have this attribute set to `true`. | `boolean` | `false` |
### Image Events ### Image Events

View File

@ -115,6 +115,22 @@ describe('Image.vue', () => {
).not.toContain('display: none') ).not.toContain('display: none')
}) })
test('native loading attributes', async () => {
const wrapper = mount(Image, {
props: {
src: IMAGE_SUCCESS,
loading: 'eager',
} as ElImageProps,
})
await doubleWait()
expect(wrapper.find('img').exists()).toBe(true)
expect(wrapper.find('img').attributes('loading')).toBe('eager')
await wrapper.setProps({ loading: undefined })
expect(wrapper.find('img').attributes('loading')).toBe(undefined)
})
test('$attrs', async () => { test('$attrs', async () => {
const alt = 'this ia alt' const alt = 'this ia alt'
const props: ElImageProps = { const props: ElImageProps = {

View File

@ -21,6 +21,10 @@ export const imageProps = buildProps({
values: ['', 'contain', 'cover', 'fill', 'none', 'scale-down'], values: ['', 'contain', 'cover', 'fill', 'none', 'scale-down'],
default: '', default: '',
}, },
loading: {
type: String,
values: ['eager', 'lazy'],
},
lazy: { lazy: {
type: Boolean, type: Boolean,
default: false, default: false,

View File

@ -1,19 +1,22 @@
<template> <template>
<div ref="container" :class="[ns.b(), $attrs.class]" :style="containerStyle"> <div ref="container" :class="[ns.b(), $attrs.class]" :style="containerStyle">
<slot v-if="loading" name="placeholder"> <img
v-if="imageSrc !== undefined && !hasLoadError"
v-bind="attrs"
:src="imageSrc"
:loading="loading"
:style="imageStyle"
:class="[ns.e('inner'), preview ? ns.e('preview') : '']"
@click="clickHandler"
@load="handleLoad"
@error="handleError"
/>
<slot v-if="isLoading" name="placeholder">
<div :class="ns.e('placeholder')" /> <div :class="ns.e('placeholder')" />
</slot> </slot>
<slot v-else-if="hasLoadError" name="error"> <slot v-else-if="hasLoadError" name="error">
<div :class="ns.e('error')">{{ t('el.image.error') }}</div> <div :class="ns.e('error')">{{ t('el.image.error') }}</div>
</slot> </slot>
<img
v-else
v-bind="attrs"
:src="src"
:style="imageStyle"
:class="[ns.e('inner'), preview ? ns.e('preview') : '']"
@click="clickHandler"
/>
<template v-if="preview"> <template v-if="preview">
<image-viewer <image-viewer
v-if="showViewer" v-if="showViewer"
@ -72,14 +75,14 @@ const ns = useNamespace('image')
const rawAttrs = useRawAttrs() const rawAttrs = useRawAttrs()
const attrs = useAttrs() const attrs = useAttrs()
const imageSrc = ref<string | undefined>()
const hasLoadError = ref(false) const hasLoadError = ref(false)
const loading = ref(true) const isLoading = ref(true)
const imgWidth = ref(0)
const imgHeight = ref(0)
const showViewer = ref(false) const showViewer = ref(false)
const container = ref<HTMLElement>() const container = ref<HTMLElement>()
const _scrollContainer = ref<HTMLElement | Window>() const _scrollContainer = ref<HTMLElement | Window>()
const supportLoading = isClient && 'loading' in HTMLImageElement.prototype
let stopScrollListener: (() => void) | undefined let stopScrollListener: (() => void) | undefined
let stopWheelListener: (() => void) | undefined let stopWheelListener: (() => void) | undefined
@ -107,49 +110,27 @@ const imageIndex = computed(() => {
return previewIndex return previewIndex
}) })
const isManual = computed(() => {
if (props.loading === 'eager') return false
return (!supportLoading && props.loading === 'lazy') || props.lazy
})
const loadImage = () => { const loadImage = () => {
if (!isClient) return if (!isClient) return
// reset status // reset status
loading.value = true isLoading.value = true
hasLoadError.value = false hasLoadError.value = false
imageSrc.value = props.src
const img = new Image()
const currentImageSrc = props.src
// load & error callbacks are only responsible for currentImageSrc
img.addEventListener('load', (e) => {
if (currentImageSrc !== props.src) {
return
}
handleLoad(e, img)
})
img.addEventListener('error', (e) => {
if (currentImageSrc !== props.src) {
return
}
handleError(e)
})
// bind html attrs
// so it can behave consistently
Object.entries(rawAttrs).forEach(([key, value]) => {
// avoid onload to be overwritten
if (key.toLowerCase() === 'onload') return
img.setAttribute(key, value as string)
})
img.src = currentImageSrc
} }
function handleLoad(e: Event, img: HTMLImageElement) { function handleLoad() {
imgWidth.value = img.width isLoading.value = false
imgHeight.value = img.height
loading.value = false
hasLoadError.value = false hasLoadError.value = false
} }
function handleError(event: Event) { function handleError(event: Event) {
loading.value = false isLoading.value = false
hasLoadError.value = true hasLoadError.value = true
emit('error', event) emit('error', event)
} }
@ -235,9 +216,9 @@ function switchViewer(val: number) {
watch( watch(
() => props.src, () => props.src,
() => { () => {
if (props.lazy) { if (isManual.value) {
// reset status // reset status
loading.value = true isLoading.value = true
hasLoadError.value = false hasLoadError.value = false
removeLazyLoadListener() removeLazyLoadListener()
addLazyLoadListener() addLazyLoadListener()
@ -248,7 +229,7 @@ watch(
) )
onMounted(() => { onMounted(() => {
if (props.lazy) { if (isManual.value) {
addLazyLoadListener() addLazyLoadListener()
} else { } else {
loadImage() loadImage()

View File

@ -6,6 +6,12 @@
height: 100%; height: 100%;
} }
%position {
position: absolute;
top: 0;
left: 0;
}
@include b(image) { @include b(image) {
position: relative; position: relative;
display: inline-block; display: inline-block;
@ -17,11 +23,13 @@
} }
@include e(placeholder) { @include e(placeholder) {
@extend %position !optional;
@extend %size !optional; @extend %size !optional;
background: getCssVar('fill-color', 'light'); background: getCssVar('fill-color', 'light');
} }
@include e(error) { @include e(error) {
@extend %position !optional;
@extend %size !optional; @extend %size !optional;
display: flex; display: flex;
justify-content: center; justify-content: center;