29 May 2013

Развиваем фронтенд Дневник.ру. Часть первая. Сборка и проверка JavaScript кода

Дневник.ру corporate blogWebsite developmentJavaScript

Вступление


За время существования Дневник.ру (а это более 4-х лет) скопился огромный объем JavaScript кода: часть находилась в отдельном проекте в виде подключаемых файлов, часть определялась прямо на разметке контролов, а часть собиралась прямо в code-behind при помощи StringBuilder. К этому прибавлялись:
  • растущее количество HTTP запросов для получения статичного контента – так, например, на всех страницах только в теге <head> загружалось 11 JavaScript файлов;
  • глобальные переменные, которые иногда перекрывали друг друга;

Решив, что с этим пора что-то делать, мы поставили себе первоочередную задачу: вынести все подключаемые по отдельности файлы из тега <head&gt в один минифицированный пакет. При этом код делился на сторонний и «наш», который планировалось проверять каким-то синтаксическим анализатором.

В этой статье мы расскажем вам о том, как решили эту задачу.

Что использовать?


Прежде всего нам предстояло определиться, с помощью каких средств мы будем организовывать автоматическую сборку этого пакета. Конечно, можно было бы использовать любую систему сборки, начиная от Ant заканчивая MSBuild; можно было написать и свой собственный простой скрипт – например, на Ruby или Python. В итоге мы решили не писать свои велосипеды и не забивать гвозди трактором, а использовать Grunt. Для тех, кто не знает: Grunt – это JavaScript task runner, работает он на node.js, а распространяется под свободной лицензией MIT. Несмотря на относительную «молодость» данного решения, оно уже успело зарекомендовать себя как прекрасный инструмент – именно его используют для сборки jQuery и QUnit, Tweetdeck в Twitter и Brackets в Adobe. Помимо этих рекомендаций у нас были и собственные причины, по которым мы выбрали именно Grunt:
  • Простота использования – для того чтобы начать с ним работать, необходимо всего лишь установить node.js.
  • Все поставленные задачи можно решать с помощью JavaScript на node.js, для синтаксической проверки использовать JSHint, для минификации кода – UglifyJS, а если заглянуть в будущее – node.js будет незаменим для модульного тестирования, проверки и сборки стилей.
  • Большой выбор плагинов для запуска различных инструментов, а также простой API для написания своих плагинов.

Кстати говоря, ни для кого уже не секрет, что наш проект работает на ASP.NET, поэтому мы рассматривали возможность использования Web Optimization Framework производного от него Bundle Transformer. Тем не менее, мы отказались от этих решений по следующим причинам:
  • с помощью этих инструментов невозможно осуществлять синтаксическую проверку кода;
  • контент, отдаваемый клиенту, формируется динамически при запросе, а эта операция в любом случае тяжелее, чем отдача веб-сервером статичного файла. Кто-то может сказать, что это экономия на спичках, но:
    во-первых, мы с этим не согласны – в нашем проекте есть довольно тяжелые операции, которые и без того нагружают сервера;
    во-вторых, сделать это чисто технически было невозможно, так как проект, в котором хранятся JavaScript файлы, не является у нас веб-приложением,
    кроме того, статические файлы нам были необходимы в связи с переходом в ближайшее время на CDN.

Однако если в будущем эти инструменты поднимутся на уровень sprockets из Ruby on Rails, то я не исключаю, что мы вернемся к их рассмотрению.

Поехали!


Итак, система для сборки выбрана и пора действовать, но перед дальнейшим повествованием стоит оговориться. Так как приложение у нас написано на ASP.NET, то большинство разработчиков работает на Windows (что неудивительно), да и процесс непрерывной интеграции, который построен у нас при помощи TeamCity (об этом мы писали в одной из предыдущих статей), также происходит на Windows. Поэтому автор просит любителей Unix-way простить его за то, что нижеследующее будет описываться именно в рамках экосистемы Windows, и воспринимать весь нижеизложенный опыт как challenge.

Установить node.js на Windows уже давно не представляет никаких проблем. Все, что для этого нужно – скачать бинарный файл с официального сайта, запустить его и потыкать в кнопку «Далее». Вместе с node.js установится и npm – пакетный менеджер, с помощью которого мы установим и Grunt, и все необходимое для его работы. Для начала создадим в проекте файл package.json, в который запишем название нашего проекта, его версию, зависимости и версию node.js. Выглядеть он будет примерно так:

{
    "name": "Dnevnik",
    "version": "0.1.0",
    "private": true,
    "dependencies": {
        "grunt": "0.4.0",
        "grunt-cli": "0.1.6", 
        "grunt-contrib-concat": "0.1.3",
        "grunt-contrib-jshint": "0.2.0",
        "grunt-contrib-uglify": "0.1.1",
        "grunt-hash": "0.2.2",
        "grunt-contrib-clean": "0.4.0"
    },
    "engines": {
        "node": "0.10.0"
    }
}

