- PVSM.RU - https://www.pvsm.ru -
Возникла у нас на проекте прихоть — рисовать на стороне сервера графики, да не простые, а максимально похожие на уже имеющиеся графики на клиентской стороне.
Да-да, именно так, на клиенте уже были всевозможные красивости, реализованные на d3.js [1].
Для исследования возможностей был применен комплексный метод анализа «google-driven investigation» и в первой итерации выбор пал на ноду [2] + фантом [3].
За подробностями прошу в глубины поста.
Раскажу вкратце о проекте, чтобы обрисовать ситуацию. Наша фирма нашла BigData-стартап, команда выйграла тендер и теперь мы вчетвером пилим аналитику в облаке для тяжеловесных датасетов.
Наш зоопарк состоит из кластеров на AWS с автодеплоем, Scala [4], Spark [5], Shark [6], Mesos [7], NodeJS и прочих страшных технологий (я надеюсь, такой проект позволит мне и моим коллегам утолить интеллектуальный голод и понаписать пару статей).
У нас недельные итерации и ретроспектива + демо в конце недели. Это накладывает ряд ограничений на исследования и поиск лучших практик.
На момент реализации решения у нас уже были «цифрожевалки» [8] на скале и рест-сервисы на ноде.
В ходе беглого изучения проблемы было обнаружено три варианта:
Вариант №1 оставлял открытым вопрос о css-стилях и общей целесообразности (не нодовское это призвание процессор рассчетами загружать).
Вариант №2 показался разумным, но гарантирующим продолжительную боль в области седалищного нерва.
Было решено использовать Вариант №3.
Последующие изучение и эксперименты показали, что:
Оказалось, что внешнего апи у фантома вообще нет. Даже для ноды. Но внутренний апи эмулируется через socket.io и переопределением обработчика alert'а на странице, открытой в фантоме.
Автору уважуха за находчивость!
Алгоритм примерно такой:
Углубившись в вариант «фантом + нода», я выяснил, что можно заиспользовать уже имеющийся javascript-код клиента для построения графиков на стороне сервера.
Фантом — это вебкит с полноценной реализацией дом-дерева, стилей и джаваскрипта. И он позволяет делать снимки отрисованной страницы. Такое решение позволяет вообще не дублировать код построения графики!
Методом научного тыка удалось выяснить, что:
page.indectJs
) скрипты в страницу только по полному пути на файловой системе.
page.includeJs
) скрипты в страницу по полному урлу, но в модуле контракт внутреннего API page.includeJs испорчен из-за особенностей реализации.
<link>
к заголовку страницы.
Я использую модуль vow [14] vow для уменьшения «макаронности» [15] кода. Плохо или хорошо использую — отпишите в комментариях!
// подключаем модуль для работы с фантомом (все зависимости объявлены в package.json)
var phantom = require("node-phantom")
// промисы
, vow = require("vow")
// конфиг нашего рест-сервера
, cfg = require("../config")
// родной модуль работы с файловой системой
, fs = require('fs')
// глобальная ссылка на процесс фантома
, pi;
// я создаю один процесс фантома сразу при старте приложения
exports.init = function () {
if (pi) {
pi.exit();
}
phantom.create(function (err, instance) {
pi = instance;
});
}
// эта функция дергается в других местах приложения - точка входа
exports.render = function (dataset, opts) {
var promise = vow.promise();
// для каждого графика открывается новая страница
pi.createPage(function (err, page) {
// мы можем определить размер области снимка страницы, если нужно
page.set("viewportSize", opts.viewport);
// полный путь к d3 на файловой системе (см. спойлер "подводные камни")
var d3Path = __dirname + "/../client/scripts/vendor/d3.v3.js";
// полный путь к клиентскому скрипту, строящему график на d3
// type - это тип графика (line, bar, pie)
// каждый файл chart.xxx.js содержит метод рисования конкретного графика
var chartJs = __dirname + "/../client/scripts/chart." + opts.type + ".js";
// полный путь к файлу стилей для графика
var chartCss = __dirname + "/../client/styles/charts.css";
var innerStyle = "";
// наша логика
// как вам такой код? читаем? отзывы в комментарии
injectLib_(page, d3Path)()
.then(injectLib_(page, chartJs))
.then(readCssStyles_(chartCss))
.then(drawChart_(page, {dataset: dataset, innerCss: innerStyle}, opts))
.then(function (res) {
// если все ок, то возвращаем путь к сохраненному графику
promise.fulfill({filename: res.filename});
})
.fail(function (err) {
promise.reject(err)
}
)
});
return promise;
}
// считываем стили из файла в буфер (строку)
// зачем так - смотрите в спойлере "подводные камни"
function readCssStyles_(chartCss) {
return function(){
var prom = vow.promise();
fs.readFile(chartCss, 'utf8', function (err,innerCss) {
if (err) {
console.log(chartCss + ": read failed, err: " + err);
prom.reject(chartCss + ": read failed, err: " + err);
} else {
console.log(chartCss + " read");
prom.fulfill(innerCss);
}
});
return prom;
}
}
function injectLib_(page, path) {
return function () {
var prom = vow.promise();
// этот вызов вставит скрипт в страницу, но не выполнит его до вызова page.evaluate
page.injectJs(path, function (err) {
if (err) {
console.log(path + " injection failed")
prom.reject(path + " injection failed");
} else {
console.log(path + " injected")
prom.fulfill();
}
});
return prom;
}
}
function drawChart_(page, data, opts) {
return function (innerCss) {
data.innerCss = innerCss;
var prom = vow.promise();
// этот метод выполнит все скрипты на странице в фантоме
// первая функция - это "эвалюатор". Его код будет выполнен в контексте страницы
// эвалюатор сериализуется, поэтому его можно писать на джаваскрипте, а не строкой
page.evaluate(function (data) {
// данные передаются только через сериализацию в строку
// это обратный процесс
data = JSON.parse(data);
// так выглядит вызов построения нашего графика
// этот апи определен в charts.xxx.js
charts.line("body",data.dataset);
// нам надо вставить стили, которые мы прочитали из файла стилей и передали строкой
var style = document.createElement("style");
style.innerHTML = data.innerCss;
document.getElementsByTagName("head")[0].appendChild(style);
}
, function (err, result) {
if (err) {
prom.reject("phantomjs evaluation failed : " + err)
}
// зададим путь для сохранения отрендеренного графика в файл на локальной файловой системе
// фантом поддерживает png, pdf, gif и jpeg
var filename = cfg.server.chartsPath + '/' + opts.type + "_" + Date.now() + ".png";
var savingPath = "client" + filename;
// этот метод непосредственно рендерит и сохраняет страницу
page.render(savingPath, function (err, res) {
console.log("Saving image: " + filename);
page.close();
prom.fulfill({filename: filename});
});
}, JSON.stringify(data));
return prom;
}
}
Автор: MrMig
Источник [16]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/javascript/35212
Ссылки в тексте:
[1] d3.js: http://d3js.org/
[2] ноду: http://nodejs.org
[3] фантом: http://phantomjs.org/
[4] Scala: http://en.wikipedia.org/wiki/Scala_(programming_language)
[5] Spark: http://spark-project.org/
[6] Shark: http://shark.cs.berkeley.edu/
[7] Mesos: http://incubator.apache.org/mesos/
[8] «цифрожевалки»: http://www.merriam-webster.com/dictionary/number%20cruncher
[9] был найден: http://eng.wealthfront.com/2011/12/converting-dynamic-svg-to-png-with.html
[10] скала-аналоги: https://github.com/wookietreiber/scala-chart
[11] несвежий модуль: https://github.com/sgentle/phantomjs-node
[12] express: http://expressjs.com/
[13] node-phantom: https://github.com/alexscheelmeyer/node-phantom
[14] vow: https://github.com/dfilatov/jspromise
[15] «макаронности»: http://en.wikipedia.org/wiki/Spaghetti_code
[16] Источник: http://habrahabr.ru/post/180927/
Нажмите здесь для печати.