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:
Peter Salanki 2017-01-17 16:58:27 -08:00 committed by Facebook Github Bot
parent a6844bdf75
commit 52d8851fc8
9 changed files with 206 additions and 2 deletions

View File

@ -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)

View File

@ -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() {

View File

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

View File

@ -29,6 +29,7 @@ var TESTS = [
require('./LayoutEventsTest'),
require('./AppEventsTest'),
require('./SimpleSnapshotTest'),
require('./ImageCachePolicyTest'),
require('./ImageSnapshotTest'),
require('./PromiseTest'),
require('./WebSocketTest'),

View File

@ -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,
/**

View File

@ -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.

View File

@ -47,6 +47,7 @@
+ (NSData *)NSData:(id)json;
+ (NSIndexSet *)NSIndexSet:(id)json;
+ (NSURLRequestCachePolicy)NSURLRequestCachePolicy:(id)json;
+ (NSURL *)NSURL:(id)json;
+ (NSURLRequest *)NSURLRequest:(id)json;

View File

@ -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];
}

View File

@ -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