在React&PeerJS中构建一个点对点文件共享组件

本文由Dan PrinceBruno Mota进行同行评审。 感谢所有SitePoint的同行评审人员使SitePoint内容达到最佳状态!

在本教程中,我们将使用PeerJSReact构建一个文件共享应用程序。 我将假设您是React的完整入门者,因此我将提供尽可能多的细节。

为了让您了解我们将要构建的内容,以下是该应用程序外观的几个屏幕截图。 首先,当组件可以使用时:

文件共享器组件准备连接

当当前用户已经连接到对等方并且对等方已与该用户共享了一些文件时,情况如下所示:

具有共享文件的filesharer组件

本教程的源代码可在GitHub上找到

技术栈

如前所述,文件共享应用程序将使用PeerJS和React。 PeerJS库允许我们通过WebRTC连接两个或更多设备,从而提供了开发人员友好的API。 如果您不知道WebRTC是什么,那么它基本上就是一种允许在Web上进行实时通信的协议。 另一方面,React是一个基于组件的视图库。 如果您熟悉Web组件,则它的相似之处在于它使您能够创建自定义独立UI元素。 如果您想深入了解这一点,我建议阅读ReactJS For Stupid People

安装依赖项

在开始构建应用程序之前,我们首先需要使用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负责将组件树呈现到浏览器中。 如果您想进一步研究它,我建议阅读ReactJS |学习虚拟DOM和React Diff算法
  • browserify –允许我们在代码中使用require语句来要求依赖关系。 这负责将所有文件放在一起(捆绑在一起),以便可以在浏览器中使用它。
  • babelify –用于Browserify的Babel转换器。 这负责将捆绑的es6代码编译为es5。
  • babel-preset-react-所有react插件的Babel预设。 它用于将JSX转换为JavaScript代码。
  • babel-preset-es2015 –将ES6代码转换为ES5的Babel预设。
  • randomstring –生成随机字符串。 我们将使用它来生成文件列表所需的密钥。
  • peerjs – PeerJS库。 负责在同级之间建立连接和共享文件。

构建应用

现在我们准备构建该应用程序。 首先让我们看一下目录结构:

-js
-node_modules
-src
    -main.js
    -components
        -filesharer.jsx
index.html
  • js –将由Browserify捆绑的JavaScript文件存储在其中。
  • 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中的地方。

首先,我们需要作出反应的框架,ReactDOM和Filesharer组件。

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

然后,我们声明一个options对象。 这用于指定Filesharer组件的选项。 在这种情况下,我们要传递peerjs_key 。 这是您从PeerJS网站获得的API密钥,因此您可以使用其Peer Cloud Service来建立对等连接。 对于我们的应用程序,它充当共享文件的两个对等设备之间的中间人。

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

接下来,我们定义主要组件。 我们通过调用React对象的createClass方法来做到这一点。 这接受一个对象作为其参数。 默认情况下,React期望在对象内部定义一个render函数。 该函数的作用是返回组件的UI。 在这种情况下,我们只是返回先前导入的Filesharer组件。 我们还将options对象作为opts属性的值传递。 在React中,这些属性称为道具 ,它们可以在组件内部使用,就像将参数传递给函数一样。 稍后,在Filesharer组件内部,您可以通过说出this.props.opts后跟您要访问的任何属性来访问选项。

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

从DOM获取主div的引用,然后使用ReactDOM的render方法渲染主组件。 如果您熟悉jQuery,则基本上类似于append方法。 所以我们要做的是将主要组件添加到main div

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

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

文件共享器组件

正如我前面提到的, Filesharer组件( src/components/filesharer.jsx )包含应用程序的主要内容。 组件的主要目的是拥有可在任何地方使用的独立代码。 其他开发人员可以导入它(就像我们在主要组件内部所做的一样),传递一些选项,呈现它,然后添加一些CSS。

分解它,我们首先导入React框架,randomstring库和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对象,用于连接到服务器。 这使我们可以获得一个唯一的ID,其他人可以使用它来连接到我们。
  • my_id –服务器分配给设备的唯一ID。
  • peer_id –您要连接的对等方的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和对等设备之间存在的防火墙或其他设备。 如果您想了解更多信息,可以阅读HTML5Rocks上有关WebRTC的文章 。 我已经在下面添加了一些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: 'webrtc@live.com' }
  ]}
})

回到正轨,接下来我们有了componentWillMount方法,该方法将在组件安装到DOM之前立即执行。 因此,这是执行我们想先运行的代码的理想场所。

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

