Jul 06, 2015 00:50
Теперь поговорим об архитектуре так называемых "одностраничных приложений" более детально.
Это будет непросто - сотни фреймворков, предлагающих десятки подходов - какой из них выбрать? У человека, решившего написать одностраничное приложение, будут большие проблемы. Такое многообразие выбора - это почти что то же самое, что и его отсутствие.
К счастью, основной критерий "хорошести" или "плохости" архитектуры очень простой. Коль скоро архитектура - это правила проектирования, то в первую очередь эти правила должны выполнять свою главную функцию - помогать проектировать:
Хорошая архитектура снабжает вас простыми и понятными правилами, которые помогают легко разбить что-то большое и сложное на набор менее сложных, слабо связанных между собой кусочков. А потом - эти кусочки разбить на более мелкие. И так далее.
Мы будем оценивать фреймворки и технологии именно с этой точки зрения - поддерживают ли они "рекурсивный паттерн проектирования", и если да (что уже неплохо) - то насколько легко пользуясь им разбить что-то сложное на набор мелких фрагментов.
FYI - есть два основных типа "рекурсивного паттерна".
Первый - "слои". Суть его в том, что система организована в "слои сверху вниз", таким образом, что верхний знает про нижние, а нижние про верхние ничего не знают. Слои отделены друг от друга строгим API, а за этим API вполне может царить Адъ. Ну и что.
Этот паттерн теоретически рекурсивен потому, что часто один слой при большом желании можно разбить на два слоя. Или он может быть слоистым внутри. Но, как правило, у каждого "слоя" есть определенный смысл, и бить его дальше некуда.
Второй - "иерархическая декомпозиция". Его суть в том, что сложный модуль состоит из более мелких, они - еще из более мелких, и т. д., таким образом, образуя "дерево". Все модуля отделены API.
"Иерархическая декомпозиция" является архитектурным паттерном только тогда, когда интерфейс "узлов" не уникален, а стандартизован. Например, если мы говорим о UI виджетах - то архитектурное правило говорит нам, как выглядит интерфейс виджета.
В реальной системе они всегда встречаются в сочетании. Например, если есть какой-то фрагмент используется в двух "деревьях" сразу - это означает, что он принадлежит "нижнему слою". Явно отделять слои, и наделять их смыслом - критически важно для архитектуры. Задавая осмысленные слои, с внятными критериями принадлежности им, вы задаете правила.
Надо отметить, что у "голой" браузерной платформы с этим исходно большие проблемы. Для описания UI требется целый трех языка: HTML, CSS, и JavaScript. И каждый более мелкий кусочек должен каким-то образом включать в себя все три. Но все, что может дать нам голый браузер - это один файл HTML, включающий в себя много файлов JS и CSS. А из JS файла сказать, что ему нужен другой JS файл нельзя вообще.
Прежде чем разбираться с фреймворками, надо разобраться с этим. Без нормальных модулей JS с зависимостями, этого элементарнейшего "рекурсивного паттерна", очевидно, ничего сложного в бразуре сделать в принципе невозможно.
Хорошая новость в том, что на JS модули есть открытая публичная спецификация CommonJS/Modules. Именно с такими модулями работает серверный JavaScript, например - nodejs.
Выглядит это так. В каждом файлике .js пишется что-то вот такое:
var iamimporting = require( 'path/to/js/file' );
...
module.exports = iamexporting;
В node это работает прекрасно, потому, что загрузка модуля с локального диска быстра, и может происходить синхронно. Чтобы это работало в браузере, модуль потребует дополнительной обработки (сборки). Например, мы можем собрать все модули, которые надо загрузить, в один большой файл, и включить его один раз тегом script.
Именно так работают browserify и webpack. При этом, (1) у нас в проекте появляется шаг сборки, и (2) возникает куча нюансов в случае, если приложение очень большое, и мы не хотим грузить все сразу. Мы хотим сразу загрузить необходимый минимум, а далее подгружать то, что нужно, по необходимости.
Это можно сделать, разбив приложение на крупные куски (единицы загрузки), и собрать эти куски
раздельно (по сути очень похоже на DLL). Это особенно удобно делается в webpack.
Или же, можно придумать что-нибудь еще. Это что-нибудь еще называется CommonJS/AMD (Asynchronous Module Definition), позволяет грузить модули без сборки, и в тот момент, когда они действительно нужны. Выглядит это так:
define( [ 'path/to/js/file', ... ], function( iamimporting, ...){
return iamexporting;
});
И есть много загрузчиков, которые позволяют грузить такие модули. Самым популярным из них является requirejs.
Но у этого модуля есть недостаток, который сразу бросается в глаза. Им невозможно пользоваться. Поэтому, помучавшись несколько лет, разработчики придумали формат асинхронного модуля "Simplified CommonJS Wrapper", который допускает асинхронную загрузку, но выглядит по человечески:
define( function( require, exports, module ){
var iamimporting = require( 'path/to/js/file' );
...
module.exports = iamexporting;
});
requirejs также позволяет выполнять сборку проекта в один или несколько больших файлов js, но в данном случае это не необходимость, а оптимизация.
Глядя на это, возникает простая идея. Раз импорт модуля - это не часть языка, и все равно делается неким фреймворком, может ли он быть чуть поумнее, и загрузить прочие нужные нам ресурсы - html-шаблоны, css?
Конечно же может. Например, в requirejs это будет выглядеть так:
define( function( require, exports, module ){
require( 'css!./widget');
var template = require( 'ejs!./widget' );
...
module.exports = mywidget;
});
В requirejs и webpack это далется при помощи контент-плагинов, они могут грузить практически все, что угодно, выполняя при этом над загружаемым объектом необходимые преобразования. Например, шаблон может на лету компилироваться в JS. И если они не умеют грузить то, что нужно вам - вы можете легко научить их это делать.
Что касается директивы import из ES6, я не думаю, что когда она будет поддерживаться браузерами, она быстро вытеснит модули CommonJS. Во-первых, синтаксис CommonJS совсем не плох, и никакого выигрыша сама по себе замена 'require' на 'import' не даст. Во-вторых, возможность тонко контролировать процесс загрузки, и выполнять преобразования над загружаемыми объектами - это очень большое преимущество для браузерных приложений. Вряд-ли от него кто-то просто так откажется.
Вообще, я бы не советовал уделять слишком большое внимание системе модулей. Она должна быть CоmmonJS, и выбирайте любой загрузчик. Его потом будет не так сложно заменить. Я бы советовал выбирать между requirejs и webpack. Первый - если не хотите вводить в проект шаг сборки. Сел, и поехал. Второй - готовы делать сборку, озабочены тем, чтобы "быть на острие технологий", и не хотите потом горько жалеть о том, что выбрали какую-то фигню.
Но уверяю вас, система сборки и загрузки модулей - это совсем не то острие, которое изменит
всю игру.
С модулями CommonJS и контент-плагинами уже вполне понятно, как в принципе бить приложение на кусочки, разложив их по файлам. Это необходимо, но не достаточно для того, чтобы построить наше приложение - нам надо знать, как примерно будут выглядеть эти кусочки. "UI виджет - это js модуль" - это слишком общо.
Не всякий модуль экспортирует виджет, в конце концов.
архитектура ПО,
javascript,
single-page applications,
html5