• Создание профессиональных сайтов с помощью 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.

    Метки: , ,
  • Canvas как способ оптимизации графики

    Мы постепенно начинаем обновлять дизайн сайта Аймобилко и уже выкатили пару новых макетов. Самое заметное изменение — это главная страница сайта:

    ss01

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

    Центральный элемент страницы — сцена, на которой показываются новинки нашего каталога. На фоне сцены находится очень большая картинка с кулисами. Если присмотреться, то эти кулисы покрыты лёгким шумом, что придаёт им особый колорит:

    example

    Однако вся эта красота на проверку оказалась очень тяжёлой: в одной картинке объединилось всё худшее, что плохо влияет на сжатие. Это и красный цвет (даёт очень сильные артефакты сжатия в JPEG), и мелкий шум (сильные артефакты в JPEG; плохо упаковывается в PNG). Приемлемое качество картинки было достигнуто при размере в 330 КБ, что, на мой взгляд, довольно много для одной картинки. Очень хочется, чтобы главная страница загружалась как можно быстрее. Поэтому я решился на один эксперимент.

    Изучаем картинку

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

    Простой алгоритм монохромного шума выглядит так:

     var canvas = document.createElement('canvas'); canvas.width = canvas.height = 200; var ctx = canvas.getContext('2d'); // получаем все пиксели изображения var imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); var pixels = imageData.data; for (var i = 0, il = pixels.length; i < il; i += 4) {	var color = Math.round(Math.random() * 255);	// так как шум монохромный, в каналы R, G и B кладём одно и то же значение	pixels[i] = pixels[i + 1] = pixels[i + 2] = color;	// делаем пиксель непрозрачным	pixels[i + 3] = 255; } // записываем пиксели обратно на холст ctx.putImageData(imageData, 0, 0); document.body.appendChild(canvas); 

    Получим вот такой результат:

    noise

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

    Режимы наложения

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

    blending-modes

    Кому-то они могут показаться запредельно сложными с точки зрения программной реализации, однако на самом деле практически все режимы наложения основаны на очень простых алгоритмах. Например, алгоритм режима наложения Multiply выглядит так:

    (colorA * colorB) / 255

    То есть просто умножаем два цвета и делим результат на 255 (отсюда и название Multiply: «умножение»).

    Доработаем нашу функцию: загрузим картинку, сгенерируем шум и наложим его в режиме Multiply:

     // Загружаем картинку. Обязательно ждём, пока она полностью загрузится var img = new Image; img.onload = function() {	addNoise(img); }; img.src = "stage-bg.jpg"; function addNoise(img) {	var canvas = document.createElement('canvas');	canvas.width = img.width;	canvas.height = img.height;	var ctx = canvas.getContext('2d');	// нарисуем картинку на холсте, чтобы получить её пиксели	ctx.drawImage(img, 0, 0);	// получаем все пиксели изображения	var imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);	var pixels = imageData.data;	for (var i = 0, il = pixels.length; i < il; i += 4) {	// генерируем пиксель «шума»	var color = Math.random() * 255;	// накладываем пиксель шума в режиме multiply на каждый канал	pixels[i] = pixels[i] * color / 255;	pixels[i + 1] = pixels[i + 1] * color / 255;	pixels[i + 2] = pixels[i + 2] * color / 255;	}	ctx.putImageData(imageData, 0, 0);	document.body.appendChild(canvas); } 

    Получим что-то типа этого:

    stage-noise

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

    Альфа-композиция

    Процесс смешивание двух цветов с учётом прозрачности называется «альфа-композиция». В простейшем варианте алгоритм смешивания выглядит так:

    colorA * alpha + colorB * (1 - alpha)

    где alpha — это коэффициент смешивания (прозрачность) от 0 до 1. В данном случае важно правильно выбрать, что будет фоновым изображением (colorB), а что будет накладываемым (colorA). В нашем случае фоновой будет сцена, а шум — накладываемым.

    Добавим в функцию addColor() дополнительный параметр alpha и модифицируем сам алгоритм с учётом альфа-композиции:

     var img = new Image; img.onload = function() {	addNoise(img, 0.4); }; img.src = "stage-bg.jpg"; function addNoise(img, alpha) {	var canvas = document.createElement('canvas');	canvas.width = img.width;	canvas.height = img.height;	var ctx = canvas.getContext('2d');	ctx.drawImage(img, 0, 0);	var imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);	var pixels = imageData.data, r, g, b;	for (var i = 0, il = pixels.length; i < il; i += 4) {	// генерируем пиксель «шума»	var color = Math.random() * 255;	// высчитываем итоговый цвет в режиме multiply без альфа-композиции	r = pixels[i] * color / 255;	g = pixels[i + 1] * color / 255;	b = pixels[i + 2] * color / 255;	// альфа-композиция	pixels[i] = r * alpha + pixels[i] * (1 - alpha);	pixels[i + 1] = g * alpha + pixels[i + 1] * (1 - alpha);	pixels[i + 2] = b * alpha + pixels[i + 2] * (1 - alpha);	}	ctx.putImageData(imageData, 0, 0);	document.body.appendChild(canvas); } 

    Получаем именно то, что нам нужно: слой шума, наложенный на картинку в режиме Multiply и прозрачностью 20%:

    stage-noise-alpha

    Оптимизация

    У меня картинка генерируется примерно за 400 мс, что довольно заметно. Поэтому мы оптимизируем код, чтобы он работал быстрее.

    Размер моей картинки 1293×897 пикселей, что в итоге даёт 1 159 821 итераций цикла. Это довольно много, поэтому в первую очередь нужно оптимизировать операции вычисления, а именно убрать ненужные и повторяющиеся операции.

    Например, в цикле три раза высчитывается значение 1 - alpha, хотя это постоянное значение для всей функции, поэтому делаем новую переменную за пределами цикла:

    var alpha1 = 1 - alpha;

    Далее, при генерации пикселя шума используется формула Math.random() * 255, однако дальше мы делим этот цвет на 255: r = pixels[i] * color / 255. Соответственно, умножение и деление на 255 можно смело убирать.

    Эти простые операции снизили время выполнения скрипта с 400 мс до 300 мс (-25%).

    Помним, что у нас больше миллиона итераций по массиву, поэтому каждая мелочь на счету. В цикле мы два раза обращаемся к массиву pixels, сначала чтобы получить значения для вычисления multiply-пикселя, а затем чтобы сделать альфа-композицию. Доступ к массиву довольно дорогая операция, поэтому сохраняем оригинальное значение пикселя в переменную (то есть оставляем только одно обращение к массиву):

     var origR = pixels[i],	origG = pixels[i + 1],	origB = pixels[i + 2]; 

    Это экономит ещё около 40 мс.

    С учётом всех оптимизаций функция addNoise() выглядит вот так:

     function addNoise(img, alpha) {	var canvas = document.createElement('canvas');	canvas.width = img.width;	canvas.height = img.height;	var ctx = canvas.getContext('2d');	ctx.drawImage(img, 0, 0);	var imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);	var pixels = imageData.data, r, g, b, origR, origG, origB;	var alpha1 = 1 - alpha	for (var i = 0, il = pixels.length; i < il; i += 4) {	// генерируем пиксель «шума»	var color = Math.random();	origR = pixels[i];	origG = pixels[i + 1];	origB = pixels[i + 2];	// высчитываем итоговый цвет в режиме multiply без альфа-композиции	r = origR * color;	g = origG * color;	b = origB * color;	// альфа-композиция	pixels[i] = r * alpha + origR * alpha1;	pixels[i + 1] = g * alpha + origG * alpha1;	pixels[i + 2] = b * alpha + origB * alpha1;	}	ctx.putImageData(imageData, 0, 0);	document.body.appendChild(canvas); } 

    Скорость выполнения скрипта — около 170 мс (было 400 мс), что довольно неплохо.

    Ещё больше оптимизаций

    Внимательный читатель мог заметить, что картинка с кулисами — красная. То есть информация об изображении присутствует только в красном канале, в синем и зелёном её нет. Если её нет, зачем делать вычисления для этих каналов? Поэтому оставляем расчёты только для красного канала: конкретно в моём случае это даст аналогичный результат, а время выполнения снизит до 80 мс:

     for (var i = 0, il = pixels.length; i < il; i += 4) {	origR = pixels[i];	pixels[i] = origR * Math.random() * alpha + origR * alpha1; } 

    Добавлено: читатель @Sergeyev указал, что можно ещё сократить время выполнения скрипта (-20%), убрав ненужные операции:

     for (var i = 0, il = pixels.length; i < il; i += 4) {	pixels[i] = pixels[i] * (Math.random() * alpha + alpha1); } 

    Результат

    Результат получился довольно неплохим:

    • Вес изображения снизился с 330 КБ до 70 КБ + 1 КБ пожатого JS-кода. На самом деле, картинку можно было бы ещё больше ужать, потому что слой с шумом скроет большинство артефактов JPEG-сжатия.
    • Такая оптимизация соответствует практикам progressive enhancement: пользователи с браузерами, в которых нет canvas (например, IE6) или отключён JS всё равно получат картинку, но менее детализированную.

    Единственный минус, который я вижу — это выполнение наложения каждый раз при загрузке страницы, в то время как обычная картинка может быть просто закэширована браузером. Но, во-первых, время выполнения наложения довольно низкое (80 мс), а во-вторых, как вариант, результат можно хранить в localStorage в виде data:url и при следующей загрузке страницы доставать из кэша. Но моя картинка занимает более 1 МБ, так что я не стал сохранять её — доступное пространство можно и нужно использовать с большей пользой.

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

    Добавлено: некоторые читатели и Денис в частности справедливо заметил в комментариях, что практически идентичного результата можно добиться наложением слоя с шумом поверх картинки. Тут скорее речь идёт о не совсем удачном примере, выбранном для статьи, нежели о неправильности подхода в целом.

    Метки: , ,
  • Веб-разработка в Eclipse: JavaScript

    Как отмечалось ранее, для работы JS вместо Spket IDE я теперь использую Eclipse JSDT, который входит в состав Eclipse Web Tools Project. Причины для такого перехода вполне естественные: проекты, с которыми я работаю, становятся всё сложнее и больше, нужно больше удобства и контроля над ситуацией.

    В JSDT меня больше всего привлекло следующее:

    • Рефакторинг: переименование объектов, выделение блока в отдельную переменную, объединение определения переменной и присваивания и т.д. Некоторые вещи вроде выделения в метод пока толком не работают, но, надеюсь, в ближайшее время это будет исправлено.
    • Валидация кода. Помимо обычной проверки синтаксиса, можно настроить более сложные проверки вроде поиска неиспользованных переменных, недостижимый код, переопределение переменной из внешней области видимости и т.д.
    • Выделение фрагментов camelCase-переменных с помощью Shift+Alt+← и Shift+Alt+→
    • Поддержка JSDoc.
    • Удобный Outline/Quick outline; дополнительные окна, в которых показывается документация и код определения текущего объекта.
    • Автоматическая отбивка кода при его перемещении из/в блок.
    • Подключение внешних библиотек.
    • Встроенный дебаггер.

    Лучше всего будет, если читатель поставит Eclipse for JavaScript Web Developers и изучит все настройки и менюшки — в том числе контекстные — JSDT (лучше включить перспективу JavaScript), потому что возможностей действительно очень много и их сложно описать в одной статье.

    Однако при всей «крутости» этой среды разработки, в ней есть ряд проблем, с которыми пришлось столкнуться прежде, чем окончательно перейти на JSDT.

    Начинаем работу

    Для того, чтобы полноценно использовать все возможности JSDT, обязательно нужно создать проект с JavaScript-природой. Делается очень легко: вызываем File > New > Project… и в появившемся окошке выбираем JavaScript Project. В появившемся диалоговом окне вводим название проекта и жмём Finish. По умолчанию создаётся веб-проект с поддержкой DOM, папкой с JS-исходниками является сам проект. Когда вы лучше освоитесь с JSDT, то сможете более тонко настраивать проект: указывать подключаемые библиотеки, исключать ненужные файлы и папки из индекса. Пока оставим как есть.

    Module pattern

    Главным преимуществом для меня в Spket IDE была поддержка современных паттернов, в том числе и модуля:

     var module = (function() {	return {	method: function() {}	}; })(); 

    В Spket такая конструкция без проблем отображается в outline и по ней работает code complete, но в JSDT ни то, ни другое не работает:

    ss011

    Небольшой JSDoc исправит ситуацию:

     /** * @type module */ var module = (function() {	return {	/** * @memberOf module */	method: function() {}	}; })(); 

    Теперь работает как надо:

    ss021

    Но есть ещё одна проблема: приватные переменные и методы модуля не отображаются в Outline, а очень хотелось бы. Это можно исправить, описав самовызывающуюся функцию как конструктор несуществующего класса:

     /** * @memberOf __module * @type module */ var module = (/** @constructor */ function() {	function myPrivateMethod() {	}	return {	/** * @memberOf module */	method: function() {}	}; })(); 

    ss031

    Как видно из примера, я описал несуществующий класс __module, двойное подчёркивание я использую в качестве своеобразного соглашения об именовании объектов. Проблема в том, что этот несуществующий класс попадёт в code complete всего проекта, и использование двойного подчёркивания — простой и понятный способ отфильтровать ненужные данные при вызове code complete. Однако этот недостаток очень легко можно превратить в достоинство: таким образом можно описывать структуры объектов, доступ к описанию которых затруднён из-за отсутствия строгой типизации в JS:

    ss041

    Поддержка популярных библиотек

    Базовый набор библиотек для JSDT довольно невелик: это стандартные объекты JavaScript (Array, String и прочее) и стандартный DOM (HTMLElement, document и так далее). То есть если мы напишем, например, document. и вызовем code complete, то увидем список свойств и методов объекта document, экземпляра класса Document. Но современная веб-разработка немыслима без использования популярных библиотек вроде jQuery.

    Имея в своём распоряжении JSDoc, можно создать описание практически любого популярно фреймворка и использовать его для подсказок в коде. Так как таких описаний в интернете найдено не было, я воспользовался своим любимым принципом «если хочешь, чтобы что-то было сделано хорошо, сделай это сам» и запустил проект, в котором создаю описания популярных библиотек и фреймворков, с которыми работаю:

    jsdt-docs — JSDoc для популярных библиотек

    В этом проекте сейчас есть следующие библиотеки:

    • Modernizr 2
    • Browser Addons — разные методы и свойства, которые почему-то отсутствуют в стандартном описании DOM в JSDT.
    • console — небольшое описание объекта console, который присутствует в современных браузерах.
    • CSS2Properties — список CSS-свойств для свойства style DOM-элементов. Несмотря на то, что он называется CSS2, в нём присутствуют и CSS3-свойства: такое название выбрано потому, что в стандартном описании Element.prototype.style является объектом класса CSS2Properties.
    • JS SIgnals
    • Node.JS
    • Socket.IO
    • Underscore.js.
    • Zepto.js

    Добавить библиотеку довольно просто:

    1. Идём в настройки: Preferences > JavaScript > Include Path > User Libraries.
    2. Создаём новую библиотеку: нажимаем кнопку New…
    3. Вводим название библиотеки в появившемся окне и жмём ОК.
    4. Выделив только что добавленную библиотеку, нажимаем на кнопку Add .js file… и выбираем один или несколько файлов, относящихся к данной библиотеке.

    После того, как библиотека была создана, нужно добавить её в проект:

    1. Идём в настройки проекта: Project > Properties > JavaScript > Include Path > Libraries.
    2. Жмём кнопку Add JavaScript Library, выбираем User Library, а затем и библиотеки, которые хотим добавить.

    Теперь у вас в проекте будет работать code complete для указанных библиотек:

    ss051

    Поддержка jQuery

    Добавление поддержки своего любимого фреймворка оказалась не такой уж и простой задачей. Во-первых, он довольно большой и содержит внушительных объемов документацию. Во-вторых — многие методы в нём «перегружены»: например, val() возвращает текстовое значение поля, а val(str) — записывает его и возвращает уже jQuery-объект.

    К счастью, у документации к jQuery есть публичный API, который выдаёт описание всех методов в XML. Я написал парсер на node.js, который опрашивает API и преобразует XML в JSDoc, понятный для JSDT (и, надеюсь, другим IDE). Так что в случае выхода новой версии jQuery можно быстро перегенерировать всё документацию. Парсер я писал для себя и по-простому, он не поддерживает передачу параметров через командную строку, всё настраивается в main.js:

    • Можно сгенерировать документацию для определённой версии jQuery (TARGET_VERSION).
    • Можно сгенерировать описание в виде нескольких файлов. Это связано с особенностью JSDT. Как уже отмечалось ранее, в jQuery много «перегруженных» методов, и если их описать в одном JS-файле, то JSDT будет использовать только одно (самое последнее) описание. Однако если сохранить описание методов с одинаковым названием в разных файлах, то JSDT вполне неплохо покажет по ним code complete и документацию. Так что у вас есть выбор: при создании библиотеки jQuery добавить все файлы вида jquery-jsdoc-N.js (Eclipse JSDT) или же только jquery-jsdoс.js если ваша IDE способна прочитать описание с перегруженными методами.
    • Можно переписать некоторые определения для более удобной работы с code complete (class_map, prefix_map). Например, так переписываются описания для Deferred и Promise объектов, чтобы можно было использовать их описание в JSDoc:

    ss06

    В описании есть ряд классов, которые можно использовать в JSDoc: __jQueryDeferred, __jQueryPromise и __jQueryEvent.

    Пишем плагины к jQuery

    Собственно, как писать плагины рассказывать нет смысла, это подробно описано в документации. Покажу лишь как сделать так, чтобы ваш плагин появился в code complete. А сделать это очень просто, достаточно методу плагина указать, что он является членом класса jQuery:

     jQuery.extend(jQuery.fn, {	/** @memberOf jQuery */	myPlugin: function() {	} }); $('div').m // тут можно вызвать code complete 

    Поддержка Node.JS

    Отдельно стоит упомянуть Node.JS, так как к модулям нужно обращаться не напрямую, а через функцию require():

     var http = require('http'); http.createServer(function() { }); 

    То есть подсказки для модуля должны зависеть от того, какой аргумент передали в функцию require(). В целом, в JSDT эта ситуация решаема: нужно написать отдельный плагин в виде подключаемой библиотеки, который будет находить вызовы функции require(), смотреть на аргумент и возвращать виртуальный объект с необходимыми методами, и может быть я когда-нибудь напишу такую библиотеку. А пока будем довольствоваться малым: указывать тип модуля через JSDoc.

    Все модули в моей документации описаны в виде классов Node{Name}Module, например: httpNodeHttpModule, utilNodeUtilModule. Поэтому для переменной, в которую записывается модуль, указываем через JSDoc нужный тип:

     /** @type NodeHttpModule */ var http = require('http'); http. // вызываем code complete 

    Советы

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

    • Не используйте фигурные скобки при указании типа переменной с помощью тэга @type — в некоторых случаях JSDT не сможет подцепить тип переменной. Лучше писать так: @type Array (вместо @type {Array}).
    • При описании модуля тэг @memberOf moduleName достаточно указать только у одного свойства/метода в литерале, все остальные подцепятся автоматически.
    • Если у вас в проекте есть непосредственно код библиотек (например, jQuery), то его лучше исключать из индекса, чтобы он не мешал работе code complete. Делается это так: заходите в настройки проекта Project > Properties > JavaScript > Include Path > Source. Разворачиваете папку с исходниками, двойной клик по фильтру Excluded и в диалоговом окне в секцию Exclusion patterns добавляете файлы, которые надо исключить.

← старое