Add blob implementation with WebSocket integration

Summary:
This is the first PR from a series of PRs grabbou and me will make to add blob support to React Native. The next PR will include blob support for XMLHttpRequest.

I'd like to get this merged with minimal changes to preserve the attribution. My next PR can contain bigger changes.

Blobs are used to transfer binary data between server and client. Currently React Native lacks a way to deal with binary data. The only thing that comes close is uploading files through a URI.

Current workarounds to transfer binary data includes encoding and decoding them to base64 and and transferring them as string, which is not ideal, since it increases the payload size and the whole payload needs to be sent via the bridge every time changes are made.

The PR adds a way to deal with blobs via a new native module. The blob is constructed on the native side and the data never needs to pass through the bridge. Currently the only way to create a blob is to receive a blob from the server via websocket.

The PR is largely a direct port of https://github.com/silklabs/silk/tree/master/react-native-blobs by philikon into RN (with changes to integrate with RN), and attributed as such.

> **Note:** This is a breaking change for all people running iOS without CocoaPods. You will have to manually add `RCTBlob.xcodeproj` to your `Libraries` and then, add it to Build Phases. Just follow the process of manual linking. We'll also need to document this process in the release notes.

Related discussion - https://github.com/facebook/react-native/issues/11103

- `Image` can't show image when `URL.createObjectURL` is used with large images on Android

The websocket integration can be tested via a simple server,

```js
const fs = require('fs');
const http = require('http');

const WebSocketServer = require('ws').Server;

const wss = new WebSocketServer({
  server: http.createServer().listen(7232),
});

wss.on('connection', (ws) => {
  ws.on('message', (d) => {
    console.log(d);
  });

  ws.send(fs.readFileSync('./some-file'));
});
```

Then on the client,

```js
var ws = new WebSocket('ws://localhost:7232');

ws.binaryType = 'blob';

ws.onerror = (error) => {
  console.error(error);
};

ws.onmessage = (e) => {
  console.log(e.data);
  ws.send(e.data);
};
```

cc brentvatne ide
Closes https://github.com/facebook/react-native/pull/11417

Reviewed By: sahrens

Differential Revision: D5188484

Pulled By: javache

fbshipit-source-id: 6afcbc4d19aa7a27b0dc9d52701ba400e7d7e98f
This commit is contained in:
Philipp von Weitershausen
2017-07-26 08:12:12 -07:00
committed by Facebook Github Bot
parent 91493f6b9d
commit ed903099b4
26 changed files with 1800 additions and 307 deletions

View File

@@ -9,6 +9,28 @@
#import <React/RCTEventEmitter.h>
@interface RCTWebSocketModule : RCTEventEmitter
NS_ASSUME_NONNULL_BEGIN
@protocol RCTWebSocketContentHandler <NSObject>
- (id)processMessage:(id __nullable)message forSocketID:(NSNumber *)socketID
withType:(NSString *__nonnull __autoreleasing *__nonnull)type;
@end
@interface RCTWebSocketModule : RCTEventEmitter
// Register a custom handler for a specific websocket. The handler will be strongly held by the WebSocketModule.
- (void)setContentHandler:(id<RCTWebSocketContentHandler> __nullable)handler forSocketID:(NSNumber *)socketID;
- (void)sendData:(NSData *)data forSocketID:(nonnull NSNumber *)socketID;
@end
@interface RCTBridge (RCTWebSocketModule)
- (RCTWebSocketModule *)webSocketModule;
@end
NS_ASSUME_NONNULL_END

View File

