mirror of
https://github.com/zhigang1992/react-native-web.git
synced 2026-04-29 12:54:53 +08:00
[fix] Image loading for source={{ uri: '' }}
Avoid an error being thrown from attempting to call `match` on an object value. Fix #962
This commit is contained in:
86
packages/react-native-web/src/exports/Image/ImageSourcePropType.js
vendored
Normal file
86
packages/react-native-web/src/exports/Image/ImageSourcePropType.js
vendored
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2015-present, Facebook, Inc.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*
|
||||||
|
* @flow
|
||||||
|
* @format
|
||||||
|
*/
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
const ImageURISourcePropType = PropTypes.shape({
|
||||||
|
/**
|
||||||
|
* `uri` is a string representing the resource identifier for the image, which
|
||||||
|
* could be an http address, a local file path, or the name of a static image
|
||||||
|
* resource (which should be wrapped in the `require('./path/to/image.png')`
|
||||||
|
* function).
|
||||||
|
*/
|
||||||
|
uri: PropTypes.string,
|
||||||
|
/**
|
||||||
|
* `bundle` is the iOS asset bundle which the image is included in. This
|
||||||
|
* will default to [NSBundle mainBundle] if not set.
|
||||||
|
* @platform ios
|
||||||
|
*/
|
||||||
|
bundle: PropTypes.string,
|
||||||
|
/**
|
||||||
|
* `method` is the HTTP Method to use. Defaults to GET if not specified.
|
||||||
|
*/
|
||||||
|
method: PropTypes.string,
|
||||||
|
/**
|
||||||
|
* `headers` is an object representing the HTTP headers to send along with the
|
||||||
|
* request for a remote image.
|
||||||
|
*/
|
||||||
|
headers: PropTypes.objectOf(PropTypes.string),
|
||||||
|
/**
|
||||||
|
* `body` is the HTTP body to send with the request. This must be a valid
|
||||||
|
* UTF-8 string, and will be sent exactly as specified, with no
|
||||||
|
* additional encoding (e.g. URL-escaping or base64) applied.
|
||||||
|
*/
|
||||||
|
body: PropTypes.string,
|
||||||
|
/**
|
||||||
|
* `cache` determines how the requests handles potentially cached
|
||||||
|
* responses.
|
||||||
|
*
|
||||||
|
* - `default`: Use the native platforms default strategy. `useProtocolCachePolicy` on iOS.
|
||||||
|
*
|
||||||
|
* - `reload`: The data for the URL will be loaded from the originating source.
|
||||||
|
* No existing cache data should be used to satisfy a URL load request.
|
||||||
|
*
|
||||||
|
* - `force-cache`: The existing cached data will be used to satisfy the request,
|
||||||
|
* regardless of its age or expiration date. If there is no existing data in the cache
|
||||||
|
* corresponding the request, the data is loaded from the originating source.
|
||||||
|
*
|
||||||
|
* - `only-if-cached`: The existing cache data will be used to satisfy a request, regardless of
|
||||||
|
* its age or expiration date. If there is no existing data in the cache corresponding
|
||||||
|
* to a URL load request, no attempt is made to load the data from the originating source,
|
||||||
|
* and the load is considered to have failed.
|
||||||
|
*
|
||||||
|
* @platform ios
|
||||||
|
*/
|
||||||
|
cache: PropTypes.oneOf(['default', 'reload', 'force-cache', 'only-if-cached']),
|
||||||
|
/**
|
||||||
|
* `width` and `height` can be specified if known at build time, in which case
|
||||||
|
* these will be used to set the default `<Image/>` component dimensions.
|
||||||
|
*/
|
||||||
|
width: PropTypes.number,
|
||||||
|
height: PropTypes.number,
|
||||||
|
/**
|
||||||
|
* `scale` is used to indicate the scale factor of the image. Defaults to 1.0 if
|
||||||
|
* unspecified, meaning that one image pixel equates to one display point / DIP.
|
||||||
|
*/
|
||||||
|
scale: PropTypes.number
|
||||||
|
});
|
||||||
|
|
||||||
|
const ImageSourcePropType = PropTypes.oneOfType([
|
||||||
|
ImageURISourcePropType,
|
||||||
|
// Opaque type returned by require('./image.jpg')
|
||||||
|
PropTypes.number,
|
||||||
|
PropTypes.string,
|
||||||
|
// Multiple sources
|
||||||
|
PropTypes.arrayOf(ImageURISourcePropType)
|
||||||
|
]);
|
||||||
|
|
||||||
|
export default ImageSourcePropType;
|
||||||
@@ -147,6 +147,13 @@ describe('components/Image', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('prop "source"', () => {
|
describe('prop "source"', () => {
|
||||||
|
test('does not throw', () => {
|
||||||
|
const sources = [null, '', {}, { uri: '' }, { uri: 'https://google.com' }];
|
||||||
|
sources.forEach(source => {
|
||||||
|
expect(() => shallow(<Image source={source} />)).not.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test('is not set immediately if the image has not already been loaded', () => {
|
test('is not set immediately if the image has not already been loaded', () => {
|
||||||
const uri = 'https://google.com/favicon.ico';
|
const uri = 'https://google.com/favicon.ico';
|
||||||
const source = { uri };
|
const source = { uri };
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import createElement from '../createElement';
|
|||||||
import { getAssetByID } from '../../modules/AssetRegistry';
|
import { getAssetByID } from '../../modules/AssetRegistry';
|
||||||
import ImageLoader from '../../modules/ImageLoader';
|
import ImageLoader from '../../modules/ImageLoader';
|
||||||
import ImageResizeMode from './ImageResizeMode';
|
import ImageResizeMode from './ImageResizeMode';
|
||||||
|
import ImageSourcePropType from './ImageSourcePropType';
|
||||||
import ImageStylePropTypes from './ImageStylePropTypes';
|
import ImageStylePropTypes from './ImageStylePropTypes';
|
||||||
import ImageUriCache from './ImageUriCache';
|
import ImageUriCache from './ImageUriCache';
|
||||||
import requestIdleCallback, { cancelIdleCallback } from '../../modules/requestIdleCallback';
|
import requestIdleCallback, { cancelIdleCallback } from '../../modules/requestIdleCallback';
|
||||||
@@ -20,7 +21,7 @@ import StyleSheet from '../StyleSheet';
|
|||||||
import StyleSheetPropType from '../../modules/StyleSheetPropType';
|
import StyleSheetPropType from '../../modules/StyleSheetPropType';
|
||||||
import View from '../View';
|
import View from '../View';
|
||||||
import ViewPropTypes from '../ViewPropTypes';
|
import ViewPropTypes from '../ViewPropTypes';
|
||||||
import { bool, func, number, oneOf, oneOfType, shape, string } from 'prop-types';
|
import { bool, func, number, oneOf, shape } from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
|
|
||||||
const emptyObject = {};
|
const emptyObject = {};
|
||||||
@@ -31,16 +32,6 @@ const STATUS_LOADING = 'LOADING';
|
|||||||
const STATUS_PENDING = 'PENDING';
|
const STATUS_PENDING = 'PENDING';
|
||||||
const STATUS_IDLE = 'IDLE';
|
const STATUS_IDLE = 'IDLE';
|
||||||
|
|
||||||
const ImageSourcePropType = oneOfType([
|
|
||||||
number,
|
|
||||||
shape({
|
|
||||||
height: number,
|
|
||||||
uri: string.isRequired,
|
|
||||||
width: number
|
|
||||||
}),
|
|
||||||
string
|
|
||||||
]);
|
|
||||||
|
|
||||||
const getImageState = (uri, shouldDisplaySource) => {
|
const getImageState = (uri, shouldDisplaySource) => {
|
||||||
return shouldDisplaySource ? STATUS_LOADED : uri ? STATUS_PENDING : STATUS_IDLE;
|
return shouldDisplaySource ? STATUS_LOADED : uri ? STATUS_PENDING : STATUS_IDLE;
|
||||||
};
|
};
|
||||||
@@ -56,26 +47,28 @@ const resolveAssetDimensions = source => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const svgDataUriPattern = /^(data:image\/svg\+xml;utf8,)(.*)/;
|
const svgDataUriPattern = /^(data:image\/svg\+xml;utf8,)(.*)/;
|
||||||
const resolveAssetSource = source => {
|
const resolveAssetUri = source => {
|
||||||
let uri;
|
let uri = '';
|
||||||
if (typeof source === 'number') {
|
if (typeof source === 'number') {
|
||||||
// get the URI from the packager
|
// get the URI from the packager
|
||||||
const asset = getAssetByID(source);
|
const asset = getAssetByID(source);
|
||||||
const scale = asset.scales[0];
|
const scale = asset.scales[0];
|
||||||
const scaleSuffix = scale !== 1 ? `@${scale}x` : '';
|
const scaleSuffix = scale !== 1 ? `@${scale}x` : '';
|
||||||
uri = asset ? `${asset.httpServerLocation}/${asset.name}${scaleSuffix}.${asset.type}` : '';
|
uri = asset ? `${asset.httpServerLocation}/${asset.name}${scaleSuffix}.${asset.type}` : '';
|
||||||
} else if (source && source.uri) {
|
} else if (typeof source === 'string') {
|
||||||
|
uri = source;
|
||||||
|
} else if (source && typeof source.uri === 'string') {
|
||||||
uri = source.uri;
|
uri = source.uri;
|
||||||
} else {
|
|
||||||
uri = source || '';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const match = uri.match(svgDataUriPattern);
|
if (uri) {
|
||||||
// inline SVG markup may contain characters (e.g., #, ") that need to be escaped
|
const match = uri.match(svgDataUriPattern);
|
||||||
if (match) {
|
// inline SVG markup may contain characters (e.g., #, ") that need to be escaped
|
||||||
const [, prefix, svg] = match;
|
if (match) {
|
||||||
const encodedSvg = encodeURIComponent(svg);
|
const [, prefix, svg] = match;
|
||||||
return `${prefix}${encodedSvg}`;
|
const encodedSvg = encodeURIComponent(svg);
|
||||||
|
return `${prefix}${encodedSvg}`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return uri;
|
return uri;
|
||||||
@@ -137,7 +130,7 @@ class Image extends Component<*, State> {
|
|||||||
constructor(props, context) {
|
constructor(props, context) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
// If an image has been loaded before, render it immediately
|
// If an image has been loaded before, render it immediately
|
||||||
const uri = resolveAssetSource(props.source);
|
const uri = resolveAssetUri(props.source);
|
||||||
const shouldDisplaySource = ImageUriCache.has(uri);
|
const shouldDisplaySource = ImageUriCache.has(uri);
|
||||||
this.state = { shouldDisplaySource };
|
this.state = { shouldDisplaySource };
|
||||||
this._imageState = getImageState(uri, shouldDisplaySource);
|
this._imageState = getImageState(uri, shouldDisplaySource);
|
||||||
@@ -161,8 +154,8 @@ class Image extends Component<*, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentWillReceiveProps(nextProps) {
|
componentWillReceiveProps(nextProps) {
|
||||||
const uri = resolveAssetSource(this.props.source);
|
const uri = resolveAssetUri(this.props.source);
|
||||||
const nextUri = resolveAssetSource(nextProps.source);
|
const nextUri = resolveAssetUri(nextProps.source);
|
||||||
if (uri !== nextUri) {
|
if (uri !== nextUri) {
|
||||||
ImageUriCache.remove(uri);
|
ImageUriCache.remove(uri);
|
||||||
const isPreviouslyLoaded = ImageUriCache.has(nextUri);
|
const isPreviouslyLoaded = ImageUriCache.has(nextUri);
|
||||||
@@ -172,7 +165,8 @@ class Image extends Component<*, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
ImageUriCache.remove(resolveAssetSource(this.props.source));
|
const uri = resolveAssetUri(this.props.source);
|
||||||
|
ImageUriCache.remove(uri);
|
||||||
this._destroyImageLoader();
|
this._destroyImageLoader();
|
||||||
this._isMounted = false;
|
this._isMounted = false;
|
||||||
}
|
}
|
||||||
@@ -200,7 +194,7 @@ class Image extends Component<*, State> {
|
|||||||
...other
|
...other
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const displayImage = resolveAssetSource(shouldDisplaySource ? source : defaultSource);
|
const displayImage = resolveAssetUri(shouldDisplaySource ? source : defaultSource);
|
||||||
const imageSizeStyle = resolveAssetDimensions(shouldDisplaySource ? source : defaultSource);
|
const imageSizeStyle = resolveAssetDimensions(shouldDisplaySource ? source : defaultSource);
|
||||||
const backgroundImage = displayImage ? `url("${displayImage}")` : null;
|
const backgroundImage = displayImage ? `url("${displayImage}")` : null;
|
||||||
const originalStyle = StyleSheet.flatten(this.props.style);
|
const originalStyle = StyleSheet.flatten(this.props.style);
|
||||||
@@ -260,7 +254,7 @@ class Image extends Component<*, State> {
|
|||||||
this._destroyImageLoader();
|
this._destroyImageLoader();
|
||||||
this._loadRequest = requestIdleCallback(
|
this._loadRequest = requestIdleCallback(
|
||||||
() => {
|
() => {
|
||||||
const uri = resolveAssetSource(source);
|
const uri = resolveAssetUri(source);
|
||||||
this._imageRequestId = ImageLoader.load(uri, this._onLoad, this._onError);
|
this._imageRequestId = ImageLoader.load(uri, this._onLoad, this._onError);
|
||||||
this._onLoadStart();
|
this._onLoadStart();
|
||||||
},
|
},
|
||||||
@@ -286,7 +280,7 @@ class Image extends Component<*, State> {
|
|||||||
if (onError) {
|
if (onError) {
|
||||||
onError({
|
onError({
|
||||||
nativeEvent: {
|
nativeEvent: {
|
||||||
error: `Failed to load resource ${resolveAssetSource(source)} (404)`
|
error: `Failed to load resource ${resolveAssetUri(source)} (404)`
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -296,7 +290,7 @@ class Image extends Component<*, State> {
|
|||||||
_onLoad = e => {
|
_onLoad = e => {
|
||||||
const { onLoad, source } = this.props;
|
const { onLoad, source } = this.props;
|
||||||
const event = { nativeEvent: e };
|
const event = { nativeEvent: e };
|
||||||
ImageUriCache.add(resolveAssetSource(source));
|
ImageUriCache.add(resolveAssetUri(source));
|
||||||
this._updateImageState(STATUS_LOADED);
|
this._updateImageState(STATUS_LOADED);
|
||||||
if (onLoad) {
|
if (onLoad) {
|
||||||
onLoad(event);
|
onLoad(event);
|
||||||
|
|||||||
Reference in New Issue
Block a user