Создание Peer-to-Peer компонента обмена файлами на React и PeerJS

В этом уроке мы создадим приложение расшаривания файлов при помощи PeerJS и React. Данный урок ориентирован на разработчиков, которые мало знакомы с React, поэтому я буду описывать максимальное количество возможных деталей и нюансов.

Ниже представлю несколько скриншотов, чтобы вы могли понять как будет выглядеть наше приложение в целом. Компонент готовый к использованию:

react file sharing

А вот как это выглядит, когда пользователь подключился к другому участнику и добавил несколько файлов:

react app sharing

Код нашего урока доступен на GitHub.

Как уже упоминалось ранее, мы воспользуемся библиотеками PeerJS и React для создания нашего приложения общего доступа. Библиотека PeerJS позволяет соединить два или более устройств с помощью WebRTC, предоставляя разработчику качественный API. WebRTC это протокол, предназначенный для организации передачи потоковых данных между браузерами или другими поддерживающими его приложениями по технологии точка-точка. Библиотека React является компонентно-ориентированной, которая позволяет создавать пользовательские автономные элементы пользовательского интерфейса.

Установка зависимостей

Перед тем, как приступить к созданию приложения, в первую очередь необходимо установить следующие зависимости, используя npm:

npm install --save react react-dom browserify babelify babel-preset-react babel-preset-es2015 randomstring peerjs

Краткое описание, что делает каждое из них:

  • react - библиотека React.
  • react-dom - позволяет нам показывать React компоненты в DOM. React не взаимодействует непосредственно с DOM, но вместо этого он использует виртуальный DOM. ReactDOM отвечает за визуализацию дерева компонентов в браузере.
  • browserify - позволяет нам использовать require утверждения. Он отвечает за доставку всех файлов, также может быть использован в браузере.
  • babelify - траспилятор Babel для Browserify. Отвечает за компиляцию ES6 кода в ES5.
  • babel-preset-react - предустановленный Babel для всех React плагинов. Он используется для преобразования JSX в JavaScript код.
  • babel-preset-es2015 - предустановленный Babel, который переводит ES6 в код ES5.
  • randomstring - генерирует случайную строку. Мы будем использовать эту функцию для генерации ключей, необходимых для списка файлов.
  • peerjs - библиотека PeerJS. Отвечает за установку связей и обмена файлами между пользователями.

Создание приложения

Прежде чем перейти к коду, давайте взглянем на структуру каталогов:

-js
-node_modules
-src
    -main.js
    -components
        -filesharer.jsx
index.html
  • js - здесь хранятся файлы JavaScript, которые поставляются в комплекте с Browserify.
  • src - здесь хранятся компоненты React. Внутри будет находится файл main.js, в который мы импортируем React компоненты, используемые приложением.
  • index.html - основной файл приложения.

index.html

Давайте начнем с файла index.html. Данный файл содержит структуру приложения по умолчанию. Внутри раздела <head> мы добавили основную таблицу стилей и библиотеку PeerJS. Внутри раздела <body> мы добавили заголовок приложения и главный контейнер <div>, в котором разместим сам компонент React. В конце <body> мы подключили основной JavaScript файл нашего приложения.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>React File Sharer</title>

    <link href="http://cdn.muicss.com/mui-0.4.6/css/mui.min.css" rel="stylesheet" type="text/css" />
</head>
<body>

    <div class="mui-appbar mui--appbar-line-height">
        <div class="mui-container">
          <span class="mui--text-headline">
            React FileSharer
          </span>
        </div>
    </div>
    <br />
    <div class="mui-container">
        <div id="main" class="mui-panel"></div>
    </div>

    <script src="/js/main.js"></script>
</body>
</html>

Основной JavaScript файл

Перейдем к файлу src/main.js в котором мы будем описывать основной DOM компонент.

Для начала определим необходимые переменные.

var React = require('react');
var ReactDOM = require('react-dom');
var Filesharer = require('./components/filesharer.jsx');

