зеркало из https://github.com/microsoft/reactxp.git
Emulate Cache-Control: max-stale on iOS (#853)
* Emulate Cache-Control: max-stale on iOS iOS does not seem to respect the max-stale header. It does not load expired images from the cache unless the force-cache option (internally NSURLRequestReturnCacheDataElseLoad) is passed. We can't use force-cache always because it has the opposite problem of loading only from the cache and using stale data even when the app is online and the data served under the URL has changed. The proposed solution is to fall back to force-cache only on encountering the first error. * Reset _forceCache on prop update * PR feedback: compare only relevant props * PR feedback: use state, only-if-cached, and document the behavior * Fix comment * Initialize state * Initialize state without constructor
This commit is contained in:
Родитель
439fcfcdda
Коммит
ab6c8b5b08
|
@ -11,6 +11,8 @@ This component displays an image, which can come from a local source or from the
|
|||
|
||||
If child elements are specified, the image acts as a background, and the children are rendered on top of it.
|
||||
|
||||
If headers contains 'Cache-Control: max-stale' with no value specified and the image fails to load, the component tries again passing cache: 'only-if-cached' to the underlying native Image (iOS only). This way the app can render otherwise inaccessible stale cached images.
|
||||
|
||||
## Props
|
||||
``` javascript
|
||||
// Alternate text to display if the image cannot be loaded
|
||||
|
|
|
@ -12,8 +12,10 @@ import * as React from 'react';
|
|||
import * as RN from 'react-native';
|
||||
import * as SyncTasks from 'synctasks';
|
||||
|
||||
import * as _ from './utils/lodashMini';
|
||||
import { Types } from '../common/Interfaces';
|
||||
import { DEFAULT_RESIZE_MODE } from '../common/Image';
|
||||
import Platform from './Platform';
|
||||
import Styles from './Styles';
|
||||
|
||||
const _styles = {
|
||||
|
@ -29,7 +31,12 @@ export interface ImageContext {
|
|||
isRxParentAText?: boolean;
|
||||
}
|
||||
|
||||
export class Image extends React.Component<Types.ImageProps, Types.Stateless> implements React.ChildContextProvider<ImageContext> {
|
||||
export interface ImageState {
|
||||
forceCache?: boolean;
|
||||
lastNativeError?: any;
|
||||
}
|
||||
|
||||
export class Image extends React.Component<Types.ImageProps, ImageState> implements React.ChildContextProvider<ImageContext> {
|
||||
static childContextTypes: React.ValidationMap<any> = {
|
||||
isRxParentAText: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
@ -57,6 +64,7 @@ export class Image extends React.Component<Types.ImageProps, Types.Stateless> im
|
|||
protected _mountedComponent: RN.Image | null = null;
|
||||
private _nativeImageWidth: number | undefined;
|
||||
private _nativeImageHeight: number | undefined;
|
||||
readonly state: ImageState = { forceCache: false, lastNativeError: undefined };
|
||||
|
||||
protected _getAdditionalProps(): RN.ImageProperties | {} {
|
||||
return {};
|
||||
|
@ -109,6 +117,14 @@ export class Image extends React.Component<Types.ImageProps, Types.Stateless> im
|
|||
);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps: Types.ImageProps) {
|
||||
const sourceOrHeaderChanged = (nextProps.source !== this.props.source ||
|
||||
!_.isEqual(nextProps.headers || {}, this.props.headers || {}));
|
||||
if (sourceOrHeaderChanged) {
|
||||
this.setState({ forceCache: false, lastNativeError: undefined });
|
||||
}
|
||||
}
|
||||
|
||||
protected _onMount = (component: RN.Image | null) => {
|
||||
this._mountedComponent = component;
|
||||
}
|
||||
|
@ -164,8 +180,17 @@ export class Image extends React.Component<Types.ImageProps, Types.Stateless> im
|
|||
return;
|
||||
}
|
||||
|
||||
if (this.props.onError) {
|
||||
this.props.onError(new Error(e.nativeEvent.error));
|
||||
if (!this.state.forceCache && this._shouldForceCacheOnError()) {
|
||||
// Some platforms will not use expired cache data unless explicitly told so.
|
||||
// Let's try again with cache: 'only-if-cached'.
|
||||
this.setState({ forceCache: true, lastNativeError: e.nativeEvent.error });
|
||||
} else if (this.props.onError) {
|
||||
if (this.state.forceCache) {
|
||||
// Fire the callback with the error we got when we failed without forceCache.
|
||||
this.props.onError(new Error(this.state.lastNativeError));
|
||||
} else {
|
||||
this.props.onError(new Error(e.nativeEvent.error));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -179,10 +204,29 @@ export class Image extends React.Component<Types.ImageProps, Types.Stateless> im
|
|||
if (this.props.headers) {
|
||||
source.headers = this.props.headers;
|
||||
}
|
||||
if (this.state.forceCache) {
|
||||
source.cache = 'only-if-cached';
|
||||
}
|
||||
|
||||
return source;
|
||||
}
|
||||
|
||||
private _shouldForceCacheOnError(): boolean {
|
||||
if (Platform.getType() !== 'ios') {
|
||||
return false;
|
||||
}
|
||||
if (this.props.headers) {
|
||||
for (let key in this.props.headers) {
|
||||
// We don't know how stale the cached data is so we're matching only the simple 'max-stale' attribute
|
||||
// without a value.
|
||||
if (key.toLowerCase() === 'cache-control' && this.props.headers[key].toLowerCase() === 'max-stale') {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Note: This works only if you have an onLoaded handler and wait for the image to load.
|
||||
getNativeWidth(): number|undefined {
|
||||
return this._nativeImageWidth;
|
||||
|
|
Загрузка…
Ссылка в новой задаче