如何使用Node.js和Ractive.js构建WI-FI仪表板

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

在过去的几个月中,我发布了有关仪表板的迷你系列。 在标题为“使用Node.js创建电池:入门和服务器”的第一篇文章中,我展示了如何创建一个Node.js服务器,该服务器检查笔记本电脑上的电池状态并返回一些有用的信息。 在第二篇文章的标题为“使用Node.js创建电池,即客户端”中 ,我解释了如何构建Web应用程序以更好和用户友好的方式可视化这些信息。

在本文中,我们将基于该仪表板并添加有关可用WI-FI网络的信息。 将显示可用网络列表,并列出最相关的详细信息(名称,地址,受保护与开放状态等),并且在选择后,该网络的更多详细信息将出现在另一个面板中。

看看我们的目标:

仪表板

如果需要,可以将其作为起点。 我强烈建议您自定义它,因为这是使用我们将要使用的技术来提高技能的最佳方法。

服务器

在服务器端,我们将重用和扩展为电池即用而创建的内容。 在本教程中,我们将重点讨论Ubuntu,但是服务器代码的结构方式如下:您只需要编写几个适配器即可支持Mac或Windows机器。

重击命令

首先,我们通过为新端点添加命令和回调来扩展原始配置方法。 为了防止与电池小部件的命令冲突,必须进行一些重命名。

function switchConfigForCurrentOS () {
      switch(process.platform) {
        case 'linux':
          return {
            batteryCommand: 'upower -i /org/freedesktop/UPower/devices/battery_BAT0 | grep -E "state|time to empty|to full|percentage"',
            batteryProcessFunction: processBatteryStdoutForLinux,
            wifiCommand: 'iwlist wlan0 scanning | egrep "Cell |Address|Channel|Frequency|Encryption|Quality|Signal level|Last beacon|Mode|Group Cipher|Pairwise Ciphers|Authentication Suites|ESSID"',
            wifiProcessFunction: processWifiStdoutForLinux
          };
        case 'darwin': //MAc OsX
        ...
      }
    }

处理命令输出

我们处理命令输出的方式实际上与我们已经为电池完成的方式相似。 我们逐行浏览输出并对其进行处理,以从读数中提取有意义的参数。 但是在这种情况下,我们得到的是有关项目列表的读数,而不是单个项目! 因此,我们需要确定新项目实际何时开始在输出中,并为每个项目创建一个新对象。 然后,我们将过滤有效行,将读取的属性添加到当前项目中。

function processWifiStdoutForLinux(stdout) {
      var networks = {};
      var net_cell = "";
      var cell = {};

      stdout.split('\n').map(trimParam).forEach(function (line) {
        if (line.length > 0) {
          //check if the line starts a new cell
          if (stringStartsWith(line, NET_CELL_PREFIX)) {
            if (net_cell.length > 0) {
              networks[net_cell] = mapWifiKeysForLinux(cell);
            }
            cell = {};
            line = line.split("-");
            net_cell = line[0].trim();
            line = line[1];
          }
          //Either way, now we are sure we have a non empty line with (at least one) key-value pair
          //       and that cell has been properly initialized
          processWifiLineForLinux(cell, line);
        }

      });
      if (net_cell.length > 0) {
        networks[net_cell] = mapWifiKeysForLinux(cell);
      }
      return networks;
    }

在详细了解processWifiLineForLinux内部会发生什么之前,让我重点介绍以下几点:

  • 由于仅在下一个描述开始时才将单元格添加到哈希中,因此否则我们将错过最终的if语句(以捕获输出中的最后一个网络)。
  • 上面的代码假定两个单元不能共享相同的名称。 这是一个合理的假设,因为网络未按其名称编制索引(该信息由ESSID字段捕获)。 它们被列出并被分配一个渐进标识符“ Cell 0X”
  • 存储属性之前,我们要做的最后一件事是调用mapWifiKeysForLinux ,在这种情况下,它们只是返回未mapWifiKeysForLinux的键。

甚至处理每一行的功能都与我们为电池创建的功能非常相似:由于每一行都包含一个字段名称及其值,因此我们首先检查需要以特殊方式处理的边沿情况,剩下的行我们将它们拆分并适当存储键值对。