Затем мы объявляем объект options. Данный объект используется для установки параметров компонента Filesharer. В нашем случае мы передаем peerjs_key. Это API ключ, который вы получаете с сайта PeerJS, для того чтобы использовать его в Peer Cloud Service и установки peer-to-peer соединений. В случае нашего приложения, он служит в качестве посредника между двумя узлами (устройствами) для совместного использования файлов.

var options = {
    peerjs_key: 'your peerjs key'
}

Далее мы определим основной компонент. Сделаем это путем вызова метода createClass объекта React. Он принимает объект в качестве аргумента. По умолчанию React должен определить функцию render внутри объекта. Данная функция должна возвращать интерфейс компонента. В этом случае мы просто возвращаем компонент Filesharer, который импортировали ранее. Также передаем объект options в качестве значения для атрибута opts. В React эти атрибуты называются props и они становятся доступными для использования внутри компонента, в качестве передачи аргументов функции. Затем, внутри компонента Filesharer, вы сможете получить доступ к параметрам, при помощи вызова this.props.opts.

var Main = React.createClass({
  render: function () {
    return <Filesharer opts={options} />;
  }
});

Получаем ссылку на главный контейнер div из DOM, а затем создаем компонент используя ReactDOM метод render, данный метод аналогичен jQuery методу append.

var main = document.getElementById('main');

ReactDOM.render(<Main/>, main);

Компонент Filesharer

Компонент Filesharer (src/components/filesharer.jsx), как уже говорили ранее, содержит основной код приложения. Основная цель компонентов - иметь автономный код, который может быть использован в любом месте. Другие разработчики могут просто импортировать его (как мы делали внутри основного компонента), устанавливают несколько параметров, рендерят, а затем добавляют CSS.

Сначала импортируем структуру React, библиотеку randomstring и клиент PeerJS.

var React = require('react');
var randomstring = require('randomstring');
var Peer = require('peerjs');

Ранее, в нашем главном файле JavaScript мы добавили атрибут prop для кастомизации label, которые будут отображаться в компоненте обмена файлами. Убедитесь что указали правильное название свойства (opts) и типа данных (React.PropTypes.object) которые передаются в компонент, для этого мы используем propTypes.

propTypes: {
    opts: React.PropTypes.object
},

Внутри объекта, который мы передаем в метод createClass, у нас есть метод getInitialState это то, что React использует для возвращения состояния компонента по умолчанию. Здесь мы возвращаем объект, содержащий следующие параметры:

  • peer - объект PeerJS, который используется для подключения к серверу. Это позволяет получить уникальный идентификатор, который может быть использован другими пользователями для подключения.
  • my_id - уникальный идентификатор, присвоенный сервером к устройству.
  • peer_id - идентификатор пользователя к которому вы подключаетесь.
  • initialized - логическое значение, которое определяет, были ли мы уже подключены к серверу или нет.
  • files - массив для хранения расшаренных файлов.
getInitialState: function(){
    return {
        peer: new Peer({key: this.props.opts.peerjs_key}),
        my_id: '',
        peer_id: '',
        initialized: false,
        files: []
    }
}

Обратите внимание, что код инициализации PeerJS, который мы использовали выше, только для целей тестирования, а это значит, что он будет работать только тогда, когда вы обмениваетесь файлами между двумя открытыми браузерами на вашем компьютере, или когда вы получаете общий доступ к файлам в той же сети. Если на самом вы деле хотите создать продуктивную программу, вам придется использовать PeerServer вместо Peer Cloud Service. Так как Peer Cloud Service имеет ограничение на количество одновременных соединений вашего приложения. Вы также должны указать свойство config, в котором настраивается конфигурация ICE сервера. В основном это параметры NATs и брандмауэров или других подключенных устройств. Если вы хотите узнать больше, прочтите статью про WebRTC на HTML5ROCKS. Я добавил ICE конфигурация сервера ниже. Но в случае, если он не будет работать, вы можете выбрать конфигурацию здесь или создать свою собственную.

