react-native/RNTester/js/WebSocketExample.js
Philipp von Weitershausen ed903099b4 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
2017-07-26 08:23:20 -07:00

366 lines
8.7 KiB
JavaScript

/**
* 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.
*
* @providesModule WebSocketExample
* @format
*/
'use strict';
/* eslint-env browser */
const React = require('react');
const ReactNative = require('react-native');
const {
Image,
PixelRatio,
ScrollView,
StyleSheet,
Text,
TextInput,
TouchableOpacity,
View,
} = ReactNative;
const DEFAULT_WS_URL = 'ws://localhost:5555/';
const DEFAULT_HTTP_URL = 'http://localhost:5556/';
const WS_EVENTS = ['close', 'error', 'message', 'open'];
const WS_STATES = [
/* 0 */ 'CONNECTING',
/* 1 */ 'OPEN',
/* 2 */ 'CLOSING',
/* 3 */ 'CLOSED',
];
class Button extends React.Component {
render(): React.Element<any> {
const label = (
<Text style={styles.buttonLabel}>
{this.props.label}
</Text>
);
if (this.props.disabled) {
return (
<View style={[styles.button, styles.disabledButton]}>
{label}
</View>
);
}
return (
<TouchableOpacity onPress={this.props.onPress} style={styles.button}>
{label}
</TouchableOpacity>
);
}
}
class Row extends React.Component {
render(): React.Element<any> {
return (
<View style={styles.row}>
<Text>
{this.props.label}
</Text>
{this.props.value
? <Text>
{this.props.value}
</Text>
: null}
{this.props.children}
</View>
);
}
}
class WebSocketImage extends React.Component {
ws: ?WebSocket = null;
state: {blob: ?Blob} = {blob: null};
componentDidMount() {
let ws = (this.ws = new WebSocket(this.props.url));
ws.binaryType = 'blob';
ws.onmessage = event => {
if (event.data instanceof Blob) {
const blob = event.data;
if (this.state.blob) {
this.state.blob.close();
}
this.setState({blob});
}
};
ws.onopen = event => {
ws.send('getImage');
};
}
componentUnmount() {
if (this.state.blob) {
this.state.blob.close();
}
this.ws && this.ws.close();
}
render() {
if (!this.state.blob) {
return <View />;
}
return (
<Image
source={{uri: URL.createObjectURL(this.state.blob)}}
style={{width: 50, height: 50}}
/>
);
}
}
function showValue(value) {
if (value === undefined || value === null) {
return '(no value)';
}
if (
typeof ArrayBuffer !== 'undefined' &&
typeof Uint8Array !== 'undefined' &&
value instanceof ArrayBuffer
) {
return `ArrayBuffer {${String(Array.from(new Uint8Array(value)))}}`;
}
return value;
}
type State = {
url: string,
httpUrl: string,
fetchStatus: ?string,
socket: ?WebSocket,
socketState: ?number,
lastSocketEvent: ?string,
lastMessage: ?string | ?ArrayBuffer,
outgoingMessage: string,
};
class WebSocketExample extends React.Component<any, any, State> {
static title = 'WebSocket';
static description = 'WebSocket API';
state: State = {
url: DEFAULT_WS_URL,
httpUrl: DEFAULT_HTTP_URL,
fetchStatus: null,
socket: null,
socketState: null,
lastSocketEvent: null,
lastMessage: null,
outgoingMessage: '',
};
_connect = () => {
const socket = new WebSocket(this.state.url);
WS_EVENTS.forEach(ev => socket.addEventListener(ev, this._onSocketEvent));
this.setState({
socket,
socketState: socket.readyState,
});
};
_disconnect = () => {
if (!this.state.socket) {
return;
}
this.state.socket.close();
};
_onSocketEvent = (event: MessageEvent) => {
const state: any = {
socketState: event.target.readyState,
lastSocketEvent: event.type,
};
if (event.type === 'message') {
state.lastMessage = event.data;
}
this.setState(state);
};
_sendText = () => {
if (!this.state.socket) {
return;
}
this.state.socket.send(this.state.outgoingMessage);
this.setState({outgoingMessage: ''});
};
_sendHttp = () => {
this.setState({
fetchStatus: 'fetching',
});
fetch(this.state.httpUrl).then(response => {
if (response.status >= 200 && response.status < 400) {
this.setState({
fetchStatus: 'OK',
});
}
});
};
_sendBinary = () => {
if (
!this.state.socket ||
typeof ArrayBuffer === 'undefined' ||
typeof Uint8Array === 'undefined'
) {
return;
}
const {outgoingMessage} = this.state;
const buffer = new Uint8Array(outgoingMessage.length);
for (let i = 0; i < outgoingMessage.length; i++) {
buffer[i] = outgoingMessage.charCodeAt(i);
}
this.state.socket.send(buffer);
this.setState({outgoingMessage: ''});
};
render(): React.Element<any> {
const socketState = WS_STATES[this.state.socketState || -1];
const canConnect =
!this.state.socket || this.state.socket.readyState >= WebSocket.CLOSING;
const canSend = socketState === 'OPEN';
return (
<ScrollView style={styles.container}>
<View style={styles.note}>
<Text>To start the WS test server:</Text>
<Text style={styles.monospace}>
./RNTester/js/websocket_test_server.js
</Text>
</View>
<Row label="Current WebSocket state" value={showValue(socketState)} />
<Row
label="Last WebSocket event"
value={showValue(this.state.lastSocketEvent)}
/>
<Row
label="Last message received"
value={showValue(this.state.lastMessage)}
/>
<Row label="Last image received">
{canSend ? <WebSocketImage url={this.state.url} /> : null}
</Row>
<TextInput
style={styles.textInput}
autoCorrect={false}
placeholder="Server URL..."
onChangeText={url => this.setState({url})}
value={this.state.url}
/>
<View style={styles.buttonRow}>
<Button
onPress={this._connect}
label="Connect"
disabled={!canConnect}
/>
<Button
onPress={this._disconnect}
label="Disconnect"
disabled={canConnect}
/>
</View>
<TextInput
style={styles.textInput}
autoCorrect={false}
placeholder="Type message here..."
onChangeText={outgoingMessage => this.setState({outgoingMessage})}
value={this.state.outgoingMessage}
/>
<View style={styles.buttonRow}>
<Button
onPress={this._sendText}
label="Send as text"
disabled={!canSend}
/>
<Button
onPress={this._sendBinary}
label="Send as binary"
disabled={!canSend}
/>
</View>
<View style={styles.note}>
<Text>To start the HTTP test server:</Text>
<Text style={styles.monospace}>
./RNTester/js/http_test_server.js
</Text>
</View>
<TextInput
style={styles.textInput}
autoCorrect={false}
placeholder="HTTP URL..."
onChangeText={httpUrl => this.setState({httpUrl})}
value={this.state.httpUrl}
/>
<View style={styles.buttonRow}>
<Button
onPress={this._sendHttp}
label="Send HTTP request to set cookie"
disabled={this.state.fetchStatus === 'fetching'}
/>
</View>
<View style={styles.note}>
<Text>
{this.state.fetchStatus === 'OK'
? 'Done. Check your WS server console to see if the next WS requests include the cookie (should be "wstest=OK")'
: '-'}
</Text>
</View>
</ScrollView>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
note: {
padding: 8,
margin: 4,
backgroundColor: 'white',
},
monospace: {
fontFamily: 'courier',
fontSize: 11,
},
row: {
height: 40,
padding: 4,
backgroundColor: 'white',
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
borderBottomWidth: 1 / PixelRatio.get(),
borderColor: 'grey',
},
button: {
margin: 8,
padding: 8,
borderRadius: 4,
backgroundColor: 'blue',
alignSelf: 'center',
},
disabledButton: {
opacity: 0.5,
},
buttonLabel: {
color: 'white',
},
buttonRow: {
flexDirection: 'row',
justifyContent: 'center',
},
textInput: {
height: 40,
backgroundColor: 'white',
margin: 8,
padding: 8,
},
});
module.exports = WebSocketExample;