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

    У меня сложилось впечатление, что в обществе все же есть предубеждение против использования глобальных переменных в служебных целях. В связи с этим, хочу дать некоторые разъяснения с примерами, которые снимут всякие сомнения и будут полезны всем, кто жаждет модульности и гибкости в 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, который нашел ошибку в приватном методе.
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 6

      0
      Замечу, что Asynchronous module definition (AMD) и CommonJS не поддерживают склеивание файлов.
      AMD поддерживает. Насчет CommonJS не знаю, но 90%, что подобная тулза есть.
        0
        Согласен, я некорректно выразился, тут нужны пояснения. Сам AMD — это принцип асинхронной модульности, он не предполагает объединения в один файл, но вот для нескольких его основных реализаций, (как require.js и curl.js) есть утилиты, позволяющие объединять файлы. Для CommonJS так же есть uglify.js. Для объединения, конечно же, вокруг каждого файла делается дополнительная обертка, а в нашем случае можно клеить простой конкатенацией файлов. Но при объединении теряется основной смысл асинхронной подгрузки модулей (AMD), т.к. он не столько в защите глобального пространства имен (его можно защитить более простым паттерном, а не целой библиотекой), как в возможности подгружать по требованию, по условию, лениво, динамически и главное — в середине работы страницы, когда уже часть интерфейса загружена и пользователь начал работу, то мы имеем запас времени на подгрузку второй порции скриптов.
        0
        А как у вас зависимости определяются для модуля? (что то я не вижу… ткните пальцем если это описано)
          0
          Это паттерн для системных глобальных модулей. Например, для драйверов баз данных, конфигов, API-оберток, которые должны быть автоматически видны во всех модулях без дополнительных require. В ноде, например, если его подключить через require('moduleName'), то обращаться к нему можно через глобальную переменную moduleName. Для браузера можно его добавлять через тег script на странице или динамически загружать, добавляя тег script в процессе работы, для этого можно взять вот такую реализацию require для браузеров и добавить в global.js

          global.scripts = {};
          
          global.require = function(scripts, callback) {
          	var counter=0,
          		scriptLoaded = function() {
          			counter++;
          			this.script.loaded = true;
          			global.scripts[script.namespace] = this.script;
          			if (counter == scripts.length && callback) callback();
          		},
          		scriptError = function() {
          			counter++;
          			delete this.script;
          			head.removeChild(this);
          			if (counter == scripts.length && callback) callback();
          		}
          	for (var i=0; i<scripts.length; ++i) {
          		var path = scripts[i],
          			file = path.replace(/^.*[\\\/]/, ''),
          			namespace = file.replace(/\.[^/.]+$/, "");
          		if (!global.scripts[namespace]) {
          			var script = {"namespace":namespace,"file":file,"url":path,"element":null,"loaded":false};
          			script.element = document.createElement('script');
          			script.element.script = script;
          			addEvent(script.element, 'load', scriptLoaded);
          			addEvent(script.element, 'error', scriptError);
          			script.element.src = path;
          			head.appendChild(script.element);
          		}
          	}
          }
          
            0
            У этого require, правда есть некоторые зависимости:
            	global.html = document.documentElement || document.getElementsByTagName('html')[0];
            	global.head = document.head || document.getElementsByTagName('head')[0];
            	global.body = null;
            

            я его выделю, протестирую, и добавлю в global.js в статье и в github, чуть позже
              0
              На github уже обновленная версия, но там кода больше, если его в статью пихнуть, то будет не наглядно.

        Only users with full accounts can post comments. Log in, please.