peer = new Peer({
  host: 'yourwebsite.com', port: 3000, path: '/peerjs',
  debug: 3,
  config: {'iceServers': [
    { url: 'stun:stun1.l.google.com:19302' },
    { url: 'turn:numb.viagenie.ca', credential: 'muazkh', username: 'Этот адрес электронной почты защищён от спам-ботов. У вас должен быть включен JavaScript для просмотра.' }
  ]}
})

Продолжим работу с нашим компонентом. На данный момент у нас есть следующий метод componentWillMount , который выполняется перед тем, как компонент попадет в DOM. Поэтому это идеальное место для выполнения кода, перед попаданием в DOM.

componentWillMount: function() {
    ...
});

В нашем случае мы используем его для прослушивания события open, инициированное объектом peer. Когда это событие срабатывает, это означает, что мы были подключены к серверу со стороны партнера. Уникальный идентификатор, присвоенный сервером партнера, передается в качестве аргумента, поэтому мы используем его для обновления состояния. После того, как у нас появится идентификатор, мы также должны обновить initialized в true. Данный элемент нашего компонента показывает текстовое поле для подключения к партнеру. В React, состояние используется для хранения данных, которые доступны в течение работы всего компонента. Вызов метода setState обновляет указанное свойство, если оно уже существует, в противном случае он просто добавляет новое. Также обратите внимание, что обновление состояния вызывает повторный рендеринг всего компонента.

this.state.peer.on('open', (id) => {
    console.log('My peer ID is: ' + id);
    this.setState({
        my_id: id,
        initialized: true
    });
});

Далее мы прослушиваем событие connection. Оно срабатывает каждый раз при подключении другого человека. Когда это событие срабатывает, мы обновляем состояние, для установки текущего соединения. Это представляет собой связь между текущим пользователем и пользователем на другом конце. Мы используем его для прослушивания события open и data. Обратите внимание, что здесь мы пропустили функцию обратного вызова в качестве второго аргумента метода setState, так как мы используем объект conn для обработки состояния open и data. Поэтому мы хотим, чтобы они были доступны сразу при запуске. Метод setState асинхронный, поэтому мы прослушиваем события сразу же после вызова.

this.state.peer.on('connection', (connection) => {
    console.log('someone connected');
    console.log(connection); 

    this.setState({
        conn: connection
    }, () => {

        this.state.conn.on('open', () => {
            this.setState({
                connected: true
            });
        });

        this.state.conn.on('data', this.onReceiveData);

    });


});

Событие open срабатывает при успешном подключении к партнеру его сервером. После этого мы устанавливаем connected в состояние true. После чего появятся файлы пользователя.

Событие data срабатывает всякий раз, когда пользователь на другой стороне (с этого момента я буду называть его "партнером") посылает файл к текущему пользователю. После этого, мы вызываем метод onReceiveData, который мы определим позже (данная функция отвечает за обработку файлов, которые мы получили от партнера).

Кроме того, необходимо добавить функцию componentWillUnmount(), которая выполняется непосредственно перед уничтожением компонента из DOM. Здесь мы очищаем любые обработчики событий, которые были добавлены при установки компонента. Мы вызовем метод destroy объекта peer, который закроет соединение с сервером и завершает все существующие соединения. Таким образом, у нас не будет существовать каких-либо других слушателей событий, если этот компонент используется где-то еще на текущей странице.

componentWillUnmount: function(){

    this.state.peer.destroy();

},

