solovyov.net

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 запускается там, где это имеет больше смысла - фотки качаются сразу после того, как скачался сет, а комментарии - после того, как отрисовалась фотография.

Честно говоря, писать подробный туториал по бэкбону желания особенного нет - их уже много. Так что, если интересно, то стоит пойти посмотреть на исходники.

Эпилог

У меня были мысли приделать поддержку Пикасы еще, но немного лениво - я сам ею не пользуюсь, а работы хватает, привести два довольно разных апи к общему знаменателю… Ну и это не тема этой статьи (хотя если кому-то хочется, патчи я с радостью принимаю).

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