В зависимостях укажем Grunt и его версию, а также необходимые плагины. На начальном этапе мы использовали всего шесть плагинов:
  • grunt-cli – плагин для запуска Grunt из командной строки
  • grunt-contrib-concat – плагин для конкатенации списка файлов в один
  • grunt-contrib-jshint – плагин для проверки JavaScript кода с помощью утилиты JSHint
  • grunt-contrib-uglify – плагин для минификации JavaScript кода с помощью UglifyJS2
  • grunt-hash – плагин для добавления к именам файлов хеш-суммы (для того чтобы сбрасывать кэш, когда меняется содержимое файла)
  • grunt-contrib-clean – плагин для очистки директории от временных файлов и артефактов

Для установки всех пакетов с их зависимостями требуется выполнить всего лишь одну команду в консоли относительно директории, в которой находится package.json:

> npm install

После удачного завершения в ней появится папка .\node_modules, в которой и будут содержаться все необходимые модули (это стандартное название папки для модулей, устанавливаемых через npm).
Далее необходимо создать Gruntfile.js в корневой директории приложения, в нем будет содержаться вся логика работы Grunt. Его структура очень проста:

module.exports = function (grunt) {
    'use strict';

    grunt.initConfig({});

    grunt.loadNpmTasks('grunt-contrib-jshint');
    grunt.loadNpmTasks('grunt-contrib-concat');
    grunt.loadNpmTasks('grunt-contrib-uglify');
    grunt.loadNpmTasks('grunt-hash');
    grunt.loadNpmTasks('grunt-contrib-clean');

    grunt.registerTask('default', ['jshint', 'concat', 'uglify', 'hash', 'clean']);
}; 

По сути, это JavaScript сценарий для node.js, состоящий из:
  • функции-обертки, которая принимает параметр grunt,
  • функции grunt.initConfig(), в которую передаются JavaScript объект с конфигурацией всех задач,
  • функции grunt.loadNpmTasks(), которая загружает задачи из npm пакетов,
  • функции grunt.registerTask(), которая регистрирует собственные задачи.

При старте какой-либо задачи она пытается найти атрибут со своим именем в объекте, который был передан функции grunt.initConfig(), и из него получает все настройки через атрибут option и цели через остальные атрибуты. Целей в задаче может быть неограниченное количество, и каждая цель может переопределять для себя некоторые настройки. Подробнее о конфигурации задач можно прочитать в официальной документации.

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

> .\node_modules\.bin\grunt.cmd

Опционально можно передать параметр с именем задачи и цели, которую нужно выполнить. Если происходит запуск без параметров, выполнится задача с именем default.
Далее нам предстояло разбить те пресловутые 11 файлов, в которых находились различные библиотеки и jQuery плагины, на отдельные атомарные файлы. Часть из них была сжата, а для удобства разработки хотелось иметь весь код в нормальном, удобочитаемом виде. И если найти не минифицированную версию jQuery просто, то найти нужную версию какого-нибудь древнего плагина было уже не так тривиально, с этим пришлось повозиться. Однако результат стоил потраченных усилий: теперь в проекте не было минифицированного кода, и можно было без проблем залезать дебаггером даже в исходный код jQuery.

Для того чтобы у каждого из разработчиков не было необходимости устанавливать node.js и собирать пакет, мы сделали простой маппинг файл в формате JSON (конечно не самое красивое решение, но мы решили делать сначала все как можно проще), в котором имени пакета соответствовал набор из нескольких файлов:

{"package.js": ["jquery.js", "foo.js", … "bar.js"]}

При старте приложения этот файл считывался, десериализовался, и файлы добавлялись на страницу. А при сборке Grunt получал информацию о том, какие пакеты из каких файлов необходимо построить, и на конечном этапе преобразовывал его. Так вместо набора файлов в списке оказывалось одно имя собранного пакета.

После того как код для запуска Grunt был написан, а задачи подготовлены, необходимо было установить node.js на все build agents в TeamCity и попробовать его в действии, запустив через PowerShell скрипт. Для того чтобы каждый раз не скачивать из сети необходимые зависимости (не подумайте, дело не в нашей скупости на трафик – просто мы не хотим зависеть от стабильности работы интернета или репозитория npm), мы решили сохранить их в отдельную папку на каждом build agent и копировать в нужное место перед использованием. «Дёшево и сердито», – думали мы (о том, к чему это привело, читайте ниже). Однако при таком раскладе стоит учитывать, что пути в папке .\node_modules могут быть гораздо длиннее, чем максимально разрешенные в Windows 260 символов (Привет, MS-DOS), поэтому команды copy и xcopy будут вылетать с ошибкой, тут на помощь может прийти разве что robocopy c флагом /E.

С какими проблемами мы столкнулись и как их решали.