Метод connect выполняется, когда текущий пользователь пытается подключиться к партнеру. Мы подключаемся к партнеру с помощью вызова метода connect объекта peer и передаем ему peer_id, который мы получаем от состояния. Позже вы увидите, как мы присваиваем значение к peer_id. На данный момент известно, что peer_id является значением текстового поля идентификатора партнера. Значение, возвращаемое функцией connect сохраняется в состоянии. Затем делаем то же что и раньше: прослушиваем события open и data текущего соединения. Обратите внимание, что на этот раз мы обрабатываем события для пользователя, который пытается подключиться к партнеру. Ранее мы определили для пользователя, который в настоящее время уже подключен. Нам нужно использовать оба варианта, так что общий доступ к файлам будет двухсторонним.

connect: function(){

    var peer_id = this.state.peer_id;

    var connection = this.state.peer.connect(peer_id);

    this.setState({
        conn: connection
    }, () => {
        this.state.conn.on('open', () => {
            this.setState({
                connected: true
            });
        });

        this.state.conn.on('data', this.onReceiveData);

    });

},

Метод sendFile выполняется каждый раз при выборе файла, но вместо того чтобы использовать this.files, чтобы получить данные файла, мы используем event.target.files. По умолчанию this в React относится к компоненту, поэтому мы не можем его использовать. Далее мы извлекаем первый файл из массива, а также создаем объект blob в который передаем файлы и объект, содержащий тип файла в качестве аргумента объекта Blob. Наконец мы отправляем его нашему партнеру вместе с именем и типом файла с помощью вызова метода send по текущему соединению со стороны партнера.

sendFile: function(event){
    console.log(event.target.files);
    var file = event.target.files[0];
    var blob = new Blob(event.target.files, {type: file.type});

    this.state.conn.send({
        file: blob,
        filename: file.name,
        filetype: file.type
    });

},

Метод onReceiveData отвечает за обработку данных, полученных от PeerJS. Он принимает все, что передается по методу sendFile. Таким образом, ему передается аргумент data.

onReceiveData: function(data){
    ...
});

Метод send преобразует blob в нужный нам формат, который легко можно передать по сети. Когда мы получим его, он будет уже не в том виде в котором мы его посылали. Поэтому нам снова нужно создать новый объект blob, но на этот раз мы должны поместить его в массив. Затем, мы используем функцию URL.createObjectURL чтобы преобразовать blob в объект URL. Вызываем функцию addFile, для добавления нового файла в общий список.

console.log('Received', data);

var blob = new Blob([data.file], {type: data.filetype});
var url = URL.createObjectURL(blob);

this.addFile({
    'name': data.filename,
    'url': url
});

Функция addFile. Данная функция добавляет новый файл к остальным файлам и обновляет состояние. file_id используется в качестве значения для ключевого атрибута, который требуется для React при создании списков.

addFile: function (file) {

    var file_name = file.name;
    var file_url = file.url;

    var files = this.state.files;
    var file_id = randomstring.generate(5);

    files.push({
        id: file_id,
        url: file_url,
        name: file_name
    });

    this.setState({
        files: files
    });
},

Метод handleTextChange обновляет состояние всякий раз, когда меняется значение текстового поля идентификатора партнера. Данное состояние сохраняется с текущим значением текстового поля, которое равно идентификатору.

handleTextChange: function(event){

    this.setState({
      peer_id: event.target.value
    });

},

Метод render, рендерит пользовательский интерфейс компонента. По умолчанию, он рендерит загрузочный текст перед тем как компонент получит уникальный идентификатор партнера. После этого происходит обновление состояния которое запускает повторный рендеринг компонента, но на этот раз с result внутри состояния this.state.initialized. Также у нас есть условие которое проверяет, подключен ли текущий пользователь к партнеру (this.state.connected). Если подключен, мы вызываем метод renderConnected, если нет, то renderNotConnected.

render: function() {
    var result;

    if(this.state.initialized){
        result = (
            <div>
                <div>
                    <span>{this.props.opts.my_id_label || 'Your PeerJS ID:'} </span>
                    <strong className="mui--divider-left">{this.state.my_id}</strong>
                </div>
                {this.state.connected ? this.renderConnected() : this.renderNotConnected()}
            </div>
        );
    } else {
        result = <div>Loading...</div>;
    }

    return result;
},

