Skip to content

Commit

Permalink
refactor: ImageLoader 组件重构完成,新增单元测试
Browse files Browse the repository at this point in the history
  • Loading branch information
chenchen32 committed Sep 23, 2021
1 parent c6af508 commit bbe9391
Show file tree
Hide file tree
Showing 5 changed files with 421 additions and 162 deletions.
5 changes: 2 additions & 3 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ const refactoredComp = [
'Menu',
'Layout',
'Dropdown',
'Input',
'ImageLoader',
];

const getRefactoredCompMatch = name => {
Expand Down Expand Up @@ -59,9 +61,6 @@ module.exports = {
},
globals: {
'ts-jest': {
diagnostics: {
warnOnly: true,
},
babelConfig: '.babelrc',
tsConfig: 'tsconfig.json',
},
Expand Down
288 changes: 129 additions & 159 deletions source/components/ImageLoader/ImageLoader.tsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,29 @@
import React from 'react';
import PropTypes from 'prop-types';
import omit from 'omit.js';
import { polyfill } from 'react-lifecycles-compat';
import useUpdateEffect from '../../hooks/useUpdateEffect';

const defaultSizeMap = {
small: {
width: 90,
height: 60
height: 60,
},
default: {
width: 150,
height: 100
height: 100,
},
large: {
width: 240,
height: 160
}
};

const Status = {
PENDING: 'pending',
LOADING: 'loading',
LOADED: 'loaded',
FAILED: 'failed'
height: 160,
},
};

type StatusValue = 'pending' | 'loading' | 'loaded' | 'failed';
enum Status {
PENDING = 'pending',
LOADING = 'loading',
LOADED = 'loaded',
FAILED = 'failed',
}

const noop = () => {};

Expand All @@ -42,85 +40,23 @@ interface ImageLoaderProps {
imgProps?: object;
}

interface ImageLoaderState {
status?: StatusValue;
prevProps?: ImageLoaderProps;
}

class ImageLoader extends React.Component<ImageLoaderProps, ImageLoaderState> {
static propTypes = {
className: PropTypes.string,
prefixCls: PropTypes.string,
placeholderSize: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
style: PropTypes.object,
preLoader: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),
failedLoader: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),
src: PropTypes.string,
onLoad: PropTypes.func,
onError: PropTypes.func,
imgProps: PropTypes.object
};

static defaultProps = {
prefixCls: 'fishd-image-loader',
placeholderSize: 'default',
onLoad: noop,
onError: noop
};

static getDerivedStateFromProps: React.GetDerivedStateFromProps<
ImageLoaderProps,
ImageLoaderState
> = (nextProps, prevState) => {
const { prevProps = { src: '' } } = prevState;
const newState: ImageLoaderState = { prevProps: nextProps };
if (prevProps.src !== nextProps.src) {
newState.status = (nextProps.src ? Status.LOADING : Status.PENDING) as StatusValue;
}

return newState;
};

private img: HTMLImageElement;

constructor(props: ImageLoaderProps) {
super(props);

this.state = {
status: (props.src ? Status.LOADING : Status.PENDING) as StatusValue,
prevProps: props
};
}

componentDidMount() {
if (this.state.status === Status.LOADING) {
this.createLoader();
}
}

