Статьи

Как создать панель мониторинга WI-FI с помощью Node.js и Ractive.js

Эта статья была рецензирована Марком Таулером . Спасибо всем рецензентам SitePoint за то, что сделали контент SitePoint как можно лучше!

В последние месяцы я опубликовал мини-сериал о панелях управления. В первой статье под названием «Создание аккумулятора, а именно с использованием Node.js: Начало работы и сервер» я показал, как создать сервер Node.js, который проверял состояние аккумулятора на ноутбуке и возвращал несколько полезной информации. Во втором разделе, озаглавленном «Создание батареи, а именно с использованием Node.js: клиент» , я объяснил, как создать веб-приложение для визуализации этой информации в более удобной и удобной для пользователя форме.

В этой статье мы собираемся использовать эту панель и добавить информацию о доступных сетях WI-FI. Список доступных сетей будет отображен с кратким изложением наиболее важных сведений (имя, адрес, защищенный или открытый и т. Д.), А после выбора дополнительные сведения о сети появятся на другой панели.

Взгляните на нашу цель:

приборная доска

Если вы хотите, вы можете рассмотреть это как отправную точку. Я настоятельно рекомендую вам настроить его, так как это лучший способ улучшить свои навыки с помощью технологий, которые мы собираемся использовать.

сервер

На стороне сервера мы собираемся повторно использовать и расширять то, что мы создали для батареи, а именно. В этом уроке мы сосредоточимся на Ubuntu, но код сервера структурирован таким образом, что вам потребуется написать всего пару адаптеров для поддержки компьютеров Mac или Windows.

Bash Command

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

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 Они перечислены и имеют прогрессивный идентификатор «Ячейка 0X» .
  • Последнее, что мы делаем перед сохранением свойств, это вызов 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

Конечная точка

Добавление новой конечной точки на наш сервер тривиально благодаря 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);
      }  

      ...

    }

На данный момент все, что нам нужно сделать, это просто создать обратный вызов, который запустит команду, преобразует ее вывод и, наконец, отправит результат JSONHTTPhttp.createServer

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

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

клиент

Теперь позвольте мне представить вам самую забавную часть этого примера. Мы собираемся широко использовать Ractive.js для веб-клиента. Это легкий, мощный фреймворк, который сочетает двухстороннее связывание (стиль AngularJS) с HTML-шаблонами (такими как усы или руль ).

Упор на шаблоны (даже больше, чем 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

Результат выражения внутри скобок будет экранирован, поэтому он будет простым текстом. Но иногда вам может понадобиться добавить несколько строк HTML в ваши элементы. Есть альтернативный способ сделать это, но если вы действительно думаете, что вам это нужно, вы можете использовать triple-stache data

Использование triple-stache безопасно, потому что сценарии будут экранированы и не будут выполнены, но это медленнее, чем double-stache, поэтому вы должны стараться избегать его как можно больше.
Вторая часть шаблона гораздо интереснее. Мы перебираем список сетей с {{{ }}}{{#wifiNetworks: num}}

Для каждого элемента в списке мы добавляем функцию обработки обратных вызовов (см. Ниже) и показываем сводку его значений.

Обратите внимание, что закрывающие теги не должны совпадать с текстом открывающих тегов:

 num

Открывающий — это тег {{#isNetworkEncrypted(this)}}
...
{{/Encription}}
Таким образом, мы можем использовать осмысленное сообщение для сопряжения двух тегов, просто ради обслуживания.

Выбранные детали Wi-Fi

 if

Панель с деталями сети довольно проста: мы показываем ее, только если мы присвоили значение полю {{#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}}
selectedNetwork
Затем мы показываем имя сети (поле ractive

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

JavaScript

Мы настроим демон опроса, который асинхронно запрашивает сервер через заданные интервалы времени. Каждый звонок Ajax будет предоставлять обновленный список сетей WI-FI. Все, что нам нужно сделать, когда мы получаем ответ JSON от сервера, это подтвердить, что мы получили успешный ответ, и обновить поля, в которых мы храним список сетей внутри объекта ESSID

Настроить

Как мы показали в предыдущей статье , чтобы связать шаблон с некоторыми данными, нам просто нужно создать новый объект ractiveRactive#meterVizTemplate

Затем нам просто нужно добавить все объекты или значения, которые мы хотим использовать в шаблоне, в качестве полей panels Это можно сделать при инициализации (как ractive.dataractive.set()

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

Демоны

Мы будем использовать тот же механизм для демона и для приостановки / перезапуска запросов к серверу, что и для батареи. Для краткости мы не будем повторять это здесь, но если вы хотите углубить эту тему, вы можете посмотреть эту статью или репозиторий GitHub .

Ajax Calls

Единственное, что делает наш новый демон, — это делает вызов 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.

Стоит отметить, что метод jQuery.getJSON()$.ajax()

1. Вы не включаете строку 'callback='JSON
2. Вы можете доверять серверу, которому звоните.

Поскольку мы не используем предоставляемый пользователем контент для URL, можно подумать, что это не должно вызывать беспокойства.

Однако, если бы наш сервер был взломан, у нас не было бы никаких барьеров, чтобы защитить нас от внедренного кода. Если явный заголовок 'dataType'jQuery

Хотя такая возможность не так распространена, мы не можем полностью исключить ее. По этой причине неплохо добавить дополнительный уровень защиты за счет небольшого набора текста.

Обновление панели инструментов

Наиболее подходящим дополнением для этого шага будет то, что мы отвечаем на клики в списке и показываем детали для выбранной сети:

 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, нам пришлось бы:

  • Вызовите метод, который будет принимать идентификатор выбранной сети;
  • Используйте его, чтобы найти сетевой объект для этого идентификатора (вероятно, хранится в словаре);
  • Найдите элемент DOM для «выбранной сетевой панели»;
  • Удалите старое дерево DOM внутри панели и итеративно создайте новый список, отображающий ассоциации ключ-значение, смешивая множество строк HTML внутри нашего кода JavaScript.

Ractive.js позаботится обо всем этом для нас, и он сделает это лучше, чем мы (в среднем), изменяя только наименьшее возможное поддерево DOM.

Во-первых, объект события, отправляемый обработчику при on-clickcontext Другими словами, мы получаем сетевой объект данных «бесплатно».

Как только мы это получим, единственное, что нам нужно сделать, это использовать его для обновления нашего ractive Движок Ractive.js сделает все остальное, обновит DOM и отобразит изменения.

Выводы

Законченный! У нас есть приборная панель «сутенер». Как я уже говорил во введении, это только отправная точка.
Если вы последовали этому примеру, теперь вы сможете легко отображать списки сложных элементов, обрабатывать выбор элементов и безопасно общаться с сервером.

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

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