пятница, 20 мая 2011 г.

Node.JS: Учимся создавать маленький парсер шаблонов

Приветствую тебя, читатель. Ещё давеча я даже не знал о существовании Node.js, но зато успел увесисто поработать с JavaScript, после которого так не хочется писать на чём-либо другом. Многие, кто вник в JS и его дзен, что всё есьм — объект, в замыкания, анонимные функции, предпочитают работать только с ним. Однажды с товарищем обменивались мыслями о PHP, покритиковали скудные и неудобные возможности ООП, и я тут выдал: «вот бы вместо PHP на серверной стороне писать на JS», — на что получил мгновенный ответ: «nodejs тебе в руки». Не долго думая я начал рыть по wiki и другим ресурсам, далее в кротчайший срок я поднял Ubuntu Server на Virtual Box и поставил туда сам Node.js.

Сразу предупреждаю, что если вы не работали с JavaScript или просто скудно с ним знакомы, начните именно с него, иначе Node.js может показаться вам тёмным лесом. Хочу познакомить вас с языком сразу на практике, и так же показать идею по опять же практическому применению. Я приведу пример реализации маленького парсера шаблонов, всё это уложится в 2-а небольших файла, нарочито без излишеств, чтобы наглядно и прозрачно посмотреть «как это работает».

Итак, концепт таков: мы будем использовать вставки в шаблоне следующего вида:
<script type="nodejs">100+235;</script>
Этот код выведет 335 на экран. Почему именно такой приём, а не например: <%=100+235%>? В этом и заключается идея, во первых, в этих тегах будет заключаться обычный JS-код, и такая конструкция будет поддерживать подсветку синтаксиса в редакторах без каких-либо ритуалов жертвоприношений с бубном. Почему именно JS-код? «Ведь это же варварство!» Ну мне лично нравится модель шаблонов 1С-Битрикса, по-моему очень удобно, просто делать условия и циклы на том языке, что знаешь, а не учить каждый раз новый синтаксис шаблонизатора и влезать в его тесные рамки. Плюс ко всему это же JS! Ловите мысль? Обыкновенно верстальщики как верстают, так и пишут на JS, а тут вставки, с тем же JS, то-есть они как бы и верстать продолжают, только зная что их код заменится на серверной стороне тем, что они в нём вернут в конце манипуляции. По мне так лучшее решение.

Теперь наконец перейдём к любимой практике, поэтапно напишем код с описанием действий. Первым делом создайте два файла: start.js для нашего скрипта и template.html для шаблона. Дальнейший код пишем в start.js. Подключим встроенные модули Node.js, которые необходимы для работы примера:
var http = require('http'); // для создания сервера и работы с запросами
var fs = require('fs'); // для работы с файлами, в частности с файлом шаблона
var vm = require('vm'); // для исполнения JS-кода в шаблонах, изолированно от внешней среды скрипта

Обратите внимание, что для Node.js не требуется никаких Апачей и прочего, если вы сравниваете его с PHP, то это иная стихия. Сервер поднимается прямо в скрипте и создаётся при выполнении скрипта из консоли. Объявим переменную для создания сервера и так же сразу объявим для примера JSON-структуру данных для экспорта в шаблон:
var host = {
    addr: '127.0.0.1',
    port: 8070
};

var jsonExport = {
    title: 'Мой первый парсер',
    list: [
        {name:'Первый пункт списка', msg:'это только начало'},
        {name:'Следующий', msg:'продолжение'},
        {name:'Третий', msg:'и так сколько пожелаете!'}
    ]
};
Обратите внимание на порт 8070, это на всякий случай, если 80-ый занят у вас Apache-м, или ещё чем-нибудь. Если вы уверены, что порт свободен, можете и 80-ый поставить. 127.0.0.1 — это локальная машина, и доступен сервер будет только в её рамках, этого нам пока достаточно.

Далее создаём сам сервер с обработчиком запросов от браузера:
var server = http.createServer(function (request, response) {
    // код обработчика
});

Пока проскочим код обработчика и сразу запустим прослушку порта 8070 на локальной машине и отправим отчёт в консоль:
server.listen(host.port, host.addr);
console.log('Сервер успешно запущен: '+host.addr+':'+host.port);

Теперь пора описать код обработчика:
response.writeHead(200, {'content-type':'text/html; charset=utf-8'});
fs.readFile('./template.html', function (err, data) {
    // код обработки шаблона
});
Первая строчка — возврат в заголовках 200-ого ответа сервера, аля OK, всё хорошо. Второй параметр объектом передаёт остальные заголовки, в данном случае тип контента и кодировку. Следующая строка — это чтение файла шаблона и обработчик.

