Пользователь
0,0
рейтинг
30 мая 2013 в 15:07

Разработка → Путь JavaScript модуля



На момент написания этой статьи в JavaScript еще не существовало официальной модульной системы и все эмулировали модули как могли.

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

Вот в JavaScript своя атмосфера — в языке нет официальных модулей, более того все файлы лежат удаленно, один поток приложения. Приходится постоянно решать какие-то странные проблемы с загрузкой, хитро паковать модули в один файлы, чтобы ускорить время загрузки. Бывает, что нужно воевать с двойными стандартами, адаптировать модули другого формата.

Дело в том, что раньше не думали, что на JavaScript можно делать огромные проекты, а не просто «пропатчить DOM», поэтому о модулях не думали. Да и вообще не думали о будущем. И тут Внезапно будущее нагрянуло! Все вроде-бы уже есть, а модули в JavaScript, мягко говоря, запаздывают. Поэтому разработчикам приходится крутиться и выдумывать какие-то эмуляторы модульности.

Думаю многие из вас читали прекрасную статью Addy Osmani Writing Modular JavaScript With AMD, CommonJS & ES Harmony, которая стала одной из глав его книги Learning JavaScript Design Patterns в этой статье рассказывается про «современные» JavaScript модули или же читали достаточно старую статью JavaScript Module Pattern: In-Depth 2010 года про «старые» модули.

Я не буду переводить эти статьи и не буду делать из них солянку. В своей статья я хочу рассказать о моем модульном пути. О том как я проделал путь от «старых» модулей к «новым» и что использую сейчас и почему.

Эта статья состоит из 3 частей: Путь модуля, Матчасть по кишкам модулей и Распространенные виды модулей

tl;dr
Я прошел длинный путь от «не модулей» через AMD и browserify к LMD, который удовлетворяет все мои потребности и делает жизнь проще. В будущем делаю ставку на ECMAScript 6 Modules.

Путь модуля


Этап 1: Без модулей

В те времена, когда кода JavaScript было мало я вполне обходился и без модулей. Тогда они были мне не нужны. Введение модульной системы превратили бы мои 50 строк кода в 150. И быстренько пропатчить DOM я мог и без модулей. Я вполне обходился пространствами имен и не использовал сборку, а минификаторы тогда не были развиты.

Модуль
MyNs.MyModule = function () {};
MyNs.MyModule.prototype = {
	// ...
};

Сборка
<script src="myNs.js"/>
<script src="myNs/myModule.js"/>

