Статьи

Как написать и протестировать универсальные модули JavaScript (браузер, Node.js)

В Node.js есть очень хорошая модульная система, которая проста для понимания, но различает экспорт модуля и вещи, которые должны быть частными для него. В этом посте объясняется, как можно изменить код модуля Node.js, чтобы он работал как в Node.js, так и в веб-браузерах. Также объясняется, как выполнить юнит-тестирование такого кода.

1. Написание универсального модуля

1.1. Модули Node.js

Модуль Node.js выглядит следующим образом:

    var privateVariable = 123;
    function privateFunction() {
    }
    
    exports.publicVariable = 345;
    exports.publicFunction = function () {
        return privateVariable * exports.publicVariable;
    }

Вы добавляете свои общедоступные данные в экспорт объектов, которым управляет Node.js. Кроме того, Node.js гарантирует, что все остальные данные остаются конфиденциальными для вашего модуля. Внутри модуля вы обращаетесь непосредственно к частным данным, к открытым данным через экспорт. Ниже показано, как используется такой модуль.

1.2. Делаем код Node.js универсальным

Вы можете использовать код модуля Node.js в браузерах, если обернуть его следующим образом:

    "use strict";

    (function(exports) {
        
        // Your Node.js code goes here

    }(typeof exports === "undefined" ? (this.moduleName = {}) : exports));

Давайте рассмотрим код оболочки:

  • Включение строгого режима: первая строка включает строгий режим [1] в ECMAScript 5 и ничего не делает в старых версиях JavaScript.
  • Немедленно вызванное определение функции (IIFE): мы определяем функцию и немедленно ее вызываем [2]. Это служит двум целям:

    • Неэкспортированные данные остаются закрытыми в браузерах, где Node.js не делает этого за нас.
    • Он условно создает переменную export, которая существует в Node.js, но должна быть создана в браузере. Создание новой области с новой переменной экспорта является единственным способом сделать это, вы не можете добавить переменную в существующую область, если ее еще нет. То есть вы не можете делать следующее:
          if (typeof exports !== "undefined") {
              var exports = {}; // (*)
          }
      

      Причина: объявление (но не присвоение!) В (*) переносится в начало текущей функции, что означает, что экспорт всегда будет неопределенным при выполнении проверки. Обратите внимание, что это будет работать без подъёма, но не с масштабированием блока, потому что тогда экспорт, объявленный в (*), будет существовать только внутри блока then.

  • typeof exports === «undefined»: существует ли переменный экспорт?
  • this.moduleName = {}: На глобальном уровне это относится к глобальному объекту. Следовательно, это назначение создает глобальную переменную moduleName (имя модуля в браузере).

Бегущий пример:

    "use strict";

    (function(exports) {

        exports.StringSet = function () {
            this.data = {};
        }

        exports.StringSet.prototype.add = function(elem) {
            if (typeof elem !== "string") {
                throw new TypeError("Argument is not a string: "+elem);
            }
            this.data[elem] = true;
        }

        exports.StringSet.prototype.contains = function(elem) {
            // Comparison ensures boolean result
            return this.data[elem] === true;
        }
        
        exports.StringSet.prototype.copy = function() {
            var result = new exports.StringSet();
            Object.keys(this.data).forEach(function(elem) {
                result.add(elem);
            });
            return result;
        }

    }(typeof exports === "undefined" ? (this.strset = {}) : exports));

2. Использование универсального модуля

Узел:

    var strset = require("./strset");
    var s = new strset.StringSet();

Браузер:

    <script src="strset.js"></script>
    <script>
        var s = new strset.StringSet();
    </script>

3. Юнит-тестирование универсального модуля

3.1. Node.js: юнит-тестирование через модуль assert

Для модульного тестирования мы используем встроенные средства Node.js — модуль
assert .

    var assert = require('assert');
    var strset = require('./strset');

    // contains
    (function() {
        var sset = new strset.StringSet().add("a");
        assert.ok(sset.contains("a"));
        assert.ok(!sset.contains("b"));
    }());

    // copy
    (function() {
        var copy = new strset.StringSet().add("a").copy(); 
        assert.ok(copy.contains("a"));
        assert.ok(!copy.contains("b"));
    }());

    // add - illegal arguments
    (function() {
        assert.throws(function() {
            new strset.StringSet().add(3);
        });
    }());

Эти тесты выполняются путем сохранения их в файле strset-node-test.js и выполнения его через командную строку:

    > node strset-node-test.js

Если один из тестов не пройден, будет исключение. Если все тесты пройдены успешно, ничего не происходит.

3.2. Браузер: модульное тестирование с помощью jQuery’s QUnit

В браузере у нас (слишком) много вариантов: большинство фреймворков имеют собственную поддержку модульного тестирования. Давайте использовать JQuery’s
QUnit .

<!doctype html>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
        <title>strset-test</title>
        <script src="http://code.jquery.com/jquery-latest.js"></script>
        <link rel="stylesheet" href="http://code.jquery.com/qunit/git/qunit.css" type="text/css" media="screen">
        <script type="text/javascript" src="http://code.jquery.com/qunit/git/qunit.js"></script>
        <script type="text/javascript" src="strset.js"></script>
        <script>
            $(document).ready(function(){
                test("contains", function() {
                    var sset = new strset.StringSet().add("a");
                    ok(sset.contains("a"));
                    ok(!sset.contains("b"));
                });
                test("copy", function() {
                    var copy = new strset.StringSet().add("a").copy(); 
                    ok(copy.contains("a"));
                    ok(!copy.contains("b"));
                });
                test("add - illegal arguments", function() {
                    raises(function() {
                        new strset.StringSet().add(3);
                    });
                });
            });
        </script>
    </head>
    <body>
        <h1 id="qunit-header">strset-test</h1>
        <h2 id="qunit-banner"></h2>
        <div id="qunit-testrunner-toolbar"></div>
        <h2 id="qunit-userAgent"></h2>
        <ol id="qunit-tests"></ol>
        <div id="qunit-fixture">test markup, will be hidden</div>
    </body>
</html>

Вы выполняете эти тесты, сохраняя их в файле .html и открывая в браузере.

4. Связанное чтение

Связанное чтение

  1. Строгий режим JavaScript: резюме
  2. Область видимости переменной JavaScript и ее подводные камни
  3. Модули и пространства имен в JavaScript
  4. шаблон для модуля нацеливания на браузер и nodejs . Вдохновение для вышеупомянутого образца. Делает то же самое, но в обратном порядке — используется одна и та же переменная для экспорта общедоступных данных как в Node.js, так и в браузерах. В Node.js эта переменная указывает на объект экспорта, в браузерах — на объект в глобальной переменной.

 

С http://www.2ality.com/2011/08/universal-modules.html