Первую свинью Grunt подсунул нам сразу же после запуска на TeamCity – мы не смогли получить лог его работы. Поковырявшись в своих PowerShell скриптах и поняв, что проблема не на нашей стороне, мы стали смотреть issue tracker в репозитории Grunt и нашли там примечательное сообщение. Оказывается, такая проблема появилась не только у нас и связана она с багом в node.js, при котором потоки stdin/stdout/stderr не блокируются в Windows. Его обещают поправить в версии 0.12.0, а пока, чтобы Grunt у нас все-таки заработал, нам пришлось прибегнуть к не очень красивому хаку: мы запускали Grunt два раза – в первый раз мы получали корректный exit code, а во второй перенаправляли поток вывода в файл, после чего выводили содержимое этого файла.

Не так давно появился патч для Grunt, исправляющий эту ошибку, но он пока что не находится в основном репозитории. Поэтому нам пришлось скачать форк прямо с Github, и тут мы столкнулись с еще одной неприятностью. Дело в том, что, когда мы только начинали работу с Grunt, в нашем парке build agents было всего три машины. Теперь их стало восемь, и копировать на каждую из них новый пакет – дело утомительное. Недолго думая, мы решили замутить у себя локальный npm репозиторий, откуда бы могли всегда быстро забирать пакеты и куда могли бы складывать свои, вне зависимости от соединения и доступности официального репозитория.

Официальный репозиторий npm работает при помощи CouchDB, и для создания локального репозитория нам было необходимо просто создать его репликацию. Мы быстро подняли виртуальную машину (опять же под управлением Windows) и поставили на нее CouchDB – благо это не сложнее, чем установить node.js. Далее, для того чтобы можно было обращаться к репозиторию из локальной сети, в конфигурационном файле CouchDB <CouchDB install directory>\etc\couchdb\local.ini необходимо изменить два значения:

secure_rewrites = false
bind_adress = 0.0.0.0

Проверить корректность настройки можно, послав обычный GET запрос на 5984 порт виртуальной машины и получив примерно такой JSON ответ:

{"couchdb":"Welcome","version":"1.2.1"}

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

> npm shrinkwrap

Она создаст файл npm-shrinkwrap.json, в котором будет находиться вся информация о проекте, включая все зависимости. Но, так как нам нужны будут только их названия, придется еще чуть-чуть потрудиться, написав небольшой рекурсивный скрипт, который их достанет из полученного файла (я не буду приводить его код, так как он невероятно банален). Получив список с названиями пакетов, нам остается выполнить обычный HTTP запрос к CouchDB для их репликации. Воспользуемся для этого утилитой curl (хотя можно воспользоваться и любой другой) и для удобства создадим JSON файл с названием deps.json со следующим содержанием:

{
    "source": "http://isaacs.iriscouch.com/registry/",
    "target": "registry", 
    "create_target": true,
    "doc_ids": ["_design/app", "_design/ghost"]
}

где значение атрибута "doc_ids" нужно дополнить списком необходимых зависимостей (пакеты "_design/app", "_design/ghost" организуют работу базы как репозитория). А теперь просто выполняем следующую команду в консоли:

> .\curl.exe -X POST http://user:password@npm:5984/_replicate -d@deps.json -H "Content-Type: application/json"

Ответ от сервера будет опять же в JSON формате, и в нем стоит обратить внимание на два атрибута: "ok" и "doc_write_failures". Если в первом значение true, а во втором 0, то репликация пакетов прошла успешно.

Все, что после этого нам оставалось сделать, – опубликовать полученный с Github форк с патчем для Grunt. Для этого нужно изменить название версии в файле package.json этого форка, зарегистрировать в локальном репозитории пользователя с помощью команды:

> npm adduser --registry="http://npm:5984/registry/_design/app/_rewrite/"

И опубликовать пакет:

> npm publish --registry="http://npm:5984/registry/_design/app/_rewrite/"

Все, дело сделано, пакет опубликован в нашем локальном репозитории, нужно только не забыть изменить его версию в package.json проекта.
Теперь для установки всех пакетов можно (и нужно) использовать следующую команду:

> npm install --registry="http://npm:5984/registry/_design/app/_rewrite/"

Кстати, не так давно мы отказались от использования PowerShell скрипта для запуска Grunt на TeamCity и перешли на использование плагина для него. Плагин называется TeamCity.Node и позволяет запускать node.js скрипты, npm, Grunt и PhantomJS на TeamCity, при этом он проверяет, установлен ли node.js и npm на build agent. Пока его работой мы абсолютно довольны, хотя бы потому, что именно с его помощью мы узнали, что забыли поставить node.js на один из агентов.

Что дальше?


Ждем выхода node.js 0.12 и Grunt 0.5, в которых описанные выше ошибки должны быть исправлены. А наш план на будущее примерно такой: во-первых, нам необходимо отказаться от использования маппинг файла, во-вторых, необходимо переместить весь JavaScript код из контролов в отдельные файлы, для того чтобы уменьшить количество кода и улучшить его поддержку.

Но об этом мы расскажем в наших следующих статьях.
Tags:дневник.руwindowsjavascriptnode.jsnpmgruntcouchdbteamcity
Hubs: Дневник.ру corporate blog Website development JavaScript
+16
13.8k 115
Comments 19
Top of the last 24 hours