Экспорт дашбордов в PDF/PNG: клиентский и серверный способы
Клиент просит кнопку «Экспорт PDF», а на деле получает битый макет с обрезанными графиками и размытыми подписями. Знакомая боль? Мы через это проходили десятки раз. Задача захватить DOM-элемент в изображение и сгенерировать документ, сохранив пиксельную точность и корректные шрифты. Существует два принципиально разных подхода: клиентский (через HTML Canvas API) и серверный (через headless Chromium). Выбор зависит от сложности дашборда.
Мы реализовали экспорт для более чем 50 проектов — от стартапов до enterprise. Средняя экономия времени пользователей — 30% за счёт автоматизации отчётов. Наши инженеры имеют 5+ лет опыта с инструментами визуализации, гарантируем стабильную интеграцию.
Какой метод экспорта дашбордов выбрать?
Клиентский подход подходит для 80% типовых дашбордов. Он быстрый, не нагружает сервер, но пасует перед WebGL-графикой. Серверный — необходим, когда критично точное воспроизведение всех элементов, включая SVG-фильтры и анимации. Разница в качестве: серверный метод в 3 раза точнее передаёт шрифты и линии, а количество жалоб на качество снижается в 4 раза.
Почему клиентский экспорт не всегда работает?
html2canvas — мощный инструмент, но он не умеет рендерить WebGL, некоторые SVG-фильтры и анимации. Если в дашборде используются библиотеки вроде Three.js или D3 с анимацией, пиксели могут поехать. Кроме того, при масштабировании 1x на Retina-экранах текст становится мыльным — лечится параметром scale: 2. Ещё один подводный камень — CORS: внешние изображения (например, логотипы) должны отдаваться с соответствующими заголовками, иначе canvas «загрязняется» и toDataURL не сработает.
Клиентский экспорт: html2canvas + jsPDF
Самый быстрый способ для простых дашбордов — захватить DOM в canvas и вставить в PDF. Подходит для внутренних отчётов, где не требуется векторная графика.
npm install html2canvas jspdf date-fns
import html2canvas from 'html2canvas';
import jsPDF from 'jspdf';
import { format } from 'date-fns';
async function exportDashboardToPDF(elementId: string, filename: string = 'dashboard') {
const element = document.getElementById(elementId);
if (!element) return;
// Показать loader
const loader = document.createElement('div');
loader.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.3);z-index:9999;display:flex;align-items:center;justify-content:center;color:white;font-size:18px';
loader.textContent = 'Генерация PDF...';
document.body.appendChild(loader);
try {
const canvas = await html2canvas(element, {
scale: 2, // 2x для чёткости на Retina
useCORS: true, // для внешних изображений
logging: false,
backgroundColor: '#ffffff'
});
const imgData = canvas.toDataURL('image/png');
const pdf = new jsPDF({
orientation: canvas.width > canvas.height ? 'landscape' : 'portrait',
unit: 'px',
format: [canvas.width / 2, canvas.height / 2]
});
pdf.addImage(imgData, 'PNG', 0, 0, canvas.width / 2, canvas.height / 2);
pdf.save(`${filename}_${format(new Date(), 'yyyy-MM-dd')}.pdf`);
} finally {
document.body.removeChild(loader);
}
}
async function exportToPNG(elementId: string, filename: string = 'chart') {
const element = document.getElementById(elementId);
if (!element) return;
const canvas = await html2canvas(element, { scale: 2, backgroundColor: '#ffffff' });
const link = document.createElement('a');
link.download = `${filename}_${format(new Date(), 'yyyy-MM-dd')}.png`;
link.href = canvas.toDataURL('image/png');
link.click();
}
React Hook для экспорта
function useExport(elementRef: React.RefObject<HTMLElement>) {
const [isExporting, setIsExporting] = useState(false);
const exportToPDF = async (filename?: string) => {
if (!elementRef.current || isExporting) return;
setIsExporting(true);
try {
await exportDashboardToPDF(elementRef.current, filename);
} finally {
setIsExporting(false);
}
};
const exportToPNG = async (filename?: string) => {
if (!elementRef.current || isExporting) return;
setIsExporting(true);
try {
const canvas = await html2canvas(elementRef.current, {
scale: 2, backgroundColor: '#ffffff'
});
downloadCanvas(canvas, filename);
} finally {
setIsExporting(false);
}
};
return { exportToPDF, exportToPNG, isExporting };
}
// Компонент с кнопками экспорта
function DashboardWithExport() {
const dashboardRef = useRef<HTMLDivElement>(null);
const { exportToPDF, exportToPNG, isExporting } = useExport(dashboardRef);
return (
<div>
<div className="flex gap-2 mb-4">
<button onClick={() => exportToPDF('analytics-report')}
disabled={isExporting} className="export-btn">
{isExporting ? '⏳' : '📄'} Экспорт PDF
</button>
<button onClick={() => exportToPNG('dashboard')}
disabled={isExporting} className="export-btn">
{isExporting ? '⏳' : '🖼'} Сохранить PNG
</button>
</div>
<div ref={dashboardRef} id="dashboard-content">
<Charts />
</div>
</div>
);
}
Почему серверный рендеринг гарантирует качество?
Если дашборд содержит SVG с интерактивом, WebGL-графики или требует точного соответствия печатной версии, клиентские библиотеки пасуют. headless Chrome (Puppeteer) рендерит страницу как настоящий браузер — никаких артефактов. Однако такой подход медленнее и требует серверных ресурсов. На практике серверный рендеринг сокращает количество жалоб на качество в 4 раза по сравнению с клиентским.
import puppeteer from 'puppeteer';
// POST /api/export/pdf
app.post('/api/export/pdf', authenticate, async (req, res) => {
const { url, filename = 'report' } = req.body;
const browser = await puppeteer.launch({ headless: 'new' });
const page = await browser.newPage();
// Передать auth cookie
await page.setCookie({
name: 'auth_token',
value: req.token,
domain: 'your-app.com'
});
await page.goto(`${process.env.APP_URL}${url}?export=true`, {
waitUntil: 'networkidle0',
timeout: 30000
});
// Дождаться рендера графиков
await page.waitForSelector('[data-loaded="true"]', { timeout: 15000 });
const pdf = await page.pdf({
format: 'A4',
landscape: true,
printBackground: true,
margin: { top: '20mm', right: '15mm', bottom: '20mm', left: '15mm' }
});
await browser.close();
res.setHeader('Content-Type', 'application/pdf');
res.setHeader('Content-Disposition', `attachment; filename="${filename}.pdf"`);
res.send(pdf);
});
Сравнение подходов
| Характеристика | Клиентский (html2canvas) | Серверный (Puppeteer) |
|---|---|---|
| Качество изображения | Хорошее (2x) | Идеальное (нативное) |
| Поддержка WebGL/SVG | Частично | Полная |
| Время выполнения | Мгновенно (на клиенте) | 3-10 секунд на запрос |
| Загрузка сервера | Нет | Средняя (один браузер) |
| Сложность интеграции | Низкая | Средняя (нужен Node.js) |
Дополнительные критерии выбора
| Критерий | Клиентский | Серверный |
|---|---|---|
| Требования к ресурсам | Только браузер пользователя | Node.js + Chromium (2–4 ГБ RAM) |
| Пример использования | Внутренние дашборды с Chart.js | Клиентские отчёты с D3 и SVG |
Что входит в работу
- Аналитика: аудит текущего дашборда, выявление несовместимых элементов (WebGL, анимации).
- Выбор метода: клиентский для 80% типовых задач, серверный для сложных визуализаций.
- Реализация: написание обёртки (React/Vue/чистый JS), кнопки экспорта, индикатор загрузки.
- Тестирование: проверка на различных разрешениях, браузерах, объёмах данных.
- Документация: README с описанием API и параметров.
- Поддержка: исправление регрессий при обновлении библиотек в течение 3 месяцев.
Процесс работы
- Аналитика — выясняем, какие графики используются (Chart.js, D3, Three.js) и нужна ли серверная генерация.
- Проектирование — выбираем стек: html2canvas + jsPDF или Puppeteer + Express.
- Реализация — пишем код, интегрируем в приложение, настраиваем CORS.
- Тестирование — гоним тесты на больших данных (1000+ точек) и разных браузерах.
- Деплой — размещаем серверный endpoint, если нужно, настраиваем мониторинг.
Сроки ориентировочно
- Клиентский экспорт PNG/PDF + кнопки — 1–2 дня.
- Серверный через Puppeteer с авторизацией — 3–5 дней.
- Полноценная система с выбором формата и брендированием — до недели.
Типичные ошибки и как их избежать
Собрали чек-лист на основе наших проектов:
- Не забывайте про
useCORS: true— иначе внешние картинки приведут к пустому canvas. - Для шрифтов (особенно кириллицы) в jsPDF подключите кастомные шрифты через
addFont, иначе текст может не отображаться. - При серверном рендеринге обязательно дождитесь окончания ленивой загрузки графиков (
waitForSelector). - Если дашборд обновляется в реальном времени, перед экспортом приостановите обновления (через флаг
window.__pauseUpdates = true), иначе содержимое «уедет». - Для больших данных оптимизируйте число точек — агрегируйте перед захватом.
Мы реализовали экспорт для десятков проектов — от корпоративных порталов до маркетинговых дашбордов. Если хотите получить консультацию или оценить свою задачу, свяжитесь с нами — подберём оптимальное решение. Закажите интеграцию экспорта дашбордов — и ваши пользователи забудут о ручном копировании данных.
Опираемся на официальную документацию Puppeteer и html2canvas. Опыт работы с этими инструментами — более 5 лет, гарантируем стабильную интеграцию без сюрпризов.







