Я за последние пару-тройку недель по вечерам и выходным понемногу сделал дикий, но симпатичный (и, может, полезный) сайт Showkr. И решил рассказать миру о том, зачем и как я это сделал - я думаю, что вторая часть может пригодиться, учитывая растущую популярность приложений в браузере.
Зачем
У фликра довольно тяжелые страницы, и когда смотришь целый сет фотографий - утомительно ждать, пока каждая следующая загрузится и отрисуется (я вот только что попробовал, от нажатия кнопки до рендера где-то до секунды проходит на быстром интернете).
А тут, получается, открыл,
дождался ответа апи фликра, и сиди себе просматривай, оно всë на одной
странице. Тем более, что созданы все удобства - фотки, комментарии и
хоткеи: поддерживается всë, что имеет хоть какой-то смысл - j
/k
,
up
/down
, space
/shift+space
. Welcome!
Как
Первый момент, который мне сохранил кучу времени - это Twitter Bootstrap. Тут мне рассказывать особенно нечего, если вы его не знаете -теперь будете знать. Хороший CSS framework, экономит тучу времени.
Make
Второй момент - GNU Make
. Я никогда толком не умел писать мейкфайлы -
был испуган в детстве результатами запусков autoconf/automake. Но
какое-то время назад я начал юзать мейкфайлы, как рубисты юзают рейк -
для каких-то мелких задачек. Чисто как организатор шелл-команд, короче.
Но какое-то время назад я понял, что пришла пора. CoffeeScript хочет стать джаваскриптом, темплейты хотят стать джаваскриптом, индекс.хтмл хочет быть разным для разработки и работающего сайта, и все они не хотят это делать, когда не нужно (например, во время отдачи сайта - зачем, если всë можно сделать заранее?).
И вырос прекрасный и довольно понятный мейкфайл. И сейчас я процитирую основные моменты, чтоб не только я, но и другие такие же необразованные (кто не знает классики - необразован, правда?) могли проникнуться полезностью.
Основы
Итак, у меня есть директория с кофескриптовыми файлами, я хочу их превратить в джаваскрипт (включать кофескрипт на клиент-сайд - моветон имхо, зачем нам лишние тормоза). Начнëм с того, что у нас есть эти самые файлы:
SOURCE = $(wildcard app/*.coffee)
И правило, чтоб их скомпилить:
build/%.js: app/%.coffee
@mkdir -p $(@D)
coffee -pc $< > $@
Всë это выглядит немного стрëмно, но я сейчас объясню, а с внешним видом можно жить - это на самом деле довольно неплохой DSL, хотя можно и поприятнее сделать было бы. В мейкфайле есть:
- переменные
- функции
- правила
Всë остальное пока не волнует. У нас здесь есть переменная SOURCE
,
которой присваивается результат исполнения функции wildcard
. И
переменные, и функции раскрываются с помощью оборачивания в конструкцию
$(...)
(исключая однобуквенные переменные, тогда просто $x
).
Функции, конечно, еще параметров хотят.
Нечто с двоеточием и строками с отступами - это правило. Говорит нам,
что файл, который заканчивается на .js
и находится в директории
build/
, зависит от файла с точно таким же именем, только в директории
app/
и с расширением .coffee
.
В правиле есть две инструкции. Эти инструкции - это вызов обычных
команд, причëм каждая инструкция запускается в своëм инстансе шелла
(переменные не сохраняются). Каждая инструкция во время выполнения
выводится на экран как команда, или, если она начинается с @
(как наш
мкдир) - не выводится.
Еще мейк внутри правила даëт какое-то количество
переменных
с видом разной степени стрëмности. $@
- файл-цель (который мы хотим
получить), $<
- его первая (здесь - и единственная) зависимость.
$(@D)
- родительская директория файла-цели. Я забил на слежение за
тем, чтоб директории были созданы заранее, и просто создаю их в каждом
правиле, которое пишет в файлы, первой строкой. Паттерн “хватит
беспокоиться”. ;)
И теперь мажорный аккорд, правило, которое заставит это работать:
all: $(patsubst app/%.coffee, build/%.js, $(SOURCE))
Это правило идëт первым, чтоб запуск просто make
запускал его, и
говорит нам, что правило all
зависит от таких-то файлов (а правило для
постройки этих файлов мы определили выше по тексту). От каких файлов -
от всего в $(SOURCE)
, только надо заменить app
на build
, а
coffee
на js
- ну, понятно, компиляция зависит от того, чтоб в
директории билд были все нужные джаваскриптовые файлы. А каждый файл
зависит уже (определили раньше) от кофескриптового.
Теперь запуск make
в директории скомпилирует каждый файл в
джаваскриптовый. Кроме того, если еще раз запустить make
, то он
запустит обработку только тех файлов, которые изменились - он смотрит на
время изменения файла и не делает лишних движений.
Казалось бы, зачем это надо, если coffee -bco build/ app/
сделает то
же самое. Ну, во-первых, то же самое он не сделает - он не следит за
временем изменения, а компилирует всë (и всего может случайно стать
много), а во-вторых, не кофескриптом единым! Но не будем забегать
вперëд.
Зачистка
Итак, у нас есть первая инкарнация мейк-файла:
SOURCE = $(wildcard app/*.coffee)
all: $(patsubst app/%.coffee, build/%.js, $(SOURCE))
build/%.js: app/%.coffee
@mkdir -p $(@D)
coffee -pc $< > $@
Что тут неплохо бы подчистить? Ну, нам не нужен список исходных файлов. Только результатов, поэтому заменим начало на такое:
SOURCE = $(patsubst app/%.coffee, build/%.js, $(wildcard app/*.coffee))
all: $(SOURCE)
Ещëëë
Теперь проще понять, чего хочет главное правило - оно хочет исходники!
Ок, понятно. Чего еще нам надо? Нам надо вот это всë динамически
сгенерированное запихать в index.html
.
Маленькое отступление: фактически в showkr’e у меня не используется `require.js`_, потому что мне лень скрещивать ender с ним, а потому загрузка модулей синхронна и все файлы хотят быть загружены прямо из индекса. В ином случае этого бы момента не было и индекс был бы статическим, но, мне кажется, это хороший повод порисовать еще правил. Итак.
Для начала наше главное правило захочет еще index.html
:
all: $(SOURCE) build/index.html
Что делать с индексом? Я решил не ломать себе мозги, а взять awk
(еще
одна штука, про которую стоит знать) и… В общем, индекс выглядит
как-то так:
...
<head>
...
<!-- js-deps -->
</head>
...
И у меня есть прекрасный скрипт на awk
, который берëт переменную
DEPS
из окружения (со списком зависимостей) и влепляет в хтмл:
/<!-- js-deps -->/ {
split(ENVIRON["DEPS"], DEPS)
# this way it goes from 1 to 9 instead of random ordering
for (i = 1; DEPS[i]; i++)
printf("<script type=\"text/javascript\" src=\"%s\"></script>\n", DEPS[i])
next
}
1 # print everything else
Я мог бы расписывать, как работает авк, но давайте вы лучше почитаете на английском, на русском, или вообще что-нибудь еще.
Правило при этом для постройки индекса выглядит так:
build/index.html: index.html $(SOURCE)
@mkdir -p $(@D)
DEPS="$(SOURCE:build/%=%)" awk -f build.awk $< > $@
Что у нас новенького? Ну, убираем имя директории, ссылаясь на переменную
с заменой
(аналогично тому $(patsubst ...)
, что мы использовали раньше). Вроде всë,
создали директорию, авк прочитал файл, изменил, мы его направили в нашу цель
($@ == build/index.html
). Красота.
Теперь make
при запуске сначала скомпилирует наш кофескрипт (если
надо), а потом index.html
. Ура.
Публичная версия
А теперь надо собрать версию для сайта - один джаваскриптовый файл. Отлично:
prod: all prod/app.js prod/index.html
prod/index.html: index.html
@mkdir -p $(@D)
DEPS="app.js" awk -f build.awk $< > $@
prod/app.js: $(SOURCE:build/%=prod/%)
@mkdir -p $(@D)
cat $^ | uglifyjs > $@
Теперь make prod
возьмëт все зависимости prod/app.js
(вспомните,
$^
- это все зависимости правила) и минифицирует их в нужный нам
файлик. И скомпилирует еще index.html
.
Надо сказать, что меня эти замены директорий в переменных серьëзно раздражают, поэтому мы сейчас этот момент зачистим. Итак, результат трудов вместе с зачисткой:
SOURCE = $(patsubst app/%.coffee, %.js, $(wildcard app/*.coffee))
all: $(addprefix build/, $(SOURCE) index.html)
build/%.js: app/%.coffee
@mkdir -p $(@D)
coffee -pc $< > $@
build/index.html: index.html $(addprefix build/, $(SOURCE))
@mkdir -p $(@D)
DEPS="$(SOURCE:build/%=%)" awk -f build.awk $< > $@
prod: all $(addprefix prod/, app.js index.html)
prod/index.html: index.html
@mkdir -p $(@D)
DEPS="app.js" awk -f build.awk $< > $@
prod/app.js: $(addprefix prod/, $(SOURCE))
@mkdir -p $(@D)
cat $^ | uglifyjs > $@
Может, еще немножко?
Вот такой отличный мейкфайл. А теперь добавим сюда темплейты! Они лежат
в директории app/templates
и имеют расширение .eco
, а результаты
будут иметь расширение .eco.js
(чтоб отличать от просто .js
).
TEMPLATES = $(patsubst app/%, %.js, $(wildcard app/templates/*.eco))
all: $(addprefix build/, $(TEMPLATES) $(SOURCE) index.html)
build/templates/%.js: app/templates/%
@mkdir -p $(@D)
./eco.js $< $(<:app/%=%) > $@
prod/app.js: $(addprefix prod/, $(TEMPLATES) $(SOURCE))
@mkdir -p $(@D)
cat $^ | uglifyjs > $@
Здесь ./eco.js
- самописный скрипт для вызова компиляции
эко-темплейтов, который применяет к результату нужную мне обëртку.
Первым параметром у него путь к файлу, а вторым - имя, под которым
темплейт будет известен (templates/something.eco
). Темплейты будут
сминифицированы в один файл с приложением.
Важные моменты
У меня важен порядок файлов джаваскриптовых, поэтому я просто задаю их руками:
SOURCE = $(patsubst %,%.js,util api models viewing browsing showkr)
А выше показан вариант относительно того, когда устраивает сортировка по алфавиту.
Функция wildcard
не умеет рекурсивно находить файлы, поэтому если есть
поддиректории в структуре, то я использую $(shell find ...)
- обычный
find
.
Ну вот и всë, надеюсь, что какие-то базовые основы я рассказал понятно, а полный код Makefile‘а (ссылка стоит на ту версию, которая существовала на момент написания статьи) можно найти в репозитории.
Архитектура
Вернëмся к собственно самому приложению. Оно построено на `backbone.js`_, который сейчас самая модная библиотека для мвц на джаваскрипте, наверное. Бэкбон стоит того - он не пытается скрыть детали имплементации (как эмбер, например -его я тоже пробовал), но организует всë отлично.
Ядро
Центральная часть приложения - Router
Showkr
. С его инициализацией запускается приложение.
Основная функция, помимо роутинга (вызова нужной функции по адресу в хеше), -управление вьюхами. Роутер умеет создавать View по уникальному идентификатору и переключаться между уже созданными. Раз созданные, вьюхи не уничтожаются, чтоб второй раз за сессию не ждать от фликра одних и тех же данных.
Остальное
А дальше всë банально - вьюхи инициализируют модели и внутренние вьюхи,
модели качают данные с фликра (используя переопределëнные методы sync
и parse
).
У большинства моделей есть какая-нибудь вложенная коллекция, поэтому
получилась иерархия
User -> SetList -> Set -> PhotoList -> Photo -> CommentList -> Comment
.
Вложенные коллекции инициализируются в инициализации модели, fetch
запускается там, где это имеет больше смысла - фотки качаются сразу
после того, как скачался сет, а комментарии - после того, как
отрисовалась фотография.
Честно говоря, писать подробный туториал по бэкбону желания особенного нет - их уже много. Так что, если интересно, то стоит пойти посмотреть на исходники.
Эпилог
У меня были мысли приделать поддержку Пикасы еще, но немного лениво - я сам ею не пользуюсь, а работы хватает, привести два довольно разных апи к общему знаменателю… Ну и это не тема этой статьи (хотя если кому-то хочется, патчи я с радостью принимаю).
Я хотел сказать, что если вдруг остались или возникли вопросы, пишите мне - я либо отвечу там, либо дополню статью. Надеюсь, что она была полезна.