Showkr - приложение в браузере
Я за последние пару-тройку недель по вечерам и выходным понемногу сделал дикий, но симпатичный (и, может, полезный) сайт 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 запускается там, где это имеет больше смысла — фотки качаются сразу после того, как скачался сет, а комментарии — после того, как отрисовалась фотография.
Честно говоря, писать подробный туториал по бэкбону желания особенного нет — их уже много. Так что, если интересно, то стоит пойти посмотреть на исходники.
Эпилог
У меня были мысли приделать поддержку Пикасы еще, но немного лениво — я сам ею не пользуюсь, а работы хватает, привести два довольно разных апи к общему знаменателю... Ну и это не тема этой статьи (хотя если кому-то хочется, патчи я с радостью принимаю).
Я хотел сказать, что если вдруг остались или возникли вопросы, пишите мне — я либо отвечу там, либо дополню статью. Надеюсь, что она была полезна.
Все замечания и вопросы можно отправлять письмами на