Cache policy control for image source
Summary: In the context of an app an image exists in three resolutions on the server: `thumb` (30px) `feed` (300px) `full` (900px). When looking at an individual item a user can come either from the feed, via a permalink or from other parts of the app. This allows a situation where the `feed` image might or might not already be loaded somewhere in the app. In the detail view I want to render `thumb` with a blur (to quickly display something), then the `feed` image if it exists to have something decent to display until `full` loads. However it is quite a waste to load the `feed` image if it isn't already in cache, and will slow down the time until `full` is loaded. It is possible to track the navigation from feed->detail and that the `feed` image has actually completed loading by the feed component however as component hierarchies grow this turns into quite a lot of prop passing and bad separation of concerns. NSURLRequests accepts a [Cache Policy](https://developer.apple.com/reference/fo Closes https://github.com/facebook/react-native/pull/10844 Differential Revision: D4425959 Pulled By: lacker fbshipit-source-id: 679835439c761a2fc894f56eb6d744c036cf0b49
This commit is contained in:
parent
a6844bdf75
commit
52d8851fc8
|
@ -67,6 +67,7 @@ RCT_TEST(IntegrationTestHarnessTest)
|
|||
RCT_TEST(TimersTest)
|
||||
RCT_TEST(AsyncStorageTest)
|
||||
RCT_TEST(AppEventsTest)
|
||||
RCT_TEST(ImageCachePolicyTest)
|
||||
//RCT_TEST(ImageSnapshotTest) // Disabled: #8985988
|
||||
//RCT_TEST(LayoutEventsTest) // Disabled due to flakiness: #8686784
|
||||
RCT_TEST(SimpleSnapshotTest)
|
||||
|
|
|
@ -304,6 +304,35 @@ exports.examples = [
|
|||
},
|
||||
platform: 'ios',
|
||||
},
|
||||
{
|
||||
title: 'Cache Policy',
|
||||
description: 'First image has never been loaded before and is instructed not to load unless in cache.' +
|
||||
'Placeholder image from above will stay. Second image is the same but forced to load regardless of' +
|
||||
' local cache state.',
|
||||
render: function () {
|
||||
return (
|
||||
<View style={styles.horizontal}>
|
||||
<Image
|
||||
defaultSource={require('./bunny.png')}
|
||||
source={{
|
||||
uri: smallImage.uri + '?cacheBust=notinCache' + Date.now(),
|
||||
cache: 'only-if-cached'
|
||||
}}
|
||||
style={styles.base}
|
||||
/>
|
||||
<Image
|
||||
defaultSource={require('./bunny.png')}
|
||||
source={{
|
||||
uri: smallImage.uri + '?cacheBust=notinCache' + Date.now(),
|
||||
cache: 'reload'
|
||||
}}
|
||||
style={styles.base}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
},
|
||||
platform: 'ios',
|
||||
},
|
||||
{
|
||||
title: 'Border Color',
|
||||
render: function() {
|
||||
|
|
|
@ -0,0 +1,115 @@
|
|||
/**
|
||||
* Copyright (c) 2015-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree. An additional grant
|
||||
* of patent rights can be found in the PATENTS file in the same directory.
|
||||
*
|
||||
* @flow
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
var React = require('react');
|
||||
var ReactNative = require('react-native');
|
||||
var {
|
||||
Image,
|
||||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
} = ReactNative;
|
||||
var { TestModule } = ReactNative.NativeModules;
|
||||
|
||||
/*
|
||||
* The reload and force-cache tests don't actually verify that the complete functionality.
|
||||
*
|
||||
* reload: Should have the server set a long cache header, then swap the image on next load
|
||||
* with the test comparing the old image to the new image and making sure they are different.
|
||||
*
|
||||
* force-cache: Should do the above but set a no-cache header. The test should compare the first
|
||||
* image with the new one and make sure they are the same.
|
||||
*/
|
||||
|
||||
const TESTS = ['only-if-cached', 'default', 'reload', 'force-cache'];
|
||||
|
||||
type Props = {}
|
||||
type State = {
|
||||
'only-if-cached'?: boolean,
|
||||
'default'?: boolean,
|
||||
'reload'?: boolean,
|
||||
'force-cache'?: boolean,
|
||||
}
|
||||
|
||||
class ImageCachePolicyTest extends React.Component {
|
||||
state = {}
|
||||
|
||||
shouldComponentUpdate(nextProps: Props, nextState: State) {
|
||||
const results: Array<?boolean> = TESTS.map(x => nextState[x]);
|
||||
|
||||
if (!results.includes(undefined)) {
|
||||
const result: boolean = results.reduce((x,y) => x === y === true, true)
|
||||
TestModule.markTestPassed(result);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
testComplete(name: string, pass: boolean) {
|
||||
this.setState({[name]: pass});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<View style={{flex: 1}}>
|
||||
<Text>Hello</Text>
|
||||
<Image
|
||||
source={{
|
||||
uri: 'https://facebook.github.io/react/img/logo_small_2x.png?cacheBust=notinCache' + Date.now(),
|
||||
cache: 'only-if-cached'
|
||||
}}
|
||||
onLoad={() => this.testComplete('only-if-cached', false)}
|
||||
onError={() => this.testComplete('only-if-cached', true)}
|
||||
style={styles.base}
|
||||
/>
|
||||
<Image
|
||||
source={{
|
||||
uri: 'https://facebook.github.io/react/img/logo_small_2x.png?cacheBust=notinCache' + Date.now(),
|
||||
cache: 'default'
|
||||
}}
|
||||
onLoad={() => this.testComplete('default', true)}
|
||||
onError={() => this.testComplete('default', false)}
|
||||
style={styles.base}
|
||||
/>
|
||||
<Image
|
||||
source={{
|
||||
uri: 'https://facebook.github.io/react/img/logo_small_2x.png?cacheBust=notinCache' + Date.now(),
|
||||
cache: 'reload'
|
||||
}}
|
||||
onLoad={() => this.testComplete('reload', true)}
|
||||
onError={() => this.testComplete('reload', false)}
|
||||
style={styles.base}
|
||||
/>
|
||||
<Image
|
||||
source={{
|
||||
uri: 'https://facebook.github.io/react/img/logo_small_2x.png?cacheBust=notinCache' + Date.now(),
|
||||
cache: 'force-cache'
|
||||
}}
|
||||
onLoad={() => this.testComplete('force-cache', true)}
|
||||
onError={() => this.testComplete('force-cache', false)}
|
||||
style={styles.base}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
base: {
|
||||
width: 100,
|
||||
height: 100,
|
||||
},
|
||||
});
|
||||
|
||||
ImageCachePolicyTest.displayName = 'ImageCachePolicyTest';
|
||||
|
||||
module.exports = ImageCachePolicyTest;
|
|
@ -29,6 +29,7 @@ var TESTS = [
|
|||
require('./LayoutEventsTest'),
|
||||
require('./AppEventsTest'),
|
||||
require('./SimpleSnapshotTest'),
|
||||
require('./ImageCachePolicyTest'),
|
||||
require('./ImageSnapshotTest'),
|
||||
require('./PromiseTest'),
|
||||
require('./WebSocketTest'),
|
||||
|
|
|
@ -140,7 +140,8 @@ const Image = React.createClass({
|
|||
* This prop can also contain several remote URLs, specified together with
|
||||
* their width and height and potentially with scale/other URI arguments.
|
||||
* The native side will then choose the best `uri` to display based on the
|
||||
* measured size of the image container.
|
||||
* measured size of the image container. A `cache` property can be added to
|
||||
* control how networked request interacts with the local cache.
|
||||
*/
|
||||
source: ImageSourcePropType,
|
||||
/**
|
||||
|
|
|
@ -42,6 +42,32 @@ const ImageURISourcePropType = PropTypes.shape({
|
|||
* 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.
|
||||
|
|
|
@ -47,6 +47,7 @@
|
|||
+ (NSData *)NSData:(id)json;
|
||||
+ (NSIndexSet *)NSIndexSet:(id)json;
|
||||
|
||||
+ (NSURLRequestCachePolicy)NSURLRequestCachePolicy:(id)json;
|
||||
+ (NSURL *)NSURL:(id)json;
|
||||
+ (NSURLRequest *)NSURLRequest:(id)json;
|
||||
|
||||
|
|
|
@ -118,6 +118,14 @@ RCT_CUSTOM_CONVERTER(NSData *, NSData, [json dataUsingEncoding:NSUTF8StringEncod
|
|||
}
|
||||
}
|
||||
|
||||
RCT_ENUM_CONVERTER(NSURLRequestCachePolicy, (@{
|
||||
@"default": @(NSURLRequestUseProtocolCachePolicy),
|
||||
@"reload": @(NSURLRequestReloadIgnoringLocalCacheData),
|
||||
@"force-cache": @(NSURLRequestReturnCacheDataElseLoad),
|
||||
@"only-if-cached": @(NSURLRequestReturnCacheDataDontLoad),
|
||||
}), NSURLRequestUseProtocolCachePolicy, integerValue)
|
||||
|
||||
|
||||
+ (NSURLRequest *)NSURLRequest:(id)json
|
||||
{
|
||||
if ([json isKindOfClass:[NSString class]]) {
|
||||
|
@ -140,8 +148,9 @@ RCT_CUSTOM_CONVERTER(NSData *, NSData, [json dataUsingEncoding:NSUTF8StringEncod
|
|||
|
||||
NSData *body = [self NSData:json[@"body"]];
|
||||
NSString *method = [self NSString:json[@"method"]].uppercaseString ?: @"GET";
|
||||
NSURLRequestCachePolicy cachePolicy = [self NSURLRequestCachePolicy:json[@"cache"]];
|
||||
NSDictionary *headers = [self NSDictionary:json[@"headers"]];
|
||||
if ([method isEqualToString:@"GET"] && headers == nil && body == nil) {
|
||||
if ([method isEqualToString:@"GET"] && headers == nil && body == nil && cachePolicy == NSURLRequestUseProtocolCachePolicy) {
|
||||
return [NSURLRequest requestWithURL:URL];
|
||||
}
|
||||
|
||||
|
@ -164,6 +173,7 @@ RCT_CUSTOM_CONVERTER(NSData *, NSData, [json dataUsingEncoding:NSUTF8StringEncod
|
|||
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:URL];
|
||||
request.HTTPBody = body;
|
||||
request.HTTPMethod = method;
|
||||
request.cachePolicy = cachePolicy;
|
||||
request.allHTTPHeaderFields = headers;
|
||||
return [request copy];
|
||||
}
|
||||
|
|
|
@ -89,6 +89,26 @@ Many of the images you will display in your app will not be available at compile
|
|||
<Image source={{uri: 'https://facebook.github.io/react/img/logo_og.png'}} />
|
||||
```
|
||||
|
||||
### Cache Control (iOS Only)
|
||||
|
||||
In some cases you might only want to display an image if it is already in the local cache, i.e. a low resolution placeholder until a higher resolution is available. In other cases you do not care if the image is outdated and are willing to display an outdated image to save bandwidth. The `cache` source property gives you control over how the network layer interacts with the cache.
|
||||
|
||||
* `default`: Use the native platforms default strategy.
|
||||
* `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.
|
||||
|
||||
```javascript
|
||||
<Image source={{uri: 'https://facebook.github.io/react/img/logo_og.png', cache: 'only-if-cached'}}
|
||||
style={{width: 400, height: 400}} />
|
||||
````
|
||||
|
||||
## Local Filesystem Images
|
||||
|
||||
See [CameraRoll](docs/cameraroll.html) for an example of
|
||||
|
|
Loading…
Reference in New Issue