Метод renderNotConnected. Данный метод отображает идентификатор партнеру у текущего пользователя, текстовое поле для ввода идентификатора другого пользователя, и кнопка для подключения. При изменении значения в текстовом поле, срабатывает функция onChange. Это вызывает метод handleTextChange который мы определили ранее. Он обновляет текст в текстовом поле, а также значение peer_id. При нажатии на кнопку, происходит вызов функции connect, которая инициирует связь между пользователями.

renderNotConnected: function () {
    return (
        <div>
            <hr />
            <div className="mui-textfield">
                <input type="text" className="mui-textfield" onChange={this.handleTextChange} />
                <label>{this.props.opts.peer_id_label || 'Peer ID'}</label>
            </div>
            <button className="mui-btn mui-btn--accent" onClick={this.connect}>
                {this.props.opts.connect_label || 'connect'}
            </button>
        </div>
    );
},

С другой стороны, функция renderConnected показывает кнопку добавления файла и список всех расшаренных файлов. Каждый раз когда пользователь нажимает кнопку добавления нового файла, открывается окно выбора файла. После того, как пользователь выбрал файл, он вызывает слушателя onChange который в свою очередь вызывает метод sendFile который отправляет файл партнёру. Далее мы вызываем либо метод renderListFiles, либо renderNoFiles в зависимости от наличия файлов.

renderConnected: function () {
    return (
        <div>
            <hr />
            <div>
                <input type="file" name="file" id="file" className="mui--hide" onChange={this.sendFile} />
                <label htmlFor="file" className="mui-btn mui-btn--small mui-btn--primary mui-btn--fab">+</label>
            </div>
            <div>
                <hr />
                {this.state.files.length ? this.renderListFiles() : this.renderNoFiles()}
            </div>
        </div>
    );
},

Метод renderListFiles, как следует из названия, отвечает за составления списка файлов. Он перебирает все файлы с помощью функции map. Для каждой итерации, мы вызываем функцию renderFile которая возвращает ссылку для каждого файла.

renderListFiles: function(){

    return (
        <div id="file_list">
            <table className="mui-table mui-table--bordered">
                <thead>
                  <tr>
                    <th>{this.props.opts.file_list_label || 'Files shared to you: '}</th>
                  </tr>
                </thead>
                <tbody>
                    {this.state.files.map(this.renderFile, this)}
                </tbody>
            </table>
        </div>
    );

},

Функция renderFile возвращает строку таблицы, содержащую ссылку на файл.

renderFile: function (file) {
    return (
        <tr key={file.id}>
            <td>
                <a href={file.url} download={file.name}>{file.name}</a>
            </td>
        </tr>
    );
},

Наконец, у нас есть функция, которая отвечает за рендеринг пользовательского интерфейса в котором отсутствуют файлы.

renderNoFiles: function () {
    return (
        
            {this.props.opts.no_files_label || 'No files shared to you yet'}
        
    );
},

Мы используем команду browserify, чтобы связать код внутри каталога src. Ниже представлена полная команда, которую вы должны выполнить внутри корневой директории проекта:

browserify -t [ babelify --presets [ es2015 react ] ] src/main.js -o js/main.js

Заключение

Вот и все! В этом уроке вы узнали, как работать с PeerJS и React и создали приложение расшаривания файлов. Вы также узнали, как использовать Browserify, Babelify и Babel-React-preset для преобразования JSX код в код JavaScript, который может работать в браузерах.

 

Перевод статьи Build a Peer-to-Peer File Sharing Component in React & PeerJS

Тэги: react.jspeerjsbrowserifybabeles6

Вход

Уважаемый пользователь! Мы обнаружили, что вы используете AdBlock и вынуждены скрыть часть материалов на нашем сайте. Siteacademy существует и развивается за счет доходов от рекламы. Просим внести наш сайт в список исключений или отключить Блокировщик рекламы на нашем сайте.