Это руководство расскажет, как создать приложение с диаграммой Ганта, используя Node.js и REST API для серверной коммуникации. В настройке будет использоваться MySQL для управления базой данных. Если вы используете другую технологию, доступны другие варианты интеграции:
Эта реализация будет использовать существующие модули Node.js, чтобы минимизировать шаблонный код. Полный исходный код доступен на GitHub.
Доступно также видео-руководство, которое поможет вам создать диаграмму Ганта с Node.js.
Начните с создания папки для вашего проекта и установки необходимых зависимостей. Будут использоваться следующие модули:
Создайте папку проекта с именем "dhx-gantt-app" и перейдите в нее:
mkdir dhx-gantt-app
cd dhx-gantt-app
Создайте файл package.json
с помощью следующей команды:
npm init -y
После создания файла обновите его, чтобы включить необходимые зависимости. Он должен выглядеть так:
package.json
{
"name": "dhx-gantt-app",
"version": "1.0.2",
"description": "",
"main": "server.js",
"dependencies": {
"body-parser": "^1.19.1",
"express": "^4.17.2"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node server.js"
},
"keywords": [],
"author": "",
"license": "MIT"
}
Установите зависимости с помощью:
npm install
Бэкенд будет простым Express приложением с одним JavaScript файлом (server.js
) для логики сервера, папкой для статических файлов (public
) и HTML страницей.
Структура проекта будет выглядеть следующим образом:
dhx-gantt-app
├── node_modules
├── server.js
├── package.json
└── public
└── index.html
Создайте файл server.js
и добавьте следующий код:
server.js
const express = require('express');
const bodyParser = require('body-parser');
const path = require('path');
const port = 1337;
const app = express();
app.use(express.static(path.join(__dirname, "public")));
app.use(bodyParser.urlencoded({ extended: true }));
app.listen(port, () =>{
console.log("Server is running on port "+port+"...");
});
Этот код настраивает сервер для:
public
Далее создайте папку public
для хранения главной страницы (index.html
).
Папка public
также может содержать JavaScript и CSS файлы для dhtmlxGantt. Для упрощения, в этом руководстве тултип будет загружаться с CDN, поэтому будет включена только HTML страница.
Создайте папку public
и добавьте файл index.html
со следующим содержимым:
index.html
<!DOCTYPE html>
<head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8">
<script src="https://cdn.dhtmlx.com/gantt/edge/dhtmlxgantt.js"></script>
<link href="https://cdn.dhtmlx.com/gantt/edge/dhtmlxgantt.css" rel="stylesheet">
<style type="text/css"> html, body {
height: 100%;
padding: 0;
margin: 0;
overflow: hidden;
}
</style>
</head>
<body>
<div id="gantt_here" style='width:100%; height:100%;'></div>
<script type="text/javascript"> gantt.init("gantt_here");
</script>
</body>
Запустите сервер с помощью:
node server.js
Посетите http://127.0.0.1:1337
в вашем браузере, чтобы увидеть пустую диаграмму Ганта.
Создайте базу данных с двумя таблицами для задач и ссылок:
CREATE TABLE `gantt_links` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`source` INT(11) NOT NULL,
`target` INT(11) NOT NULL,
`type` VARCHAR(1) NOT NULL,
PRIMARY KEY (`id`)
);
CREATE TABLE `gantt_tasks` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`text` VARCHAR(255) NOT NULL,
`start_date` datetime NOT NULL,
`duration` INT(11) NOT NULL,
`progress` FLOAT NOT NULL,
`parent` INT(11) NOT NULL,
PRIMARY KEY (`id`)
);
Добавьте тестовые данные:
INSERT INTO `gantt_tasks` VALUES
('1', 'Project #1', '2017-04-01 00:00:00', '5', '0.8', '0'),
('2', 'Task #1', '2017-04-06 00:00:00', '4', '0.5', '1'),
('3', 'Task #2', '2017-04-05 00:00:00', '6', '0.7', '1'),
('4', 'Task #3', '2017-04-07 00:00:00', '2', '0', '1'),
('5', 'Task #1.1', '2017-04-05 00:00:00', '5', '0.34', '2'),
('6', 'Task #1.2', '2017-04-11 13:22:17', '4', '0.5', '2'),
('7', 'Task #2.1', '2017-04-07 00:00:00', '5', '0.2', '3'),
('8', 'Task #2.2', '2017-04-06 00:00:00', '4', '0.9', '3');
Для получения более подробной информации, обратитесь к этому примеру.
Чтобы загрузить данные, установите необходимые модули MySQL:
npm install bluebird@3.7.2 --save
npm install promise-mysql@5.1.0 --save
npm install date-format-lite@17.7.0 --save
Обновите server.js
, добавив следующее:
server.js
const express = require('express');
const bodyParser = require('body-parser');
const path = require('path');
const port = 1337;
const app = express();
app.use(express.static(path.join(__dirname, "public")));
app.use(bodyParser.urlencoded({ extended: true }));
app.listen(port, () =>{
console.log("Server is running on port "+port+"...");
});
const Promise = require('bluebird');
require("date-format-lite");
const mysql = require('promise-mysql');
async function serverСonfig() {
const db = await mysql.createPool({
host: 'localhost',
user: 'root',
password: '',
database: 'gantt_howto_node'
});
app.get("/data", (req, res) => {
Promise.all([
db.query("SELECT * FROM gantt_tasks"),
db.query("SELECT * FROM gantt_links")
]).then(results => {
let tasks = results[0],
links = results[1];
for (let i = 0; i < tasks.length; i++) {
tasks[i].start_date = tasks[i].start_date.format("YYYY-MM-DD hh:mm:ss");
tasks[i].open = true;
}
res.send({
data: tasks,
collections: { links: links }
});
}).catch(error => {
sendResponse(res, "error", null, error);
});
});
function sendResponse(res, action, tid, error) {
if (action == "error")
console.log(error);
let result = {
action: action
};
if (tid !== undefined && tid !== null)
result.tid = tid;
res.send(result);
}
};
serverСonfig();
Этот код подключается к базе данных и настраивает маршрут (GET /data
) для получения данных о задачах и ссылках, форматируя их для клиента.
Обновите index.html
, чтобы загрузить данные:
public/index.html
gantt.config.date_format = "%Y-%m-%d %H:%i:%s";
gantt.init("gantt_here");
gantt.load("/data");
Посетите http://127.0.0.1:1337
, чтобы увидеть диаграмму Ганта с тестовыми данными.
Чтобы обрабатывать обновления с клиентской стороны, настройте сохранение данных. Добавьте gantt.dataProcessor в index.html
:
public/index.html
gantt.config.date_format = "%Y-%m-%d %H:%i:%s";
gantt.init("gantt_here");
gantt.load("/data");
const dp = new gantt.dataProcessor("/data");dp.init(gantt);dp.setTransactionMode("REST");
Это позволяет отправлять обновления с клиентской стороны обратно на сервер.
Когда пользователи выполняют действия, такие как добавление, изменение или удаление задач или ссылок, DataProcessor отправляет AJAX-запрос на соответствующий URL. Эти запросы включают все необходимые параметры для сохранения изменений в базе данных.
Поскольку DataProcessor установлен в режим REST, он использует определенные HTTP-глаголы для различных операций. Вы можете найти подробный список этих глаголов вместе с деталями запросов и ответов в документации по REST API.
Следующий шаг - добавление необходимых маршрутов и обработчиков в файл server.js
, чтобы изменения, внесенные на клиентской стороне, правильно отражались в базе данных. Вот как выглядит код:
server.js
// добавление новой задачи
app.post("/data/task", (req, res) => {
let task = getTask(req.body);
db.query("INSERT INTO gantt_tasks(text, start_date, duration, progress, parent)"
+ " VALUES (?,?,?,?,?)",
[task.text, task.start_date, task.duration, task.progress, task.parent])
.then(result => {
sendResponse(res, "inserted", result.insertId);
})
.catch(error => {
sendResponse(res, "error", null, error);
});
});
// обновление задачи
app.put("/data/task/:id", (req, res) => {
let sid = req.params.id,
task = getTask(req.body);
db.query("UPDATE gantt_tasks SET text = ?, start_date = ?, "
+ "duration = ?, progress = ?, parent = ? WHERE id = ?",
[task.text, task.start_date, task.duration, task.progress, task.parent, sid])
.then(result => {
sendResponse(res, "updated");
})
.catch(error => {
sendResponse(res, "error", null, error);
});
});
// удаление задачи
app.delete("/data/task/:id", (req, res) => {
let sid = req.params.id;
db.query("DELETE FROM gantt_tasks WHERE id = ?", [sid])
.then(result => {
sendResponse(res, "deleted");
})
.catch(error => {
sendResponse(res, "error", null, error);
});
});
// добавление ссылки
app.post("/data/link", (req, res) => {
let link = getLink(req.body);
db.query("INSERT INTO gantt_links(source, target, type) VALUES (?,?,?)",
[link.source, link.target, link.type])
.then(result => {
sendResponse(res, "inserted", result.insertId);
})
.catch(error => {
sendResponse(res, "error", null, error);
});
});
// обновление ссылки
app.put("/data/link/:id", (req, res) => {
let sid = req.params.id,
link = getLink(req.body);
db.query("UPDATE gantt_links SET source = ?, target = ?, type = ? WHERE id = ?",
[link.source, link.target, link.type, sid])
.then(result => {
sendResponse(res, "updated");
})
.catch(error => {
sendResponse(res, "error", null, error);
});
});
// удаление ссылки
app.delete("/data/link/:id", (req, res) => {
let sid = req.params.id;
db.query("DELETE FROM gantt_links WHERE id = ?", [sid])
.then(result => {
sendResponse(res, "deleted");
})
.catch(error => {
sendResponse(res, "error", null, error);
});
});
function getTask(data) {
return {
text: data.text,
start_date: data.start_date.date("YYYY-MM-DD"),
duration: data.duration,
progress: data.progress || 0,
parent: data.parent
};
}
function getLink(data) {
return {
source: data.source,
target: data.target,
type: data.type
};
}
Эта настройка включает маршруты для обработки операций как с задачами, так и с ссылками. Запросы, связанные с задачами, используют URL "/data/task"
, а для ссылок - "/data/link"
.
Операции просты:
Ответы отправляются обратно в виде JSON-объектов, указывающих тип выполненной операции или ошибку, если что-то пошло не так. Для POST-запросов ответ также включает ID новой записи в базе данных, что помогает сопоставить новый элемент на клиентской стороне.
Как только все настроено, открытие http://127.0.0.1:1337
должно показать полностью рабочую диаграмму Ганта.
Диаграмма Ганта на клиентской стороне позволяет пользователям изменять порядок задач путем перетаскивания. Если вы планируете использовать эту функцию, вам нужно будет хранить порядок задач в базе данных. Подробную информацию об этом можно найти здесь.
Вот как включить эту функцию в приложении.
Начните с включения изменения порядка задач в интерфейсе. Обновите конфигурацию Ганта в представлении "Index" следующим образом:
public/index.html
gantt.config.order_branch = true;gantt.config.order_branch_free = true;
gantt.init("gantt_here");
Далее отразите эти изменения на сервере. Порядок задач будет храниться в столбце под названием sortorder
. Обновите таблицу gantt_tasks
следующим образом:
CREATE TABLE `gantt_tasks` (
`id` int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
`text` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
`start_date` datetime NOT NULL,
`duration` int(11) NOT NULL,
`progress` float NOT NULL DEFAULT 0,
`parent` int(11) NOT NULL,
`sortorder` int(11) NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
Если таблица уже существует, вы можете просто добавить столбец:
ALTER TABLE `gantt_tasks` ADD COLUMN `sortorder` int(11) NOT NULL;
Теперь обновите файл server.js
:
sortorder
:server.js
app.get("/data", (req, res) => {
Promise.all([
db.query("SELECT * FROM gantt_tasks ORDER BY sortorder ASC"), db.query("SELECT * FROM gantt_links")
]).then(results => {
let tasks = results[0],
links = results[1];
for (let i = 0; i < tasks.length; i++) {
tasks[i].start_date = tasks[i].start_date.format("YYYY-MM-DD hh:mm:ss");
tasks[i].open = true;
}
res.send({
data: tasks,
collections: { links: links }
});
}).catch(error => {
sendResponse(res, "error", null, error);
});
});
sortorder
для вновь добавленных задач:server.js
app.post("/data/task", (req, res) => {
let task = getTask(req.body);
db.query("SELECT MAX(sortorder) AS maxOrder FROM gantt_tasks")
.then(result => { let orderIndex = (result[0].maxOrder || 0) + 1; return db.query("INSERT INTO gantt_tasks(text, start_date, duration,"
+ "progress, parent, sortorder) VALUES (?,?,?,?,?,?)",
[task.text, task.start_date, task.duration, task.progress, task.parent,
orderIndex]); })
.then(result => {
sendResponse(res, "inserted", result.insertId);
})
.catch(error => {
sendResponse(res, "error", null, error);
});
});
server.js
// обновление задачи
app.put("/data/task/:id", (req, res) => {
let sid = req.params.id,
target = req.body.target,
task = getTask(req.body);
Promise.all([
db.query("UPDATE gantt_tasks SET text = ?, start_date = ?,"
+ "duration = ?, progress = ?, parent = ? WHERE id = ?",
[task.text, task.start_date, task.duration, task.progress,
task.parent, sid]),
updateOrder(sid, target) ])
.then(result => {
sendResponse(res, "updated");
})
.catch(error => {
sendResponse(res, "error", null, error);
});
});
function updateOrder(taskId, target) {
let nextTask = false;
let targetOrder;
target = target || "";
if (target.startsWith("next:")) {
target = target.substr("next:".length);
nextTask = true;
}
return db.query("SELECT * FROM gantt_tasks WHERE id = ?", [target])
.then(result => {
if (!result[0])
return Promise.resolve();
targetOrder = result[0].sortorder;
if (nextTask)
targetOrder++;
return db.query("UPDATE gantt_tasks SET sortorder"+
" = sortorder + 1 WHERE sortorder >= ?", [targetOrder])
.then(result => {
return db.query("UPDATE gantt_tasks SET sortorder = ? WHERE id = ?",
[targetOrder, taskId]);
});
});
}
Готовый к использованию демонстрационный проект доступен на GitHub.
Компонент Gantt не включает встроенные механизмы защиты от угроз, таких как SQL-инъекции, XSS или CSRF-атаки. Обеспечение безопасности приложения ложится на плечи разработчиков. Подробнее об этом можно узнать в руководстве по безопасности.
Если после завершения настройки диаграмма Ганта не отображает задачи и ссылки, обратитесь к руководству по устранению неполадок для помощи в диагностике проблемы.
Теперь у вас есть полностью функциональная диаграмма Ганта. Полный код доступен на GitHub, где вы можете клонировать или загрузить его для своих проектов.
Изучите дополнительные руководства по функциям Ганта или учебные материалы по интеграции Ганта с другими серверными фреймворками.
К началу