function processWifiLineForLinux(cell, line) {
      var key;
      var val;

      line = line.trim();
      if (line.length > 0) {

        switch (true) {
        case stringStartsWith(line, NET_ADDRESS_PREFIX):
          line = line.split(':');
          line.splice(0, 1);
          //INVARIANT: Address in the format Address: DC:0B:1A:47:BA:07
          if (line.length > 0) {
            cell[NET_ADDRESS_PREFIX] = line.join(":");
          }
          break;
        case stringStartsWith(line, NET_QUALITY_PREFIX):
          //INVARIANT: this line must have a similar format: Quality=41/70  Signal level=-69 dBm
          line = line.split(NET_SIGNAL_PREFIX);
          cell[NET_QUALITY_PREFIX] = line[0].split("=")[1].trim();
          if (line.length > 1) {
            cell[NET_SIGNAL_PREFIX] = line[1].split("=")[1].trim();
          }
          break;
        case stringStartsWith(line, NET_EXTRA_PREFIX):
          //INVARIANT: this line must have a similar format: Extra: Last beacon: 1020ms ago
          line = line.split(":");
          //we can ignore the prefix of the string
          if (line.length > 2) {
            cell[line[1].trim()] = line[2].trim();
          }
          break;
        default:
          //INVARIANT: the field must be formatted as "key : value"
          line = line.split(":");
          if (line.length > 1) {
            //Just stores the key-value association, so that coupling with client is reduced to the min:
            //values will be examined only on the client side
            cell[line[0].trim()] = line[1].trim();
          }
        }
      }
      return cell;
    }

这次讨论是向您展示我最近从一位同事“借来”的巧妙技巧的绝好机会。 它将允许我们使用switch语句而不是ifelse s链。

终点

由于Node的HHTP模块和我们在上一教程中创建的帮助程序方法,将新的端点添加到我们的服务器很简单。 我们只需要为要响应的路径定义正则表达式,然后向传入的请求触发的服务器回调添加if语句:

