[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:
Nicolas Gallagher
2018-05-22 15:13:57 -07:00
parent 48e62fcd64
commit 16b2fb9bd7
3 changed files with 117 additions and 30 deletions

View 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;

View File

@@ -147,6 +147,13 @@ describe('components/Image', () => {
});
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', () => {
const uri = 'https://google.com/favicon.ico';
const source = { uri };

View File

@@ -13,6 +13,7 @@ import createElement from '../createElement';
import { getAssetByID } from '../../modules/AssetRegistry';
import ImageLoader from '../../modules/ImageLoader';
import ImageResizeMode from './ImageResizeMode';
import ImageSourcePropType from './ImageSourcePropType';
import ImageStylePropTypes from './ImageStylePropTypes';
import ImageUriCache from './ImageUriCache';
import requestIdleCallback, { cancelIdleCallback } from '../../modules/requestIdleCallback';
@@ -20,7 +21,7 @@ import StyleSheet from '../StyleSheet';
import StyleSheetPropType from '../../modules/StyleSheetPropType';
import View from '../View';
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';
const emptyObject = {};
@@ -31,16 +32,6 @@ const STATUS_LOADING = 'LOADING';
const STATUS_PENDING = 'PENDING';
const STATUS_IDLE = 'IDLE';
const ImageSourcePropType = oneOfType([
number,
shape({
height: number,
uri: string.isRequired,
width: number
}),
string
]);
const getImageState = (uri, shouldDisplaySource) => {
return shouldDisplaySource ? STATUS_LOADED : uri ? STATUS_PENDING : STATUS_IDLE;
};
@@ -56,26 +47,28 @@ const resolveAssetDimensions = source => {
};
const svgDataUriPattern = /^(data:image\/svg\+xml;utf8,)(.*)/;
const resolveAssetSource = source => {
let uri;
const resolveAssetUri = source => {
let uri = '';
if (typeof source === 'number') {
// get the URI from the packager
const asset = getAssetByID(source);
const scale = asset.scales[0];
const scaleSuffix = scale !== 1 ? `@${scale}x` : '';
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;
} else {
uri = source || '';
}
const match = uri.match(svgDataUriPattern);
// inline SVG markup may contain characters (e.g., #, ") that need to be escaped
if (match) {
const [, prefix, svg] = match;
const encodedSvg = encodeURIComponent(svg);
return `${prefix}${encodedSvg}`;
if (uri) {
const match = uri.match(svgDataUriPattern);
// inline SVG markup may contain characters (e.g., #, ") that need to be escaped
if (match) {
const [, prefix, svg] = match;
const encodedSvg = encodeURIComponent(svg);
return `${prefix}${encodedSvg}`;
}
}
return uri;
@@ -137,7 +130,7 @@ class Image extends Component<*, State> {
constructor(props, context) {
super(props, context);
// 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);
this.state = { shouldDisplaySource };
this._imageState = getImageState(uri, shouldDisplaySource);
@@ -161,8 +154,8 @@ class Image extends Component<*, State> {
}
componentWillReceiveProps(nextProps) {
const uri = resolveAssetSource(this.props.source);
const nextUri = resolveAssetSource(nextProps.source);
const uri = resolveAssetUri(this.props.source);
const nextUri = resolveAssetUri(nextProps.source);
if (uri !== nextUri) {
ImageUriCache.remove(uri);
const isPreviouslyLoaded = ImageUriCache.has(nextUri);
@@ -172,7 +165,8 @@ class Image extends Component<*, State> {
}
componentWillUnmount() {
ImageUriCache.remove(resolveAssetSource(this.props.source));
const uri = resolveAssetUri(this.props.source);
ImageUriCache.remove(uri);
this._destroyImageLoader();
this._isMounted = false;
}
@@ -200,7 +194,7 @@ class Image extends Component<*, State> {
...other
} = this.props;
const displayImage = resolveAssetSource(shouldDisplaySource ? source : defaultSource);
const displayImage = resolveAssetUri(shouldDisplaySource ? source : defaultSource);
const imageSizeStyle = resolveAssetDimensions(shouldDisplaySource ? source : defaultSource);
const backgroundImage = displayImage ? `url("${displayImage}")` : null;
const originalStyle = StyleSheet.flatten(this.props.style);
@@ -260,7 +254,7 @@ class Image extends Component<*, State> {
this._destroyImageLoader();
this._loadRequest = requestIdleCallback(
() => {
const uri = resolveAssetSource(source);
const uri = resolveAssetUri(source);
this._imageRequestId = ImageLoader.load(uri, this._onLoad, this._onError);
this._onLoadStart();
},
@@ -286,7 +280,7 @@ class Image extends Component<*, State> {
if (onError) {
onError({
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 => {
const { onLoad, source } = this.props;
const event = { nativeEvent: e };
ImageUriCache.add(resolveAssetSource(source));
ImageUriCache.add(resolveAssetUri(source));
this._updateImageState(STATUS_LOADED);
if (onLoad) {
onLoad(event);