mirror of
https://github.com/zhigang1992/react-native.git
synced 2026-01-12 22:50:10 +08:00
Implement Blob support for XMLHttpRequest
Summary: This PR is a followup to https://github.com/facebook/react-native/pull/11417 and should be merged after that one is merged. 1. Add support for creating blobs from strings, not just other blobs 1. Add the `File` constructor which is a superset of `Blob` 1. Add the `FileReader` API which can be used to read blobs as strings or data url (base64) 1. Add support for uploading and downloading blobs via `XMLHttpRequest` and `fetch` 1. Add ability to download local files on Android so you can do `fetch(uri).then(res => res.blob())` to get a blob for a local file (iOS already supported this) 1. Clone the repo https://github.com/expo/react-native-blob-test 1. Change the `package.json` and update `react-native` dependency to point to this branch, then run `npm install` 1. Run the `server.js` file with `node server.js` 1. Open the `index.common.js` file and replace `localhost` with your computer's IP address 1. Start the packager with `yarn start` and run the app on your device If everything went well, all tests should pass, and you should see a screen like this: ! Pull to rerun all tests or tap on specific test to re-run it [GENERAL] [FEATURE] [Blob] - Implement blob support for XMLHttpRequest Closes https://github.com/facebook/react-native/pull/11573 Reviewed By: shergin Differential Revision: D6082054 Pulled By: hramos fbshipit-source-id: cc9c174fdefdfaf6e5d9fd7b300120a01a50e8c1
This commit is contained in:
committed by
Facebook Github Bot
parent
3fc33bb54f
commit
be56a3efee
@@ -10,6 +10,22 @@
|
||||
#import <React/RCTEventEmitter.h>
|
||||
#import <React/RCTNetworkTask.h>
|
||||
|
||||
@protocol RCTNetworkingRequestHandler <NSObject>
|
||||
|
||||
// @lint-ignore FBOBJCUNTYPEDCOLLECTION1
|
||||
- (BOOL)canHandleNetworkingRequest:(NSDictionary *)data;
|
||||
// @lint-ignore FBOBJCUNTYPEDCOLLECTION1
|
||||
- (NSDictionary *)handleNetworkingRequest:(NSDictionary *)data;
|
||||
|
||||
@end
|
||||
|
||||
@protocol RCTNetworkingResponseHandler <NSObject>
|
||||
|
||||
- (BOOL)canHandleNetworkingResponse:(NSString *)responseType;
|
||||
- (id)handleNetworkingResponse:(NSURLResponse *)response data:(NSData *)data;
|
||||
|
||||
@end
|
||||
|
||||
@interface RCTNetworking : RCTEventEmitter
|
||||
|
||||
/**
|
||||
@@ -24,6 +40,14 @@
|
||||
- (RCTNetworkTask *)networkTaskWithRequest:(NSURLRequest *)request
|
||||
completionBlock:(RCTURLRequestCompletionBlock)completionBlock;
|
||||
|
||||
- (void)addRequestHandler:(id<RCTNetworkingRequestHandler>)handler;
|
||||
|
||||
- (void)addResponseHandler:(id<RCTNetworkingResponseHandler>)handler;
|
||||
|
||||
- (void)removeRequestHandler:(id<RCTNetworkingRequestHandler>)handler;
|
||||
|
||||
- (void)removeResponseHandler:(id<RCTNetworkingResponseHandler>)handler;
|
||||
|
||||
@end
|
||||
|
||||
@interface RCTBridge (RCTNetworking)
|
||||
|
||||
@@ -18,6 +18,8 @@ const convertRequestBody = require('convertRequestBody');
|
||||
|
||||
import type {RequestBody} from 'convertRequestBody';
|
||||
|
||||
import type { NativeResponseType } from './XMLHttpRequest';
|
||||
|
||||
class RCTNetworking extends NativeEventEmitter {
|
||||
|
||||
isAvailable: boolean = true;
|
||||
@@ -32,7 +34,7 @@ class RCTNetworking extends NativeEventEmitter {
|
||||
url: string,
|
||||
headers: Object,
|
||||
data: RequestBody,
|
||||
responseType: 'text' | 'base64',
|
||||
responseType: NativeResponseType,
|
||||
incrementalUpdates: boolean,
|
||||
timeout: number,
|
||||
callback: (requestId: number) => any,
|
||||
|
||||
@@ -131,12 +131,20 @@ static NSString *RCTGenerateFormBoundary()
|
||||
NSMutableDictionary<NSNumber *, RCTNetworkTask *> *_tasksByRequestID;
|
||||
std::mutex _handlersLock;
|
||||
NSArray<id<RCTURLRequestHandler>> *_handlers;
|
||||
NSMutableArray<id<RCTNetworkingRequestHandler>> *_requestHandlers;
|
||||
NSMutableArray<id<RCTNetworkingResponseHandler>> *_responseHandlers;
|
||||
}
|
||||
|
||||
@synthesize methodQueue = _methodQueue;
|
||||
|
||||
RCT_EXPORT_MODULE()
|
||||
|
||||
- (void)invalidate
|
||||
{
|
||||
_requestHandlers = nil;
|
||||
_responseHandlers = nil;
|
||||
}
|
||||
|
||||
- (NSArray<NSString *> *)supportedEvents
|
||||
{
|
||||
return @[@"didCompleteNetworkResponse",
|
||||
@@ -297,6 +305,8 @@ RCT_EXPORT_MODULE()
|
||||
*
|
||||
* - {"formData": [...]}: list of data payloads that will be combined into a multipart/form-data request
|
||||
*
|
||||
* - {"blob": {...}}: an object representing a blob
|
||||
*
|
||||
* If successful, the callback be called with a result dictionary containing the following (optional) keys:
|
||||
*
|
||||
* - @"body" (NSData): the body of the request
|
||||
@@ -312,6 +322,15 @@ RCT_EXPORT_MODULE()
|
||||
if (!query) {
|
||||
return callback(nil, nil);
|
||||
}
|
||||
for (id<RCTNetworkingRequestHandler> handler in _requestHandlers) {
|
||||
if ([handler canHandleNetworkingRequest:query]) {
|
||||
// @lint-ignore FBOBJCUNTYPEDCOLLECTION1
|
||||
NSDictionary *body = [handler handleNetworkingRequest:query];
|
||||
if (body) {
|
||||
return callback(nil, body);
|
||||
}
|
||||
}
|
||||
}
|
||||
NSData *body = [RCTConvert NSData:query[@"string"]];
|
||||
if (body) {
|
||||
return callback(nil, @{@"body": body});
|
||||
@@ -417,6 +436,7 @@ RCT_EXPORT_MODULE()
|
||||
|
||||
- (void)sendData:(NSData *)data
|
||||
responseType:(NSString *)responseType
|
||||
response:(NSURLResponse *)response
|
||||
forTask:(RCTNetworkTask *)task
|
||||
{
|
||||
RCTAssertThread(_methodQueue, @"sendData: must be called on method queue");
|
||||
@@ -425,23 +445,31 @@ RCT_EXPORT_MODULE()
|
||||
return;
|
||||
}
|
||||
|
||||
NSString *responseString;
|
||||
if ([responseType isEqualToString:@"text"]) {
|
||||
// No carry storage is required here because the entire data has been loaded.
|
||||
responseString = [RCTNetworking decodeTextData:data fromResponse:task.response withCarryData:nil];
|
||||
if (!responseString) {
|
||||
RCTLogWarn(@"Received data was not a string, or was not a recognised encoding.");
|
||||
return;
|
||||
id responseData = nil;
|
||||
for (id<RCTNetworkingResponseHandler> handler in _responseHandlers) {
|
||||
if ([handler canHandleNetworkingResponse:responseType]) {
|
||||
responseData = [handler handleNetworkingResponse:response data:data];
|
||||
break;
|
||||
}
|
||||
} else if ([responseType isEqualToString:@"base64"]) {
|
||||
responseString = [data base64EncodedStringWithOptions:0];
|
||||
} else {
|
||||
RCTLogWarn(@"Invalid responseType: %@", responseType);
|
||||
return;
|
||||
}
|
||||
|
||||
NSArray<id> *responseJSON = @[task.requestID, responseString];
|
||||
[self sendEventWithName:@"didReceiveNetworkData" body:responseJSON];
|
||||
if (!responseData) {
|
||||
if ([responseType isEqualToString:@"text"]) {
|
||||
// No carry storage is required here because the entire data has been loaded.
|
||||
responseData = [RCTNetworking decodeTextData:data fromResponse:task.response withCarryData:nil];
|
||||
if (!responseData) {
|
||||
RCTLogWarn(@"Received data was not a string, or was not a recognised encoding.");
|
||||
return;
|
||||
}
|
||||
} else if ([responseType isEqualToString:@"base64"]) {
|
||||
responseData = [data base64EncodedStringWithOptions:0];
|
||||
} else {
|
||||
RCTLogWarn(@"Invalid responseType: %@", responseType);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
[self sendEventWithName:@"didReceiveNetworkData" body:@[task.requestID, responseData]];
|
||||
}
|
||||
|
||||
- (void)sendRequest:(NSURLRequest *)request
|
||||
@@ -523,7 +551,10 @@ RCT_EXPORT_MODULE()
|
||||
// Unless we were sending incremental (text) chunks to JS, all along, now
|
||||
// is the time to send the request body to JS.
|
||||
if (!(incrementalUpdates && [responseType isEqualToString:@"text"])) {
|
||||
[strongSelf sendData:data responseType:responseType forTask:task];
|
||||
[strongSelf sendData:data
|
||||
responseType:responseType
|
||||
response:response
|
||||
forTask:task];
|
||||
}
|
||||
NSArray *responseJSON = @[task.requestID,
|
||||
RCTNullIfNil(error.localizedDescription),
|
||||
@@ -553,6 +584,32 @@ RCT_EXPORT_MODULE()
|
||||
|
||||
#pragma mark - Public API
|
||||
|
||||
- (void)addRequestHandler:(id<RCTNetworkingRequestHandler>)handler
|
||||
{
|
||||
if (!_requestHandlers) {
|
||||
_requestHandlers = [NSMutableArray new];
|
||||
}
|
||||
[_requestHandlers addObject:handler];
|
||||
}
|
||||
|
||||
- (void)addResponseHandler:(id<RCTNetworkingResponseHandler>)handler
|
||||
{
|
||||
if (!_responseHandlers) {
|
||||
_responseHandlers = [NSMutableArray new];
|
||||
}
|
||||
[_responseHandlers addObject:handler];
|
||||
}
|
||||
|
||||
- (void)removeRequestHandler:(id<RCTNetworkingRequestHandler>)handler
|
||||
{
|
||||
[_requestHandlers removeObject:handler];
|
||||
}
|
||||
|
||||
- (void)removeResponseHandler:(id<RCTNetworkingResponseHandler>)handler
|
||||
{
|
||||
[_responseHandlers removeObject:handler];
|
||||
}
|
||||
|
||||
- (RCTNetworkTask *)networkTaskWithRequest:(NSURLRequest *)request completionBlock:(RCTURLRequestCompletionBlock)completionBlock
|
||||
{
|
||||
id<RCTURLRequestHandler> handler = [self handlerForRequest:request];
|
||||
|
||||
@@ -23,9 +23,11 @@ const invariant = require('fbjs/lib/invariant');
|
||||
* found when Flow v0.54 was deployed. To see the error delete this comment and
|
||||
* run Flow. */
|
||||
const warning = require('fbjs/lib/warning');
|
||||
const BlobManager = require('BlobManager');
|
||||
|
||||
type ResponseType = '' | 'arraybuffer' | 'blob' | 'document' | 'json' | 'text';
|
||||
type Response = ?Object | string;
|
||||
export type NativeResponseType = 'base64' | 'blob' | 'text';
|
||||
export type ResponseType = '' | 'arraybuffer' | 'blob' | 'document' | 'json' | 'text';
|
||||
export type Response = ?Object | string;
|
||||
|
||||
type XHRInterceptor = {
|
||||
requestSent(
|
||||
@@ -54,6 +56,11 @@ type XHRInterceptor = {
|
||||
): void,
|
||||
};
|
||||
|
||||
// The native blob module is optional so inject it here if available.
|
||||
if (BlobManager.isAvailable) {
|
||||
BlobManager.addNetworkingHandler();
|
||||
}
|
||||
|
||||
const UNSENT = 0;
|
||||
const OPENED = 1;
|
||||
const HEADERS_RECEIVED = 2;
|
||||
@@ -200,6 +207,10 @@ class XMLHttpRequest extends EventTarget(...XHR_EVENTS) {
|
||||
SUPPORTED_RESPONSE_TYPES[responseType] || responseType === 'document',
|
||||
`The provided value '${responseType}' is unsupported in this environment.`
|
||||
);
|
||||
|
||||
if (responseType === 'blob') {
|
||||
invariant(BlobManager.isAvailable, 'Native module BlobModule is required for blob support');
|
||||
}
|
||||
this._responseType = responseType;
|
||||
}
|
||||
|
||||
@@ -242,10 +253,11 @@ class XMLHttpRequest extends EventTarget(...XHR_EVENTS) {
|
||||
break;
|
||||
|
||||
case 'blob':
|
||||
this._cachedResponse = new global.Blob(
|
||||
[base64.toByteArray(this._response).buffer],
|
||||
{type: this.getResponseHeader('content-type') || ''}
|
||||
);
|
||||
if (typeof this._response === 'object' && this._response) {
|
||||
this._cachedResponse = BlobManager.createFromOptions(this._response);
|
||||
} else {
|
||||
throw new Error(`Invalid response for blob: ${this._response}`);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'json':
|
||||
@@ -493,10 +505,13 @@ class XMLHttpRequest extends EventTarget(...XHR_EVENTS) {
|
||||
(args) => this.__didCompleteResponse(...args)
|
||||
));
|
||||
|
||||
let nativeResponseType = 'text';
|
||||
if (this._responseType === 'arraybuffer' || this._responseType === 'blob') {
|
||||
let nativeResponseType: NativeResponseType = 'text';
|
||||
if (this._responseType === 'arraybuffer') {
|
||||
nativeResponseType = 'base64';
|
||||
}
|
||||
if (this._responseType === 'blob') {
|
||||
nativeResponseType = 'blob';
|
||||
}
|
||||
|
||||
invariant(this._method, 'Request method needs to be defined.');
|
||||
invariant(this._url, 'Request URL needs to be defined.');
|
||||
|
||||
@@ -8,25 +8,30 @@
|
||||
*
|
||||
* @providesModule convertRequestBody
|
||||
* @flow
|
||||
* @format
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
const binaryToBase64 = require('binaryToBase64');
|
||||
|
||||
const Blob = require('Blob');
|
||||
const FormData = require('FormData');
|
||||
|
||||
export type RequestBody =
|
||||
string
|
||||
| string
|
||||
| Blob
|
||||
| FormData
|
||||
| {uri: string}
|
||||
| ArrayBuffer
|
||||
| $ArrayBufferView
|
||||
;
|
||||
| $ArrayBufferView;
|
||||
|
||||
function convertRequestBody(body: RequestBody): Object {
|
||||
if (typeof body === 'string') {
|
||||
return {string: body};
|
||||
}
|
||||
if (body instanceof Blob) {
|
||||
return {blob: body.data};
|
||||
}
|
||||
if (body instanceof FormData) {
|
||||
return {formData: body.getParts()};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user