var server = http.createServer(function (request, response) {
      var requestUrl = request.url;
      var filePath = BASE_URL + requestUrl;

      if (requestUrl === '/' || requestUrl === '') {
        response.writeHead(301,
          {
            Location: BASE_URL + 'public/demo.html'
          });
        response.end();
      } else if (RE_BATTERY.test(requestUrl)) {
        getBatteryStatus(response, onBatteryInfo, onError);
      } else if (RE_NETWORKS.test(requestUrl)) {
        getWifiStatus(response, onWifiInfo, onError);
      }  

      ...

    }

此时,我们要做的只是创建一个回调,该回调将运行命令,转换其输出并最终将JSON结果发送到客户端,并包装在由http.createServer提供的HTTP响应中。

function getWifiStatus(response, onSuccess, onError) {

      child_process.exec(CONFIG.wifiCommand, function execWifiCommand(err, stdout, stderr) {
        var wifi;

        if (err) {
          console.log('child_process failed with error code: ' + err.code);
          onError(response, WIFI_ERROR_MESSAGE);
        } else {
          try {
            wifi = CONFIG.wifiProcessFunction(stdout);
            onSuccess(response, JSON.stringify(wifi));
          } catch (e) {
            console.log(e);
            onError(response, WIFI_ERROR_MESSAGE);
          }
        }
      });
    }

对于最后一步,请注意,我们重复使用了为电池端点方便地定义的onSuccess函数(与onError处理程序相同)。

客户

现在,让我向您介绍该示例中最有趣的部分。 我们将在Web客户端上大量使用Ractive.js 。 它是一个轻量级,功能强大的框架,结合了双向绑定(AngularJS风格)和HTML模板(例如mustacheHandlebars )。

模板的压力(甚至比AngularJS还要多,比React还要多),确实是Ractive.js的标志之一,其出色的性能是其出色的性能,因为它始终会计算出可能的最小DOM元素。数据更改时刷新。

我们将在面板中添加两个面板:

  • 一个用于我们周围环境的网络列表(显示每个项目的简要摘要)。
  • 另一个仅在选择网络后出现,并显示该WI-FI连接的详细信息。

模板

让我们开始讨论HTML模板以显示我们的数据,然后我们将了解如何将服务器的数据绑定到它们。

Wi-Fi清单

我们需要的最复杂的模板是显示可用网络列表的模板。 前十二行仅定义了容器面板,并使用Ractive.js的绑定有条件地显示有关服务器错误的图标警告,以及一个用于暂停/恢复服务器轮询的按钮:

<div class='col-md-6 outer-panel'>
      <div class='inner-panel networks-panel'>
        <span class='title'>Available WiFi Networks</span>

        <div class='update-error' style={{!networksUpdateError ? 'visibility:hidden;' : ''}} data-toggle="tooltip" data-placement="top" title='Unable to refresh WiFi data'>
        </div>

        <div class='play-button' on-click='networks-play' style={{!networksPaused ? 'display:none;' : ''}} data-toggle="tooltip" data-placement="top" title='Restarts WiFi updates'>
        </div>
        <div class='pause-button' on-click='networks-pause' style={{networksPaused ? 'display:none;' : ''}} data-toggle="tooltip" data-placement="top" title='Pause WiFi updates'>
        </div>

        <br>  
        <br>
        {{^wifiNetworks}}
            LOADING...
        {{/wifiNetworks}}
        <div class="list-group">
        {{#wifiNetworks: num}}
          <a href="javascript:" class="list-group-item" id={{'network_' + num}} on-click="expandWifi">
            <h5 class="list-group-item-heading">{{ESSID}}</h5>
            <p class="list-group-item-text">{{Address}}</p>
            {{#isNetworkEncrypted(this)}}
              <div class='protected-wifi'>
              </div>
            {{/Encription}}
          </a>
        {{/wifiNetworks}}
        </div>

      </div>
    </div>

双步{{ }} (在小胡子和把手中)是动态注入内容的标记。 Ractive.js允许我们在括号内使用表达式和运行函数,只要这些函数和所使用的数据是全局可用的(例如Math.round )或已添加到绑定到JavaScript的JavaScript对象的data字段中即可模板。

方括号内的表达式结果将被转义,因此将为纯文本。 但是有时您可能需要向元素添加一些HTML行。 有另一种方法可以执行此操作,但是如果您确实认为需要,可以使用三步{{{ }}}

使用三次重载是安全的,因为脚本将被转义并且不执行,但是它比双重重载要慢 ,因此您应尽量避免使用它。
模板的第二部分更加有趣。 我们使用{{#wifiNetworks: num}}遍历网络列表,捕获num变量中每个项目的索引。

对于列表中的每个项目,我们添加一个处理点击的回调(请参见下文),并显示其值的摘要。

请注意,结束标记不必与开始标记文本匹配:

{{#isNetworkEncrypted(this)}}
      ...
    {{/Encription}}

前一个是if标记,其条件是一个函数,这会使它在最后一个条件中奇怪地再次运行它。 因此,为了维护起见,我们可以使用有意义的消息将两个标签配对。

选定的Wi-Fi详细信息
{{#selectedNetwork !== null}}  
      <div class='inner-panel network-details-panel'>
      <span class='title'>Details about {{selectedNetwork.ESSID}}</span>
      <br>
      <br>
      {{#selectedNetwork:key}}
        <span class='key'>{{key}}:</span> <span class='value'>{{this}}</span>
        <br>
      {{/selectedNetwork}}  
      </div>
    {{/selectedNetwork}}

网络详细信息的面板非常简单:仅当我们为ractive对象中的selectedNetwork字段分配了值时,才显示它。 然后,我们显示网络的名称( ESSID字段),并显示我们从服务器收到的所有键值对。

目的是获得尽可能低的耦合,但是您当然可以对其进行修改以突出显示某些信息或以更有意义的方式显示它们。

的JavaScript

我们将设置一个轮询守护程序,该守护程序以给定的时间间隔异步查询服务器。 每个Ajax调用都将提供WI-FI网络的更新列表。 当我们从服务器收到JSON响应时,我们要做的就是确认我们收到了成功的响应,并更新了在ractive对象中存储网络列表的字段。

设定

如上一篇文章中所示,要将模板绑定到一些数据,我们只需要创建一个新的Ractive对象,并将其与模板的ID(下面的#meterVizTemplate )和目标DOM元素(即节点)挂钩这将是DOM树中模板的父级(下面的panels )。

然后,我们只需要将要在模板中使用的所有对象或值添加为ractive.data字段ractive.data 。 这可以在初始化时(如下所述)或更高版本使用ractive.set()

ractive = new Ractive({
      el: 'panels',
      template: '#meterVizTemplate',
      data: {
        wifiNetworks: []
        ...
      }
    });
守护进程

我们将对守护程序使用相同的机制,并暂停/重新开始查询服务器,就像对电池一样。 为了简洁起见,我们在这里不再赘述,但是如果您想加深本主题,可以查看本文GitHub存储库

Ajax通话

我们的新守护程序唯一要做的就是进行Ajax调用,然后在成功的情况下更新我们的数据,或者在发生错误的情况下现场通知网络问题。

function updateWifiNetworksList () {
      $.ajax(WIFI_SERVICE_URL, {
          dataType: 'json',
          jsonp: false
        })
        .then(function (networks) {
          ractive.set('networksUpdateError', false);
          ractive.set('wifiNetworks', networks);
        }).fail(function () {
          ractive.set('networksUpdateError', true);
        });
    }

我们还应该检查我们得到的JSON文件格式是否正确。 我们不必担心脚本注入,因为Ractive.js在将字段值添加到DOM之前已经对其进行了转义。

值得注意的是,只要上面的$.ajax()方法是jQuery.getJSON()方法的快捷方式,它就可以安全使用:

1.您的URL中不要包含'callback='字符串(这将允许执行JSON代码)。
2.您可以信任您正在呼叫的服务器。

由于我们没有将用户提供的内容用于URL,因此人们会认为这不是问题。

但是,如果我们的服务器受到威胁,那么我们将没有任何障碍来保护我们免受注入的代码的侵害。 如果未设置显式的'dataType'标头,则jQuery将尝试从响应中猜测内容,并且来自恶意服务器的响应可能包含JavaScript代码。

尽管这种可能性并不常见,但是我们不能完全排除这种可能性。 出于这个原因,以多一点输入为代价添加额外的保护层并不是一个坏主意。

更新仪表板

此步骤最相关的加载项是我们响应列表上的单击并显示所选网络的详细信息:

expandWifi:   function (event) {
      var selectedNetworkItem = ractive.get('selectedNetworkItem'),
          currentSelection = $(event.node);
      if (selectedNetworkItem && $.isFunction(selectedNetworkItem.toggleClass)) {
        selectedNetworkItem.toggleClass("active");
      }
      currentSelection.toggleClass("active");
      ractive.set('selectedNetworkItem', currentSelection);
      ractive.set('selectedNetwork', event.context);
    },

为此,我们定义了一个临时事件处理程序。 如上所述,当我们单击任何列表条目时,它将被调用,然后与单击相关的事件将携带有关所选网络本身的信息。

现在,如果我们不使用Ractive.js,那么我们只能使用jQuery,则必须:

  • 调用将采用所选网络ID的方法;
  • 使用它来查找该ID的网络对象(可能存储在字典中);
  • 找到“选定的网络面板”的DOM元素;
  • 删除面板内的旧DOM树,并迭代创建一个新的列表以显示键-值关联,并在我们的JavaScript代码中混合很多HTML字符串。

Ractive.js将为我们解决所有这些问题,并且将比我们做得更好(平均而言),仅更改最小的DOM子树。

首先,发送给on-click处理程序的事件对象将具有一个context字段,其中包含绑定到DOM元素的数据。 换句话说,我们“免费”获得网络数据对象。

一旦有了它,我们唯一要做的就是使用它来更新已经绑定到模板的ractive对象。 Ractive.js的引擎将完成剩下的工作,更新DOM并显示更改。

结论

完蛋了! 我们将仪表板“拉上”。 正如我在导言中所说,这只是一个起点。
如果您已遵循,那么现在您应该能够轻松地显示复杂项目列表,处理项目选择并安全地与服务器通信。

您可以将这些技能用于许多其他任务,而不必涉及显示笔记本电脑的统计信息。 从显示用户周围的餐馆列表到枚举家用电器,您可以通过Web界面或移动设备进行全部控制。 选择是您的,没有限制。

如果您想加深本文中涉及的主题,建议您看一下以下优秀资源:

From: https://www.sitepoint.com/how-to-build-a-wi-fi-dashboard/

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值