在这种情况下,我们使用它来侦听对peer对象触发的open事件。 触发此事件时,意味着我们已经连接到对等服务器。 对等服务器分配的唯一ID作为参数传递,因此我们使用它来更新状态。 一旦获得ID,我们还必须将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对象来侦听opendata事件。 因此,我们希望一旦完成就可以使用它。 setState方法是异步的,因此,如果在调用它之后立即侦听事件,则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卸载组件之前立即执行。 在这里,我们可以清理在安装组件时添加的所有事件侦听器。 对于此组件,我们可以通过在对peer对象上调用destroy方法来实现。 这将关闭与服务器的连接并终止所有现有连接。 这样,如果此组件在当前页面的其他地方使用,我们将不会触发任何其他事件侦听器。

componentWillUnmount: function(){

    this.state.peer.destroy();

},

当前用户尝试连接到对等方时, connect执行connect方法。 我们通过在对peer对象中调用connect方法并将其传递给peer_id (也从状态获取)来连接到对peer对象。 稍后,您将看到我们如何为peer_id分配值。 现在,知道peer_id是用户在文本字段中输入的用于输入对等ID的值。 然后,将connect函数返回的值存储在状态中。 然后,我们执行与之前相同的操作:监听当前连接上的opendata事件。 请注意,这次是针对尝试连接到对等方的用户。 另一个早先用于连接的用户。 我们需要涵盖两种情况,因此文件共享将是双向的。

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 。 默认情况下,React中的this指向组件本身,因此我们不能使用它。 接下来,我们从数组中提取第一个文件,并通过传递文件和包含文件类型的对象作为Blob对象的参数来创建Blob 。 最后,通过在当前对等方连接上调用send方法,将其与文件名和类型一起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方法发送的所有sendFile方法。 因此,传递给它的data参数基本上是我们之前传递给conn.send方法的对象。

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

在函数内部,我们根据收到的数据创建一个blob。 但是我们已经将文件转换为blob并使用PeerJS发送了,为什么还要再次创建blob? 我听到你了 答案是,当我们发送Blob时,它实际上并不停留为Blob。 如果您熟悉用于将对象转换为字符串的JSON.stringify方法,则其基本原理相同。 因此,我们传递给send方法的Blob被转换为可以通过网络轻松发送的格式。 当我们收到它时,它不再是我们发送的相同blob。 这就是为什么我们需要再次从中创建一个新的Blob。 但是这一次我们必须将其放置在数组中,因为这正是Blob对象所期望的。 有了Blob之后,便可以使用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用作React所需的key属性的值。

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
    });
},

每当用于输入对等ID的文本字段的值更改时, handleTextChange方法都会更新状态。 这是保持状态与对等ID文本字段的当前值保持最新状态的方式。

handleTextChange: function(event){

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

},

render方法呈现组件的UI。 默认情况下,它会呈现加载文本,因为组件首先需要获取唯一的对等ID。 一旦具有对等ID,便会更新状态,然后触发组件重新呈现,但是这次resultthis.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;
},

另外请注意,上面我们使用道具自定义文件的标签。 因此,如果my_id_labelmy_id_label作为属性添加到options对象中,它将使用分配给该对象的值,而不是双竖线( || )符号右侧的值。

这是renderNotConnected方法。 它所做的只是显示当前用户的对等ID,用于输入另一个用户ID的文本字段以及用于连接到另一个用户的按钮。 文本字段的值更改时,将触发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>
    );
}

最后,我们具有负责在没有文件时呈现UI的功能。

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,它使用react预设和es2015预设。 因此,发生的事情是,首先Browserify查看了我们指定的文件( src/main.js ),对其进行了解析,然后调用Babelify进行工作。 Babelify使用es2015预设将所有ES6代码转换为ES5代码。 当React预设将所有JSX代码转换为纯JavaScript时。 Browserify浏览完所有文件后,会将它们放在一起,以便可以在浏览器中运行。

考虑要点

如果您打算在自己的项目中使用从本教程中学到的知识。 请务必考虑以下内容:

  • Filesharer组件分解为较小的组件。 您可能已经注意到, Filesharer组件中有一堆代码。 通常这不是您在React中处理事情的方式。 您要做的是将项目分解为尽可能小的组件,然后导入这些较小的组件。 以Filesharer组件为例,我们可能有一个TextInput组件用于输入对等方的ID,一个List组件用于列出接收到的文件,还有一个FileInput组件用于上载文件。 想法是让每个组件仅扮演一个角色。
  • 检查浏览器中是否提供WebRTCFile API
  • 处理错误。
  • 对文件进行更改时,请使用Gulp捆绑代码,并完成实时重新加载以自动重新加载浏览器。

结论

而已! 在本教程中,您学习了如何使用PeerJS和React来创建文件共享应用程序。 您还学习了如何使用Browserify,Babelify和Babel-React-preset将JSX代码转换为可以在浏览器中运行的JavaScript代码。

From: https://www.sitepoint.com/file-sharing-component-react/

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值