Прошëл год и мы решили, что необходимо провести работу над ошибками и сделать что-то, что работало бы после двух дней бешеного забега, и, возможно, позволило бы попасть в тройку победителей. :) В этот раз по разным причинам не смогли участвовать двое из прошлого состава, так что команда получилась из меня, Севы, Валеры и Вити.
В этом году уже и опыта было побольше - я успел написать несколько приложений на кложуре (включая сам прошлогодний кложуркап, хе-хе), и куда лучше понимал, что и как писать и был уверен, что помочь остальным разобраться получится без проблем.
Before
Идея того, что писать, в голове оформилась довольно давно. Хотя она придумалась и не специально для соревнования, а в общем “неплохо бы написать”, но так как рабочего кода еще не было, мы решили еë использовать.
Идея родилась из того, что мне очень понравилось и оформление, и редактор, и общий минимализм при достаточной функциональности в medium.com, но очень напрягли две вещи: то, что я всë написанное опять отдаю в руки непонятно кому и зачем, и как его оттуда доставать в случае их продажи неудачной (для меня), и то, что мой блог как бы не мой, а медиума, и вообще везде вокруг медиум медиум медиум. В самом деле, это же просто хостинг для блога удобный (по крайней мере, так его вижу я). Я не против где-нибудь в футере ссылочку иметь, но домен я хочу видеть свой.
Вот так и родилась идея сделать блог-платформу а-ля медиум, но только чтоб данные она хранила в дропбоксе (и аналогичных ему сервисах), и генерировала статические сайты - всë равно от блога динамики не требуется. Заодно решили это делать всë без сервера - браузеры уже позволяют обойтись без этого, апи дропбокса тоже, так почему бы и не перенести всë на компьютер пользователя? Полный контроль над своими данными и всë такое. :)
Тем более результат в теории можно запаковать в node-webkit, получить полноценное приложение и редактировать блог прямо на локальном диске. Короче, пройти полный круг и получить в результате то, что было в конце 90-х. :)
Из эдак скажем 30 альтернатив было выбрано имя: Movyv. Українською: “мовив”, тобто “сказав”, “have said”. Оно одновременно символизирует суть сервиса, плюс является свободным пятибуквенным .com-ом. :) Интересно, что от англоязычных знакомых я получил пока два комментария: “looks fun” и “I like it”. Зато от друзей, для которых английский не первый язык, я получил кучу комментариев про то, что ни один американец никогда не сможет его произнести. :)
Конечно же, мы решили подготовиться куда основательнее, чем в прошлом году, и таки сделали это. :) У нас был приличный список на workflowy - какие библиотеки будем использовать для чего, более-менее обсудили архитектуру и то, как хранить данные (и, конечно, в четверг-пятницу перед соревнованием мне внезапно пришла в голову идея, что json’a в дропбоксе быть не должно и всë должно быть в html), всякие идеи, что можно сделать дополнительно.
Вдобавок в четверг мы посидели с Витей, а в пятницу с Валерой, чтоб левел кложуры в крови повысить. :)
Короче, подготовились как могли. :) Когда я смотрел на фотки подготовки других команд в твиттере - мокапы интерфейса, потоки данных, и так далее, я чуточку переживал, но не сильно - мне легче решать всë на ходу, чем строить какой-то план. Всë равно что-то забудется, и вся архитектура пойдëт к чëрту. :)
Go!
Итак, суббота, 9 утра, мы собрались перед офисом Global Logic (спасибо компании и Вите, и кто там еще за это отвечает за предоставленное помещение, оно было удобное и клëвое), и бегом побежали писать код. С бешеной скоростью, конечно - базовая архитектура приложения с двумя вьюхами и роутером на cond’e у нас появился аж к 11 утра. Не знаю, почему так долго, но что-то мы точно делали! :) Джойнились во флоудок, ха-ха.
Но зато к половине первого у нас уже был коннект к дропбоксу, прикрученный редактор (который умел только сериализовать свои данные и потом лежал без движения заметно больше суток :), базовые стили и зачатки сервера.
Распределить задачи получилось относительно неплохо - Сева пилил интеграцию с дропбоксом на клиенте, Валера делал сервер, Витя интерфейс, а я занимался тем, что мержил результаты их работы, допиливал разные штуки, которые приходили в голову, и бегал от одного к другому, решая насущные проблемы. :)
К концу дня у нас было:
- 45 коммитов
- сервер
- который умел принимать токен от клиента
- и проксировать запросы на дропбокс - чтоб сервить сгенерированные статические файлы
- клиент
- умел находить существующие сайты в дропбоксе
- создавать новые
- имел симпатичную страницу для незалогиненных пользователей
- а также поддавался advanced режиму минификации
И кучу страданий от того, как мы хендлили эрроры, приходящие из каналов core.async. Мы перенесли стандартную практику ноды в каналы, и приезжали нам от дропбокса списки - первым элементом ошибка, остальными - результаты. И несмотря на то, что вроде и нет коллбеков, код плоским назвать тяжело:
(defn create-site [site-name author-name site-title]
(go
(let [[error stat] (<! (dropbox/make-dir site-name))]
(if error
[error nil]
(let [path (str site-name "/src.html")
data (create-index-src site-name author-name site-title)
[error stat] (<! (dropbox/write-file path data))]
[error stat])))))
Мы попереживали, поискали решений в интернетах, но решили, что всë равно уже
половина одиннадцатого, так что надо ехать домой. Я решил быстренько задеплоить
и обнаружил нечто, о чëм никто не подумал заранее - Dropbox позволяет иметь
редиректить пользователя обратно только на https
, а http
работает только для
локалхоста.
В состоянии лëгкой паники я написал письмо с призывом о помощи организатору -
Теро, и мы разъехались по домам. К счастью, выход из положения нашëлся -
у него был wildcard-сертификат на *.clojurecup.com
, и он насетапил нам
прокси. Так что логин в дропбокс заработал. :)
Don’t stop
С утра в воскресенье мне в голову пришло, что то, что мы видели в интернете, вполне ок, и я быстренько написал решение проблемы с хендлингом ошибок:
(defn throw-err [rv]
(if (instance? rv js/Error)
(throw rv)
rv))
(defmacro <? [ch]
`(movyv.utils/throw-err (cljs.core.async/<! ~ch)))
Которое Сева доработал еще одним макросом:
(defmacro go-rethrow [body]
`(go (try
~body
(catch :default e
(js/console.log e)
e))))
И в результате выходило:
(defn create-site [site-name author-name site-title]
(go-rethrow
(let [_ (<? (dropbox/make-dir site-name))
path (str site-name "/src.html")
data (create-index-src site-name author-name site-title)
stat (<? (dropbox/write-file path data))]
stat)))
В принципе вышло терпимо, если нужно обработать ошибку - try
/catch
, если нужно
отдать это коду выше по стеку - go-rethrow
. Не мечта, но по крайней мере такой
код можно читать.
Добрался до GL, и всë продолжалось в довольно спокойном темпе - сменили везде обработку ошибок на новый апи, выдумали интерфейс списка из 3 колонок (список сайтов, список постов, превью поста), и начали допиливать.
Фиксили понемногу баги, подпиливали интерфейс, немного рефакторинга, я отвлëкся на написание текстов (reasons to use на индексе, плюс описания для clojurecup’a). Ничего особенно глобального не происходило первую половину дня, и мы пошли пообедаать - с 3 до 4 было сделано всего 2 коммита.
А вот потом всë как-то ускорилось. Я сделал темплейты для рендеринга собственно уже результирующих сайтов, интерфейс трëхколоночный доделался, появилась возможность открыть редактор, стили для него, опять же, генератор слагов, парсинг постов из сгенерированного сайта, сохранение постов, и так далее.
Но где-то к 6-7 вечера нам пришло в голову аж два озарения сразу. Первое - что наш трëхколоночный интерфейс чрезвычайно страшен и не сильно удобен, и срочно надо его уничтожить и сделать всë значительно проще. Второе - что наш сервер, который проксирует запросы к дропбоксу, никуда не годится. Причины озарением не были, конечно - он тормозил (в самых лучших условиях максимум 120 запросов в секунду и любой ответ не быстрее полусекунды), и ускорить его просто так никак не удавалось.
Ну как обычно. “Время на исходе, давайте всë переделаем”. В результате мы выбросили 3 колонки, и заодно роутинг по урлу - у нас с самого начала роутинг был частично по урлу, частично по состоянию, и так как в урл все состояния вносить не удавалось, мы решили просто плюнуть и переехать просто на состояние. Не так красиво, зато куда проще.
Ну и заодно переписали сервер, который теперь реагирует на POST-запрос от дропбокса (который тот делает по изменению данных) и скачивает весь сайт на диск локально, чтоб потом его отдавать с помощью nginx’a.
После 9 вечера наши 5-8 коммитов в час превратились в 12-16, частично из-за уменьшения размера задач, частично из-за горячки. К этому времени у нас уже работало сохранение, и вообще смотрю на лог сейчас - один за другим фиксы каких-то проблем, а не фичи.
Полдесятого ночи наконец появились спиннеры! :) С ними всë, по-моему, немного оживилось и стало веселее. :)
К 12 ночи начали наконец рендериться сайты, посты стало можно переименовывать,
валидация формы создания сайта, куча фиксов стилей, скачивание сайтов переехало
в ThreadExecutor
, и я начал деплоить, пока чуваки делали последние (ха-ха)
правки.
Я очень, очень рад, что занялся этим за 3 часа до конца соревнования, а не за
полчаса, как в прошлый раз. Количество траблов при деплое, как всегда,
жжот. Я уже научился даже немного читать код после его минификации в адвансед
режиме. :) Непонятно почему, но иногда еще у Closure слетает крыша и если не
очистить кеш перед минификацией, оно выдаëт нереальный хлам. По типу того, что в
таком коде case
вылетает с ошибкой, что условия :auth
не существует!
Удаление кеша и рекомпиляция помогает.
(case (:auth data)
(:not :interactive) (Index data)
:authed (Auth data)
:loading (html [:div.loader (ui/icon "spinner spin 5x")]))
Проблемы были не только с клиентом, конечно - бекенд тоже дал возможность попотеть. В результате последний час работы коммитил практически только я - остальные стояли вокруг и наблюдали, как я пытаюсь всë завести на нашем сервере. :)
Энивей, к 2 часам ночи всë наконец поехало, я в последний момент добавил Google Analytics, и мы разъехались спать. Фух.
Ощущения
Разработка с figwheel - просто какой-то анриал. Когда можно изменить код, и увидеть изменившуюся страницу без перезагрузки и необходимости бегать по меню, это ускоряет эксперименты чрезвычайно. Очень качественно работает, не к чему придраться. Может к тому, что заявлена точно такая же перегрузка CSS’а, а у нас не работала, и я не знаю, почему, но это и всë. Сева пробовал подобную штуку для джаваскрипта, она зажëвывала ошибки и вообще как-то плохо работала.
ClojureScript хорош. Вот просто хорош. Писать можно довольно быстро, преобразовывать данные - удобнее, чем в любом другом окружении, что я пробовал, paredit делает передвижение кода простым, а возможность улучшить синтаксис просто на ходу - без комментариев.
core.async козëл. Зажëвывает ошибки и репортит хлам. Его дебагать - это прямо отдельное умение. С другой стороны, мне кажется, что без него код был бы заметно стремнее - честных доказательств нет, но сейчас код довольно читаемый, по-моему. Хотя там есть даже объединение одновременных запросов, всë такое.
Еще мы замутили кое-какой недо-Flux, и я вот совершенно не уверен, какой с него
прок. Ту куцую информацию о типах (имя функции и количество еë аргументов),
которая у нас есть, мы теряем, если начинаем пользоваться :keyword
‘ами, а
наличия серьëзного плюса у общей шины я что-то не осознал. Может я что-то
не заметил? Но пока что у меня план переделать все наши хендлеры сообщений из
канала на функции, которые просто сами запускают go
-блоки.
Кода в этот раз вышло в полтора раза меньше - 150 строк на сервере и 850 на клиенте, но и объëм задачи был поменьше. Много времени ушло скорее на вылизывание, чем на дописывание фич.
Что в результате
Вышел совсем маленький сервер, у которого есть всего три урла:
(defroutes routes
(GET "/webhook" [challenge] challenge)
(POST "/webhook" {:keys [body]} (process-webhook body))
(POST "/save-token/" [site-name token] (save-token site-name token)))
POST /save-token/
- сохраняет авторизационный токен на сервере, для того, чтоб
сервер мог доступиться к пользовательскому дропбоксу. GET /webhook/
- дропбокс
проверяет, что мы умеет его вебхук, и POST /webhook/
- дропбокс нотифицирует,
что что-то изменилось, и мы скачивает сайт пользователя, чтоб его мог отдавать
nginx. Никаких преобразований сервер не делает.
И клиент, который работает с дропбоксом - создаëт там директории с сайтом и
постами, сохраняет исходники (смотрите на src.html
любой) и рендерит готовые
страницы (index.html
). Ему сервер не нужен, и он к нему обращается только в
один момент - когда создаëт новый сайт, чтоб дать серверу токен для доступа к
этому сайту (чтоб он его мог скачать, когда дропбокс скажет).
Brave new app
По-моему, у нас получилось сделать не только рабочее, а еще и полезное приложение! Или наоборот, не только полезное, но еще и рабочее. :) Оно не везде симпатичное, и умеет куда меньше, чем я хотел бы, и деплоить фиксы всю эту неделю, пока идëт голосование и судьи определяют победителя, нельзя, но в целом вроде бы всë ок!
Проснулся в понедельник и сразу пошëл смотреть, нет ли там чего, и обнаружил прекрасный пост. Очень приятно прочитать было. :) И, похоже, в этом году мы сделали больше всех коммитов - 185 (на 20 меньше, чем в прошлом).
А еще пару часов назад зарегистрировал movyv.com
и получил на него бесплатный
сертификат на StartSSL - это всë вместе с сетапом у меня
заняло с полчаса и я вполне мог бы это сделать еще в субботу, что спасло бы пару
нервных клеток.
Энивей, всë это было серьëзно круто. Всем рекомендую как-нибудь попробовать такое, эмоциональный подъëм в конце воскресенья у меня был безумный, как, по-моему, и у всех остальных. :)
И я буду очень-очень благодарен, если вы за нас проголосуете! К сожалению, в этом году голосование только с помощью Твиттера. И спасибо, что дочитали до конца. :)