Прогресс моего приложения шагнул еще на пол шага вперед, когда я стал собирать свои файлы с помощью cat
$ cat js/*.js > build.js


Этап 2: Препроцессинг

Прогресс не стоит на месте и мои 50 строк кода постепенно превратились в 1500, я стал использовать сторонние библиотеки и их плагины. И приложение, которые я писал можно было вполне назвать Rich Internet Application. Деление на модули и их частичная изоляция решала мои проблемы того времени. Для сборки я стал использовать препроцессоры. Модулей было много, у них были зависимости, а разрешать зависимости руками мне не очень хотелось, поэтому препроцессинг тогда был незаменим. Я использовал пространства имен, хотя с ними было много возни:
if (typeof MyNamespace === 'undefined') {
    var MyNamespace = {};
}

и лишней писанины:
new MyNamespace.MyConstructor(MyNamespace.MY_CONST);
// vs
new MyConstructor(MY_CONST);

и минификаторы того времени плохо сжимали такой код:
new a.MyConstructor(a.MY_CONST);
// vs
new a(b);

Мои модули шагнули еще чуть-чуть вперед, когда я стал применять тотальную изоляцию и выкинул пространство имен, заменив его областью видимости. И стал использовать вот такие модули:
include('deps/dep1.js');

var MyModule = (function () {
	var MyModule = function () {};
	MyModule.prototype = {
		// ...
	};

	return MyModule;
})();

И вот такую сборку
(function () {
include('myModule.js');
}());

И тот же препроцессинг
$ includify builds/build.js index.js

Каждый модуль имеет локальную область видимости и вся сборка обернута еще одной IEFE. Это позволяет оградить модули друг от друга и все приложение от глобалов.

Этап 3: AMD


В один прекрасный день, читая Reddit, я наткнулся на статью о AMD и RequireJS.

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

Было
YUI().use('dep1', function (Y) {
    Y.dep1.sayHello();
});

Стало
require(['dep1'], function (dep1) {
    dep1.sayHello();
});

Познакомившись с AMD я понял, что до этого времени я все делал не так. Всего 2 функции require() и define() и все мои проблемы были решены! Модули стали сами загружать свои зависимости, появился вменяемый экспорт и импорт. Модуль разделился на 3 части (импорт, экспорт, тело модуля), которые можно было легко понять. Так же стало легко найти те ресурсы, которые ему нужны и которые он экспортирует. Код стал структурированным и более чистым!

Модуль
define('myModule', ['dep1', 'dep2'], function (dep1, dep2) {
	var MyModule = function () {};
	MyModule.prototype = {
		// ...
	};

	return MyModule;
});

Сборка
$ node r.js index.js bundle.js

Но не все так просто…

Этап 4: Разочарование в AMD

То, что я показал выше — идеальный модуль и идеальная сборка. Такого в реальном проекте не бывает. А бывает так, что зависимостей у модуля очень много. Тогда он превращается в что-то такое:
require(['deps/dep1', 'deps/dep2', 'deps/dep3', 'deps/dep4', 'deps/dep5', 'deps/dep6', 'deps/dep7'],
function (     dep1,        dep2,        dep3,        dep4,        dep5,        dep6,        dep7) {
    return function () {
        return dep1 + dep2;
    };
});

Таким модулем можно пользоваться, но с ним очень много возни. Чтобы побороть эту проблему можно переделать такой модуль на Simplified CommonJS. Еще в этом случае можно совсем не писать define() обертку и создавать честный CommonJS модули, а потом их собирать используя r.js.
define(function (require, module, exports) {
    var dep1 = require('dep1'),
        dep2 = require('dep2'),
        dep3 = require('dep3'),
        dep4 = require('dep4'),
        dep5 = require('dep5'),
        dep6 = require('dep6'),
        dep7 = require('dep7');

    return function () {
        return dep1 + dep2;
    };
});


Формат Simplified CommonJS для RequireJS «не родной», просто разработчиком пришлось его сделать. Если начать писать такие модули, то RequireJS начнет искать зависимости данного модуля регулярками.



И может что-то не найти:
require("myModule//");
require("my module");
require("my" + "Module");
var require = r;
r("myModule");

Этот код валидный, но тут нет ни одного модуля. Конечно пример абстрактный и некоторые имена надуманы, но случаи с динамическим конструированием имени модуля мне часто попадались, например, с шаблонами или какими-либо фабриками.

RequireJS, конечно, имеет для этого решение — прописать каждый такой модуль в конфиге:
({
    "paths": {
    	"myModule": "modules/myModule.js"
    }
})

Еще бывает так, что таких модулей много(шаблоны) и прописывать каждый раз новый модуль в конфиг не хочется и поэтому код начинает обрастать всякой магией вроде динамической генерации конфига. А не использовать «динамические модули» глупо при доступных возможностях.

Я стал писать честные CommonJS модули, использовать сборку через r.js даже в девелопменте. Отказ от AMD так же позволил использовать данные модули с Node.js без какой-либо магии. Я начал понимать, что данный инструмент мне в принципе подходит, но с костылями и дополнительной полировкой.

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

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

Еще бывает так, что нужно сделать несколько сборок. Например, приложение с русской локалью для среды тестинг или приложение оптимизированное под IE с английским языком для корпоративной сети. Или приложение оптимизированное под iPad для Украины с отключенной рекламой. Царила анархия и копипаст…

В философии RequireJs мне не нравилось то, что require() — это универсальный завод по производству любых ресурсов. require() делает абстракцию над плагинами и уже загруженными модулями если плагин не был по какой-то причине подключен, то как-то не совсем явно загружает его, а потом с помощью него загружает ресурс.
require(['async!i18n/data', 'pewpew.js', 'text!templates/index.html'],
fucntion (data, pewpew, template) {

});

В проектах где ресурсы однообразны или ресурсов не очень много — это может быть ок.

Этап 5: Поиск модуля

Я понял, что так жить больше нельзя… но знал, что же мне нужно:

1 Модуль должен быть CommonJS

Достаточно частый случай, когда нужо запустить один и тот же модуль и под Node.js и под JS@DOM. Чаще всего это какие-то модули не связанный с внешней средой (файловая система/DOM) или абстрагированные от нее части: шаблоны (наиболее распространенная часть), функции работы с временем, функции форматирования, локализация, валидаторы…

Когда пишешь AMD и нужно что-то реиспользовать у тебя 2 пути: переписать AMD на CJS или использовать node-require. Чаще выбирают второй вариант потому как ничего менять не нужно. НО. Тогда появляется модульная каша, странная абстракция над уже существующей системой загрузки модулей в Node.js. Мне очень не нравились AMD модули в Node.js.

CJS кроме совместимости с Node.js лишен обертки define() и лишнего отступа, форматирующего тело функции. Его require и export нагляднее и ближе к ES6 Modules чем define()-way. Сравните сами:

ES6 Modules
import "dep1" as dep1;
import "dep2" as dep2;

export var name = function () {
    return dep1 + dep2;
};

CommonJS/Modules
var dep1 = require("dep1"),
	dep2 = require("dep2");

exports.name = function () {
    return dep1 + dep2;
};

AMD
require(['dep1', 'dep2'], function (dep1, dep2) {
	return {
		name: function () {
			return dep1 + dep2;
		}
	};
});

И если так получиться, что мне придется вернуться к AMD, то это будет совсем не больно — мне нужно будет всего лишь прописать одну строчку в конфиге, чтобы r.js оборачивл мои CJS модули.

2 Сборщик модулей

Сегодня собирается все, даже если вы не пишете CoffeeScript, то вы так или иначе проверяете, собираете, сжимаете ваши скрипты.

Для адаптации CJS модуля нужна обертка, которую может делать за меня сборщик. Сборщик так же мог бы проверить меня: все ли модули существуют, не ошибся ли я в имени модуля, все ли я плагины задекларировал.

В результате сборки я хотел бы получить 1 файл, который содержит и мои модули и скрипты, необходимые для их работы.

Делить приложение на «мои скрипты» и «не мои» «во благо кэширования» (подключать код загрузчика отдельно и мой код отдельно) не имело для меня смысла потому как я пишу в основном одностраничные веб-приложения, да и кэш сегодня может вымываться за минуты. Сборка все-в-одном так же позволит избавиться от проблем совместимости с «загрузчиком модулей» при обновлении.

3 Гибкая система конфигурации: зависимости, наследование, миксины

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

Например есть конфиг prod от него наследуется конфиг dev и подменяет какие-то модули. Так же есть конфиги ru и en, которые мы можем подмешать prod+en, dev+ru. Теперь вместо всяких «common» и копипаст (prod-ru, prod-en, dev-ru, dev-en) мы имеем всего 4 «сухих» конфига: prod, dev, ru, en.

4 CLI

Это интерфейс к тому роботу, который делает половину работы за тебя. Если он очень перегруженный или нужно --писать длинные --команды-для-работы, то это начинает напрягать и влечет за собой появление Makefile и трату кучи времени на старт этого самого робота, который должен экономить время.

Любые действия, которые повторяются часто должны максимально упрощаться. Должны использоваться значения по умолчанию, одинаковые имена аргументов у сабкоманд. В общем чтобы разработчик помнил и писал минимум.

Сравните
$ tool make -f path/to/build_name.js -o path/to/build.js

и
$ tool make build_name

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

Этап 6: browserify




browserify это инструмент, позволяющий запускать любые модули Node.js в браузере.

Просто browserify main.js > bundle.js и работает.

Поработав с browserify какое-то время я осознал его истинный use-case: адаптация среды Node.js для работы в браузере. browserify прекрасен для своих целей, но не для тех реалий в который создаются веб-приложения. Когда есть не адаптированные сторонние модули, когда есть динамическая загрузка больших частей приложения. Приходилось много колдовать в консоли, чтобы все работало.

Этап 7: LMD




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

В итоге был разработан инструмент, который занимался сборкой скриптовой части моих проектов.

Вот несколько особенностей, которые легли в основу LMD:

1 Сборка из конфига

Так как наличие конфига неизбежно, то почему бы ни основываться на нем?! Поведение lmd полностью определяется конфигом в нем прописаны и модули и плагины и пути экспорта результирующего файла. Конфиги можно наследовать и миксовать с другими конфигами.

Так выглядит конфиг
{
	"name": "My Config",
	"root": "../js",
	"output": "../build.lmd.js",
	"modules": {
		"main": "index.js"
	},
	"optimize": true,
	"ie": false,
	"promise": true
}

Если у вас сотня модулей — вам не нужно прописывать каждый модуль в конфиг! Достаточно прописать «rewrite rule» для однотипных модулей.
{
	"modules": {
		"main": "index.js",
		"<%= file %>Template": "templates/*.html"
	}
}

И на крайний случай вы можете написать конфиг в виде CJS модуля и сгенерирвать все на лету.

2 Абстрактная ФС: Отсутствие привязки к файловой системе

Привязка к ФС с одной стороны это естественно и HTTP сервер может однозначно отражать файловую систему. Но стоит помнить, что в браузере нет файловой системы и HTTP сервер поставляет ресурсы, а код уже понимает, что вот данный текст по данному URL — это модуль. Ресурсы могут перемещаться, выкладываться на CDN под произвольными именами.

Введение абстрактной файловой системы позволяет делать абстракции над модулями. Например у вас есть модуль locale под которым может скрываться как locale.ru.json так и locale.en.json за счет того, что эти модули имеют одинаковый интерфейс мы можем прозрачно менять один файл другим.

Вы вольны называть ваши модули как вам угодно и подключать не думая о относительных путях. Если у вас много модулей и вы забыли какой файл скрывается под данным модулем, то вам достаточно использовать lmd info:
$ lmd info build_name | grep module_name

info:    module_name                  ✘       plain    ✘          ✘        ✘
info:    module_name                   <- /Users/azproduction/project/lib/module_name.js

3 Не перегруженный require() и плагины

Мне не нравилось, что require это фабрика, поэтому его поведение было немного переписано. Теперь просто require() загружает модули из абстрактной файловой системы и больше ничего. А require.*() будет использовать плагин * и делать же свое дело. Например, require.js() загрузит любой JavaScript файл по аналогии с $.loadScript.

Плагины нужно явно прописывать в конфиг, однако LMD поможет вам не забыть включить плагин, если вы пишете «правильный код».

Например, в этом коде LMD поможет не забывть 3 плагина: css, parallel и promise
require.css(['/pewpew.css', '/ololo.css']).then(function () {

});

А вот в этом коде только плагин js
var js = require.js;

js('http://site.com/file.js').then(function () {

});

Вы можете включать и отключать плагины, используя наследование и миксы конфигов.

4 Адаптация модулей

Бывает так, что в проекте есть какие-то файлы, которые сложно назвать модулями, но их нужно использовать как и другие модули. LMD может легко адаптировать любой файл и сделать из него CJS модуль во время сборки. Кроме этого для использования текстовых файлов(шаблоны) и JSON-файлов не нужно прописывать ни плагины (смотри плагин text для RequireJS) ни адаптеры. В отличии от того же RequireJS LMD превращает данные файлы в честные модули, а не адаптирует их с shim.

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

Будущее?




Да, конечно, это ES6 Modules. Их формат схож со многими форматами модулей из других языков и соответствуют ожиданиям новичков в JavaScript. В них есть все необходимые атрибуты модуля: импорт, экспорт, обертка модуля (на случай если нужно конкатенировать несколько файлов). Они прекрасно транслируются в CJS и AMD. Однако в том виде в котором они есть сейчас в черновике их сложно использовать в реальных проектах.

Import статический. Нужно использовать сборщик модулей, чтобы ускорить старт приложения. Импорт внешнего модуля будет блокирующим:
<script>
import {get, Deferred} from "http://yandex.st/jquery/3.0/jquery.min.js";

get('/').then(console.log.bind(console));
</script>

Это практически аналогично
<script src="http://yandex.st/jquery/3.0/jquery.min.js">
<script>
var get = $.get,
    Deferred = $.Deferred;

get('/').then(console.log.bind(console));
</script>

В свою очередь, блокировку можно снять, используя <script async/>

Динамическая загрузка модулей есть, но она сейчас не совершенная:
Loader.load('http://json.org/modules/json2.js', function(JSON) {
	alert(JSON.stringify([0, {a: true}]));
});

Надеюсь, что загрузчик модулей сможет грузить сборку из нескольких модулей. Тогда этого будет достаточно.

Стандарт сейчас активно обсуждается и то, что я вам сегодня показал, возможно завтра будет выглядеть не так (но маловероятно). Сегодня модули и синтаксис импорта/экспорта похож на тот, который вы привыкли видеть в других языках. Это хорошо так как JavaScript используют многие разработчики и им больно видеть дикие хаки вроде AMD. Сегодня одно из направлений развития ECMAScript направлено на превращение языка в своеобразный асемблер для трансляции из других языков. И модули неотъемлемая часть этого направления.

Выводы

Сегодня, можно сказать, JavaScript не имеет устоявшейся модульной системы есть только эмуляторы модульности, однако у вас есть возможность использовать синтаксис ES6 Modules и компилировать ваши модули в CJS и AMD. В JavaScript своя атмосфера, много ограничений(сетевые тормоза, трафик, лаги), которые не позволяют использовать привычные многим импорты. Проблема сборки и асинхронной загрузки так или иначе решена в популярных эмуляторах модульности, но как ее будут решать разработчики ES6 — вопрос.

Матчасть


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

Я классифицировал существующие JavaScript «модули» и их инфраструктуру по особенностям. Классификация учитывает многие особенности. Давайте рассмотрим классификацию модулей, а потом уже отдельные модульные системы.

  • Разрешение зависимостей
    • Ручное управление
    • Зависимости прописываются в конфиге
    • Зависимости прописываются в самом модуле
    • Зависимости прописываются в модуле и в конфиге

  • Доступ к зависимостям
    • Произвольный
    • Динамический
    • Декларативный

  • Экспортирование из модуля
    • Хаотичный экспорт
    • Не управляемый экспорт со строгим именем
    • «Самоэкспорт» со строгим именем
    • Управляемый экспорт с произвольным именем
    • Честный import/export

  • Сбока модулей
    • Без сборки
    • Конкатенация файлов по маске
    • Препроцессинг
    • Статический анализ зависимостей
    • Сборка из конфига

  • Инициализация и интерпретация модуля
    • Инициализируется и интерпретируется при старте
    • Инициализируется при старте, интерпретируется по требованию
    • Инициализируется и интерпретируется по требованию

  • Загрузка внешних зависимостей
    • Загрузчик неуправляемого модуля
    • Загрузчик «управляемого» модуля

  • Изоляция модулей
    • Модули не изолированы
    • Модули изолированы
    • Модули тотально изолированы


Разрешение зависимостей

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

Разрешение зависимостей. Ручное управление

Управление зависимостями на плечах разработчика. Разработчик аналитически понимает какие зависимости нужно подключить.
<script src="deps/dep1.js"/>
<script src="deps/dep2.js"/>
<script src="moduleName.js"/>

И соответственно в main.js
var moduleName = function () {
    return dep1 + dep2;
};

Никаких сторонних библиотек не нужно использовать
Когда модулей не много и они все свои — это ок
Когда модулей много такой код невозможно поддерживать
Несколько файлов = несколько запросов на сервер

Подходит для «быстро накодить».

Разрешение зависимостей. Зависимости прописываются в конфиге

Зависимости прописываются во внешнем конфиге и могут наследоваться. Используя данный конфиг какой-то сборочный инструмент загружает/подключает зависимости данного модуля. Конфиг может быть написан как для конкретного модуля так и для всего проекта.

Такой конфиг используется в LMD
{
    "modules": {
        "main": "moduleName.js"
        "<%= file %>": "deps/*.js"
    }
}

И соответственно в main.js
var dep1 = require('dep1'),
    dep2 = require('dep2');

module.exports function () {
    return dep1 + dep2;
};

Модули не завязываются на файловую систему (можно дать любое имя любому файлу)
Без изменения имени модуля можно изменить его содержимое
Нужно писать такой конфиг
Нужен дополнительный инструмент/библиотека

Разрешение зависимостей. Зависимости прописываются в самом модуле

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

Такой способ использует AMD (RequireJS)
require(['deps/dep1', 'deps/dep2'], function (dep1, dep2) {
    return function () {
        return dep1 + dep2;
    };
});

Если зависимостей у одного модуля очень много, то такой синтаксис как правило деградируют до CommonJS define либо используют всякие извращения.

Извращения
require(['deps/dep1', 'deps/dep2', 'deps/dep3', 'deps/dep4', 'deps/dep5', 'deps/dep6', 'deps/dep7'],
function (    dep1,        dep2,        dep3,        dep4,        dep5,        dep6,        dep7) {
    return function () {
        return dep1 + dep2;
    };
});

Деградация до CommonJS define
define(function (require, module, exports) {
    var dep1 = require('dep1'),
        dep2 = require('dep2'),
        dep3 = require('dep3'),
        dep4 = require('dep4'),
        dep5 = require('dep5'),
        dep6 = require('dep6'),
        dep7 = require('dep7');

    return function () {
        return dep1 + dep2;
    };
});

При использовании такой деградации RequireJS ищет зависимости регулярками. Это на 95% надежный способ. Честный же способ (AST или хитрый процессинг) потребляет слишком много ресурсов (объем кода и время процессинга), но так же не покрывает всех потребностей.

Бывают случаи когда необходимо так же написать конфиг, чтобы, например, адаптировать какой-то старый модуль, который не умеет define или если какой-то «честный модуль» инициализируется динамически — require('templates/' + type) и его не может найти регулярка. Динамическая инициализация это редкая штука и в основном используется для динамической загрузки шаблонов, но не исключена.

Практически все зависимости описываются в самом файле
Конфиги асинхронно загружаются
Не нужно писать конфиг
Но иногда приходится его все-таки писать конфиг
Нужен дополнительный инструмент/библиотека

Разрешение зависимостей. Зависимости прописываются в модуле и в конфиге

Зависимости прописываются с самом файле и в специальном конфиге.

Конфиг используется любым менеджером пакетов для устранения зависимостей. Например npm и package.json
{
    "dependencies": {
        "express": "3.x",
        "colors": "*"
    }
}

И соответственно main.js
// Внешний модуль
var express = require('express');

// Локальный модуль
var dep1 = require('./deps/dep1'),
    dep2 = require('./deps/dep2');

module.exports function () {
    return dep1 + dep2;
};

Разработчик определяет список зависимостей и их версии. Менеджер пакетов загружает модули и их зависимости. Тут, в принципе, без вариантов тк менеджер ничего не знает о модуле. package.json для менеджера единственный интерфейс взаимодействия. В свою очередь каждый модуль может загружать свои части напрямую из файловой системы require('pewpew.js')

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

Все зависимости описываются в самом файле
Возможно управление версиями внешних зависимостей
Такой модуль можно без проблем использовать как на сервере так и на клиенте
Нужен дополнительный инструмент/библиотека для сборки, например browserify

Доступ к зависимостям

Определяет каким образом модуль использует зависимости внутри себя, как получает доступ к необходимому модулю.

Доступ к зависимостям. Произвольный

Все модули лежат открыто в глобальной области видимости или в неймспэйсе. Каждый модуль может без каких-либо ограничений в любом месте получить доступ к любой части приложения любым способом.
var dep1 = 1;
var dep2 = 2;

alert(dep1 + dep2);

Если модулей не много и они не большие, то это ок
Если модулей много, то такой код невозможно поддерживать
Нельзя не глаз определить зависимости модуля (нужно искать имена глобальных переменных или неймспейс)

Доступ к зависимостям. Динамический

Доступ к модулю можно получить только через «загрузчик» — require() или объявив зависимости модуля через define()

Такой способ используется в большинстве популярных библиотек, когда в «замыкание модуля» пробрасывается функция require через которую модуль и может получать доступ к другим модулям. Так же эта функция может быть доступна глобально.
var dep1 = require('./deps/dep1'),
    dep2 = require('./deps/dep2');

alert(dep1 + dep2);

Соответственно способ с define()
require(['./deps/dep1', './deps/dep2'], function (dep1, dep2) {
    alert(dep1 + dep2);
});

Легко понять/найти зависимости
Доступ к зависимостям модерируется, можно лениво инициализировать модуль, вычислять runtime-зависимости и прочее
Можно статически определить почти весь граф зависимостей
Код немного Verbose, но это хорошая плата за поддерживаемость
Нужна дополнительная библиотека

Доступ к зависимостям. Декларативный

Модули декларируется при написании кода и не загружаются динамически. Статический анализатор кода может однозначно понять какой набор модулей необходим для работы приложения. Так работают практически все конструкции import.
import * from "dep1";
import * from "dep2";

Так же под такой способ доступа к зависимостям можно отнести и статический AMD define()
define('module', ['./deps/dep1', './deps/dep2'], function (dep1, dep2) {

});

Статический импорт позволяет сборщикам собирать зависимости, а трансляторам ES6 Modules переделывать код в ES3-совместимый.

Возможен статический анализ (полный или частичный)
Возможна трансляция ES6 Modules
В чистом виде редко применимо

Экспортирование из модуля

Чаще всего модули предоставляют какие-то ресурсы, которыми могут пользоваться другие модули. Это могут быть данные, утилиты (формат дат, чисел, i18n и пр). Экспортирование из модуля определяет каким образом модуль говорит «я предоставляю такие-то ресурсы».

Экспортирование. Хаотичный экспорт

Модуль экспортирует что угодно, куда угодно, когда угодно
var a = 10,
    b = '';

for (var i = 0; i < a; i++) {
    b += i;
}

var dep1 = b;

Засорение глобальной области видимости
Ад и кошмар, при любом раскладе такое не поддерживается в принципе

Экспортирование. Не управляемый экспорт со строгим именем

Если немного модифицировать предыдущий способ, добавив IIFE, то мы получим данный способ. Модуль заранее знает где он будет лежать и как будет называться.
var dep1 = (function () {
    var a = 10,
        b = '';

    for (var i = 0; i < a; i++) {
        b += i;
    }

    return b;
})();

Или же немного другой вариант
(function () {
    var a = 10,
        b = '';

    for (var i = 0; i < a; i++) {
        b += i;
    }

    exports.dep1 = b;
})(exports);

Или именованный AMD
define('dep1', [], function () {
    var a = 10,
        b = '';

    for (var i = 0; i < a; i++) {
        b += i;
    }

    return b;
});

Это просто
Не нужны особые инструменты для сборки и использования таких модулей (кроме AMD)
Экспортируется только нужное
Модуль знает куда он экспортируется и какое имя у него будет

Экспортирование. «Самоэкспорт» со строгим именем

В основе этого способа лежит специальная функция «регистрации модуля» ready(), которую должен вызвать модуль, когда он готов. Она принимает 2 аргумента — имя модуля и ресурсы, которые он предоставляет.
(function () {
    var a = 10,
        b = '';

    for (var i = 0; i < a; i++) {
        b += i;
    }

    ready('dep1', b);
})();

Для загрузки зависимостей такого модуля используется функция load(), похожая на require()
load('dep1', 'dep2', function (dep1, dep2) {
    ready('dep3', function () {
        return dep1 + dep2;
    });
});

load('dep3', do.stuff);

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

Экспортирование. Управляемый экспорт с произвольным именем

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

Это CommonJS модуль
var a = 10,
    b = '';

for (var i = 0; i < a; i++) {
    b += i;
}

module.exports = b;

или анонимный AMD
define([], function () {
    var a = 10,
        b = '';

    for (var i = 0; i < a; i++) {
        b += i;
    }

    return b;
});

Мы можем использовать любое имя во время экспорта модуля.
var dep1 = require('deps/dep1');

Модуль не знает ни где он лежит ни как он будет называться при использовании
При переименовании модуля нужно переименовать только файл
Нужна библиотека для сборки и использования

Экспортирование. Честный import/export

Такой способ декларации модулей использует каждый второй язык программирования. Достаточно давно появилась спецификация ECMAScript 6 Modules, поэтому рано или поздно такой синтаксис придет и в JavaScript.

Декларируем модуль.
module "deps" {
    var a = 10,
        b = '';

    for (var i = 0; i < a; i++) {
        b += i;
    }

    export var dep1 = b;
    export var dep2 = b + 1;
}

Так же можно декларировать модуль без обвязки module {}.

Можно использовать имена по умолчанию и писать меньше
import * from "deps";

console.log(dep1);

Можно избежать конфликты имен, используя своеобразное «пространство имен»
import "crypto" as ns;

console.log(ns.dep1);

Можно экспортировать часть модуля
import {dep1} from "deps";

console.log(dep1);

Знакомые импорты из многих языков — привычно и наглядно
Это ECMAScript 6
Нужно транслировать ES6 модуль в ES3-совместимый код, например использовать модули из TypeScript

Сбока модулей

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

Без сборки

Все в HTML
<script src="deps/dep1.js"/>
<script src="deps/dep2.js"/>
<script src="moduleName.js"/>

Это просто
При увеличение количества модулей приложение перестает быть поддерживаемым и начинает тормозить из-за увеличения числа запросов
Смешение сущностей HTML и декларация модуля
Новая сборка — новый .html

Сборка модулей. Конкатенация файлов по маске

Собираем
$ cat **/*.js > build.js

