Pull to refresh

Паттерны JavaScript модулей в Impress для node.js и браузеров

Reading time 4 min
Views 8.3K
У меня сложилось впечатление, что в обществе все же есть предубеждение против использования глобальных переменных в служебных целях. В связи с этим, хочу дать некоторые разъяснения с примерами, которые снимут всякие сомнения и будут полезны всем, кто жаждет модульности и гибкости в JavaScript разработке. Я не могу проследить источники всех идей, приведенных ниже, но я не претендую на их авторство, а лишь на творческое обобщение. Так же я отказываюсь от претензий на один универсальных паттерн определения модулей для всех случаев жизни, надеюсь, всем ясно, что такого не может быть никогда. Все это существенно отличается от подходов RequireJS, CommonJS и того, как модули оформляются в node.js через module.exports, однако, каждый из этих паттернов имеет свое место, если подходить к задаче без фанатизма и предубеждений.

Особенности


  • Поддержка приватных и публичных методов и свойств.
  • Паттерн применим как для серверного JavaScript, так и для клиентского. Как для клиентского, так и для серверного JS модули могут подгружаться динамически, как в Require.js (AMD).
  • Поддерживается склеивание нескольких файлов содержащих код разных модулей в один файл, это позволяет оптимизировать загрузку js для браузеров, минифицировать и склеивать в один файл. Замечу, что Asynchronous module definition (AMD) и CommonJS имеют идеологию «один модуль — один файл». Хотя, средства объединения в один файл есть, но при объединении теряется основной смысл асинхронной подгрузки модулей.
  • Есть возможность разделить код одного модуля на несколько файлов, которые загружаются последовательно и дополняют друг друга. Это полезно, например, для вынесения констант, конфигурации в отдельные файлы.
  • Благодаря разделению на несколько файлов, можно делать модули с расширяемой функциональностью, т.е. делать модули, вынося в них функциональность, нужную только в некоторых случаях и загружать ее по условию.
  • Есть возможность сделать интерфейс и реализации, определяя одинаковые методы в нескольких разных подмодулях. Это нужно пояснить подробнее, на примере: нужно хранить структуру деревовидных данных в браузерных хранилищах (localstorage, WebSQL, IndexedDB), а интерфейс у них должен быть одинаковый и часть логики одинаковая. Создаем treeStorage.js, treeStorage.localstorage.js, treeStorage.websql.js, treeStorage.indexeddb.js
  • Есть возможность скрывать часть загружаемых методов и свойств в метод-обертку (wrapper) и вызывать его опционально или подгружать сразу несколько реализаций с обернутыми методами и переключать между ними, вызывая враперы с разнуми именами по условию.
  • Для Impress важно, чтобы модули попадали в глобальное пространство имен и были доступны из всех обработчиков, без необходимости подключать их в каждом обработчике отдельно.


Код



// File: global.js
// Должен быть загружен первым

if (typeof(window) != 'undefined') window.global = window;

Function.prototype.override = function(fn) {
	var superFunction = this;
	return function() {
		this.inherited = superFunction;
		return fn.apply(this, arguments);
	}
}


// File: moduleName.js
// первое определение модуля moduleName (например, для реализации абстрактного интерфейса)

(function(moduleName) {

	// Помещайте инициализационный код тут
	console.log('Инициализация moduleName');

	moduleName.publicProperty = 'Значение публичного свойства';

	var privateProperty = 'Значение приватного свойства';

	moduleName.publicMethod = function() {
		console.log('Исходный publicMethod для moduleName');
	};

	moduleName.toBeOverridden = function() {
		console.log('Исходный публичный метод toBeOverriden для модуля moduleName (будет переопределен)');
	};

	var privateMethod = function() {
		console.log('Приватный метод privateMethod для moduleName');
	};

} (global.moduleName = global.moduleName || {}));


// File: moduleName.implementationName.js
// повторное определение модуля moduleName может расширять, переопределять и вызывать унаследованную функциональность

(function(moduleName) {

	// Помещайте инициализационный код для повторного оперделения тут
	console.log('Инициализация implementationName');

	// Публичное свойство в повторном определении
	// будет перекрывать публичное свойство первого определения
	//
	moduleName.publicProperty = 'Публичное свойство перекрыто';

	// Приватное свойство в повторном определении
	// не будет перекрывать приватное свойство первого определения
	//
	var privateProperty = 'Приватное свойство не перекрыто';

	moduleName.publicMethod = function() {
		// Публичное свойство в повторном определении
		// будет перекрывать публичное свойство первого определения
		console.log('Публичный метод перекрыт');
	};  	

	var 	privateMethod = function() {
		console.log('Приватный метод не перекрыт');
	};

	// Переопределение метода через "Function.override"
	//
	moduleName.toBeOverridden = moduleName.toBeOverridden.override(function() {
		console.log('Переопределенный метод: moduleName.toBeOverridden');
		this.inherited(); // вызов предыдущей реализации метода
	});

	// Обертка части переопределения, которая будет инициализироваться опционально
	// по какому-либо условию из внешнего кода или из блока инициализации
	// 
	moduleName.wrapperName = function() {

		// Помещайте инициализационный код обертки тут
		console.log('Обертка инициализирует скрутый функционал');

		moduleName.publicMethod = moduleName.publicMethod.override(function() {
			console.log('Метод переопределен: moduleName.publicMethod');
		});

	};

} (global.moduleName = global.moduleName || {}));


// File: test.js

require('./global.js');
require('./moduleName.js');
require('./moduleName.implementationName.js');

moduleName.wrapperName();
moduleName.publicMethod();


Как этот шаблон применяется в Impress



1. Вынесение конфигурации: impress.constants.js вынесена из impress.js
2. Подмодули: db.mongodb.js расширяет db.js
3. Так как все обработчики а Impress в отдельных файлах, то в обработчиках не нужно писать require. А вот сами обработчики определяются при помощи обычного для node.js способа, т.е. через module.exports.
Пример:
module.exports = function(req, res, callback) {
	res.context.data = [];
	db.impress.sessions.find({}).toArray(function(err, nodes) {
		res.context.data = nodes;
		callback();
	});
}


Ссылки



Global.js с комменариями на русском и английском на Github: github.com/tshemsedinov/global.js
Impress на Github: https://github.com/tshemsedinov/impress
Impress в npm: https://npmjs.org/package/impress

PS. Выражаю глубокую благодарность tblasv, который нашел ошибку в приватном методе.
Tags:
Hubs:
+6
Comments 6
Comments Comments 6

Articles