mirror of
https://github.com/zoriya/react-native-web.git
synced 2026-05-22 14:21:44 +00:00
[add] Image source headers handling
Extend ImageLoader functionality to be able to work with image sources containing headers We preserve the existing strategy that works with image.src for cases where source is just an uri with no headers When sources contain headers we make a fetch request and then render a local url for the downloaded blob (URL.createObjectURL) Fix #1019 Fix #2268 Close #2442
This commit is contained in:
committed by
Nicolas Gallagher
parent
39b94b1945
commit
36f46b3510
@@ -15,6 +15,18 @@ const dataBase64Svg =
|
||||
'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0nMjAwJyBoZWlnaHQ9JzIwMCcgZmlsbD0iIzAwMDAwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiB4PSIwcHgiIHk9IjBweCIgdmlld0JveD0iMCAwIDEwMCAxMDAiIGVuYWJsZS1iYWNrZ3JvdW5kPSJuZXcgMCAwIDEwMCAxMDAiIHhtbDpzcGFjZT0icHJlc2VydmUiPjxnPjxwYXRoIGQ9Ik0yNS44NjcsNDguODUzQzMyLjgwNiw1MC4xNzYsNDYuNDYsNTIuNSw2MS4yMTUsNTIuNWgwLjAwNWM5LjcxLDAsMTguNDAxLTEuMDU3LDI1LjkzOC0yLjkxMyAgIGMwLjE1OS0wLjA0NiwwLjM1LTAuMTM1LDAuNTY1LTAuMTg3YzAuMjgyLTAuMDcyLDAuNTY1LTAuMTY0LDAuODQ0LTAuMjM4YzMuMTg0LTAuOTY0LDIuNTc3LTMuMDUxLDIuMTk5LTMuODUyICAgYy00LjE2Ni03LjcxOS0xNS4wODYtMjMuNDE1LTM1LjAyOC0yMy40MTVjLTIyLjE2OSwwLTMwLjI2MiwxMC42MzUtMzMuMTQsMTkuNTg5QzIyLjU0NSw0Mi4zMzMsMjIuNDA3LDQ3LjEzNSwyNS44NjcsNDguODUzeiAgICBNMjguNjc2LDM4LjAzMmMwLjAxMy0wLjAzNiwwLjYxNC0xLjYyNiwxLjkyMy0xLjAwOGMxLjEzMywwLjUzNSwwLjk2MSwxLjU2MywwLjg4NywxLjg1Yy0wLjAwNywwLjAyNC0wLjAxNCwwLjA0OC0wLjAyMSwwLjA3MyAgIGMwLDAuMDAxLTAuMDAxLDAuMDA0LTAuMDAxLDAuMDA0bDAsMGMtMC4yNDksMC45MjktMC40MDQsMi4wODYtMC4wMTcsMi44NmMwLjE2LDAuMzE5LDAuNDkyLDAuNzY4LDEuNTQyLDAuOTg3bDAuMzY2LDAuMDc3ICAgYzIwLjgxNiw0LjM2LDM2LDIuOTMzLDQ1LjY3OCwwLjYyNmwtMC4wMDQsMC4wMDJjMCwwLDAuMDA1LTAuMDAyLDAuMDA3LTAuMDAzYzAuMjEyLTAuMDUsMC40MjEtMC4xMDEsMC42MjgtMC4xNTIgICBjMC41MDktMC4wNSwxLjE3MywwLjA3OCwxLjM5OSwxYzAuMzUxLDEuNDI0LTAuOTczLDEuODk1LTEuMjE3LDEuOTY5Yy01LjMyNSwxLjI3OS0xMi4yNjYsMi4zMDYtMjAuODM1LDIuMzA3ICAgYy03LjUwNSwwLTE2LjI1NS0wLjc4Ny0yNi4yNTctMi44ODJsLTAuMzY0LTAuMDc3Yy0yLjEyLTAuNDQyLTMuMTExLTEuNjMzLTMuNTY5LTIuNTU1QzI3Ljk4NSw0MS40MjEsMjguMjgxLDM5LjQxNiwyOC42NzYsMzguMDMyICAgeiI+PC9wYXRoPjxjaXJjbGUgY3g9IjEwLjQ5MyIgY3k9IjIzLjQ1NSIgcj0iMC42MTkiPjwvY2lyY2xlPjxwYXRoIGQ9Ik0yLjA4LDI4LjMwOGMwLjY3Ni0wLjE3OCwwLjk4My0wLjM1MiwxLjE3NC0wLjVDNC42OSwyNi42OSw2LjUsMjcuNDgzLDcuNSwyOC4zNTd2MC4wMDJjMCwwLDEuNzExLDEuMjM1LDAuNzM3LDIuMjAyICAgYy0wLjk3NCwwLjk2NS0yLjMxOSwwLjAwNi0yLjMxOSwwLjAwNmwwLjAzNSwwLjAxNmMtMC4zMjctMC4yMDMtMC42LTAuNTYxLTAuNzgtMC41ODRjLTAuMzcsMC4yNi0wLjg3NiwwLjUtMS40NzYsMC41SDMuNyAgIGMwLDAtMS4zNDUsMC43MDksMC4xNzgsMS42NTJjMC4wMDEsMC4wMDEsMC4wMDIsMC4wNzIsMC4wMDQsMC4wNzNjMy45MzksMi4zNDIsOC4yNzEsNS43MDEsOC4yNzEsOC44OCAgIGMwLDAuNjkxLDAuMiwxNy4wNDIsMTcuNjI2LDI0LjczOWwwLjk2NywwLjQ0MmwtMC4xLDEuMDU5Yy0wLjQyMSw0LjM5LDEuMTQ1LDEwLjE5MSwxMC45OTMsMTIuODg4bDAuMTEzLDAuMDM4ICAgYzAuMDY3LDAuMDIzLDYuNzMyLDIuNDI5LDEwLjkwNywyLjQyOWMxLjU4NCwwLDIuMTU1LTAuMzUyLDIuMjQzLTAuNTYxYzAuMDg1LTAuMjAyLDAuNjEyLTIuMTY0LTYuMzMyLTkuMzg3bDAuMDAyLTAuMTgzICAgYzAsMC0yLjQ3Ny0zLjA3LDEuNTMzLTMuMDdjMC4wMSwwLDAuMDE5LDAsMC4wMjksMGMxLjI4NSwwLDIuNjA4LDAuMjE1LDMuOTgsMC4xODRjNC43NzEtMC4xMTcsOS4zMTYtMC40MjUsMTMuNTA2LTEuMDk2ICAgbDAuNDc0LTAuMDI4bDAuNjY4LDAuMTU4YzkuNjUxLDQuOTQ4LDE2LjczOCw3LjcxNiwxOS43MzgsNy43MTZ2MC4wMDZjMCwwLDAuMTY0LDAuMDExLDAuMjMsMC4wMDQgICBjLTAuMTg5LTAuNzIzLTIuMjMtMi44LTcuMjMtOS4wNzl2MC4wMjFjMCwwLTEuNTEyLTEuNjU4LDAuNzk3LTIuNjUzYzAuMDYzLTAuMDI2LDAuMDA4LDAuMDIzLDAuMDYtMC4wMDEgICBjOC42MzktMy41MDksMTMuNTAxLTguMjA0LDE1LjQxMS0xMS43NzVjMS4xNDUtMi4xMjksMC4yMDYtMi43ODQtMC42NTktMi45NzZjLTAuMzE3LTAuMDM4LTAuNjM0LTAuMDYyLTAuOTEyLTAuMDYyICAgYy0wLjIwNSwwLTAuMzc5LDAuMDEtMC41MjgsMC4wMjdsLTMuMTQzLDEuMjE0QzgzLjczMiw1My45MjYsNzMuMjE4LDU1LjUsNjEuMjIsNTUuNWMtMC4wMDIsMC0wLjAwNSwwLTAuMDA1LDAgICBjLTE1LjEyOCwwLTI5LjEwMS0yLjQzMi0zNi4wODMtMy43NzFsLTAuMTczLTAuMTExbC0wLjE2LTAuMTI2Yy01Ljg1OC0yLjY4MS01LjEzNy0xMC4yMDItNS4xMDMtMTAuNTE5bDAuMDYtMC4zICAgYzAuODk1LTIuODM4LDIuNDY3LTYuMzUyLDUuMjEzLTkuNzE5Yy0xLjgwOC0xLjM2OS00LjU5LTQuMTg4LTQuNDMtOC40OTRjMC4wNDYtMS4yNDQtMC40ODYtMi41MDgtMS40OTgtMy41NTkgICBjLTEuNDk4LTEuNTU1LTMuNzg1LTIuNDQ2LTYuMjc0LTIuNDQ2Yy0xLjc3LDAtMy41NTMsMC40NDItNS4yOTMsMS4zMTRjLTQuMDYxLDIuMDM1LTQuODU1LDQuNzM2LTUuNjkyLDcuNTk2ICAgYy0wLjEzNiwwLjQ2OC0wLjI4NCwwLjkzOS0wLjQzOCwxLjQxYy0wLjAwNiwwLjAxOS0wLjAyMiwwLjAzNS0wLjAyOCwwLjA1NkMwLjgzMywyOC40MjMsMS42OTEsMjguMzksMi4wOCwyOC4zMDh6IE0xMC40OTMsMTkuOTA4ICAgYzEuOTU2LDAsMy41NDgsMS41OTEsMy41NDgsMy41NDdjMCwxLjk1Ny0xLjU5MiwzLjU0OC0zLjU0OCwzLjU0OGMtMS45NTcsMC0zLjU0OC0xLjU5Mi0zLjU0OC0zLjU0OCAgIEM2Ljk0NCwyMS40OTksOC41MzYsMTkuOTA4LDEwLjQ5MywxOS45MDh6Ij48L3BhdGg+PC9nPjwvc3ZnPg==';
|
||||
const dataSvg =
|
||||
'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>';
|
||||
const sourceWithHeaders = {
|
||||
uri: placeholder,
|
||||
headers: {
|
||||
'x-token': '0012345'
|
||||
}
|
||||
};
|
||||
const sourceWithHeadersAndRedirect = {
|
||||
uri: source,
|
||||
headers: {
|
||||
'x-token': '0012345'
|
||||
}
|
||||
};
|
||||
|
||||
function Divider() {
|
||||
return <View style={styles.divider} />;
|
||||
@@ -118,6 +130,17 @@ export default function ImagePage() {
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
<Divider />
|
||||
<View style={styles.row}>
|
||||
<View style={styles.column}>
|
||||
<Text style={[styles.text]}>With Headers</Text>
|
||||
<Image source={sourceWithHeaders} style={styles.image} />
|
||||
</View>
|
||||
<View style={styles.column}>
|
||||
<Text style={[styles.text]}>Headers & Redirect</Text>
|
||||
<Image source={sourceWithHeadersAndRedirect} style={styles.image} />
|
||||
</View>
|
||||
</View>
|
||||
</Example>
|
||||
);
|
||||
}
|
||||
|
||||
+4
-4
@@ -329,14 +329,14 @@ exports[`components/Image prop "style" removes other unsupported View styles 1`]
|
||||
>
|
||||
<div
|
||||
class="css-view-175oi2r r-backgroundColor-1niwhzg r-backgroundPosition-vvn4in r-backgroundRepeat-u6sd8q r-bottom-1p0dtai r-height-1pi2tsx r-left-1d2f490 r-position-u8s1d r-right-zchlnj r-top-ipm5af r-width-13qz1uu r-zIndex-1wyyakw r-backgroundSize-4gszlv"
|
||||
style="filter: url(#tint-55);"
|
||||
style="filter: url(#tint-66);"
|
||||
/>
|
||||
<svg
|
||||
style="position: absolute; height: 0px; visibility: hidden; width: 0px;"
|
||||
>
|
||||
<defs>
|
||||
<filter
|
||||
id="tint-55"
|
||||
id="tint-66"
|
||||
>
|
||||
<feflood
|
||||
flood-color="blue"
|
||||
@@ -379,7 +379,7 @@ exports[`components/Image prop "tintColor" convert to filter 1`] = `
|
||||
>
|
||||
<div
|
||||
class="css-view-175oi2r r-backgroundColor-1niwhzg r-backgroundPosition-vvn4in r-backgroundRepeat-u6sd8q r-bottom-1p0dtai r-height-1pi2tsx r-left-1d2f490 r-position-u8s1d r-right-zchlnj r-top-ipm5af r-width-13qz1uu r-zIndex-1wyyakw r-backgroundSize-4gszlv"
|
||||
style="background-image: url(https://google.com/favicon.ico); filter: url(#tint-56);"
|
||||
style="background-image: url(https://google.com/favicon.ico); filter: url(#tint-67);"
|
||||
/>
|
||||
<img
|
||||
alt=""
|
||||
@@ -392,7 +392,7 @@ exports[`components/Image prop "tintColor" convert to filter 1`] = `
|
||||
>
|
||||
<defs>
|
||||
<filter
|
||||
id="tint-56"
|
||||
id="tint-67"
|
||||
>
|
||||
<feflood
|
||||
flood-color="red"
|
||||
|
||||
@@ -12,7 +12,7 @@ import Image from '../';
|
||||
import ImageLoader, { ImageUriCache } from '../../../modules/ImageLoader';
|
||||
import PixelRatio from '../../PixelRatio';
|
||||
import React from 'react';
|
||||
import { act, render } from '@testing-library/react';
|
||||
import { act, render, waitFor } from '@testing-library/react';
|
||||
|
||||
const originalImage = window.Image;
|
||||
|
||||
@@ -20,6 +20,16 @@ describe('components/Image', () => {
|
||||
beforeEach(() => {
|
||||
ImageUriCache._entries = {};
|
||||
window.Image = jest.fn(() => ({}));
|
||||
ImageLoader.load = jest
|
||||
.fn()
|
||||
.mockImplementation((source, onLoad, onError) => {
|
||||
act(() => onLoad({ source }));
|
||||
});
|
||||
ImageLoader.loadWithHeaders = jest.fn().mockImplementation((source) => ({
|
||||
source,
|
||||
promise: Promise.resolve(`blob:${Math.random()}`),
|
||||
cancel: jest.fn()
|
||||
}));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -102,10 +112,6 @@ describe('components/Image', () => {
|
||||
|
||||
describe('prop "onLoad"', () => {
|
||||
test('is called after image is loaded from network', () => {
|
||||
jest.useFakeTimers();
|
||||
ImageLoader.load = jest.fn().mockImplementation((_, onLoad, onError) => {
|
||||
onLoad();
|
||||
});
|
||||
const onLoadStartStub = jest.fn();
|
||||
const onLoadStub = jest.fn();
|
||||
const onLoadEndStub = jest.fn();
|
||||
@@ -117,15 +123,10 @@ describe('components/Image', () => {
|
||||
source="https://test.com/img.jpg"
|
||||
/>
|
||||
);
|
||||
jest.runOnlyPendingTimers();
|
||||
expect(onLoadStub).toBeCalled();
|
||||
});
|
||||
|
||||
test('is called after image is loaded from cache', () => {
|
||||
jest.useFakeTimers();
|
||||
ImageLoader.load = jest.fn().mockImplementation((_, onLoad, onError) => {
|
||||
onLoad();
|
||||
});
|
||||
const onLoadStartStub = jest.fn();
|
||||
const onLoadStub = jest.fn();
|
||||
const onLoadEndStub = jest.fn();
|
||||
@@ -139,7 +140,6 @@ describe('components/Image', () => {
|
||||
source={uri}
|
||||
/>
|
||||
);
|
||||
jest.runOnlyPendingTimers();
|
||||
expect(onLoadStub).toBeCalled();
|
||||
ImageUriCache.remove(uri);
|
||||
});
|
||||
@@ -223,6 +223,34 @@ describe('components/Image', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('prop "onLoadStart"', () => {
|
||||
test('is called on update if "headers" are modified', () => {
|
||||
const onLoadStartStub = jest.fn();
|
||||
const { rerender } = render(
|
||||
<Image
|
||||
onLoadStart={onLoadStartStub}
|
||||
source={{
|
||||
uri: 'https://test.com/img.jpg',
|
||||
headers: { 'x-custom-header': 'abc123' }
|
||||
}}
|
||||
/>
|
||||
);
|
||||
act(() => {
|
||||
rerender(
|
||||
<Image
|
||||
onLoadStart={onLoadStartStub}
|
||||
source={{
|
||||
uri: 'https://test.com/img.jpg',
|
||||
headers: { 'x-custom-header': '123abc' }
|
||||
}}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
expect(onLoadStartStub.mock.calls.length).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('prop "resizeMode"', () => {
|
||||
['contain', 'cover', 'none', 'repeat', 'stretch', undefined].forEach(
|
||||
(resizeMode) => {
|
||||
@@ -241,7 +269,8 @@ describe('components/Image', () => {
|
||||
'',
|
||||
{},
|
||||
{ uri: '' },
|
||||
{ uri: 'https://google.com' }
|
||||
{ uri: 'https://google.com' },
|
||||
{ uri: 'https://google.com', headers: { 'x-custom-header': 'abc123' } }
|
||||
];
|
||||
sources.forEach((source) => {
|
||||
expect(() => render(<Image source={source} />)).not.toThrow();
|
||||
@@ -257,11 +286,6 @@ describe('components/Image', () => {
|
||||
|
||||
test('is set immediately if the image was preloaded', () => {
|
||||
const uri = 'https://yahoo.com/favicon.ico';
|
||||
ImageLoader.load = jest
|
||||
.fn()
|
||||
.mockImplementationOnce((_, onLoad, onError) => {
|
||||
onLoad();
|
||||
});
|
||||
return Image.prefetch(uri).then(() => {
|
||||
const source = { uri };
|
||||
const { container } = render(<Image source={source} />, {
|
||||
@@ -342,6 +366,51 @@ describe('components/Image', () => {
|
||||
'http://localhost/static/img@2x.png'
|
||||
);
|
||||
});
|
||||
|
||||
test('it works with headers in 2 stages', async () => {
|
||||
const uri = 'https://google.com/favicon.ico';
|
||||
const headers = { 'x-custom-header': 'abc123' };
|
||||
const source = { uri, headers };
|
||||
|
||||
// Stage 1
|
||||
const loadRequest = {
|
||||
promise: Promise.resolve('blob:123'),
|
||||
cancel: jest.fn(),
|
||||
source
|
||||
};
|
||||
|
||||
ImageLoader.loadWithHeaders.mockReturnValue(loadRequest);
|
||||
|
||||
render(<Image source={source} />);
|
||||
|
||||
expect(ImageLoader.loadWithHeaders).toHaveBeenCalledWith(
|
||||
expect.objectContaining(source)
|
||||
);
|
||||
|
||||
// Stage 2
|
||||
return waitFor(() => {
|
||||
expect(ImageLoader.load).toHaveBeenCalledWith(
|
||||
'blob:123',
|
||||
expect.any(Function),
|
||||
expect.any(Function)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// A common case is `source` declared as an inline object, which cause is to be a
|
||||
// new object (with the same content) each time parent component renders
|
||||
test('it still loads the image if source object is changed', () => {
|
||||
const uri = 'https://google.com/favicon.ico';
|
||||
const headers = { 'x-custom-header': 'abc123' };
|
||||
const { rerender } = render(<Image source={{ uri, headers }} />);
|
||||
rerender(<Image source={{ uri, headers }} />);
|
||||
|
||||
// when the underlying source didn't change we don't expect more than 1 load calls
|
||||
return waitFor(() => {
|
||||
expect(ImageLoader.loadWithHeaders).toHaveBeenCalledTimes(1);
|
||||
expect(ImageLoader.load).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('prop "style"', () => {
|
||||
|
||||
+85
-19
@@ -8,6 +8,7 @@
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import type { ImageSource, LoadRequest } from '../../modules/ImageLoader';
|
||||
import type { ImageProps } from './types';
|
||||
|
||||
import * as React from 'react';
|
||||
@@ -165,6 +166,23 @@ function resolveAssetUri(source): ?string {
|
||||
return uri;
|
||||
}
|
||||
|
||||
function raiseOnErrorEvent(uri, { onError, onLoadEnd }) {
|
||||
if (onError) {
|
||||
onError({
|
||||
nativeEvent: {
|
||||
error: `Failed to load resource ${uri} (404)`
|
||||
}
|
||||
});
|
||||
}
|
||||
if (onLoadEnd) onLoadEnd();
|
||||
}
|
||||
|
||||
function hasSourceDiff(a: ImageSource, b: ImageSource) {
|
||||
return (
|
||||
a.uri !== b.uri || JSON.stringify(a.headers) !== JSON.stringify(b.headers)
|
||||
);
|
||||
}
|
||||
|
||||
interface ImageStatics {
|
||||
getSize: (
|
||||
uri: string,
|
||||
@@ -177,10 +195,12 @@ interface ImageStatics {
|
||||
) => Promise<{| [uri: string]: 'disk/memory' |}>;
|
||||
}
|
||||
|
||||
const Image: React.AbstractComponent<
|
||||
type ImageComponent = React.AbstractComponent<
|
||||
ImageProps,
|
||||
React.ElementRef<typeof View>
|
||||
> = React.forwardRef((props, ref) => {
|
||||
>;
|
||||
|
||||
const BaseImage: ImageComponent = React.forwardRef((props, ref) => {
|
||||
const {
|
||||
'aria-label': ariaLabel,
|
||||
blurRadius,
|
||||
@@ -300,16 +320,7 @@ const Image: React.AbstractComponent<
|
||||
},
|
||||
function error() {
|
||||
updateState(ERRORED);
|
||||
if (onError) {
|
||||
onError({
|
||||
nativeEvent: {
|
||||
error: `Failed to load resource ${uri} (404)`
|
||||
}
|
||||
});
|
||||
}
|
||||
if (onLoadEnd) {
|
||||
onLoadEnd();
|
||||
}
|
||||
raiseOnErrorEvent(uri, { onError, onLoadEnd });
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -353,14 +364,69 @@ const Image: React.AbstractComponent<
|
||||
);
|
||||
});
|
||||
|
||||
Image.displayName = 'Image';
|
||||
BaseImage.displayName = 'Image';
|
||||
|
||||
// $FlowIgnore: This is the correct type, but casting makes it unhappy since the variables aren't defined yet
|
||||
const ImageWithStatics = (Image: React.AbstractComponent<
|
||||
ImageProps,
|
||||
React.ElementRef<typeof View>
|
||||
> &
|
||||
ImageStatics);
|
||||
/**
|
||||
* This component handles specifically loading an image source with headers
|
||||
* default source is never loaded using headers
|
||||
*/
|
||||
const ImageWithHeaders: ImageComponent = React.forwardRef((props, ref) => {
|
||||
// $FlowIgnore: This component would only be rendered when `source` matches `ImageSource`
|
||||
const nextSource: ImageSource = props.source;
|
||||
const [blobUri, setBlobUri] = React.useState('');
|
||||
const request = React.useRef<LoadRequest>({
|
||||
cancel: () => {},
|
||||
source: { uri: '', headers: {} },
|
||||
promise: Promise.resolve('')
|
||||
});
|
||||
|
||||
const { onLoadStart, ...forwardedProps } = props;
|
||||
const { onError, onLoadEnd } = forwardedProps;
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!hasSourceDiff(nextSource, request.current.source)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// When source changes we want to clean up any old/running requests
|
||||
request.current.cancel();
|
||||
|
||||
if (onLoadStart) {
|
||||
onLoadStart();
|
||||
}
|
||||
|
||||
// Store a ref for the current load request so we know what's the last loaded source,
|
||||
// and so we can cancel it if a different source is passed through props
|
||||
request.current = ImageLoader.loadWithHeaders(nextSource);
|
||||
|
||||
request.current.promise
|
||||
.then((uri) => setBlobUri(uri))
|
||||
.catch(() =>
|
||||
raiseOnErrorEvent(request.current.source.uri, { onError, onLoadEnd })
|
||||
);
|
||||
}, [nextSource, onLoadStart, onError, onLoadEnd]);
|
||||
|
||||
// Cancel any request on unmount
|
||||
React.useEffect(() => request.current.cancel, []);
|
||||
|
||||
// Until the current component resolves the request (using headers)
|
||||
// we skip forwarding the source so the base component doesn't attempt
|
||||
// to load the original source
|
||||
const source = blobUri ? { ...nextSource, uri: blobUri } : undefined;
|
||||
|
||||
return <BaseImage {...forwardedProps} ref={ref} source={source} />;
|
||||
});
|
||||
|
||||
// $FlowFixMe
|
||||
const ImageWithStatics: ImageComponent & ImageStatics = React.forwardRef(
|
||||
(props, ref) => {
|
||||
if (props.source && props.source.headers) {
|
||||
return <ImageWithHeaders {...props} ref={ref} />;
|
||||
}
|
||||
|
||||
return <BaseImage {...props} ref={ref} />;
|
||||
}
|
||||
);
|
||||
|
||||
ImageWithStatics.getSize = function (uri, success, failure) {
|
||||
ImageLoader.getSize(uri, success, failure);
|
||||
|
||||
+55
-2
@@ -122,9 +122,18 @@ const ImageLoader = {
|
||||
id += 1;
|
||||
const image = new window.Image();
|
||||
image.onerror = onError;
|
||||
image.onload = (e) => {
|
||||
image.onload = (nativeEvent) => {
|
||||
// avoid blocking the main thread
|
||||
const onDecode = () => onLoad({ nativeEvent: e });
|
||||
const onDecode = () => {
|
||||
// Append `source` to match RN's ImageLoadEvent interface
|
||||
nativeEvent.source = {
|
||||
uri: image.src,
|
||||
width: image.naturalWidth,
|
||||
height: image.naturalHeight
|
||||
};
|
||||
|
||||
onLoad({ nativeEvent });
|
||||
};
|
||||
if (typeof image.decode === 'function') {
|
||||
// Safari currently throws exceptions when decoding svgs.
|
||||
// We want to catch that error and allow the load handler
|
||||
@@ -136,8 +145,41 @@ const ImageLoader = {
|
||||
};
|
||||
image.src = uri;
|
||||
requests[`${id}`] = image;
|
||||
|
||||
return id;
|
||||
},
|
||||
loadWithHeaders(source: ImageSource): LoadRequest {
|
||||
let uri: string;
|
||||
const abortController = new AbortController();
|
||||
const request = new Request(source.uri, {
|
||||
headers: source.headers,
|
||||
signal: abortController.signal
|
||||
});
|
||||
request.headers.append('accept', 'image/*');
|
||||
|
||||
const promise = fetch(request)
|
||||
.then((response) => response.blob())
|
||||
.then((blob) => {
|
||||
uri = URL.createObjectURL(blob);
|
||||
return uri;
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error.name === 'AbortError') {
|
||||
return '';
|
||||
}
|
||||
|
||||
throw error;
|
||||
});
|
||||
|
||||
return {
|
||||
promise,
|
||||
source,
|
||||
cancel: () => {
|
||||
abortController.abort();
|
||||
URL.revokeObjectURL(uri);
|
||||
}
|
||||
};
|
||||
},
|
||||
prefetch(uri: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
ImageLoader.load(
|
||||
@@ -164,4 +206,15 @@ const ImageLoader = {
|
||||
}
|
||||
};
|
||||
|
||||
export type LoadRequest = {|
|
||||
cancel: Function,
|
||||
source: ImageSource,
|
||||
promise: Promise<string>
|
||||
|};
|
||||
|
||||
export type ImageSource = {
|
||||
uri: string,
|
||||
headers: { [key: string]: string }
|
||||
};
|
||||
|
||||
export default ImageLoader;
|
||||
|
||||
Reference in New Issue
Block a user