Используем
<script src="build.js"/>

Это достаточно просто
Загружается только 1 файл
Для каждого типа сборки нужно создавать новые скрипты
Файлы могут собираться в произвольном порядке в разных OS и FS

Сборка модулей. Препроцессинг

Способ заключается в поиске специальных «меток» в файлах — include('path/name.js') или // include path/name.js и подобных
include('deps/dep1.js');
include('deps/dep2.js');

var moduleName = function () {
    return dep1 + dep2;
};

Все это разворачивается специальной утилитой в такой формат.
/* start of deps/dep1.js */
var dep1 = 1;
/* end of deps/dep1.js */

/* start of deps/dep2.js */
var dep2 = 2;
/* end of deps/dep2.js */

var moduleName = function () {
    return dep1 + dep2;
};

Соответственно у вложенных модулей могут быть еще зависимости и они также будут загружены рекурсивно.

Собирается только 1 файл
Можно сделать какое-никакое «наследование конфигов»
Для каждого типа сборки нужно создавать новый файл с перечислением всех include
Если препроцессор глупый то возможен дубликат кода и другие артефакты
При неправильном использовании возможна инъекция кода в модуль

Инъекция кода в модуль ведет к нарушению целостности модуля, влечет проблемы с "use strict", конфликтом имен и прочим неприятностям.