И как вы уже могли догадаться — код обработки шаблона, завершающая часть скрипта start.js:
var data = data.toString();
var dataArr = data.match(/\<script type=(nodejs|'nodejs'|"nodejs")\>([^\0]+?)<\/script\>/gim);
for (var i=0; i<(dataArr.length||0); i++) {
    var code = dataArr[i].replace(/^\<script([^\>]+)\>([^\0]+?)<\/script\>$/im, '$2');
    data = data.replace(/\<script type=(nodejs|'nodejs'|"nodejs")\>([^\0]+?)<\/script\>/im,
        vm.runInNewContext(code, jsonExport));
}
response.end(data);
1-ая строка — преобразуем полученный контент файла в строку, чтобы работать с регами. Подробностей о регах писать не буду, только отмечу ключи у рег, для понимания происходящего.
2-ая строка — создаем массив из найденный кусков кода парсера, который нужно обработать и заменить результатом выполнения. Ключи gim, g — более одного совпадения, i — независимость от регистра символов, m — многострочный поиск (вместо поиска в отдельных строках). По поводу рег, хочу отметить только один мой приём, который я часто использую, он может вам пригодиться. Символ точки захватывает все символы, кроме переводов строк, в данном же случае нам нужны и переводы строк! И вместо точки мы пишем: [^\0], — что значит всё, что не равняется null (пустоте). Интересно, да? Можно и просто [^], но если применять это в JavaScript, конечно же Internet Explorer выдаст ошибку, потому используем первый, более длинный вариант.
3-я строка — цикл, проходим по массиву найденных вызовов кода парсера.
1-ая строка цикла — у текущего кода парсера вытаскиваем код между <script*>...</script> и складываем в переменную code.
2-ая и 3-я строка цикла — в переменной data заменяем текущий вызов парсера на результат выполнения с помощью модуля vm, который выполняет заданный код в определённом контексте. В данном случае он передал нашу структуру JSON в виде этого контекста. Заметьте, что рега без ключа g, чтобы работать только с текущей вставкой, так как мы производим замену во всём коде целиком.

Наш файл start.js готов, вот его цельный вид:
var http = require('http');
var fs = require('fs');
var vm = require('vm');

var host = {
    addr: '127.0.0.1',
    port: 8070
};

var jsonExport = {
    title: 'Мой первый парсер',
    list: [
        {name:'Первый пункт списка', msg:'это только начало'},
        {name:'Следующий', msg:'продолжение'},
        {name:'Третий', msg:'и так сколько пожелаете!'}
    ]
};

var server = http.createServer(function (request, response) {
    response.writeHead(200, {'content-type':'text/html; charset=utf-8'});
    fs.readFile('./template.html', function (err, data) {
        var data = data.toString();
        var dataArr = data.match(/\<script type=(nodejs|'nodejs'|"nodejs")\>([^\0]+?)<\/script\>/gim);
        for (var i=0; i<(dataArr.length||0); i++) {
            var code = dataArr[i].replace(/^\<script([^\>]+)\>([^\0]+?)<\/script\>$/im, '$2');
            data = data.replace(/\<script type=(nodejs|'nodejs'|"nodejs")\>([^\0]+?)<\/script\>/im,
                vm.runInNewContext(code, jsonExport));
        }
        response.end(data);
    });
});

server.listen(host.port, host.addr);
console.log('Сервер успешно запущен: '+host.addr+':'+host.port);

А дальше, недолго думая, пишем в файл template.html целиком следующий код (пояснений по вёрстке я думаю не требуется):
<html>
<head>
    <meta http-equiv="content-type" content="text/html; charset=utf-8">
    <title><script type="nodejs">title;</script></title>
</head>

<body>
    <h1><script type="nodejs">title;</script></h1>
    <script type="nodejs">
        var out = '';
        out += '<ul>\n';
        for (var i=0; i<list.length; i++) {
            out += '\t\t<li><b>'+list[i].name+'</b> '+list[i].msg+'</li>\n';
        }
        out += '\t</ul>';
        out;
    </script>
</body>
</html>
Я думаю проанализировать этот код самим вам будет несложно. Во вставках просто выводятся значения по ключам из нашей JSON структуры контекста. Внизу выводится список, сначала всё складывается в локальную переменную, а потом она возвращается в конце для вывода.

Итак, теперь немного магии, набираем в консоли:
node start.js
Заходим на http://127.0.0.1:8070 и любуемся! Успехов с экспериментами!

P.S. Не забывайте, что это только пример, тут даже нет обработчиков ошибок и прочего, пример очень минималистичен.

UPD 2011/05/26: Первоначально не учёл, что метод match у строки, в случае отсутствия совпадений — возвращает null. И если совпадений нет (а в нашем случае шаблонных вставок), то нет и метода length у переменной dataArr, которая используется в цикле, что повергало запрос в бесконечный цикл. Исправил:
for (var i=0; i<dataArr.length; i++) {
На:
for (var i=0; i<(dataArr.length||0); i++) {

1 комментарий: