Архив за Февраль 2013

  • Создание профессиональных сайтов с помощью DocPad

    Продолжаю знакомить с open-source наработками, созданными в процессе работы над проектом Emmet. В прошлый раз это был CodeMirror Movie, а в этот раз познакомлю вас с процессом создания сайта документации на основе DocPad.


    tl;dr – Как сделать профессиональный высокопроизводительный сайт на DocPad

    • Используйте плагин docpad-plugin-menu для автоматической генерации меню сайта.
    • Используйте grunt-frontend и docpad-plugin-frontend для сборки CSS и JS ресурсов и правильного кэширования.
    • Создайте специальное debug-окружение для плагина docpad-plugin-frontend для поиска проблем в исходниках CSS и JS, а не их минифицированных версиях.
    • Настройте веб-хуки на GitHub и Gith на сервере для автоматической сборки сайта после каждого коммита.
    • Настройке nginx для правильного кэширования статических файлов и экономии ресурсов процессора.

    DocPad — это генератор статических сайтов, написанный на CoffeeScript. В отличие от сайтов, созданных с использованием обычных CMS вроде Django, Drupal и WordPress, статические сайты потребляют минимальное количество серверных ресурсов, так как представляют собой набор заранее сгенерированных обычных HTML-файлов. То есть кроме обычного веб-сервера вроде nginx или Apache для работы сайта ничего не нужно.

    Для документации Emmet это идеальный вариант, позволяющий не только удобно работать над сайтом, но и значительно сократить расходы на хостинг: можно выдерживать относительно высокую посещаемость даже на недорогом хостинге.

    Но у DocPad, как и у многих других генераторов, есть ряд недостатков, не позволяющих делать по-настоящему профессиональные и быстрые сайты. Их я и решил исправить, написав несколько плагинов:

    • docpad-plugin-menu — автоматическая генерация меню для сайта.
    • grunt-frontend — «умная» сборка CSS и JS файлов.
    • docpad-plugin-frontend – вывод собранных с помощью grunt-frontend CSS и JS файлов с учётом правильного кэширования, а также управление наборами файлов между шаблонами.

    Если вы ещё не знакомы с DocPad, рекомендую вам посмотреть и прочитать Введение в DocPad, чтобы вы представляли, о чём в дальнейшем пойдёт речь.

    Генерация меню

    Плагин docpad-plugin-menu умеет геренировать структурированное меню для всех страниц сайта (то есть для всех файлов из папки src/documents). Этот плагин добавляет метод generateMenu(url) в объект templateData, в контексте которого отрисовываются все шаблоны проекта. На вход этот метод принимает URL страницы, относительно которого нужно создать меню, на выходе вы получите структуру разделов сайта, которую удобно отрисовывать, например, с помощью partials.

    Подробнее о возможностях плагина и примерах его использования читайте на основной странице проекта.

    Сборка фронт-энд ресурсов

    Для удобства разработки я разбиваю CSS и JS файлы на отдельные модули, которые затем склеиваются и минифицируются – это стандартная практика высокопроизводительных сайтов. Для сборки я использую Grunt.js в котором, казалось бы, уже есть все необходимые инструменты для выполнения этих задач.

    Но и тут я не нашёл ничего подходящего. Дело в том, что мне важна дата последнего обновления минифицированного файла, потому что я хочу её подставлять в URL файла для эффективного сброса кэша. Поэтому обновлять конечный файл нужно только тогда, когда поменялся один исходных файлов.

    Для решения этой задачи я написал свой сборщик: grunt-frontend. Работает он следующим образом. Во время конкатенации и минификации нескольких файлов в один он записывает структуру исходных файлов и их md5-отпечаток в специальный файл .build-catalog.json. При следующей сборке плагин смотрит на структуру и содержимое исходных файлов: если ничего не поменялось, то и конечный файл не минифицируется и не обновляется.

    Это не только сокращает время сборки, но и позволяет сохранить такие важные данные конечного файла как дату обновления и md5-отпечаток. Все эти данные хранятся в .build-catalog.json, его желательно хранить вне версионного контроля.

    Для минификации используются библиотеки CSSO (с автоматическим инлайнингом всех подключённых через @import файлов) и UglifyJS.

    Подробнее об использовании grunt-frontend.

    Управление CSS и JS ресурсами

    Очень часто возникает необходимость управлять подключением CSS и JS файлов на различных страницах сайта. Скажем, на всех страницах нужно использовать набор файлов set1; для всех внутренних страниц раздела /about/ нужно дополнительно использовать set2 и set3, но для страницы /about/contacts/ вместо set2 нужно использовать set4 (то есть set1, set4, set3, именно в таком порядке). Кроме того, в URL всех ресурсов нужно подставлять дату модификации файла чтобы эффективно сбрасывать кэш.

    Для решения этих задач был написан плагин docpad-plugin-frontend. Он добавляет метод assets(prefix), который позволяет доставать отсортированный список ресурсов из текущего документа и всей цепочки шаблонов, применяемых к документу. Если в корневой папке проекта существует файл .build-catalog.json, то плагин считывает его и возвращает список ресурсов с префиксом в виде даты модификации файла.

    Например, описанную выше задачу с управлением наборов ресурсов можно решить следующим образом. Для основного шаблона default.html.eco указываем основной набор файлов в мета-данных:

    ---
    js: "/js/fileA.js"
    ---
    

    В шаблоне about.html.eco, который наследуется от основного шаблона и применяется ко всем документам /about/*, указываем следующие данные:

    ---
    layout: default
    js2: ["/js/fileB.js", "/js/fileC.js"]
    js3: ["/js/fileD.js", "/js/fileE.js"]
    ---
    

    В документе /about/contacts/index.html перекрываем набор js2:

    ---
    layout: about
    js2: "/js/contacts.js"
    ---
    

    Теперь, при рендеринге страницы /about/contacts/index.html, вызов assets('js') вернёт следующий набор файлов:

    • /js/fileA.js
    • /js/contacts.js
    • /js/fileD.js
    • /js/fileE.js

    Как видите, всё довольно просто: придумываем префикс для категории ресурсов, а сами наборы создаём с помощью числовых суффиксов. Далее вызываем assets() в шаблоне и передаём ему префикс набора ресурсов: файлы сортируются по суффиксу в порядке возрастания; наборы с одинаковым суффиксом перекрываются.

    Более подробную информацию о возможностях плагина и примерах использования читайте на главной странице репозитория.

    Режим отладки

    Очень часто бывает так, что пользователь вашего сайта сообщает вам, что в каком-то браузере возникает ошибка: не работает JavaScript или элементы наехали друг на друга. Но весь ваш CSS и JS код минифициорван и вам довольно сложно найти то самое место в исходных файлах, где эта ошибка возникает.

    В будущем эти проблемы можно будет находить с помощью Source Maps, но сейчас далеко не все минификаторы и браузеры их поддерживают.

    В плагине docpad-plugin-frontend есть специальный режим отладки. Так как структура всех минифицированных файлов хранится в JSON-каталоге, нам не составит труда при необходимости вывести список исходных файлов вместо скомпилированного.

    Для этого в DocPad я создаю отдельное окружение, в котором указываю опцию frontendDebug: true. Если опция frontendDebug равна true, то метод assets() плагина docpad-plugin-frontend будет по возможности возвращать список исходных файлов вместо минифицированных. Пример настройки docpad.coffee:

    module.exports = {
        …
        environments:
            debug:
                frontendDebug: true
    }
    

    Теперь при запуске DocPad в окружением debug, вы получите HTML-страницы с исходными CSS и JS файлами и сможете легко найти ошибку:

    docpad run --env=debug

    Автоматический деплой с GitHub

    Я настроил сервер таким образом, чтобы после каждого коммита в ветку master сайт автоматически генерировался.

    Со стороны GitHub я использовал обычный “WebHook”, а на стороне сервера – Gith.

    Gith – это удобный веб-сервер для Node.JS, который умеет принимать и фильтровать данные веб-хуков GitHub. Мой сервер, который запускает сборку сайта, выглядит следующим образом:

    var childProc = require('child_process');
    var path = require('path');
    
    var gith = require('gith').create(3000);
    
    gith({
        // Слушаем хуки только для ветки "master"
        branch: 'master'
    }).on('all', function(payload) {
        console.log('Run deply script on', new Date());
    
        // Запускаем скрипт сборки сайта
        var deploy = childProc.spawn('sh', ['/web/deploy.sh']);
    
        deploy.stdout.on('data', function(data) {
            var message = data.toString('utf8');
    
            if (~message.indexOf('subscribe')) {
                // Docpad может спросить про подписку на рассылку, откажемся
                deploy.stdin.write('n');
            } else if (~message.toLowerCase().indexOf('privacy')) {
                // Docpad может спросить про политику безопасности, согласимся
                deploy.stdin.write('y');
            }
        });
    
        deploy.stderr.on('data', function(data) {
            console.log('Error: ', data.toString('utf8'));
        });
    
        deploy.on('exit', function(code) {
            console.log('Deploy complete with exit code ' + code);
        });
    });
    

    Сам скрипт сборки проекта deploy.sh выглядит следующим образом:

    
    #! /usr/bin/env bash
    git pull
    git submodule foreach 'git checkout master && git pull origin master'
    npm install
    grunt
    docpad generate
    find ./out -type f ( -name '*.html' -o -name '*.css' -o -name '*.js' )  -exec sh -c "gzip -7 -f < {} > {}.gz" ;
    

    Настройка nginx

    В качестве веб-сервера я использую nginx, который прекрасно оптимизирован для отдачи статики. В настройках сайта нам нужно сделать следующее:

    • Прописать рерайты для статических файлов: отсекать дату модификации в начале пути и посылать правильные кэширующие заголовки.
    • Отдавать статику в gzip для снижения объёма передаваемых файлов.

    Если посмотреть на скрипт deploy.sh, то вы увидите, что в последнем шаге создаются gzip-версии всех HTML, CSS и JS файлов. У nginx есть специальный модуль HttpGzipStaticModule, который может отдавать заранее созданные gzip-версии файлов вместо автоматической генерации для каждого запроса. Этот трюк позволит нам сэкономить процессорные ресурсы. Для того, чтобы воспользоваться этим модулем, его нужно добавить в nginx при компиляции:

    ./configure --with-http_gzip_static_module

    Мой конфиг nginx выглядит так:

    
    server {
        server_name  your-server.com;
        root         /path/to/web-site/out;
    
        index  index.html index.htm;
    
        # отсекаем дату модификации со статических ресурсов
        location ~* ^/d+/(css|js)/ {
            rewrite ^/(d+)/(.*)$ /$2;
        }
    
        # кэшируем всю статику
        location ~* .(ico|css|js|gif|jpe?g|png)$ {
            expires max;
            access_log off;
            add_header Pragma public;
            add_header Cache-Control "public";
        }
    
        # включаем поддержку статических gzip-версий файлов
        gzip_static on;
    }
    
  • CodeMirror Movie

    Для создания Emmet было создано несколько сторонних проектов, с которыми я вас познакомлю в нескольких постах этого блога.

    Первое, с чем я хотел бы вас познакомить, это CodeMirror Movie. Его вы можете у видеть на многих страницах сайта с документацией: это тот самый механизм, который показывает интерактивные презентации.

    Когда я только начинал работу над документацией, мне хотелось более наглядно показать работу действий Emmet. Читать длинные тексты, описывающие особенности работы, всегда скучно и утомительно, гораздо приятнее смотреть как это работает «в живую».

    Обычно такие задачи решаются записью видео-роликов, но такой вариант не устраивал меня по многим причинам:

    • Запись качественного ролика требует слишком много времени. Например, на создание шестиминутного ролика про Zen Coding v0.5 у меня ушло около четырёх часов.
    • Видео довольно сложно обновлять. Например, если обнаружится ошибка или пользователям будет не понятно, как работает какое-то действие, скорее всего, понадобится ещё несколько часов на перезапись ролика.
    • Так как сам Emmet написан на чистом JS (а значит работает в браузерах), хотелось, чтобы пользователи не только смотрели, как работает Emmet, но и пробовали его в действии прямо на страницах документации.

    Для решения этих и многих других проблем и был создан CodeMirror Movie. Принцип его работы довольно прост: вы создаёте небольшой сценарий, в котором указываете, что нужно сделать, например, написать текст, немного подождать, а потом показать всплывающую подсказку.

    Как можно догадаться из названия, в основе проекта лежит замечательный редактор CodeMirror, а это значит, что вы можете создавать презентации для любого языка программирования, который поддерживает этот редактор.

    Создание презентации

    Как правило, для создания экземпляра редактора CodeMirror вы создаёте элемент <textarea> с начальным содержимым редактора и вызываете следующий JS-код:

    var myCodeMirror = CodeMirror.fromTextArea(myTextArea);
    

    Создать презентацию так же легко: вы создаёте <textarea> с начальным содержимым редактора и дописываете туда сценарий презентации, разделив эти секции строкой @@@:

    <textarea id="code">
    &lt;div class="content"&gt;
        |
    &lt;/div&gt;
    @@@
    type: Hello world
    wait: 1000
    tooltip: Sample tooltip
    </textarea>
    

    Для инициализации ролика нужно вызвать метод CodeMirror.movie(), передав первым параметром ID элемента <textarea> или сам элемент:

    var movie = CodeMirror.movie('code');
    
    // начинаем воспроизведение
    movie.play();
    

    Сценарий презентации

    Как было отмечено выше, для создания презентации вам нужно написать её сценарий.

    Сценарий представляет собой список команд, которые нужно выполнить. Каждая команда пишется на отдельной строке в виде название: значение. В качестве значения записывается JS-объект с параметрами команды, однако каждая команда имеет довольно неплохие базовые значения, поэтому достаточно передать всего лишь значение самого главного параметра. Например, вот как выглядит сценарий ролика, который должен набрать «Hello world», а через секунду после завершения набора показать всплывающую подсказку с текстом «Movie tooltip»:

    type: Hello world
    wait: 1000
    tooltip: Movie tooltip
    

    Более подробную информацию о всех командах и примеры использования можно найти на странице плагина. Сам плагин вы можете использовать как угодно (лицензия MIT), особенно хорошо он будет смотреться в JS-движках презентаций вроде impress.js или reveal.js. Надеюсь, вам понравится!

    Метки: , ,
    Комментарии к записи CodeMirror Movie отключены
  • Вышел Emmet v1.0

    Я рад сообщить, что после более полугода разработки в свет вышел Emmet (бывший Zen Coding) v1.0. Возможно, вы уже используете Emmet в течение нескольких месяцев, но только сейчас, после многочисленных исправлений ошибок и улучшений, я могу сказать, что он работает так, как надо.

    Что поменялось со времён Zen Coding?

    Во-первых, поменялось название. Emmet будет брэндом для новых инструментов и не все они будут связаны с написанием кода (coding).

    Во-вторых, у проекта появился полноценный сайт и обширная документация по всем возможностям. Теперь не надо искать в интернете обрывки информации о том, как пользоваться проектом, вся наиболее полная информация собрана в одном месте.

    В-третьих, улучшилась работа с CSS: значения свойств можно писать прямо в аббревиатуре. Также был учтён опыт и пожелания пользователей: благодаря модулю нечётного поиска вам необязательно запоминать громоздкие названия аббревиатур, достаточно написать всего несколько символов (например, ov:h == ov-h == o-h == oh == overflow: hidden);

    Вот список остальных значимых изменений:

    • Полностью переписан код проекта. Он стал более модульным и расширяемым.
    • Отказ от Python-версии. Мне было довольно сложно поддерживать две версии ядра. Вместо отдельной версии теперь используются мосты на Python, Objective-C и Java, это позволит править баги и добавлять новые возможности очень быстро и сразу на все платформы.
    • Улучшен модуль определения неявных имён тэгов. Ранее, если вы пытались развернуть аббревиатуру вроде .item, то в результате могли получить либо <div class="item">, либо <span class="item">, в зависимости от типа родительского тэга. Теперь модуль смотрит на название тэга и может вывести, например, <li>, <td>, <option>.
    • Поддержка расширений. Теперь, чтобы добавить новую аббревиатуру или настроить вывод результата, не надо лезть в код плагина, достаточно создать несколько простых JSON-файлов в специальной папке.
    • Генератор «Lorem Ipsum». Ранее, чтобы получить «рыбный» текст для сайта, надо было пользоваться сторонними ресурсами, а затем форматировать результат. Теперь получить такой текст можно прямо в редакторе, причём количество слов в тексте можно регулировать просто дописав число после аббревиатуры. Более того, генератор использует все возможности аббревиатур Emmet, позволяя дописывать нужные атрибуты к генерируемым элементам и регулировать количество создаваемых блоков.
    • Новый оператор ^. Несмотря на то, что в Emmet/Zen Coding довольно давно существует более мощный инструмент в виде группировки элементов, зачастую осознание того, что следующий элемент в аббревиатуре должен находится уровнем выше, приходит довольно поздно. Пользователям приходилось возвращаться обратно, добавлять скобки и дописывать нужный элемент. Теперь достаточно написать оператор ^, чтобы подняться на уровень выше, причём можно использовать несколько операторов подряд.

    Исходный код и плагины доступны в специальном репозитории. Если вы обнаружили ошибки или у вас есть пожелания по улучшению, буду рад узнать о них в разделе Issues.

    Метки: , ,