Вот типичный пример
(function () {
    "use strict";
    var i = 3;
    include('dep1'); // Не корректное подключение зависимости
    return dep1 + i;
})();

И его зависимость
var i = 4,
    dep = 01234;

Думаю, вы поняли последствия ;-)

Сборка модулей. Статический анализ зависимостей

Статический анализ контента модуля с поиском зависимостей. Такой способ использует r.js (сборщик RequireJS модулей) и browserify (адаптор CommonJS модулей и Node.js инфраструктуры под браузер). Они используют AST парсер, ищут вызовы define/require и таким образом находят зависимости и в отличии от include помещают эти зависимости вне модуля.

Например вот такой модуль
require(['dep1', 'dep2'], function (dep1, dep2) {
    return function () {
        return dep1 + dep2;
    };
});

если его прогнать через r.js будет переделан вот в такой вид
define('dep1', [], function () {
    return 1;
});

define('dep2', [], function () {
    return 2;
});

require(['dep1', 'dep2'], function (dep1, dep2) {
    return function () {
        return dep1 + dep2;
    };
});

browserify ведет себя подобным образом, но собирает в формат посложнее

Собирается только 1 файл
Все зависимости прописаны в самом модуле
Для каждого типа сборки нужно создавать новый файл или делать магию с симлинками
Препроцессор может не найти некоторые зависимости (Динамически конструируемые имена модулей)
Для исправления предыдущего пункта нужно писать конфиг, чтобы включить эти модули

Сбока модулей. Сборка из конфига

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

Такой способ использует LMD.
{
	"root": "../js",
    "modules": {
        "main": "main.js",
        "dep1": "deps/dep1.js",
        "dep2": "deps/dep2.js"
    }
}

Вариант, конечно, интересный, но зачем 2 раза писать одно и то же и в модуле и в конфиге?!

Это легко объясняется. LMD не знает о файловой системе, и конфиг фактически является абстрактной файловой системой. Это позволяет не задумываться об относительных путях и во время переноса/переименования модуля не бегать и не менять пути по всему проекту. Используя абстрактную ФС становится возможным получить дешевую Dependency Injection для локализации, подмены конфигов среды и прочих оптимизаций. Еще бывает так, что модули подключаются динамически и статический анализатор не может их найти физически, поэтому приходится вносить запись о модуле в конфиг. Понятно, что прописывать каждый раз модуль в конфиг это шаг назад, поэтому в LMD имеется возможность подключать целые директории с сабдиректориями, используя glob-инг и своеобразный rewrite rule.

Вот этот конфиг идентичен предыдущему
{
    "root": "../js",
    "modules": {
        "<%= file %>": "**/*.js"
    }
}

Вы определяете какие файлы нужны, а потом пишете шаблон и тем самым говорите как их нужно представить этот модуль LMD. Для определения имени LMD использует шаблонизатор из lodash, поэтому можно писать и более хитрые конструкции:
{
	"root": "../js",
    "modules": {
        "<%= file %><%= dir[0][0].toUpperCase() %><%= dir[0].slice(1, -1) %>": "{controllers,models,views}/*.js"
    }
}

Итоги этого способа такие:

Наглядно — все дерево проекта можно описать в одном файле
Надежно — исключены ошибки анализатора
Абстрактная файловая система
Нужно писать конфиг
Нужен сборщик

Инициализация и интерпретация модуля

Это достаточно важный момент, который позволяет сократить лаг при старте приложения, когда выполняется куча кода. Когда код попадает на страницу он инициализируется (написали функцию — она зарегистрировалась под каким-то именем) при инициализации код парсится, валидируется и перегоняется в AST для дальнейшей интерпретации и возможной JIT компиляции. Когда какая-либо функция вызывается ее код интерпретируется.

Функция не инициализирована и не интерпретирована. Инициализируется только JavaScript строка.
'function a() {return Math.PI;}';

Функция инициализирована.
function a() {
	return Math.PI;
}

Функция инициализирована и интерпретирована.
function a() {
	return Math.PI;
}

a();

Каждая декларация функции и ее вызов занимает какое-то время, особенно на мобильных, поэтому хорошо бы уменьшить это время.

Инициализируется и интерпретируется при старте

Модуль поставляется как есть и выполняется при старте программы. Даже если он нам не нужен прям сейчас. Как видите в модуле есть какие-то циклы, которые могут замедлить работу.
var dep1 = (function () {
    var a = 10,
        b = '';

    for (var i = 0; i < a; i++) {
        b += i;
    }

    return b;
})();

Не нужно использовать дополнительные средства
Если код не большой, то время инициализации не существенно
При увеличении объема кода начинает проявляться Startup Latency

Инициализируется при старте, интерпретируется по требованию

Достаточно популярный сейчас способ, который используют и AMD и модули в Node.js
define('dep1', [], function () {
    var a = 10,
        b = '';

    for (var i = 0; i < a; i++) {
        b += i;
    }

    return b;
});

Этот модуль будет инициализирован при старте. Но его тело будет выполнено по требованию, а результат return b; закэширован и при следующем вызове интерпретация проходить не будет.

Не нужно особо сильно менять вид модуля
Startup Latency существенно сокращается при большом объема кода
Нужна дополнительная библиотека

Инициализируется и интерпретируется по требованию

Небольшая модификация предыдущего метода, позволяющая отложить инициализацию кода. Используется, в основном, для оптимизации загрузки кода на мобильных устройствах. Такую оптимизацию можно сделать для RequireJS и для LMD.

Кусок сборки LMD (не конфиг)
{
	'dep1': '(function(){var a=10,b="";for(var i=0;i<a;i++){b+=i;}return b;})'
}

Когда какой-то модуль потребует ресурсы модуля dep1, то LMD интерпретирует и инициализирует этот код.

Примерно вот так так:
var resources = new Function('return ' + modules['dep1'])()(require, module, exports);

Время инициализиции кода через new Function может быть немного медленнее, чем через честную инициализацию, но если такую оптимизацию применять с умом, то мы можем выиграть время при старте. Порожденный код через new Function, в отличии от eval(), может быть оптимизирован JIT-компилятором.