@@ -36,11 +36,15 @@
@implementation RCTWebSocketModule
{
NSMutableDictionary<NSNumber *, RCTSRWebSocket *> *_sockets;
NSMutableDictionary<NSNumber *, RCTSRWebSocket *> *_sockets;
NSMutableDictionary<NSNumber *, id> *_contentHandlers;
}
RCT_EXPORT_MODULE()
// Used by RCTBlobModule
@synthesize methodQueue = _methodQueue;
- (NSArray *)supportedEvents
{
return @[@"websocketMessage",
@@ -60,7 +64,7 @@ RCT_EXPORT_MODULE()
RCT_EXPORT_METHOD(connect:(NSURL *)URL protocols:(NSArray *)protocols headers:(NSDictionary *)headers socketID:(nonnull NSNumber *)socketID)
{
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:URL];
// We load cookies from sharedHTTPCookieStorage (shared with XHR and
// fetch). To get secure cookies for wss URLs, replace wss with https
// in the URL.
@@ -72,7 +76,7 @@ RCT_EXPORT_METHOD(connect:(NSURL *)URL protocols:(NSArray *)protocols headers:(N
// Load and set the cookie header.
NSArray<NSHTTPCookie *> *cookies = [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookiesForURL:components.URL];
request.allHTTPHeaderFields = [NSHTTPCookie requestHeaderFieldsWithCookies:cookies];
// Load supplied headers
[headers enumerateKeysAndObjectsUsingBlock:^(NSString *key, id value, BOOL *stop) {
[request addValue:[RCTConvert NSString:value] forHTTPHeaderField:key];
@@ -88,15 +92,19 @@ RCT_EXPORT_METHOD(connect:(NSURL *)URL protocols:(NSArray *)protocols headers:(N
[webSocket open];
}
RCT_EXPORT_METHOD(send:(NSString *)message socketID:(nonnull NSNumber *)socketID)
RCT_EXPORT_METHOD(send:(NSString *)message forSocketID:(nonnull NSNumber *)socketID)
{
[_sockets[socketID] send:message];
}
RCT_EXPORT_METHOD(sendBinary:(NSString *)base64String socketID:(nonnull NSNumber *)socketID)
RCT_EXPORT_METHOD(sendBinary:(NSString *)base64String forSocketID:(nonnull NSNumber *)socketID)
{
NSData *message = [[NSData alloc] initWithBase64EncodedString:base64String options:0];
[_sockets[socketID] send:message];
[self sendData:[[NSData alloc] initWithBase64EncodedString:base64String options:0] forSocketID:socketID];
}
- (void)sendData:(NSData *)data forSocketID:(nonnull NSNumber *)socketID
{
[_sockets[socketID] send:data];
}
RCT_EXPORT_METHOD(ping:(nonnull NSNumber *)socketID)
@@ -110,14 +118,36 @@ RCT_EXPORT_METHOD(close:(nonnull NSNumber *)socketID)
[_sockets removeObjectForKey:socketID];
}
- (void)setContentHandler:(id<RCTWebSocketContentHandler>)handler forSocketID:(NSString *)socketID
{
if (!_contentHandlers) {
_contentHandlers = [NSMutableDictionary new];
}
_contentHandlers[socketID] = handler;
}
#pragma mark - RCTSRWebSocketDelegate methods
- (void)webSocket:(RCTSRWebSocket *)webSocket didReceiveMessage:(id)message
{
BOOL binary = [message isKindOfClass:[NSData class]];
NSString *type;
NSNumber *socketID = [webSocket reactTag];
id contentHandler = _contentHandlers[socketID];
if (contentHandler) {
message = [contentHandler processMessage:message forSocketID:socketID withType:&type];
} else {
if ([message isKindOfClass:[NSData class]]) {
type = @"binary";
message = [message base64EncodedStringWithOptions:0];
} else {
type = @"text";
}
}
[self sendEventWithName:@"websocketMessage" body:@{
@"data": binary ? [message base64EncodedStringWithOptions:0] : message,
@"type": binary ? @"binary" : @"text",
@"data": message,
@"type": type,
@"id": webSocket.reactTag
}];
}
@@ -131,21 +161,36 @@ RCT_EXPORT_METHOD(close:(nonnull NSNumber *)socketID)
- (void)webSocket:(RCTSRWebSocket *)webSocket didFailWithError:(NSError *)error
{
NSNumber *socketID = [webSocket reactTag];
_contentHandlers[socketID] = nil;
[self sendEventWithName:@"websocketFailed" body:@{
@"message":error.localizedDescription,
@"id": webSocket.reactTag
@"message": error.localizedDescription,
@"id": socketID
}];
}
- (void)webSocket:(RCTSRWebSocket *)webSocket didCloseWithCode:(NSInteger)code
reason:(NSString *)reason wasClean:(BOOL)wasClean
- (void)webSocket:(RCTSRWebSocket *)webSocket
didCloseWithCode:(NSInteger)code
reason:(NSString *)reason
wasClean:(BOOL)wasClean
{
NSNumber *socketID = [webSocket reactTag];
_contentHandlers[socketID] = nil;
[self sendEventWithName:@"websocketClosed" body:@{
@"code": @(code),
@"reason": RCTNullIfNil(reason),
@"clean": @(wasClean),
@"id": webSocket.reactTag
@"id": socketID
}];
}
@end
@implementation RCTBridge (RCTWebSocketModule)
- (RCTWebSocketModule *)webSocketModule
{
return [self moduleForClass:[RCTWebSocketModule class]];
}
@end

View File

@@ -8,11 +8,24 @@
*/
#import <React/RCTDefines.h>
#import <React/RCTWebSocketObserverProtocol.h>
#if RCT_DEV // Only supported in dev mode
@interface RCTWebSocketObserver : NSObject <RCTWebSocketObserver>
@protocol RCTWebSocketObserverDelegate
- (void)didReceiveWebSocketMessage:(NSDictionary<NSString *, id> *)message;
@end
@interface RCTWebSocketObserver : NSObject
- (instancetype)initWithURL:(NSURL *)url;
@property (nonatomic, weak) id<RCTWebSocketObserverDelegate> delegate;
- (void)start;
- (void)stop;
@end
#endif

View File

@@ -0,0 +1,25 @@
/**
* 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.
*/
#import <React/RCTDefines.h>
#if RCT_DEV // Only supported in dev mode
@protocol RCTWebSocketObserverDelegate
- (void)didReceiveWebSocketMessage:(NSDictionary<NSString *, id> *)message;
@end
@protocol RCTWebSocketObserver
- (instancetype)initWithURL:(NSURL *)url;
@property (nonatomic, weak) id<RCTWebSocketObserverDelegate> delegate;
- (void)start;
- (void)stop;
@end
#endif

View File

@@ -11,17 +11,35 @@
*/
'use strict';
const NativeEventEmitter = require('NativeEventEmitter');
const Platform = require('Platform');
const RCTWebSocketModule = require('NativeModules').WebSocketModule;
const WebSocketEvent = require('WebSocketEvent');
const binaryToBase64 = require('binaryToBase64');
const Blob = require('Blob');
const EventTarget = require('event-target-shim');
const NativeEventEmitter = require('NativeEventEmitter');
const NativeModules = require('NativeModules');
const Platform = require('Platform');
const WebSocketEvent = require('WebSocketEvent');
const base64 = require('base64-js');
const binaryToBase64 = require('binaryToBase64');
const invariant = require('fbjs/lib/invariant');
const {WebSocketModule} = NativeModules;
import type EventSubscription from 'EventSubscription';
type ArrayBufferView =
| Int8Array
| Uint8Array
| Uint8ClampedArray
| Int16Array
| Uint16Array
| Int32Array
| Uint32Array
| Float32Array
| Float64Array
| DataView
type BinaryType = 'blob' | 'arraybuffer'
const CONNECTING = 0;
const OPEN = 1;
const CLOSING = 2;
@@ -58,22 +76,22 @@ class WebSocket extends EventTarget(...WEBSOCKET_EVENTS) {
_socketId: number;
_eventEmitter: NativeEventEmitter;
_subscriptions: Array<EventSubscription>;
_binaryType: ?BinaryType;
onclose: ?Function;
onerror: ?Function;
onmessage: ?Function;
onopen: ?Function;
binaryType: ?string;
bufferedAmount: number;
extension: ?string;
protocol: ?string;
readyState: number = CONNECTING;
url: ?string;
// This module depends on the native `RCTWebSocketModule` module. If you don't include it,
// This module depends on the native `WebSocketModule` module. If you don't include it,
// `WebSocket.isAvailable` will return `false`, and WebSocket constructor will throw an error
static isAvailable: boolean = !!RCTWebSocketModule;
static isAvailable: boolean = !!WebSocketModule;
constructor(url: string, protocols: ?string | ?Array<string>, options: ?{origin?: string}) {
super();
@@ -87,13 +105,35 @@ class WebSocket extends EventTarget(...WEBSOCKET_EVENTS) {
if (!WebSocket.isAvailable) {
throw new Error('Cannot initialize WebSocket module. ' +
'Native module RCTWebSocketModule is missing.');
'Native module WebSocketModule is missing.');
}
this._eventEmitter = new NativeEventEmitter(RCTWebSocketModule);
this._eventEmitter = new NativeEventEmitter(WebSocketModule);
this._socketId = nextWebSocketId++;
this._registerEvents();
RCTWebSocketModule.connect(url, protocols, options, this._socketId);
WebSocketModule.connect(url, protocols, options, this._socketId);
}
get binaryType(): ?BinaryType {
return this._binaryType;
}
set binaryType(binaryType: BinaryType): void {
if (binaryType !== 'blob' && binaryType !== 'arraybuffer') {
throw new Error('binaryType must be either \'blob\' or \'arraybuffer\'');
}
if (this._binaryType === 'blob' || binaryType === 'blob') {
const BlobModule = NativeModules.BlobModule;
invariant(BlobModule, 'Native module BlobModule is required for blob support');
if (BlobModule) {
if (binaryType === 'blob') {
BlobModule.enableBlobSupport(this._socketId);
} else {
BlobModule.disableBlobSupport(this._socketId);
}
}
}
this._binaryType = binaryType;
}
close(code?: number, reason?: string): void {
@@ -106,18 +146,25 @@ class WebSocket extends EventTarget(...WEBSOCKET_EVENTS) {
this._close(code, reason);
}
send(data: string | ArrayBuffer | $ArrayBufferView): void {
send(data: string | ArrayBuffer | ArrayBufferView | Blob): void {
if (this.readyState === this.CONNECTING) {
throw new Error('INVALID_STATE_ERR');
}
if (data instanceof Blob) {
const BlobModule = NativeModules.BlobModule;
invariant(BlobModule, 'Native module BlobModule is required for blob support');
BlobModule.sendBlob(data, this._socketId);
return;
}
if (typeof data === 'string') {
RCTWebSocketModule.send(data, this._socketId);
WebSocketModule.send(data, this._socketId);
return;
}
if (data instanceof ArrayBuffer || ArrayBuffer.isView(data)) {
RCTWebSocketModule.sendBinary(binaryToBase64(data), this._socketId);
WebSocketModule.sendBinary(binaryToBase64(data), this._socketId);
return;
}
@@ -129,7 +176,7 @@ class WebSocket extends EventTarget(...WEBSOCKET_EVENTS) {
throw new Error('INVALID_STATE_ERR');
}
RCTWebSocketModule.ping(this._socketId);
WebSocketModule.ping(this._socketId);
}
_close(code?: number, reason?: string): void {
@@ -137,9 +184,9 @@ class WebSocket extends EventTarget(...WEBSOCKET_EVENTS) {
// See https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent
const statusCode = typeof code === 'number' ? code : CLOSE_NORMAL;
const closeReason = typeof reason === 'string' ? reason : '';
RCTWebSocketModule.close(statusCode, closeReason, this._socketId);
WebSocketModule.close(statusCode, closeReason, this._socketId);
} else {
RCTWebSocketModule.close(this._socketId);
WebSocketModule.close(this._socketId);
}
}
@@ -154,9 +201,16 @@ class WebSocket extends EventTarget(...WEBSOCKET_EVENTS) {
if (ev.id !== this._socketId) {
return;
}
this.dispatchEvent(new WebSocketEvent('message', {
data: (ev.type === 'binary') ? base64.toByteArray(ev.data).buffer : ev.data
}));
let data = ev.data;
switch (ev.type) {
case 'binary':
data = base64.toByteArray(ev.data).buffer;
break;
case 'blob':
data = Blob.create(ev.data);
break;
}
this.dispatchEvent(new WebSocketEvent('message', { data }));
}),
this._eventEmitter.addListener('websocketOpen', ev => {
if (ev.id !== this._socketId) {