Эта статья была рецензирована Дэном Принсом и Бруно Мота . Спасибо всем рецензентам SitePoint за то, что сделали контент SitePoint как можно лучше!
В этом уроке мы собираемся создать приложение для обмена файлами с PeerJS и React . Я предполагаю, что вы полный новичок, когда речь заходит о React, поэтому я буду предоставлять как можно больше подробностей.
Чтобы вы имели представление о том, что мы собираемся построить, вот несколько скриншотов того, как будет выглядеть приложение. Во-первых, когда компонент готов к использованию:
А вот как это выглядит, когда текущий пользователь уже подключен к одноранговому узлу, и он поделился некоторыми файлами с пользователем:
Исходный код этого руководства доступен на GitHub .
Технический стек
Как упоминалось ранее, приложение для обмена файлами будет использовать PeerJS и React. Библиотека PeerJS позволяет нам подключать два или более устройств через WebRTC , предоставляя удобный для разработчиков API. Если вы не знаете, что такое WebRTC, это в основном протокол, который позволяет в реальном времени общаться в сети. С другой стороны, React является библиотекой представлений на основе компонентов. Если вы знакомы с веб-компонентами, это похоже на то, что дает вам возможность создавать собственные элементы пользовательского интерфейса. Если вы хотите углубиться в это, я рекомендую прочитать ReactJS For Stupid People .
Установка зависимостей
Прежде чем мы начнем создавать приложение, нам сначала нужно установить следующие зависимости с помощью npm :
npm install --save react react-dom browserify babelify babel-preset-react babel-preset-es2015 randomstring peerjs
Вот краткое описание того, что каждый из них делает:
- реагировать — библиотека Реакт.
- response-dom — это позволяет нам рендерить компоненты React в DOM. React не взаимодействует напрямую с DOM, а использует виртуальный DOM. ReactDOM отвечает за отображение дерева компонентов в браузере. Если вы хотите углубиться в это, я рекомендую прочитать ReactJS | Learning Virtual DOM и алгоритм React Diff .
- browserify — позволяет нам использовать в нашем коде операторы require, чтобы требовать зависимости. Это отвечает за объединение всех файлов (связывание), чтобы его можно было использовать в браузере.
- babelify — трансформер Babel для Browserify. Это ответственно за компиляцию связанного кода es6 в es5.
- babel-preset-реакции — пресет Babel для всех плагинов реагирования. Он используется для преобразования 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 и компоненты, используемые приложением. В этом случае у нас есть толькоfilesharer.jsx
который содержит основное ядро приложения. - 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.
Во-первых, нам требуется инфраструктура React, ReactDOM и компонент Filesharer
.
var React = require('react'); var ReactDOM = require('react-dom'); var Filesharer = require('./components/filesharer.jsx');
Затем мы объявляем объект options
. Это используется для указания опций для компонента Filesharer
. В этом случае мы peerjs_key
. Это ключ API, который вы получаете с веб-сайта PeerJS, чтобы вы могли использовать их Peer Cloud Service для настройки одноранговых соединений. В случае нашего приложения оно служит посредником между двумя пирами (устройствами), которые обмениваются файлами.
var options = { peerjs_key: 'your peerjs key' }
Далее мы определим основной компонент. Мы делаем это, вызывая метод createClass
объекта React
. Это принимает объект в качестве аргумента. По умолчанию React ожидает, что функция render
будет определена внутри объекта. Эта функция возвращает пользовательский интерфейс компонента. В этом случае мы просто возвращаем компонент Filesharer
который мы импортировали ранее. Мы также options
объект options
в качестве значения для атрибута opts
. В React эти атрибуты называются реквизитами и становятся доступными для использования внутри компонента, что-то вроде передачи аргументов функции. Позже, внутри компонента Filesharer
, вы можете получить доступ к опциям, указав this.props.opts
а затем любое свойство, к которому вы хотите получить доступ.
var Main = React.createClass({ render: function () { return <Filesharer opts={options} />; } });
Получите ссылку на основной div
из DOM, а затем визуализируйте основной компонент, используя метод render
ReactDOM. Если вы знакомы с jQuery, это в основном похоже на метод append
. Итак, что мы делаем, это добавляем основной компонент в основной div
.
var main = document.getElementById('main'); ReactDOM.render(<Main/>, main);
Компонент Fileharer
Компонент Filesharer
( src/components/filesharer.jsx
), как я упоминал ранее, содержит основное Filesharer
приложения. Основное назначение компонентов — иметь автономный код, который можно использовать где угодно. Другие разработчики могут просто импортировать его (как мы делали внутри основного компонента), передать некоторые параметры, отобразить его, а затем добавить немного CSS.
Разбивая его, мы сначала импортируем инфраструктуру React, библиотеку случайных строк и клиент PeerJS.
var React = require('react'); var randomstring = require('randomstring'); var Peer = require('peerjs');
Мы выставляем компонент на внешний мир:
module.exports = React.createClass({ ... });
Ранее в нашем основном файле JavaScript мы передавали дополнительную prop
для настройки меток, которые будут отображаться в компоненте обмена файлами. Чтобы гарантировать, что правильное имя свойства ( opts
) и тип данных ( React.PropTypes.object
) передаются компоненту, мы используем propTypes
чтобы указать, что мы ожидаем.
propTypes: { opts: React.PropTypes.object },
Внутри объекта, переданного методу createClass
, у нас есть метод getInitialState
который React использует для возврата состояния компонента по умолчанию. Здесь мы возвращаем объект, содержащий следующее:
-
peer
— объект PeerJS, который используется для подключения к серверу. Это позволяет нам получить уникальный идентификатор, который может использоваться другими для подключения к нам. -
my_id
— уникальный идентификатор, назначаемый сервером устройству. -
peer_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. По сути, это позволяет вашему приложению справляться с NAT и межсетевыми экранами или другими устройствами, которые существуют между узлами. Если вы хотите узнать больше, вы можете прочитать эту статью на 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: '[email protected]' } ]} })
Возвращаясь к правильному пути, теперь у нас есть метод componentWillMount
, который выполняется непосредственно перед монтированием компонента в 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
является асинхронным, поэтому, если мы прослушиваем события сразу после его conn
объект conn
может быть недоступен в этом состоянии, поэтому нам нужна функция обратного вызова.
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
выполняется, когда текущий пользователь пытается подключиться к узлу. Мы соединяемся с одноранговым peer_id
, вызывая метод connect
в peer
объекте и передавая ему peer_id
, который мы также получаем из состояния. Позже вы увидите, как мы присваиваем значение 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
. Наконец, мы отправляем его нашему партнеру вместе с именем и типом файла, вызывая метод 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
аргумент data
— это в основном объект, который мы передали методу conn.send
ранее.
onReceiveData: function(data){ ... });
Внутри функции мы создаем BLOB из данных, которые мы получили … Подождите, что? Но мы уже преобразовали файл в большой двоичный объект и отправили его с помощью PeerJS, так почему же нужно снова создавать большой двоичный объект? Я слышу тебя. Ответ в том, что когда мы отправляем BLOB-объект, он фактически не остается в виде BLOB-объекта Если вы знакомы с методом JSON.stringify
для преобразования объектов в строки, он в основном работает так же. Таким образом, большой двоичный объект, который мы передали методу send
преобразуется в формат, который можно легко отправить по сети. Когда мы его получим, это уже не тот блоб, который мы отправили. Вот почему нам нужно снова создать новый блоб. Но на этот раз мы должны поместить его в массив, так как это то, что ожидает объект Blob
. Получив URL.createObjectURL
объект, мы используем функцию URL.createObjectURL
чтобы преобразовать его в объектный 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
используется в качестве значения для key
атрибута, требуемого 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
( 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; },
Также обратите внимание, что выше мы используем реквизиты для настройки метки файлов. Поэтому, если my_id_label
добавлен как свойство в объекте options
ранее, он будет использовать значение, присвоенное этому, вместо значения в правой части символа двойной трубы ( ||
).
Вот метод renderNotConnected
. Все, что он делает, это показывает идентификатор текущего пользователя, текстовое поле для ввода идентификатора другого пользователя и кнопку для подключения к другому пользователю. Когда значение текстового поля изменяется, onChange
функция 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
события onChange
который, в свою очередь, вызывает метод sendFile
который отправляет файл одноранговому 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 ( <span id="no_files_message"> {this.props.opts.no_files_label || 'No files shared to you yet'} </span> ); },
Собираем все вместе
Мы используем команду browserify
для связывания кода внутри каталога src . Вот полная команда, которую вы должны выполнить, находясь в корневом каталоге проекта:
browserify -t [ babelify --presets [ es2015 react ] ] src/main.js -o js/main.js
Разбивая его, сначала мы указываем опцию -t
. Это позволяет нам использовать модуль преобразования. Здесь мы используем Babelify, которая использует предустановку реагирования и предустановку es2015. Итак, что происходит, так это то, что сначала Browserify просматривает указанный нами файл ( src/main.js
), анализирует его и вызывает Babelify, чтобы выполнить свою работу. Babelify использует предустановку es2015 для перевода всего кода ES6 в код ES5. В то время как предустановка React преобразует весь код JSX в обычный JavaScript. После того, как Browserify проверит все файлы, он объединит их, чтобы запустить в браузере.
Очки для рассмотрения
Если вы планируете использовать то, что вы узнали из этого урока, в своих собственных проектах. Обязательно учтите следующее:
- Разбейте компонент
Filesharer
на более мелкие. Вы могли заметить, что внутри компонентаFilesharer
есть куча кода. Обычно это не тот способ, которым вы ведете себя в React. Что вы хотели бы сделать, это разбить проект на более мелкие компоненты, насколько это возможно, а затем импортировать эти меньшие компоненты. Используя компонентFilesharer
в качестве примера, мы могли бы иметь компонентTextInput
для ввода идентификатора партнера, компонент List для перечисления полученных файлов и компонентFileInput
для загрузки файлов. Идея состоит в том, чтобы каждый компонент выполнял только одну роль. - Проверьте, доступны ли в браузере WebRTC и File API .
- Обрабатывать ошибки.
- Используйте Gulp для связывания кода при внесении изменений в файлы и при перезагрузке в реальном времени, чтобы автоматически перезагрузить браузер, как только это будет сделано.
Вывод
Это оно! Из этого руководства вы узнали, как работать с PeerJS и React, чтобы создать приложение для обмена файлами. Вы также узнали, как использовать Browserify, Babelify и предустановку Babel-React для преобразования кода JSX в код JavaScript, который может работать в браузерах.