Эта операция прозрачна для разработчика
Нужна дополнительная библиотека
Нужно правильно применять

Загрузка внешних зависимостей

Как я уже сказал, в JavaScript@DOM своя атмосфера, поэтому привычные способы загрузки модулей тут не работают. Модули лежат удаленно и их синхронная загрузка не реальна. Если в десктопном приложении мы можем синхронно прилинковать библиотеку «со скоростью света», то в JavaScript@DOM такое вряд-ли реально из-за блокировки EventLoop.

Загружать все сразу мы так же не можем, поэтому приходится что-то придумывать и страдать :)

Загрузчик неуправляемого модуля

Под неупровляемым модулем я понимаю просто любой код, который не требует какой-то дополнительной обработки. Таким загрузчиком, например, является jQuery.getScript(file)

Делает он примерно следующее:
var script = document.createElement('script');
script.src = file;
script.onload = done;
document.head.appendChild(script);

Если загружать несколько модулей одновременно, то выполнятся они в порядке загрузки. Бывает так, что нужно выполнить модули в порядке их перечисления. Библиотека LAB.js, например, использует XHR для одновременной загрузки кода скриптов, а потом выполняет этот код последовательно. XHR, в свою очередь, вносит свои ограничения.
$LAB
.script("framework.js").wait()
.script("plugin.framework.js");

Остальные загрузчики, вроде YepNope и script.js делаю примертно то же самое.

Дешевое решение
Могут быть ограничения со стороны XHR или дополнительной писанины

Загрузчик «управляемого» модуля

Любая взрослая модульная система поставляется с собственным загрузчиком и может загружать любые модули и их зависимости. Например, это делает функция require() и define() из RequireJS.

Функция require() из RequireJS загрузит необходимые зависимости и зависимости зависимостей и выполнит код этих модулей в указанном порядке.
require(['dep1', 'dep2'], function (dep1, dep2) {
    console.log(dep1 + dep2);
});

В LMD, например, есть такое понятие как бандл — несколько модулей, собранных в один файл. При загрузке этого бандла все его модули становятся доступны любому модулю.
_e4fg43a({
	'dep1': function () {
		return 1;
	},

	'dep2': 2,

	'string': 'Hello, <%= name %>!'
});

require.bundle('name').then(function () {
	// do stuff
});

Управление как загрузкой модулей так и их инициализацией
Практически прозрачная для разработчика загрузка
Требует дополнительных инструментов и конфигурации

Изоляция модулей

Защищенность модулей или их изоляция нужна, скорее для разработчиков, чем для тех, кто ломает их труды. Прямой и хаотичный доступ к свойствам модулей может при неправильном использовании «испортить код». С другой стороны если в глобальной области видимости нет следов вашего JavaScript, то исследователю вашего кода будет сложнее понять и «сломать» что-то, но тут больше вопрос времени.

Модули не изолированы

Модуль или какие-то его части доступны глобально, любой разработчик из любого места может взять и использовать.
var dep1 = (function () {
    var a = 10,
        b = '';

    for (var i = 0; i < a; i++) {
        b += i;
    }

    return b;
})();

Опять же это просто
Не нужны инструменты
Нужно задумывать о пространствах имен
Нет разделения труда у модуля. Он и делает свое дело он же и управляет получением зависимостей

Модули изолированы

Модуль не доступен глобально, но его можно получить зная имя — require('pewpew'). Скрытие, как я уже сказал, это не цель модульной системы, а следствие. В AMD есть 2 функции с помощью которых можно так или иначе получить доступ к модулю — это require() и define(). Достаточно только знать кодовое имя модуля, чтобы получить его ресурсы.
define('dep3', ['dep1', 'dep2'], function (dep1, dep2) {
    return function () {
    	return dep1 + dep2;
    };
});

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

Модули тотально изолированы

Цель таких модулей сделать так, чтобы нельзя было достучаться до модуля извне. Думаю, многие уже видели такие «модули», вот, например:
$(function () {
	var dep1 = (function () {
		var a = 10,
			b = '';

		for (var i = 0; i < a; i++) {
			b += i;
		}

		return b;
	})();

	$('button').click(function () {
		console.log(dep1);
	});
});

Фактически это тотально изолированный модуль, до его внутренностей нельзя достучаться извне. Но это пример одного модуля. Если каждый такой модуль оборачивать в «замыкание», то они не смогут взаимодействовать. Для изоляции нескольких модулей их можно поместить в общую область видимости или прокидывать в их области видимости какие-то общие ресурсы. С помощью этих ресурсов такие модули смогут общаться друг с другом.

Достаточно обернуть такие модули в IEFE:
(function () {
/* start of deps/dep1.js */
var dep1 = 1;

/* start of deps/dep2.js */
var dep2 = 2;

var moduleName = function () {
    return dep1 + dep2;
};
})();

Такой способ сборки использует, например, jQuery.

LMD и browserify так же тотально изолируют модули от окружающей среды, но в отличии от сборки «все-в-одном» их модули изолируются от друг друга и от «управляющей части» сборки.

Собираются они примерно вот в такую структуру:
(function (main, modules) {
	function lmd_require() {}
	// ...
	main(lmd_require);
})
(function (require) {
	var dep1 = require('dep1');
	// ...
}, {
	dep1: function (r,m,e) {}
});

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

Сравнительная таблица популярных эмуляторов модулей в JavaScript


AMD,YUI ES6 CJS/LMD IEFE
Разрешение зависимостей В модуле+конфиг В модуле В конфиге Ручное
Доступ к зависимостям Динамический Декларативный Динамический Произвольный
Экспорт С произвольныйм именем Честный import/export С произвольныйм именем Хаотичный/Неуправляемый
Сбока модулей Статический анализ Не нужна/Конкатенация Сборка из конфига Конкатенация
Интерпретация модуля По требованию Нативное решение По требованию При старте
Изоляция модулей Изолированы Изолированы Тотально изолированы Не изолированы

Распостраненные форматы модулей


И напоследок немного справочной информации по существующим сегодня «эмуляторам» модульности в JavaScript.

No module

var moduleName = function () {
    return dep1 + dep2;
};

Namespace

var MyNs.moduleName = function () {
    return MyNs.dep1 + MyNs.dep2;
};

IIFE return

var moduleName = (function (dep1, dep2) {
    return function () {
        return dep1 + dep2;
    };
}(dep1, dep2));

IIFE exports

(function (exports, dep1, dep2) {
    exports.moduleName = function () {
        return dep1 + dep2;
    };
}(window, dep1, dep2));

AMD

YUI модули семнтически схожи с AMD. Не буду их демонстрировать.
define(["dep1", "dep2"], function (dep1, dep2) {
    return function () {
        return dep1 + dep2;
    };
});

AMD обертка для CommonJS

define(function (require, module, exports) {
    var dep1 = require('dep1'),
        dep2 = require('dep2');

    module.exports = function () {
        return dep1 + dep2;
    };
});

CommonJS

var dep1 = require('dep1'),
    dep2 = require('dep2');

module.exports = function () {
    return dep1 + dep2;
};

UMD

Видно, что сейчас есть минимум 3 формата модулей, которые нужно поддерживать. Одно дело если вы пишете свой проект и можете писать на чем угодно. Другое же дело Open-Source проекты в которых хорошо бы поддерживать все форматы. Все эти модули это всего лишь разные обертки, которые по сути делают одно и то же — забирают ресурсы и предоставляют ресурсы. Не так давно появился проект UMD: Universal Module Definition, который «стандартизировал» универсальную обертку под все форматы.
(function (root, factory) {
    if (typeof exports === 'object') {
        // Формат 1: CommonJS
        factory(exports, require('dep1'), require('dep2'));
    } else if (typeof define === 'function' && define.amd) {
        // Формат 2: AMD (анонимный модуль)
        define(['exports', 'dep1', 'dep2'], factory);
    } else {
        // Формат 3: Экспорт в глобалы
        factory(window, root.dep1, root.dep2);
    }
})(this, function (exports, dep1, dep2) {

    // Экспортируем
    exports.moduleName = function () {
        return dep1 + dep2;
    };
});

Понятно, что в разработке такое использовать как-то странно, но на «экспорт» самое то.

Почитать


  1. JavaScript Module Pattern: In-Depth
  2. Creating YUI Modules
  3. Writing Modular JavaScript With AMD, CommonJS & ES Harmony
  4. Why AMD?
  5. AMD is Not the Answer
  6. Why not AMD?
  7. Proposal ES6 Modules
  8. Playing with ECMAScript.Harmony Modules using Traceur
  9. Author In ES6, Transpile To ES5 As A Build-step: A Workflow For Grunt

Ошибки и опечатки шлите, пожалуйста, в ЛС.
Какие модули вы используете в браузерном JavaScript?

Проголосовало 742 человека. Воздержалось 300 человек.

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

