mirror of
https://github.com/zhigang1992/react-native-web.git
synced 2026-01-12 22:51:09 +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"', () => {
|
||||
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 };
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user