В процессе приготовления я буду использовать MS Windows. Аналогичный процесс в осях семейства Linux почти не отличается, за исключением некоторых моментов, рассматривать которые в рамках настоящего изложения я не планирую.
Довольно лирики, приступим.
Открываем консоль. Каталог приложения по имени chart я создал заранее.
Установим модули mysql (понятно для чего) и colors (раскрасим вывод консоли).
Проверим подключение к базе данных. У меня установлен denwer с настройками по умолчанию, поэтому подключение от имени root без пароля.
var util = require('util');
var colors = require('colors');
var mysql = require('mysql');
var pool = mysql.createPool({
host:'127.0.0.1',
user:'root',
password:'',
database:'test_db',
insecureAuth: true,
debug: true
});
pool.getConnection(function(err, connection) {
if (err) {
util.log('pool.getConnection error:'.red + '\n' + err);
process.exit(1);
}
util.log('Connection set successfully'.green);
connection.release();
process.exit(0);
});
Разумеется на забываем запустить MySQL и изменить настройки подключения.
В случае успеха наблюдаем следующую картину:
Создадим базу данных и таблицы. Я использую phpMyAdmin.
create database if not exists test_db character set utf8 collate utf8_general_ci; use test_db; create table if not exists `consumer` ( `id` mediumint unsigned not null auto_increment, `name` varchar(20) not null, primary key(`id`) ); create table if not exists `money` ( `date` date not null, `consumer_id` mediumint not null, `sum` float(9,2) unsigned not null, primary key(`date`, `consumer_id`) ); create table if not exists `user` ( `id` smallint unsigned not null auto_increment, `name` varchar(20) not null, `pw` varchar(32) not null, primary key(`id`) );
Добавим тестовые данные, которые будут олицетворять существующие данные вашей организации.
var util = require('util'),
colors = require('colors'),
crypto = require('crypto');
var mysql = require('mysql');
var pool = mysql.createPool({
host:'127.0.0.1',
user:'root',
password:'',
database:'test_db',
insecureAuth: true,
debug: true
});
// проверяем подключение, в случае ошибки выполнять следующий код не имеет смысла
pool.getConnection(function(err, connection) {
if (err) {
util.log('pool.getConnection error:'.red + '\n' + err);
process.exit(1);
}
util.log('Connection set successfully'.green);
connection.release();
});
// закидываем тестовые данные
pool.getConnection(function(err, connection) {
if (err) {util.log('pool.getConnection error:'.red + '\n' + err); return;}
var val = [['microsoft'],['google'],['apple']];
connection.query('INSERT INTO `consumer` (`name`) VALUES ?', [val], function(err, result) {
if (err) {util.log('connection.query error:'.red + '\n' + err); return;}
val = [];
for (var i=0; i<30; i++) { // рандомные даты, consumer_id и суммы
val.push(['2013-09-0' + rnd(5,9), rnd(1,3), rnd(5000,10000) + '.' + rnd(1,99)]);
}
connection.query('INSERT INTO money VALUES ? ON DUPLICATE KEY UPDATE `sum` = `sum` + VALUES(`sum`)', [val], function(err, result) {
if (err) {util.log('connection.query error:'.red + '\n' + err); return;}
val = [['user', crypto.createHash('md5').update('pw').digest('hex')]];
connection.query('INSERT INTO user (name, pw) VALUES ?', [val], function(err, result) {
if (err) {util.log('connection.query error:'.red + '\n' + err); return;}
connection.release();
process.exit(0);
} );
});
});
});
function rnd(from, to) {
return Math.floor(Math.random()*(to-from+1)+from);
}
В результате у нас три таблицы с данными:
- consumer - потребители наших сервисов
- user - пользователи сервиса отчетов
- money - без комментариев :)
Продолжаем разговор. Установим глобально express:
- npm install express -g
Cоздадим каркас приложения:
- express ..\chart
Установим express и jade локально:
- npm install
Cмотрим что получилось. Запускаем приложение:
- node app
В браузере идем по адресу http://localhost:3000/:
Каркас готов. Разберемся как это работает.
Посмотрим из чего состоит файл package.json, который находится в корневом каталоге приложения.
{
"name": "application-name",
"version": "0.0.1",
"private": true,
"scripts": {
"start": "node app.js"
},
"dependencies": {
"express": "3.3.8",
"jade": "*"
}
}
Нетрудно догадаться что к чему. Поясню только, что "dependencies" - модули express и jade с целью портабельности приложения были установлены локально ранее в процессе выполнения команды npm install в каталог node_modules.
Откроем файл запуска приложения - app.js.
/**
* Module dependencies.
*/
var express = require('express');
var routes = require('./routes');
var user = require('./routes/user');
var http = require('http');
var path = require('path');
var app = express();
// all environments
app.set('port', process.env.PORT || 3000);
app.set('views', __dirname + '/views');
app.set('view engine', 'jade');
app.use(express.favicon());
app.use(express.logger('dev'));
app.use(express.bodyParser());
app.use(express.methodOverride());
app.use(app.router);
app.use(express.static(path.join(__dirname, 'public')));
// development only
if ('development' == app.get('env')) {
app.use(express.errorHandler());
}
app.get('/', routes.index);
app.get('/users', user.list);
http.createServer(app).listen(app.get('port'), function(){
console.log('Express server listening on port ' + app.get('port'));
});
Получив запрос наш http-сервер отправил его маршрутизатору (app.get('/', routes.index);), который приложение получило с помощью инструкции var routes = require('./routes'); .Рассмотрим код маршрутизатора - файл index.js в каталоге routes.
/*
* GET home page.
*/
exports.index = function(req, res){
res.render('index', { title: 'Express' });
};
Маршрутизатор рендерит содержимое файла index.jade из каталога views с помощью движка jade (app.set('view engine', 'jade');) и отправляет его в ответ на запрос.
Открываем файл index.jade.
extends layout
block content
h1= title
p Welcome to #{title}
Контент наследует макет из файла layout.jade. Откроем файл макета.
doctype 5 html head title= title link(rel='stylesheet', href='/stylesheets/style.css') body block content
Приложение раздает статические файлы из каталога public (app.use(express.static(path.join(__dirname, 'public')));).
Макет цепляет стиль из файла style.css, который находится в каталоге public/stylesheets/.
Теперь, когда механизм работы приложения стал немного яснее, переходим к кодингу.
Начнем с аутентификации. В корневом каталоге приложения создаем файл модуля работы с базой данных MySQL по имени db.js.
var crypto = require('crypto');
var mysql = require('mysql');
var pool = mysql.createPool({
host:'127.0.0.1',
user:'root',
password:'',
database:'test_db',
insecureAuth: true
});
// аутентификация
module.exports.auth = function(user, pw, cb) {
pool.getConnection(function(err, connection) {
if (err) {throw err; return;}
connection.query('SELECT * FROM user WHERE name = ? AND pw = ?', [user, crypto.createHash('md5').update(pw).digest('hex')], function(err, data) {
if (err) {throw err; return;}
connection.release();
cb(null /* error */, data.length);
});
});
}
Редактируем код файла запуска приложения app.js.
var express = require('express');
var routes = require('./routes');
var http = require('http');
var path = require('path');
var app = express();
var db = require('./db'); // для аутентификации
// all environments
app.set('port', process.env.PORT || 3000);
app.set('views', __dirname + '/views');
app.set('view engine', 'jade');
app.use(express.favicon());
app.use(express.logger('dev'));
app.use(express.bodyParser());
app.use(express.methodOverride());
// аутентификация
var auth = express.basicAuth(function(user, pw, cb) {
if (!user || !pw) {cb(null, false); return;}
db.auth(user, pw, cb);
});
app.use(app.router);
app.use(express.static(path.join(__dirname, 'public')));
// development only
if ('development' == app.get('env')) {
app.use(express.errorHandler());
}
app.get('/', auth, routes.index);
http.createServer(app).listen(app.get('port'), function(){
console.log("Express server listening on port %d in %s mode", app.get('port'), app.settings.env);
});
Запускаем приложение, тестируем.Пытаемся авторизоваться.
Вспоминаем, что на этапе заполнения таблиц тестовыми данными в таблицу user мы закинули пользователя user с паролем pw.
Займемся контентом.
В каталоге view создадим следующие файлы (по умолчанию присутствуют только index.jade и layout.jade):
По алфавиту:
- about.jade - о себе любимом - единственный контент, доступ к которому не требует авторизации
extends layout block content h3 Designed by Anatoly Demidovich
- bar.jade - контент диаграммы Bar Chart, наследует от layout.jade
extends layout block content include ./bar-in
- bar-in.jade - инклуд диаграммы Bar Chart, не наследует от layout.jade, содержит клиентский скрипт визуализации
#bar_div
script.
google.load('visualization', '1', {packages:['corechart']});
google.setOnLoadCallback(drawTable);
function drawTable() {
var data = new google.visualization.DataTable();
data.addColumn('string', 'Date');
var mydata =!{JSON.stringify(mydata1)};
var tmp = [], tmpd = []; // временные массивы для уникальных значений
for (var i=0; i<mydata.length; i++) { // ищем уникальных потребителей
if (tmp.indexOf(mydata[i].name) === -1) {
tmp.push(mydata[i].name);
data.addColumn('number', mydata[i].name);
}
}
for (var i=0; i<mydata.length; i++) {
if (tmpd.indexOf(mydata[i].date) === -1) { // ищем уникальные даты
data.addRow();
data.setCell(tmpd.length, 0, new Date(mydata[i].date).toLocaleDateString());
tmpd.push(mydata[i].date);
}
for (var j=0; j<tmp.length; j++) {
if (tmp[j] === mydata[i].name) {
data.setCell(tmpd.length-1, j+1, mydata[i].sum);
break;
}
}
}
var table = new google.visualization.BarChart(document.getElementById('bar_div'));
table.draw(data);
}
- column.jade - контент диаграммы Column Chart, наследует от layout.jade
extends layout block content include ./column-in
- column-in.jade - инклуд диаграммы Column Chart, не наследует от layout.jade, содержит клиентский скрипт визуализации
#column_div
script.
google.load('visualization', '1', {'packages':['corechart']});
google.setOnLoadCallback(drawTable);
function drawTable() {
var data = new google.visualization.DataTable();
data.addColumn('string', 'Name');
data.addColumn('number', 'Sum');
var mydata =!{JSON.stringify(mydata2)}
for (var i=0; i<mydata.length; i++) {
data.addRow([mydata[i].name, mydata[i].sum]);
}
var table = new google.visualization.ColumnChart(document.getElementById('column_div'));
table.draw(data);
}
- index.jade - контент главной страницы приложения, включает в себя все диаграммы приложения, наследует от layout.jade
extends layout block content include ./table-in include ./bar-in include ./pie-in include ./column-in
- layuot.jade - макет приложения, включает в себя ссылки на внешние библиотеки, файл стиля и меню навигации
doctype 5 html head title= title link(rel='stylesheet', href='stylesheets/style.css') script(src='http://code.jquery.com/jquery-1.9.1.js') script(src='http://code.jquery.com/ui/1.10.3/jquery-ui.js') script(src='https://www.google.com/jsapi') body include ./nav hr block content hr a(href='./about') About
- nav.jade - меню навигации приложения, также содержит jQuery Datepicker и клиентский скрипт
a(href='./') All
a(href='./table') Table
a(href='./bar') Bar
a(href='./pie') Pie
a(href='./column') Column
form
label(for='from') Date from:
input#from_dt(type='text', name='from')
label(for='to') Date to:
input#to_dt(type='text', name='to')
input#submit(type='submit', value='Submit')
script.
$(document).ready(function() {
$("#from_dt").datepicker({ dateFormat: 'mm-dd-yy'});
$("#to_dt").datepicker({ dateFormat: 'mm-dd-yy'});
});
- pie.jade - контент диаграммы Pie Chart, наследует от layout.jade
extends layout block content include ./pie-in
- pie-in.jade - инклуд диаграммы Pie Chart, не наследует от layout.jade, содержит клиентский скрипт визуализации
#pie_div
script.
google.load('visualization', '1', {'packages':['corechart']});
google.setOnLoadCallback(drawTable);
function drawTable() {
var data = new google.visualization.DataTable();
data.addColumn('string', 'Name');
data.addColumn('number', 'Sum');
var mydata =!{JSON.stringify(mydata2)}
for (var i=0; i<mydata.length; i++) {
data.addRow([mydata[i].name, mydata[i].sum]);
}
var options = {is3D: true};
var table = new google.visualization.PieChart(document.getElementById('pie_div'));
table.draw(data, options);
}
- table.jade - контент визуализации Table, наследует от layout.jade
extends layout block content include ./table-in
- table-in.jade - инклуд визуализации Table, не наследует от layout.jade, содержит клиентский скрипт визуализации
#table_div
script.
google.load('visualization', '1', {packages:['table']});
google.setOnLoadCallback(drawTable);
function drawTable() {
var data = new google.visualization.DataTable();
data.addColumn('date', 'Date');
data.addColumn('string', 'Name');
data.addColumn('number', 'Sum');
var mydata =!{JSON.stringify(mydata1)}
for (var i=0; i<mydata.length; i++) {
data.addRow([new Date(mydata[i].date), mydata[i].name, mydata[i].sum]);
}
var options = {'showRowNumber': true};
var table = new google.visualization.Table(document.getElementById('table_div'));
table.draw(data, options);
}
Не забываем, что jade использует отступы в два пробела для определения местонахождения HTML-элементов. По этому поводу есть версия, на мой взгляд вполне правдоподобная, что jade придумали "питонисты" (поклонники языка python).
Как вы уже наверняка заметили, в jade-шаблоны данные прилетают в виде JSON:
- var mydata =!{JSON.stringify(mydata1)} , или var mydata =!{JSON.stringify(mydata2)},
после чего обрабатываются клиентским скриптом.
Отправлять эти самые данные в шаблоны мы будем из маршрутизатора.
Открываем файл index.js в каталоге routes, редактируем.
var db = require('../db'),
conf = require('../conf');
// главная страница - все диаграммы
exports.index = function(req, res){
var obj = {'title': 'All Charts'};
db.get(req, conf['table']['query'], function (data1) {
obj['mydata1'] = data1;
db.get(req, conf['pie']['query'], function (data2) {
obj['mydata2'] = data2;
res.render('index', obj);
});
});
};
// отдельная диаграмма
exports.chart = function(req, res){
var obj = {'title': req.params.page.charAt(0).toUpperCase() + req.params.page.substr(1) + ' Chart'};
db.get(req, conf[req.params.page]['query'], function (data) {
obj[conf[req.params.page]['data']] = data;
res.render(req.params.page, obj);
});
};
exports.about = function(req, res){
res.render('about', {'title': 'About'});
}
В корневом каталоге приложения создадим файл conf.js, в котором будем хранить настройки приложения, в том числе текст запросов серверу MySQL. Маршрутизатор импортирует эти настройки (conf = require('../conf');) и использует их для отправки запросов, после чего отправляет полученные данные движку jade (res.render('index', obj);).
Редактируем созданный ранее модуль работы с базой данных, функцию get() которого активно использует наш маршрутизатор в процессе получения данных сервера MySQL.
В коде файла запуска приложения app.js растасуем маршруты согласно функциям, экспортируемым маршрутизатором:
var q1 = 'SELECT m.date, c.name, m.sum FROM money m JOIN consumer c ON c.id = m.consumer_id WHERE `date` >= DATE(?) AND `date` <= DATE(?) ORDER BY m.date',
q2 = 'SELECT c.id, c.name, SUM(m.sum) as sum FROM money m JOIN consumer c ON c.id = m.consumer_id WHERE `date` >= DATE(?) AND `date` <= DATE(?) GROUP BY c.id ORDER BY c.id';
module.exports = {
'table': {'query': q1, 'data': 'mydata1'},
'bar': {'query': q1, 'data': 'mydata1'},
'pie': {'query': q2, 'data': 'mydata2'},
'column': {'query': q2, 'data': 'mydata2'}
};
Редактируем созданный ранее модуль работы с базой данных, функцию get() которого активно использует наш маршрутизатор в процессе получения данных сервера MySQL.
var crypto = require('crypto');
var mysql = require('mysql');
var pool = mysql.createPool({
host:'127.0.0.1',
user:'root',
password:'',
database:'test_db',
insecureAuth: true
});
// аутентификация
module.exports.auth = function(user, pw, cb) {
pool.getConnection(function(err, connection) {
if (err) {throw err; return;}
connection.query('SELECT * FROM user WHERE name = ? AND pw = ?', [user, crypto.createHash('md5').update(pw).digest('hex')], function(err, data) {
if (err) {throw err; return;}
connection.release();
cb(null /* error */, data.length);
});
});
}
// запросы
module.exports.get = function getData(req, q, cb) {
pool.getConnection(function(err, connection) {
if (err) {throw err; return;}
var from = req.query.from ? new Date(req.query.from) : new Date();
var to = req.query.to ? new Date(req.query.to) : new Date();
connection.query(q, [from, to], function(err, data) {
if (err) {throw err; return;}
connection.release();
cb(data);
});
});
}
В коде файла запуска приложения app.js растасуем маршруты согласно функциям, экспортируемым маршрутизатором:
app.get('/', auth, routes.index);
app.get('/:page((table|bar|pie|column))', auth, routes.chart);
app.get('/about', routes.about);
В итоге код файла app.js выглядит следующим образом:
Я решил не использовать стандартную тему виджета jQuery Datepicker, поэтому посвятил ему несколько строчек кода css, приведенного выше.
Сервис отчетов готов к употреблению. Тестируем:
- стартуем приложение: node app
- посещаем адрес http://localhost:3000/, в случае необходимости авторизуемся - логин: user, пароль: pw
- выбираем даты в диапазоне 05.09.2013 - 09.09.2013 (поле `date` таблицы `money`)
- правда нарядный календарик получился? подтверждаем отправку данных формы на сервер - нажимаем Submit... ta-da :) (см. вступительную картинку)
- выбираем, к примеру, Pie Chart:
- не забываем нажать на кнопочку About :)
В реальных условиях запускаем сервис с помощью forever (я сто раз так делал) или pm2 (ни разу не пробовал, нужно найти время, по описанию - классный модуль).
Манагеры в восторге, Рио де Жанейро, белые парусиновые штаны, креолки и мулатки :).
var express = require('express');
var routes = require('./routes');
var http = require('http');
var path = require('path');
var app = express();
var db = require('./db'); // для аутентификации
// all environments
app.set('port', process.env.PORT || 3000);
app.set('views', __dirname + '/views');
app.set('view engine', 'jade');
app.use(express.favicon());
app.use(express.logger('dev'));
app.use(express.bodyParser());
app.use(express.methodOverride());
// аутентификация
var auth = express.basicAuth(function(user, pw, cb) {
if (!user || !pw) {cb(null, false); return;}
db.auth(user, pw, cb);
});
app.use(app.router);
app.use(express.static(path.join(__dirname, 'public')));
// development only
if ('development' == app.get('env')) {
app.use(express.errorHandler());
}
app.get('/', auth, routes.index);
app.get('/:page((table|bar|pie|column))', auth, routes.chart);
app.get('/about', routes.about);
http.createServer(app).listen(app.get('port'), function(){
console.log("Express server listening on port %d in %s mode", app.get('port'), app.settings.env);
});
Наведем красоту - редактируем код файла style.css, проживающего по адресу public/stylesheets/:body {
font: 14px "Lucida Grande", Helvetica, Arial, sans-serif;
}
form{
margin: 2px 10px;
}
#from_dt, #to_dt{
width: 70px;
height: 20px;
margin: 2px;
}
#submit{
margin: 2px;
padding: 5px;
font-weight: bold;
font-size: 16px;
border: none;
background-color: #dcdcdc;
}
a {
min-width: 50px;
text-decoration: none;
}
a, .ui-datepicker-title, form{
display: inline-block;
}
a, .ui-datepicker-title, .ui-datepicker-calendar thead{
color: #000;
margin: 2px;
padding: 5px;
text-align: center;
background-color: #dcdcdc;
font-weight: bold;
font-size: 16px;
}
.ui-datepicker-title:hover, a:hover, .ui-datepicker-calendar thead:hover, #submit:hover{
color: #fff;
background-color: #4169e1;
}
.ui-datepicker-prev, .ui-datepicker-next, #submit{
cursor: pointer;
}
a, .ui-datepicker-title, .ui-datepicker-calendar thead, #submit{
transition: all 0.6s ease-in-out;
-webkit-transition: all 0.6s ease-in-out;
-moz-transition:: all 0.6s ease-in-out;
-ms-transition: all 0.6s ease-in-out;
-o-transition: all 0.6s ease-in-out;
opacity: 0.9;
-moz-opacity: 0.9;
-khtml-opacity: 0.9;
-webkit-opacity: 0.9;
-ms-filter: progid:DXImageTransform.Microsoft.Alpha(opacity=90);
filter: alpha(opacity=90);
}
Я решил не использовать стандартную тему виджета jQuery Datepicker, поэтому посвятил ему несколько строчек кода css, приведенного выше.
Сервис отчетов готов к употреблению. Тестируем:
- стартуем приложение: node app
- посещаем адрес http://localhost:3000/, в случае необходимости авторизуемся - логин: user, пароль: pw
- выбираем даты в диапазоне 05.09.2013 - 09.09.2013 (поле `date` таблицы `money`)
- правда нарядный календарик получился? подтверждаем отправку данных формы на сервер - нажимаем Submit... ta-da :) (см. вступительную картинку)
- выбираем, к примеру, Pie Chart:
- не забываем нажать на кнопочку About :)
В реальных условиях запускаем сервис с помощью forever (я сто раз так делал) или pm2 (ни разу не пробовал, нужно найти время, по описанию - классный модуль).
Манагеры в восторге, Рио де Жанейро, белые парусиновые штаны, креолки и мулатки :).


















Комментариев нет:
Отправить комментарий
Комментарий будет опубликован после модерации