componentDidUpdate() {
if (this.state.status === Status.LOADING && !this.img) {
this.createLoader();
}
}
const InternalImageLoader = (props: ImageLoaderProps, ref) => {
const [status, setStatus] = React.useState<Status>(props.src ? Status.LOADING : Status.PENDING);
const imgLoaderRef = React.useRef<HTMLImageElement>(null);

componentWillUnmount() {
this.destroyLoader();
}

getClassName() {
const { prefixCls } = this.props;
let className = `${prefixCls} ${prefixCls}-${this.state.status}`;
if (this.props.className) {
className = `${className} ${this.props.className}`;
const getClassName = () => {
const { prefixCls } = props;
let className = `${prefixCls} ${prefixCls}-${status}`;
if (props.className) {
className = `${className} ${props.className}`;
}
return className;
}
};

base64Img = cls => {
const { placeholderSize } = this.props;
const base64Img = cls => {
const { placeholderSize } = props;
let sizeProps = {
width: '100%'
width: '100%',
};
if (typeof placeholderSize === 'object') {
sizeProps = Object.assign({}, sizeProps, placeholderSize);
Expand All @@ -130,96 +66,130 @@ class ImageLoader extends React.Component<ImageLoaderProps, ImageLoaderState> {
return <div className={cls} style={{ ...sizeProps }} />;
};

createLoader() {
this.destroyLoader(); // We can only have one loader at a time.
const handleLoad = event => {
destroyLoader();
setStatus(Status.LOADED);

this.img = new Image();
this.img.onload = this.handleLoad.bind(this);
this.img.onerror = this.handleError.bind(this);
this.img.src = this.props.src;
}
props.onLoad?.(event);
};

destroyLoader() {
if (this.img) {
this.img.onload = null;
this.img.onerror = null;
this.img = null;
}
}
const handleError = error => {
destroyLoader();
setStatus(Status.FAILED);

handleLoad(event) {
this.destroyLoader();
this.setState({ status: Status.LOADED as StatusValue });
props.onError?.(error);
};

if (this.props.onLoad) this.props.onLoad(event);
}
const createLoader = () => {
destroyLoader(); // We can only have one loader at a time.

handleError(error) {
this.destroyLoader();
this.setState({ status: Status.FAILED as StatusValue });
imgLoaderRef.current = new Image();
const img = imgLoaderRef.current;
img.onload = handleLoad;
img.onerror = handleError;
img.src = props.src;
};

if (this.props.onError) this.props.onError(error);
}
const destroyLoader = () => {
const img = imgLoaderRef.current;
if (img) {
img.onload = null;
img.onerror = null;
imgLoaderRef.current = null;
}
};

renderImg() {
const { src, imgProps } = this.props;
const renderImg = () => {
const { src, imgProps } = props;
const otherProps = omit(imgProps, ['src']);

return <img src={src} {...otherProps} />;
}
return <img ref={ref} src={src} alt={''} {...otherProps} />;
};

useUpdateEffect(() => {
const status = props.src ? Status.LOADING : Status.PENDING;
setStatus(status);
}, [props.src]);

render() {
const { style, preLoader, failedLoader } = this.props;
let wrapperProps = {
style: {},
className: this.getClassName()
React.useEffect(() => {
createLoader();
return () => {
destroyLoader();
};
}, [props.src]);

if (style) {
wrapperProps.style = style;
}
const { style, preLoader, failedLoader } = props;
let wrapperProps = {
style: {},
className: getClassName(),
};

if (style) {
wrapperProps.style = style;
}

let content = null;

switch (this.state.status) {
case Status.LOADED:
// 加载成功
content = this.renderImg();
break;

case Status.FAILED:
// 使用自定义加载失败图片
if (failedLoader) {
if (typeof failedLoader === 'function') {
content = failedLoader();
} else {
content = failedLoader;
}
// 使用系统预置加载失败图片
let content;

switch (status) {
case Status.LOADED:
// 加载成功
content = renderImg();
break;

case Status.FAILED:
// 使用自定义加载失败图片
if (failedLoader) {
if (typeof failedLoader === 'function') {
content = failedLoader();
} else {
content = this.base64Img('failed-img');
content = failedLoader;
}
break;

default:
// 使用自定义占位图片
if (preLoader) {
if (typeof preLoader === 'function') {
content = preLoader();
} else {
content = preLoader;
}
// 使用系统预置占位图片
// 使用系统预置加载失败图片
} else {
content = base64Img('failed-img');
}
break;

default:
// 使用自定义占位图片
if (preLoader) {
if (typeof preLoader === 'function') {
content = preLoader();
} else {
content = this.base64Img('preload-img');
content = preLoader;
}
break;
}

return <div {...wrapperProps}>{content}</div>;
// 使用系统预置占位图片
} else {
content = base64Img('preload-img');
}
break;
}
}

polyfill(ImageLoader);
return <div {...wrapperProps}>{content}</div>;
};

const ImageLoader = React.forwardRef(InternalImageLoader);

ImageLoader.displayName = 'ImageLoader';

ImageLoader.propTypes = {
className: PropTypes.string,
prefixCls: PropTypes.string,
placeholderSize: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
style: PropTypes.object,
preLoader: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),
failedLoader: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),
src: PropTypes.string,
onLoad: PropTypes.func,
onError: PropTypes.func,
imgProps: PropTypes.object,
};

ImageLoader.defaultProps = {
prefixCls: "fishd-image-loader",
placeholderSize: "default",
onLoad: noop,
onError: noop
};

export default ImageLoader;
Loading

0 comments on commit bbe9391

Please sign in to comment.