solovyov.net

Showkr - приложение в браузере

8 min read · javascript, programming, project

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

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

Эпилог

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

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

If you like what you read — subscribe to my Twitter, I always post links to new posts there. Or, in case you're an old school person longing for an ancient technology, put a link to my RSS feed in your feed reader (it's actually Atom feed, but who cares).

Other recent posts

ngrok for the wicked, or expose your ports comfortably
PostgreSQL collation
History snapshotting in TwinSpark.js
Code streaming: hundred ounces of nuances
Useful shell prompt