- PVSM.RU - https://www.pvsm.ru -
Всем привет. Меня зовут Макс Дегтерев (у меня тут нет аккаунтра, так что вот мой twitter: @suprMax [1] и сайт maxdegterev.name [2] ). Мы недавно запустили новую классную версию мобильного сайта. Про него-то я вам сейчас и расскажу.
Работая над новым мобильным сайтом, я уже имел за плечами опыт разработки старого. На момент написания этих букв он еще должен быть доступен по адресу http://ostrovok.ru/m [3], но сколько он проработает, я не знаю. Старый сайт работал очень просто. Он живет в общем проекте OTA и, соответственно, использует тот же Back-end (Django), стандартный медиагенератор, простые HTML шаблоны (server-side), немного JavaScipt шаблонов на underscore, SCSS и обычный JavaScipt. Это все мы уже проходили сотню раз, это все очень скучно.
Перед разработкой нового сайта у меня стояли следующие задачи:
Так уж исторически сложилось, что сайт всегда разрабатывается с дизайном мобильного приложения, этот дизайн на нем обкатывается. Вроде такого эксперимента. Вот и тут дизайн был абсолютно новый, flat (iOS 7 style, модно!). Поэтому не удивляйтесь, что цвета, да и вообще внешний вид, немного отличается от всего остального Островка.
Мне не хотелось зависеть от OTA, как в прошлый раз. Плюс я работал над сайтом один, и учить Django совсем не интересно, а постоянно отвлекать кого-то из разработчиков OTA – не комильфо. Поэтому я решил мыслить как пират. В Островке уже есть мобильное API, которое покрывает бóльшую часть того, что требовалось от мобильного сайта. Сдобрив это частями API от десктопного сайта, можно получить вполне себе неплохое решение. Именно такой подход я и выбрал.
За основу я взял NodeJS. Всегда хотелось попробовать. В качестве фреймворка выступил ExpressJS, который очень похож на Sinatra (привет Ruby!). Мне нужно было вести пользовательские сессии, так что без Redis не обойтись. Чтобы меньше печатать вместо обычного JavaScript использовал CoffeeScript. Для шаблонов взял Jade, а для CSS — Stylus. Нам нужен модный сайт, да и на мобильных телефонах можно получить большой выигрыш в скорости, так что все решил делать singlepage. Сначала думал взять SpineJS, но у него комьюнити не особо большое, поэтому классическая связка: Backbone, Lo-Dash, Zepto.
Пришлось немного повозиться со сборкой ассетов. Хочется, чтобы все было прикольно, автоматически, как у рельсовиков. Вполне неплохим решением оказался asset-rack, который полностью берет на себя задачи сборки JS, CSS и даже Jade шаблонов. Конечно же он умеет прекомпилировать шаблоны, кладет их в указанный неймспейс (я выбрал app.templates). Так же он берет на себя головную боль по прекомпиляции CoffeeScript и системе requirements, предоставляя 2 варианта: Snockets (Sprockets) и классический для NodeJS вариант с RequireJS (Browserify). Тем не менее NodeJS штука новая, так что были проблемы:
compressAsset = (filename, contents)-> console.log("[#{(new Date()).toUTCString()}] #{logPrefix} Compressing asset #{filename}.gz") zlib.gzip(contents, (e, buffer) -> fs.writeFile(filename + '.gz', buffer)) generateAssets = -> for asset in assets.assets filename = __dirname + '/public' + asset.url.replace('.', "-#{asset.md5}.") if not fs.existsSync(filename) console.log("[#{(new Date()).toUTCString()}] #{logPrefix} Saving asset #{filename}") fs.writeFileSync(filename, asset.contents, encoding: 'utf-8') compressAsset(filename, asset.contents) assets.on('complete', -> generateAssets() unless config.is_dev
Подитог:
Задачу с тем, чтобы было модно, молодежно и прикольно, я, таким образом, решил.
Немного о том, как организован сам проект. A picture is worth a thousand words:
Я не буду вдаваться в детали разработки на Backbone и CoffeeScript. Об этом написаны тысячи статей. Я расскажу очень кратко о структуре модулей, и вообще, как я делал, чтобы не сойти с ума.
Очевидно, что Backbone имеет collections, models и views. Я положил их в разные папки. Еще мне понадобились модули. Это такие штуки, которые нужны постоянно, вне зависимости от того, на какой странице я нахожусь. Они загружаются на старте приложения и уже не выгружаются, когда view, к примеру, уничтожаются сразу при переходе на другую страницу. Как и некоторые модели/коллекции (например, список бронирований при нажатии кнопки «Выйти»). У меня модули — это модальные окна, сайдбар, информация о физическом местоположении телефона и т.п.
В итоге вот так выглядит мой главый файл app.coffee:
#= require ../../data/app.config.js #= require ../helpers.js #= require app.utils.js #= require_tree modules #= require router.coffee #= require_tree models #= require_tree collections #= require_tree views app = _.extend(app [5], Backbone.Events) # ... # Layout modules app.size = new app.modules.Size() # Data modules app.geo = new app.modules.Geo() app.user = new app.modules.User() app.analytics = new app.modules.Analytics() # Modals and extra views app.overlay = new app.modules.Overlay() app.modal = new app.modules.Modal() # Router relies heavily on stuff above app.router = new app.Router() # ... Backbone.history.start(pushState: true, hashChange: false)
Вот и helpers.js, который используется и на Back-end. Подключение папок через require_tree дает возможность не задумываться о подключении индивидуальных файлов, но вот порядок загрузки не гарантирует, так что понадобится делать дополнительные require, если требуется наследовать от какого-то другого класса (например view). У меня такой задачи не возникало, а к моменту вызова Backbone.history.start все модули и компоненты системы уже в памяти, так что роутер может делать свою работу, вызывать view и т.д.
Теперь немножко про Stylus. Он очень похож на SASS, но имеет ряд дополнительных возможностей. Очевидно, что он identation based. Это дает возможности не заморачиваться о пересечении названий классов. Но это все мы уже давно видели, это скучно. Как насчет возможности использовать значение любого CSS правила в качестве переменной?
.my-awesome-block width: 100px height: 100px margin: (@width / 2) auto line-height: height [6]
Неплохо. А как насчет поддержки vendor prefixes? Мы живем в ужасном мире конкурентной борьбы. К счастью, выглядит это вот так:
.my-awesome-block box-sizing: border-box transition: all .2s ease
Stylus сам смотрит на то, какие mixin у него есть, и если находит properties с таким же названием, то осуществляет замену. Соответственно, если больше не требуется заполнять vendor prefixes, например, для border-radius (что, кстати, правда), то можно просто удалить один mixin и даже не открывать остальные файлы. Я не уверен, но мне кажется, это может сэкономить примерно сотни времени в будущем. Ну и небольшой пример того, как выглядит мой app.styl:
import [7] 'config' import [7] 'includes/reset.css' import [7] 'includes/fonts.css' import [7] 'includes/mixins' import [7] 'plugins/iswipe' import [7] 'plugins/zepto.sidebarify' import [7] 'plugins/zepto.calendar.css' import [7] 'plugins/zepto.input.numselect.css' import [7] 'plugins/zepto.listselect.css' import [7] 'plugins/zepto.textarea_autogrow' import [7] 'partials/partial_date' import [7] 'partials/partial_spinner' // ...
Выходит, смешанные импорты тоже не проблема. Единственный недостаток на данный момент – для того чтобы вызвать mixin нужны аргументы. Без этого у меня не работал вызов, хотя это уже могли исправить. У меня написано вот так:
html, body // lol! noselect plz body, select, input, button, textarea color: #4b5c66 font: normal 14px Helvetica, Arial, sans-serif line-height: 1.4em //...
Для стилизации я всегда использую только классы, чтобы победить злых specificity ситхов [8] Чтобы дать 146% гарантию того, что классы ну никогда не пересекутся, я использовал подход SMACSS. Ну, или не совсем, но очень похоже. Все, что отностится к структуре страницы, и не меняется при переходах, я делал с префиксом l (layout). Так же у меня есть p (page), блоки внутри страниц b (block) и т.д. В случае вложенности элемента в элемент его класс будет наследовать в себе часть названия родителя. А может и не наследовать, но корневой класс наследуется всегда. Вот пример стилей:
.p-awesomepage .p-a-header // styles .p-a-h-soopermenulink color: hotpink .p-a-content // styles .p-a-loading // styles .p-a-title // styles
Для вот такой разметки:
.p-awesomepage header.p-a-header wow, header! .p-a-content .p-a-title my awesome title .p-a-loading .p-a-title loading is being loaded
Если сочетать такой подход с вложенностью правил в Stylus (главное не перевкладывать, а то получится колбаса на выходе), то пересечений быть не должно.
Про Jade особо рассказывать нечего. Вполне себе шаблонизатор. Кто использовал Slim – то же самое. Поддерживает include и partial. Хелперы я просто прокинул через общий helpers.js файл. Хотя можно регистрировать свои. Например, уже есть :markdown.
Если про то, как разрабатывать разработку, все понятно практически сразу, то вот как деплоить деплои приходится немного задуматься. У рельсовиков уже придумано 9999999 решений вроде capistrano, а для ноды пока-что ничего работоспособного мне найти не удалось.
Я решил на тестовый
task 'update', '[VPS]: Update current state with new from repo', -> console.log('[Cake]: Pulling updates from repo') exec('git pull', (error, stdout, stderr) -> unless error console.log('[Cake]: Installing npm packages') exec('npm install', (error, stdout, stderr) -> unless error console.log('[Cake]: Restarting forever') # exec('forever restartall') exec('killall forever') exec('killall nodejs') console.log('[Cake]: Cleaning up old assets') exec('find ./public/assets -regextype posix-egrep -regex ".*.(js|css|gz|gzip)$" -delete') exec('cake forever') sendMail() else console.warn("[Cake]: Installation failed with error: #{error}") ) else console.warn("[Cake]: Update failed with error: #{error}") ) task 'deploy', '[DEV]: Deploy current repo state to dev VPS', -> console.log('[Cake]: Connecting to VPS mobota@mobota-dev.ostrovok.ru && running update') exec('ssh mobota@mobota-dev.ostrovok.ru 'cd /var/www/mobota && cake update'', (error, stdout, stderr) -> if error console.warn("[Cake]: Deploy failed with error: #{error}") else console.log('[Cake]: Deployed!') )
Для выкатывания на production Денис Орлихин наладил собиралку debian пакетов. Работает череp teamcity, прогоняет тесты перед сборкой. Очень прикольно. Детали не расскажу, но может он как-нибудь расскажет.
Кстати о тестах. Я обычно тесты не пишу, потому что хаха-фронтенд и в принципе очень ленивый. Но тут integration tests действительно понадобились, так как приходится зависеть от стороннего API. Я писал тесты на mocha + chaiJS. Очень просто и удобно, а главное хороший reporter:
(Сижу в парке, поэтому интернет не супер быстрый и тесты свалились)
И действительно, пора бы уже про самое главное. Какие отличия я заметил при разработке под мобильные устройства в сравнении с полноценными браузерами. Учитывая, что приходится работать под несколько платформ сразу, а у каждой платформы свои проблемы, получается не так уж и просто. Могу сказать что, ситуация с прошлого года изменилась не сильно. Главная проблема это Android телефоны 2-й версии, и ранние 4-ки. С iOS все куда проще, хотя и у них бывают чудеса.
Сначала детальнее о проблемах. Некоторые совсем очевидные, но я повторюсь. Некоторые были для меня новыми, а значит могут быть полезны и кому-то еще.
.android2 * text-shadow: none !important;
Экономит много времени и нервов.
meta(name="viewport", content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no")
Еще нужно у mobile html5boilerplate [9] подсмотреть метод preventZoom(). Очень помогает. Да и вообще, оттуда много классных вещей можно взять.
С определением высоты/ширины экрана могут быть проблемы. Android/iOS неконсистентны в своих ответах. Android, в частности, вообще может сказать, что он в 2 раза шире и выше, чем на самом деле. Я предпочитаю по возможности использовать CSS, и вам советую.Но если все-таки очень хочется, у меня получилось вот так:
class Size _min_width: 320 _min_height: 416 _iOS_toolbar_height: 44 _android_delay: 300 constructor: -> @width = null height [6] = null if $.os.ios app.dom.win.on('orientationchange load', @orientationChange) else app.dom.win.on('orientationchange resize load', => window.setTimeout(@orientationChange, @_android_delay) ) @orientationChange() orientationChange: => prevWidth = @width prevHeight = height [6] if $.os.ios and not window.orientation @width = window.screen.availWidth height [6] = window.screen.availHeight - @_iOS_toolbar_height else @width = app.dom.win.width() height [6] = app.dom.win.height() @width = Math.max(@_min_width, @width) height [6] = Math.max(@_min_height, height [6]) if @width isnt prevWidth or height [6] isnt prevHeight app.trigger('size:resize', width: @width height: height [6] )
С определением высоты контента/окна ожидайте много приключений, мобильные браузеры в этом плане очень классные и нормально сделать что-то на полный экран и с overlay/modal будет непросто.
В целом, подход к разработке похож на работу с большими сайтами на очень слабых машинах. Нужно не забывать, что батарейка и ресурсы процессора/памяти очень ограничены, и всегда проверять производительность на реальных устройствах. Конечно, это только мой личный опыт, основанный на устройствах, которые были доступны для тестирования. Я могу ошибаться по некоторым пунктам.
Бонусы разработки под мобильные, в отличие от десктопных, браузеры:
Мы живем в отличное время, для того чтобы быть разработчиком. В голове появилась идея, и ты легко ее реализуешь. Остальные люди живут с идеями, которые для них ну никак не воплотить в жизнь. Так что пишите код и используйте NodeJS – быстро, удобно, асинхронно, молодежно. И больше времени останется чтобы придумывать идеи, а не печатать буквы.
Думаю, я выложу что-нибудь из сайта на GitHub, можно будет посмотреть и использовать у себя. Всем удачи, поцоны.
Автор: Ostrovok
Источник [10]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/javascript/37236
Ссылки в тексте:
[1] @suprMax: https://twitter.com/suprMax
[2] maxdegterev.name: http://maxdegterev.name
[3] http://ostrovok.ru/m: http://ostrovok.ru/m
[4] хостинг: https://www.reg.ru/?rlink=reflink-717
[5] app: http://habrahabr.ru/users/app/
[6] height: http://habrahabr.ru/users/height/
[7] import: http://habrahabr.ru/users/import/
[8] specificity ситхов: http://www.stuffandnonsense.co.uk/archives/images/specificitywars-05v2.jpg
[9] mobile html5boilerplate: http://html5boilerplate.com/mobile/
[10] Источник: http://habrahabr.ru/post/183746/
Нажмите здесь для печати.