В 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. Связанное чтение
Связанное чтение
- Строгий режим JavaScript: резюме
- Область видимости переменной JavaScript и ее подводные камни
- Модули и пространства имен в JavaScript
- шаблон для модуля нацеливания на браузер и nodejs . Вдохновение для вышеупомянутого образца. Делает то же самое, но в обратном порядке — используется одна и та же переменная для экспорта общедоступных данных как в Node.js, так и в браузерах. В Node.js эта переменная указывает на объект экспорта, в браузерах — на объект в глобальной переменной.