Mikhail Davydov @azproduction
карма
448,5
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (71)

  • +4
    Теперь знаю, куда отправлять новичков при вопросе об организации JS кода)
    • –12
      Великолепная статья! Восхищаюсь вашему упорству!
  • +3
    Очень крутой пост.
    Спасибо!
  • 0
    Замечательная статья. Спасибо, такая подробная работа будет очень полезной.
  • +3
    Дошел до раздела «Выводы», думал, что уже конец, но не тут то было…
    Спасибо за материал!
    • +1
      Пожалуйста! Я хотел побить статью на 2, но решил опубликовать все-в-одном, чтобы читатель не потерял «контекст» рассказа, изучая «кишки» эмуляторов модульности.
      • 0
        В этой статье это было бы оправдано. Хорошо, что я к примеру запомнил, где остановился…
  • +7
    Я просто ахренел от объёма статьи! Отличная, качественная статья.
    • +8
      Рад, что вам понравилось. Месяц писания статьи не прошел даром :)
      • +5
        Доааа. Читал-читал, удивлялся, кто так круто пишет. А тут — ты. Как всегда — респектище.
        • +2
          Очень круто, но разбить на 2-3 статьи таки не мешало бы. Сам не выдержал на середине :)
  • +1
    Спасибо за статью. И спасибо за LMD, несмотря на некоторые недостатки, отличная вещь!
  • 0
    Кстати, а как на счёт того, чтобы писать на ES6, а потом сборщиком всё собирать?
    • 0
      Это можно, ждем поддержку со стороны IDE, да и стандарт пока в черновике. На свой страх и риск, но можно.
      • 0
        В WebStorm смотрел?
        • 0
          WebStorm 6.0 с включенным ECMAScript Harmony ругается на синтаксис модуля. yield и прочие — ок.
      • 0
        Google traceur compiler очень плохой код делает на выходе. Не только для production не подходит, так из-за багов может легко «сломать» логику кода. Только для поиграться подходит. А ES6 Module Transpiler, насколько я понял, синтаксис ES6 модулей поддерживает не полностью.
        • 0
          Кстати TypeScript модули это практически ES6 Modules. Можно использовать «компилятор» TS для трансляции. Модули, к тому же транслируются в ES3. А вот у Traceur Compiler в ES5.
          • +1
            Для себя лично пока решил проблему с ES6 так: для let и const использую defs. Добавил в него поддержку rest, sparse и default params. Пока для меня достаточно es6 плюшек. Модули пишу на CommonJS.
            А использовать TS — это хорошая идея, нужно будет попробовать объединить эти библиотеки.
  • 0
    Вот это статейка! Реальный крутяк.

    Эх, я не один так невзлюбил AMD в Node.js, а в итоге и на клиенте забросил из за сложности и каши в конечном результате.
    • 0
      Можете рассказать, почему Вы и azproduction называете модули Node.js термином «AMD»? Насколько я понимаю, налицо шаг вперёд по отношению к AMD — это упрощение кода модулей в Node.js, и состоит оно в том, что Node.js самостоятельно дописывает аналог обёртки вокруг модуля:

      define(function(require, module, exports){
         // . . . здесь код модуля . . .
      });
      

      так что можно не заморачиваться её копированием из модуля в модуль.

      (Кстати, и реальная обёртка в Node несколько поярче вышеприведённого кода, кажется. Можно сделать модуль наследником некоторого класса, например.)
      • 0
        Не помню, что я называл Node.js модули AMD они, безусловно, CommonJS. Ну и, конечно, Node.js сам оборачивает CommonJS модуль контекстом c require, module, exports. И я, думаю, как и Вы, на стороне CommonJS модулей. Возможно, Вас смутил тот факт, что AMD модули можно использовать в Node.js с адаптором node-requirejs.
      • 0
        А мы и не называем их AMD. Возможно, я не совсем верно выразился, хотел сказать «AMD вместе с Node.js».

        Напротив, Node.js использует CommonJS и родное подключение модулей в Node куда проще и юзабельнее, а AMD из require.js там выглядит чужеродно и громоздко. Не исключаю, что я не просек какую-то фишку, но с require.js я пришел к такой же каше, как и azproduction. А в модулях Node — все устраивает.
        • +1
          Упс, долго же не обновлял комменты :) Ну, в общем, суть понятна — мы понимаем, что Node.js не использует AMD.
  • +1
    Миша, ты как всегда архипедантичен и архискрупулёзен при походе к вопросу. Снимаю шляпу и отложу до выходных детальное изучение многобукаф. Надо будет осмыслить сказанное и проанализировать свой путь модульности.
  • 0
    Использую Browserify + Grunt на Node.js пока все устраивает. Как говорил на одной презентации substack, в browserify можно использовать библы из node.js, чем сильно облегчить себе жизнь в некоторых случаях. Возможно при большом объеме кода возникнут какие то траблы, но пока все гладко.
  • 0
    Смотрели component.js? Весьма привлекательная концепция
    • 0
      Да, конечно. По сути это менеджер пакетов и сборщик как npm+browserify или bower+bem-tools. Он так же использует CJS и может быть хорошим инструментом для работы с Web Components.
  • 0
    отставлю тут ссылку github.com/ymaps/modules
    • +3
      Желание получить «Асинхронный require модулей» я понимаю.

      «Асинхронный provide модулей» — не совсем понимаю (те загрузился какой-то модули и он как-то внутри себя понимает, что он должет загрузить один из ресурсов a, b или с и только потом задекларировать себя) это же совсем не явный подход.

      А вот «Возможность передекларации/додекларации модуля» совсем не понимаю — это подход же извращает интерфейс модуля. Вот например, разработчик загружает модуль и оказывается так, что какая-то функция модуля не доступна. Какие цели у этого подхода? Горячая замена кода? Зачем нужна додекларация? Хорошо бы пример.

      • 0
        Асинхронный provide модулей очень нужен, когда модуль изолирован от данных (например, все данные получаются из REST API). Поэтому, пока модуль не получит данные и не инициализирует свое состояние — он бесполезен в использовании (если от него зависят другие модули, могут возникнуть коллизии из-за асинхронности загрузки данных в модуль).
        В LMD как я понимаю сейчвс реализовать такой модуль возможности нет?
        • 0
          В LMD все модули (CommonJS) синхронны с возможностью пополнять «абстрактную ФС» модулей.

          Я считаю, что нуджно разделять модули и данные. Если наличие модуля мы можем гарантировать, то вот гарантировать наличие данных мы не можем. Достаточно представить ситуацию, когда модуль полез за данными и произошел сетевой сбой и предоставить свой интерфейс он не может.
          У модуля есть несколько путей решения этой проблемы:
          1) повторять запрос — тем самым он заблокирует модуль-родитель и многократный повтор запроса не гарантирует поступление данных
          2) выбросить исключение — тогда неизвестно кто его будет отлавливать или если кто-то как-то отловит(например Promise-way), то как прозрачно повторить попытку инициализации модуля?!
          3) сообщить пользователю об ошибке — тогда модуль получает знание о приложении и подчинит себе логику — самый плохой случай
          • 0
            Ок, тогда задам другой вопрос. Как лучше спроектировать архитектуру проекта в таком случае:
            Храним в API данные пользователя и некоторые настройки отображения сайта (например, в какой валюте отображать цены и что-нибудь подобное).
            Есть много модулей (профиль пользователя, настройки, корзина, карточка товара и т.п.) которые непосредственно зависят от данных пользователя (например авторизован пользователь или нет, от его настроек и т.п.).
            Получается, что каждый из этих модулей будет так или иначе запрашивать данные из API (следовательно для каждого из них нужно продумывать вариант исключений — что делать, если данные не загрузились), а так же добавлять логику работы с этими данными (например значение user.settings.currency="rub" означает что все цены нужно умножить на 30, ну и т.д.)

            У меня в голове сложилась мысль, что лучше всего для таких данных API сделать модуль-интерфейс, который загрузит данные и предоставит к ним удобный интерфейс взаимодействия (причем как на чтение, так и на запись). Ну и самый очевидный вариант реализации — асинхронный provide. (или я что-то упускаю или смотрю не в ту сторону?)

            Конечно, можно попробовать наполнить модуль минимальным набором данных «по умолчанию» (на случай проблем с доступом к API) и реализовать механизм оповещений об обновлении данных (его так или иначе реализовывать придется, если данные можно изменять), но все таки, на момент первого обращения к модулю хотелось бы получить его стабильное состояние (с загруженными или дефолтными [если загрузка невозможна], но окончательными данными) с которыми можно работать. (чтобы например избежать лишнего пересчета: сначала все делаем по данным дефолта, а как пришли данные из API — по полученным).

            Быть может я просто зациклился на одном варианте реализации и не вижу «леса за деревьями», не помешал бы свежий профессиональны взгляд :)
            • +1
              В твоем случае настройки пользователя — это обязательные данные без которых ничего не получится. Но это данные, а не модуль, а значит мы можем повлиять на их доступность.

              Лучше если эти данные будут доступны при старте приложения (будут отрендерены в html). За этими данными нам в любом случае придется сходить и лучше если мы обратимся к ним без лишнего запроса на сервер (приложение запустится быстрее и не будет проблем с сетевыми ошибками и лагами).

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

              var request = require('request');
              request.get(config).then(run, epicFail);
              
              // и с обработкой ошибки и перезапросом
              var Attempt = require('attempt');
              
              new Attempt(function promiseGenerator() {
                  return request.get(config);
              }, function timeoutGenerator(error, attemptNo) {
                  // Если 404 или попыток больше 3 - не повторяем запрос
                  if (error.status === 404 || attemptNo > 3) {
                      return Infinity;
                  }
                  var repeatRequestIn = attemptNo * 2000;
                  
                  return repeatRequestIn;
              })
              .then(run, epicFail, notifyUser);
              

              timeoutGenerator в свою очередь может быть асинхронным.
              • 0
                Так, а если модуль ничего не делает, кроме как предоставляет удобный интерфейс доступа к данным. То есть например на основе параметра settings.currency предоставляет функцию getPrice (которая внутри себя содержит всю логику просчета цены исходя из настроек). Ну и помимо этого, дополняет данные функциями уведомления об изменении этих данных, а так же непосредственно предоставляет интерфейс изменения этих данных. То есть наш модуль это модель в MV* (например модуль возвращает объект на основе Backbone.Model, с данными загруженными из API). Соответственно отсутствие данных = отсутствие объекта-модели = невозможность работы приложения, которая обрабатывается в штатном режиме (то есть для всей системы наш модуль — это и есть данные, и только сам модуль определяет откуда реальные данные берутся, из файла, из АПИ, может быть несколько запросов к АПИ и т.п.)
                • +1
                  Понимаю твою идею. В случае Model приложение не должно стартовать пока не произошел sync. Но я бы старался сделать конфиг пользователя доступным синхронно и это более чем реально сделать.

                  Ну и на крайний случай вот так:
                  // config.js
                  var request = require('request'),
                      _ = require('underscore');
                  
                  var promise = request.get('/cfg.json').then(function (data) {
                      _.extends(exports, data);
                  });
                  
                  exports.then = promise.then.bind(promise);
                  

                  // index.js
                  require('config').then(function () {
                      require('main');
                  }, require('epicFailHandler'));
                  

                  // main.js
                  var config = require('config');
                  
                  exports.getPrice = function () {
                      console.log(config.currency);
                  };
                  
                  • 0
                    У нас приложение целиком построено на данных из REST API (собственно апи предоставляет данные для сайта и для iOS приложения, и это апи с сайтом никак не связано). Поэтому сделать данные синхронными можно только добавив между пользователем и API промежуточное звено, которое получит данные из API и отдаст их клиенту вместе со страницей. Я думаю, что это звено лишнее.

                    Пример реализации мне нравится, стоит попробовать, спасибо!
                    • +1
                      Пока ходил, понял, что в LMD это можно сделать лучше без лишней обвязки с синхрнно-асинхронном конфигом:
                      // .lmd.js
                      {
                          ...,
                          "modules": {
                              "config": "@http://site.com/user/config",
                              ...
                          },
                          "async": true,
                          "shortcuts": true,
                          "promise": true
                      }
                      

                      // index.js
                      // грузим асинхронный модуль, который резолвится по ссылке на http://site.com/user/config
                      // и после загрузки будет доступен синхронно под именем "config" и "http://site.com/user/config"
                      require.async('config').then(function (config) {
                          require('main');
                      }, require('epicFailHandler'));
                      
                      

                      // main.js
                      var config = require('config');
                      
                      exports.getPrice = function () {
                          console.log(config.currency);
                      };
                      
          • 0
            всё описанное также справедливо для синхронных provide — достаточно представить ситуацию, когда модуль начал делать синхронный provide (return) и произошло исключение и предоставить свой интерфейс он не может…
            • 0
              Если модуль выбросил исключение — это штатная ситуация. Если моуль пошел за данными и у него ничего не получилось и он выбросил исключение — это не штатная ситуация. Объясню на примере из жизни:

              Есть завод(наше приложение) есть поставщик станков(модуль) и поставщик ресурсов(REST API). Если при поставке станков поезд сошел с рельс — это нормально — этот случай прописывался в договоре(документации к модулю) и завод застраховал этот риск (try catch) и сообщил клиентам, что предоставить какие-то ресурсы он не может.

              Другое дело если станки поставляются вместе с их ресурсами: поставщик станков тем сымым подчиняет заказчика и задерживает поставку и станков и ресурсов(на время запроса). Если ресурсы не доходят, то поставщик станков говорит, что я не могу поставить станки потому, что ресурсы, которые обрабатывают эти станки не были поставлены. Это звучит по меньшей мере глупо. Завод не может сообщить своим клиентам о прогрессе(не знает о состоянии ресурсов) и более того не может повлиять на поставщика ресурсов (перезапросить).
              • 0
                предположи, что у тебя для синхронного провайда используется сторонний модуль в котором «вдруг» вылетает эксепшен — это ни чем по смыслу не отличается от любых проблем с асинхронным вызовом, все гарантии примерно одинаковые (а именно, никаких) и в обоих случаях по одинаковой стратегии нужно поступать
                • 0
                  Стоит определить, что есть модуль, а что есть данные. Ошибки данных мы можем контролировать, а исключения модулей мы не можем контролировать (если модуль делает throw при инциализации то все — конец) даже если обработать исключение, то толку не будет и приложение от этого не заработает.
                  А вот если модуль ходит за данными и делает асинхронный provide(), то тогда ошибки данных становятся ошибками модуля и мы теряем над ними контроль.
                  • 0
                    технически разницы между данными и модулями нет — поэтому, на мой взгляд, система должна выдерживать все эти кейсы

                    ну и см. каменды Димы ниже
                    • 0
                      Технически все ресурсы мы забираем по HTTP и все они текстовые — тут не поспоришь. Логически же мы их все-таки разделяем на скрипты, css, json и пр. по тем или иным признакам. Так же, я думаю, следует и различать модули и данные. И использовать для них разные «загрузчики».
      • 0
        Пример как мы используем асинхронный provide модулей — мы используем api 2.0 Яндекс.карт для наших приложений. Он не умеет в принципе загружаться синхронно. Но при этом у нас есть куча классов-наследников от апишных классов. Как ты предлагаешь делать такие модули-наследники?
        У нас это написано просто, есть модуль ymaps:

        modules.define('ymaps', ['loader'], function(provide) {
        
        loader.load('тут_урл_для_ymaps_api', function() {
            ymaps.ready(function() {
                provide(ymaps);
            });
        })
        
        });
        


        Потом, где нам надо наследоваться от классов, предоставляемых api, мы просто это делаем, не думая, как оно там загрузилось:

        modules.define('my-module', ['ymaps', 'inherit'], function(provide, ymaps, inherit) {
        
        var MyClass = inherit(ymaps.GeoObject, { .... });
        
        provide(MyClass);
        
        });
        


        Как ты предлагаешь разруливать такую ситуацию с синхронным провайдом модуля? Представленная выше ситуация далеко не единственная. И тут речь не идет ни о каких данных для модуля, речь идет о самом модуле.
        • 0
          Я перешел с асинронных модулей на синхронные, поняв тот факт, что в продакшене все модули так или иначе грузятся синхронно (упаковыны в 1 файл), а вся эта обвязка с define — это лишняя писанина.

          Если говорить о собственном коде(не сторонних апи), то асинхронно догружаютя только крупные части приложения (бандлы): несколько модулей, упаковынных в 1 файл и такая дозагрузка в моем коде встречается крайне редко и только тогда я превращаю мой синхонный модуль в асинхронный, притом, что пришедший бандл может сам себя запустить и избавить меня от писанины:
          // Активный по своей конфигурации
          require.bundle('userSettings'); // сам себя запустит и будет использовать как модули из основой части так и свои
          
          // Пассивный по своей конфигурации
          require.bundle('bunchOfViews').then(function () {
              require('viewFromBunchOfViews').doStuff();
          });
          

          Я так же использую я.карты, но в максимально синхронном виде. Если говорить об собственном коде и стороннем апи карт, то в твоем случае все модули синхронны кроме ymaps и без ymaps ни один модуль не запустится. И тк загрузка апи асинхронна и без этого апи запуск приложения невозможен я выношу всю асинхронность в один модуль, позволяя остальным модулям быть синхронными:
          // ready.js
          var lang = require('lang'),
              config = require('config'),
              ymapsApi = config.ymapsApi.replace('%lang', lang);
          
          var maps = $.getScript(ymapsApi).pipe(function () {
              var dfd = $.Deferred();
              window.ymaps.ready(dfd.resolve);
              return dfd.promise();
          });
          
          var ready = $.Deferred();
          $(ready.resolve);
          
          module.exports = $.when(maps, ready.promise());
          

          // index.js
          require('ready').pipe(function () {
              require('mapView')('#map');
          });
          

          // views/map.js
          var ymaps = require('ymaps'); // из глобалов
          // ...
          
          • 0
            Ты, все-таки, проигнорировал мой основной вопрос — как мне предоставить мои модули-классы, являющиеся наследниками классов стороннего кода, который не умеет быть синхронным, своему остальному коду?
            • 0
              Асинхронно загрузить сторонний код перед стартом приложения или его части, а потом унаследовать синхронно. В этом случае сторонний код будет тем самым редким случаем о котором я писал выше.
              • 0
                Ты предлагаешь разделить код по неким, достаточно искусственным, принципам. Опять вводя странные зависимости, обеспечиваемые неким кодом снаружи. То есть, опять вводя знания в свой код, что он неработоспособен, пока кто-то где-то как-то снаружи не обеспечит это. А мне кажется (вернее я даже уверен в этом), это одна из задач именно модульной системы. Тем более, верхний пример, вроде как прекрасно это доказывает.
                В мире javascript вообще уже довольно странно делить код на синхронный/асинхронный. Например, те же промисы, позволяют тебе не думать об этом. Почему же для модульной системы эти различия должны играть такую роль?
                • 0
                  То есть, опять вводя знания в свой код, что он неработоспособен, пока кто-то где-то как-то снаружи не обеспечит это.
                  Пример с внешним api это исключение, которое чинится 1 «асинхронным» модулем. Притом, что эту зависимость мы можем представить как синхронно (тег скрипт) так и асинхронно и вызов require('ymaps') в обоих случаях отработает одинакого. Зависимость модуля обеспечивается либо кодом, либо конфигом, либо явной зависимостью(тег скрипт).
                  Почему же для модульной системы эти различия должны играть такую роль?
                  В «асинхронности» для модулей нет смысла потому как 95% модулей могут быть получены без видимой блокировки основного потока (они загружаются при старте приложения). А для тех 5% модулей, которые мы должны догружать мы можем сделать обвязку, которая примит на себя всю асинхронноть(за счет теж же промисов) и обеспечит точечый контроль ошибок дозагрузки модуля. Я устал писать обертки над модулями(AMD-стиль), которые никогда не будут использоваться по назначению, поэтому я ввожу это различие. И хочу использовать весь потенциал динамических require без регулярок и прочей магии.
                  • 0
                    Притом, что эту зависимость мы можем представить как синхронно (тег скрипт) так и асинхронно и вызов require('ymaps') в обоих случаях отработает одинакого.

                    ymaps нельзя загрузить синхронно через тэг script. Через тэг ты получишь только заглушку, у которой нужно дождаться еще ready.

                    В «асинхронности» для модулей нет смысла потому как 95% модулей могут быть получены без видимой блокировки основного потока (они загружаются при старте приложения)

                    О какой блокировке основного потока идет речь при «асинхронности» модулей?
                    • 0
                      Через тэг ты получишь только заглушку, у которой нужно дождаться еще ready.
                      Действуем по аналогии с DOMContentLoaded — см мой пример в ready.js

                      О какой блокировке основного потока идет речь при «асинхронности» модулей?
                      Это если бы я синхронно грузил модули с сервера
                      • 0
                        Действуем по аналогии с DOMContentLoaded — см мой пример в ready.js
                        Все-таки, это больше похоже на «Делаем костыли по аналогии с DOMContentLoaded» ;)

                        В наших проектах мы примерно так и делали до поры до времени, но, в какой-то момент устали костылять подобные вещи (ymaps это далеко не единственный пример). В какой-то момент переписали все на наши асинхронные модули и избавились от этих странных прослоек, и больше даже никогда не задумываемся о них. Для нас игра стоила свеч.
                        • 0
                          Хорошее обсуждение получилось. Мне в свою очередь важен формат CommonJS. Этот «хак» c DOMContentLoaded как правило нужен перед стартом(ну в самом деле не вешаеть же его в зависимости каждому модулю). Ну и 1 «хакнутый» модуль это не «хак» а особеннсоть ;-) И, кстати, многие модульные системы «чинят» DOMContentLoaded.
                          • 0
                            Зачем каждому-то? У нас он только в бутстрапе приложения:
                            modules.require(['jquery', 'app'], function($, app) {
                            
                            $(function() {
                                app.start();
                            });
                            
                            });
                            
                            • 0
                              Ага, аналогично и у меня с картами и DOMContentLoaded
      • 0
        Теперь по второму пункту, возможности додекларации/передекларации модуля, примеры:
        • Код, который работает и в браузере, и на сервере (node.js). Есть некая общая часть, предоставляющая единый интерфейс в модуле для всего остального кода. И есть некоторые специфичные части, зависимые от платформы, на которой код исполняется. Например, модуль, который ходит по http: есть файл http.vanilla.js, в котором декларируется общая часть модуля http, есть файл http.browser.js, в котором додекларируется модуль http реализацией через XmlHttpRequest, и, есть файл http.node.js, где додекларируется http реализаций походом через нодовский http.request.
          Для всего остального кода это просто модуль http, он не знает его подробностей. Эту проблему можно было бы решить и по-другому, например, через третьи модули, но зачем? Так гораздо естественней.
        • Использование в BEM-предметной области, где есть уровни переопределения и финальный рантайм собирается из их суммы.
        • Мы используем эту возможность в тестах: тесты у нас собираются динамически, то есть мы просто говорим — хочу запустить все тесты из такой-то папки. Каждый тест представляет из себя додекларацию модуля test. В итоге, в раннере тестов мне достаточно сделать modules.require(['test'], function(test) { test.run() }).

        Ну, то есть, это тупо удобно.
        • 0
          Так гораздо естественней
          http монжо так или иначе абстрагировать реально естественными способами вроде заводов или наследования. (Естественными=Привычными способами) Манкипатчинг exports модуля это какаое-то не привычное для разработчика поведение. Эту особенность тянут за собой уровни переопределения BEM?

          Покажи, пожалуйста, пример, а то я не пойму в чем удобство.
          • 0
            Я написал, что эту проблему можно решить с помощью третьих модулей, но часто это бывает просто избыточно.
            Пример чего именно ты хочешь чтобы я привел? Вроде я привел их в верхнем комментарии.
            • 0
              Есть такая штука как поддерживаемость кода и
              часто это бывает просто избыточно
              и подобные мысли чаще ведут к не поддерживаемому коду и мыслям: «А давай ка я просто проманкипатчу, зачем писать абстракцию?!»
              Пример чего именно ты хочешь чтобы я привел?
              Пример реализации интерфейса http под ноду и браузер, учитывая поддерживаемость и переносимость кода.
              • 0
                А есть еще такая штука — «опьянение шаблонами» и «оверинжиниринг».

                Простой пример, чтобы суть донести:

                http.vanilla.js:
                modules.define('http', ['inherit'], function(provide, inherit) {
                
                provide(inherit({
                    ...
                    request : function(params) {
                        if(this.hasCache(params)) {
                            return this.getCache(params);
                        }
                        
                        var result = this.doRequest(params);
                        this.setCache(params, result);
                        return result;
                    },
                
                    doRequest : function(params) {}
                    ...
                });
                
                });
                


                http.browser.js:
                modules.define('http', ['inherit'], function(provide, inherit, base) {
                
                provide(inherit(base, {
                    ...
                    doRequest : function(params) {
                        var xhr = this._createXhr();
                        return xhr.send(...);
                    }
                    ...
                });
                
                });
                


                http.node.js:
                modules.define('http', ['inherit'], function(provide, inherit, base) {
                
                var http = require('http');
                
                provide(inherit(base, {
                    ...
                    doRequest : function(params) {        
                        return http.request(...);
                    }
                    ...
                });
                
                });
                


                Остальной код (что в браузере, что в ноде), просто использует этот модуль http.
                • 0
                  base это, как я понял, непрописанный http.vanilla. Вижу неявное перекрытие абстрактного модуля и внедрение ненужной абстракции над модульной системой Node.js и не вижу преимуществ над:
                  // node_modules/request.js - node
                  var http = require('http'),
                      abstractHttp = require('../lib/abstract-http');
                  
                  // extends - как вариант наследования
                  module.exports = abstractHttp.extends({
                      doRequest : function(params) {        
                          return http.request(...);
                      }
                  });
                  

                  // js/request.js - browser
                  var $ = require('$'),
                      abstractHttp = require('abstract-http');
                  
                  module.exports = abstractHttp.extends({
                      doRequest : function(params) {        
                          return $.ajax(...);
                      }
                  });
                  

                  // index.js - common
                  var request = require('request');
                  
                  request.get('http://ya.ru/').then(function () {
                  
                  });
                  

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

                    Я думаю, тут стоит остановиться ) У любых задач есть несколько способов решения, каждый выбирает какой ему ближе.
                    • 0
                      Согласен, спасибо, что разъяснил твой модульный подход!
                      • 0
                        Тебе спасибо за статью и комментарии, теперь есть куда ссылку давать, чтобы каждый раз не объяснять все заново :)
  • +3
    $ cat js/*.js build.js
    

    Вероятно, вы имели ввиду:
    $ cat js/*.js > build.js
    
  • 0
    > Например, в этом коде LMD поможет не забывть 3 плагина
    забыть, опечатка
    > И если так получиться, что мне придется вернуться к AMD
    всё таки «получится»
    Ещё что-то было, но потерял, когда перечитывал.

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

    Модули ES6 мне не по нраву, как и многое другое, что предлагается в новом стандарте. Мне очень по нраву CJS-модули.
    • 0
      LMD как и browserify основываются на CJS модулях и являются инструментами по сборке оных. browserify заточен под CJS и Node.js-way — отличная штука если нужно запустить node.js приложение в браузере.

      Напомню, что в JS@DOM cвоя атмосфера: ресурсы далеко, ресурсы медленные, зачастую мы не можем грузить все ресурсы, HTTP не стабилен и каждый новый запрос может привести к ошибке. Есть еще сторонние не-CJS-модули (старые плагины к jQuery), что с ними делать? Что если приложение оптимизировано под 3 платформы(3 набора скриптов)?

      LMD в отличии от browserify ближе к браузеру(браузерам и средам) и понимает его проблемы: трансформация не-CJS-модулей при сборке, текстовые файлы без пагинов, абстрактная модульная фс, группировка модулей по бандлам… и еще куча апгрейдов вроде кэша в LS и ленивой инициализации.
  • +2
    Было бы любопытно узнать, к чему пришел автор столь монументального труда два года спустя. ES6/ES2015 с последующей babel-изацией? Webpack? Browserify?
    • +1
      Автор сравнительно недавно был ведущим коллективного твиттер-аккаунта @jsunderhood. Он касался этой темы.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.