Просмотр исходного кода

fix: [Image] Fix the image is not loaded normally when the src in Image changes in lazy loading mode (#1579)

YyumeiZhang 2 лет назад
Родитель
Сommit
fb51ad5a68

+ 11 - 2
cypress/integration/image.spec.js

@@ -501,7 +501,7 @@ describe('image', () => {
 
     // 测试在预览状态下,图片改变 ratio 状态后,切换图片,ratio 状态是否正确
     //(在未受控 ratio ,无默认 ratio 情况下,切换后的图片ratio 应该为适应页面)
-    it.only('ratio status after change pic', () => {
+    it('ratio status after change pic', () => {
         cy.visit('http://127.0.0.1:6006/iframe.html?id=image--basic-preview&args=&viewMode=story');
         cy.wait(2000);
         // 进入预览状态
@@ -515,4 +515,13 @@ describe('image', () => {
         // 当前 ratio 状态应该为适应页面
         cy.get('.semi-icon-real_size_stroked').should('exist');
     });
-});
+
+    // 测试懒加载状态下 src 变化时候图片是否正常加载
+    it('src change', () => {
+        cy.visit('http://127.0.0.1:6006/iframe.html?id=image--issue-1526&args=&viewMode=story');
+        // 等待 5000 ms, 确保当前src 已经完全改变上
+        cy.wait(5000);
+        // 进入预览状态
+        cy.get('.semi-image-img').eq(0).should('have.attr', 'src', 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/lion.jpeg');
+    });
+});

+ 35 - 1
packages/semi-ui/image/_story/image.stories.jsx

@@ -1,4 +1,4 @@
-import React, { useState, useCallback, useMemo } from "react";
+import React, { useState, useCallback, useMemo, useEffect } from "react";
 import {
     Image,
     Button,
@@ -549,3 +549,37 @@ export const CustomRenderTitle = () => (
         </ImagePreview>
     </>
 );
+
+export const issue1526 = () => {
+    // 测试懒加载状态下,image src 改变时候加载是否符合预期
+    const [src, setSrc] = useState([]);
+    const srcList1 = [
+        "https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/lion.jpeg",
+        "https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/seaside.jpeg",
+        "https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/beach.jpeg",
+    ];
+
+    // 模拟远程加载
+    useEffect(() => {
+       setTimeout(() => {
+        setSrc(srcList1);
+       }, 1000);
+    }, []);
+
+    return (
+        <ImagePreview zIndex={1000} >
+            {src.map((file, index) => (
+                <div
+                    style={{
+                        position: 'relative',
+                        cursor: 'pointer',
+                        display: 'inline-block',
+                    }}
+                    key={file}
+                >
+                    <Image key={file} src={file} width={96} height={96} />
+                </div>
+            ))}
+        </ImagePreview>
+    )
+}

+ 20 - 1
packages/semi-ui/image/image.tsx

@@ -50,6 +50,7 @@ export default class Image extends BaseComponent<ImageProps, ImageStates> {
 
     context: PreviewContextProps;
     foundation: ImageFoundation;
+    imgRef: React.RefObject<HTMLImageElement>;
 
     constructor(props: ImageProps) {
         super(props);
@@ -60,6 +61,7 @@ export default class Image extends BaseComponent<ImageProps, ImageStates> {
         };
 
         this.foundation = new ImageFoundation(this.adapter);
+        this.imgRef = React.createRef<HTMLImageElement>();
     }
 
     static getDerivedStateFromProps(props: ImageProps, state: ImageStates) {
@@ -80,6 +82,22 @@ export default class Image extends BaseComponent<ImageProps, ImageStates> {
         return willUpdateStates;
     }
 
+    componentDidMount() {
+        this.observeImage();
+    }
+
+    componentDidUpdate(prevProps: ImageProps, prevState: ImageStates) {
+        prevProps.src !== this.props.src && this.observeImage();
+    }
+
+    observeImage() {
+        if (!this.isLazyLoad()) {
+            return;
+        }
+        const { previewObserver } = this.context;
+        previewObserver.observe(this.imgRef.current);
+    }
+
     isInGroup() {
         return Boolean(this.context && this.context.isGroup);
     }
@@ -169,7 +187,7 @@ export default class Image extends BaseComponent<ImageProps, ImageStates> {
             <IconEyeOpened size="extra-large"/>
             <span className={`${prefixCls}-mask-info-text`}>{this.getLocalTextByKey("preview")}</span>
         </div>
-    </div>)
+    </div>);
 
     render() {
         const { src, loadStatus, previewVisible } = this.state;
@@ -189,6 +207,7 @@ export default class Image extends BaseComponent<ImageProps, ImageStates> {
                 onClick={this.handleClick}
             >
                 <img
+                    ref={this.imgRef}
                     {...restProps}
                     src={this.isInGroup() && this.isLazyLoad() ? undefined : src}
                     data-src={src}

+ 7 - 11
packages/semi-ui/image/preview.tsx

@@ -70,6 +70,7 @@ export default class Preview extends BaseComponent<PreviewProps, PreviewState> {
     foundation: PreviewFoundation;
     previewGroupId: string;
     previewRef: React.RefObject<PreviewInner>;
+    previewObserver: IntersectionObserver;
 
     constructor(props) {
         super(props);
@@ -80,27 +81,21 @@ export default class Preview extends BaseComponent<PreviewProps, PreviewState> {
         this.foundation = new PreviewFoundation(this.adapter);
         this.previewGroupId = getUuidShort({ prefix: "semi-image-preview-group", length: 4 });
         this.previewRef = React.createRef<PreviewInner>();
-    }
-
-    componentDidMount() {
-        const { lazyLoadMargin } = this.props;
-        const allElement = document.querySelectorAll(`.${prefixCls}-img`);
-        // use IntersectionObserver to lazy load image
-        const observer = new IntersectionObserver(entries => {
+        this.previewObserver = new IntersectionObserver(entries => {
             entries.forEach(item => {
                 const src = (item.target as any).dataset?.src;
                 if (item.isIntersecting && src) {
                     (item.target as any).src = src;
-                    observer.unobserve(item.target);
+                    (item.target as any).removeAttribute("data-src");
+                    this.previewObserver.unobserve(item.target);
                 }
             });
         },
         {
             root: document.querySelector(`#${this.previewGroupId}`),
-            rootMargin: lazyLoadMargin, 
+            rootMargin: props.lazyLoadMargin, 
         }
         );
-        allElement.forEach(item => observer.observe(item));
     }
 
     static getDerivedStateFromProps(props: PreviewProps, state: PreviewState) {
@@ -114,7 +109,7 @@ export default class Preview extends BaseComponent<PreviewProps, PreviewState> {
         return willUpdateStates;
     }
 
-    handleVisibleChange = (newVisible : boolean) => {
+    handleVisibleChange = (newVisible: boolean) => {
         this.foundation.handleVisibleChange(newVisible);
     };
 
@@ -174,6 +169,7 @@ export default class Preview extends BaseComponent<PreviewProps, PreviewState> {
                     currentIndex,
                     visible,
                     lazyLoad,
+                    previewObserver: this.previewObserver,
                     setCurrentIndex: this.handleCurrentIndexChange,
                     handleVisibleChange: this.handleVisibleChange,
                 }}

+ 1 - 0
packages/semi-ui/image/previewContext.tsx

@@ -7,6 +7,7 @@ export interface PreviewContextProps {
     titles: ReactNode[];
     currentIndex: number;
     visible: boolean;
+    previewObserver: IntersectionObserver;
     setCurrentIndex: (current: number) => void;
     handleVisibleChange: (visible: boolean, preVisible?: boolean) => void
 }