在物联网和自动化领域,Node-RED作为一款强大的可视化编程工具,经常需要将仪表盘中的图表数据导出为PDF格式进行存档或分享。本文将详细介绍如何通过Node-RED实现这一功能,从环境搭建到完整实现,包含所有关键细节和避坑指南。
提示:本方案基于Node-RED 3.0+版本,使用dashboard 3.1+和pdf节点最新版测试通过
首先确保已安装Node.js(建议LTS版本)和Node-RED基础环境。可以通过以下命令检查版本:
bash复制node -v
npm -v
node-red -v
如果尚未安装,推荐通过官方提供的安装脚本进行部署:
bash复制# 对于Linux/macOS系统
curl -sL https://deb.nodesource.com/setup_lts.x | sudo -E bash -
sudo apt-get install -y nodejs
# 对于Windows系统
# 建议直接下载官方安装包
我们需要两个核心节点包来实现PDF导出功能:
通过Node-RED管理面板安装最为便捷:
或者通过命令行安装:
bash复制cd ~/.node-red
npm install node-red-dashboard node-red-contrib-pdf
安装完成后需要重启Node-RED服务使节点生效。
首先构建一个包含图表和数据展示的基础仪表盘:
ui_tab节点,设置标签为"数据看板"ui_group节点,命名为"统计图表"ui_chart节点,配置如下:
为了演示效果,我们可以创建一个模拟数据源:
javascript复制// 在function节点中创建模拟数据
const timestamps = [];
const values = [];
const now = Date.now();
for(let i=0; i<24; i++){
timestamps.push(new Date(now - (23-i)*3600000).toISOString());
values.push(Math.floor(Math.random()*100));
}
msg.payload = [
{series: ["温度"], data: [values], labels: [timestamps]}
];
return msg;
将此function节点连接到chart节点,设置每5秒自动刷新一次。
完整的PDF导出流程包含以下节点连接:
code复制ui_button → ui_template → function → pdf节点
配置触发按钮的关键参数:
这是整个流程中最关键的技术点,负责将图表canvas转换为图像数据:
html复制<script>
(function(scope){
scope.$watch('msg', function(msg){
if(msg && msg.payload === "exportPDF"){
// 获取所有图表canvas元素
const canvases = document.querySelectorAll('.nr-dashboard-chart canvas');
const images = [];
// 遍历所有图表(支持多图表导出)
canvases.forEach((canvas, index) => {
images.push({
id: `chart${index}`,
data: canvas.toDataURL('image/png')
});
});
// 发送图像数据数组
scope.send({ payload: images });
}
});
})(scope);
</script>
重要提示:原方案只导出第一个图表,改进版支持导出所有图表
构建PDF的HTML模板,增强版支持:
javascript复制// 接收图像数据数组
const charts = msg.payload;
// 构建图表HTML片段
let chartsHTML = '';
charts.forEach((chart, index) => {
chartsHTML += `
<div class="chart-container">
<h3>图表 ${index + 1}</h3>
<img src="${chart.data}" style="max-width: 100%; height: auto;">
</div>
<hr>
`;
});
// 完整PDF模板
msg.payload = `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
body { font-family: Arial; margin: 20px; }
.header { text-align: center; margin-bottom: 30px; }
.footer { margin-top: 30px; text-align: right; font-size: 0.8em; color: #666; }
.chart-container { margin-bottom: 40px; }
hr { border: 0; height: 1px; background: #eee; margin: 30px 0; }
</style>
</head>
<body>
<div class="header">
<h1>数据统计报告</h1>
<p>生成时间:${new Date().toLocaleString()}</p>
</div>
${chartsHTML}
<div class="footer">
<p>© ${new Date().getFullYear()} 公司名称 - 机密文件</p>
</div>
</body>
</html>
`;
// 设置PDF文件名(动态日期)
const now = new Date();
msg.filename = `C:/Reports/数据报告_${now.getFullYear()}${(now.getMonth()+1).toString().padStart(2,'0')}${now.getDate().toString().padStart(2,'0')}.pdf`;
return msg;
pdf节点需要特别注意以下参数:
高级选项中可以设置:
当仪表盘包含多个图表时,可以改进为每个图表单独一页:
javascript复制// 在function节点中修改模板
charts.forEach((chart, index) => {
chartsHTML += `
<div style="page-break-after: always;">
<h3>图表 ${index + 1}</h3>
<img src="${chart.data}" style="max-width: 100%; height: auto;">
</div>
`;
});
可以根据时间、数据内容等生成更有意义的文件名:
javascript复制// 获取当前日期时间
const now = new Date();
const dateStr = `${now.getFullYear()}-${(now.getMonth()+1).toString().padStart(2,'0')}-${now.getDate().toString().padStart(2,'0')}`;
const timeStr = `${now.getHours().toString().padStart(2,'0')}${now.getMinutes().toString().padStart(2,'0')}`;
// 包含数据特征的名称
const dataType = context.get('currentDataType') || '综合'; // 从全局上下文获取
msg.filename = `C:/Reports/${dataType}_报告_${dateStr}_${timeStr}.pdf`;
结合email节点可以实现PDF自动发送:
javascript复制msg.payload = {
from: 'reports@yourdomain.com',
to: 'recipient@company.com',
subject: `数据报告 ${new Date().toLocaleDateString()}`,
text: '请查收附件中的最新数据报告',
attachments: [{
filename: msg.filename.split('/').pop(),
path: msg.filename
}]
};
return msg;
现象:导出的PDF中图表只显示部分内容
原因:canvas转换时图表尚未完全渲染
解决方案:
javascript复制setTimeout(() => {
const canvas = document.querySelectorAll('.nr-dashboard-chart canvas')[0];
if(canvas){
scope.send({ payload: canvas.toDataURL('image/png') });
}
}, 500); // 500ms延迟
现象:PDF中的中文变为方框或乱码
解决方案:
html复制<style>
@font-face {
font-family: 'SimSun';
src: local('SimSun');
}
body {
font-family: 'SimSun', Arial, sans-serif;
}
</style>
现象:无法保存到指定目录
解决方案:
javascript复制msg.filename = `./reports/report_${Date.now()}.pdf`;
现象:高分辨率图表导出后模糊
解决方案:
javascript复制// 在template节点中
const canvas = document.querySelectorAll('.nr-dashboard-chart canvas')[0];
const scale = 2; // 缩放因子
const offScreenCanvas = document.createElement('canvas');
offScreenCanvas.width = canvas.width * scale;
offScreenCanvas.height = canvas.height * scale;
const ctx = offScreenCanvas.getContext('2d');
ctx.scale(scale, scale);
ctx.drawImage(canvas, 0, 0);
scope.send({ payload: offScreenCanvas.toDataURL('image/png') });
批量导出优化:
内存管理:
缓存策略:
日志记录:
javascript复制// 在function节点中添加日志
const fs = require('fs');
const logEntry = `${new Date().toISOString()}, ${msg.filename}\n`;
fs.appendFileSync('./pdf_export.log', logEntry);
| 特性 | node-red-contrib-pdf | pdfkit节点 | 外部API调用 |
|---|---|---|---|
| 安装复杂度 | 低 | 中 | 高 |
| 功能丰富度 | 基础 | 高 | 取决于API |
| 本地运行 | 是 | 是 | 否 |
| 中文支持 | 需要配置 | 好 | 取决于API |
| 性能 | 一般 | 较好 | 依赖网络 |
| 适合场景 | 简单报表 | 复杂文档 | 企业级应用 |
对于需要更复杂排版的情况,可以使用node-red-contrib-pdfkit:
javascript复制const PDFDocument = require('pdfkit');
const fs = require('fs');
const doc = new PDFDocument();
doc.pipe(fs.createWriteStream(msg.filename));
doc.font('fonts/SimSun.ttf')
.fontSize(25)
.text('专业数据报告', 100, 80);
// 添加图表图像
const img = new Buffer.from(msg.payload.split(',')[1], 'base64');
doc.image(img, {
fit: [500, 400],
align: 'center',
valign: 'center'
});
doc.end();
在某工厂设备监控系统中,我们实现了:
关键实现代码:
javascript复制// 在function节点中添加设备状态标记
charts.forEach((chart, index) => {
const status = chart.data.some(v => v > 90) ?
'<span style="color:red">(异常)</span>' :
'<span style="color:green">(正常)</span>';
chartsHTML += `
<div class="chart-container">
<h3>设备 ${index + 1} ${status}</h3>
<img src="${chart.imageData}">
</div>
`;
});
为环保部门实现的特性:
javascript复制// 安全的文件名处理
const unsafeName = msg.reportName || 'report';
const safeName = unsafeName.replace(/[^a-z0-9_-]/gi, '_');
msg.filename = `./reports/${safeName}.pdf`;
敏感数据保护:
资源限制:
javascript复制// 在function节点中添加检查
if(msg.payload.length > 10 * 1024 * 1024) { // 10MB
node.warn("PDF内容过大,已拒绝");
return null;
}
逐步验证法:
日志记录:
javascript复制// 记录导出次数
context.set('exportCount', (context.get('exportCount') || 0) + 1);
node.log(`PDF已导出 ${context.get('exportCount')} 次`);
html复制<script>
console.log('调试信息', document.querySelectorAll('.nr-dashboard-chart'));
</script>
经过多个项目的实践验证,以下配置组合最为稳定可靠:
节点版本:
性能配置:
文件管理:
javascript复制// 在function节点中添加自动清理
const fs = require('fs');
const path = require('path');
const reportDir = './reports';
fs.readdir(reportDir, (err, files) => {
if(err) return;
const now = Date.now();
const sevenDays = 7 * 24 * 60 * 60 * 1000;
files.forEach(file => {
const filePath = path.join(reportDir, file);
const stat = fs.statSync(filePath);
if(now - stat.mtimeMs > sevenDays) {
fs.unlinkSync(filePath);
}
});
});
与数据库集成:
OCR增强:
数字签名:
多语言支持:
javascript复制// 多语言模板示例
const templates = {
en: {
title: "Data Report",
time: "Generated at"
},
zh: {
title: "数据报告",
time: "生成时间"
}
};
const lang = context.get('userLang') || 'zh';
msg.payload = `
<h2>${templates[lang].title}</h2>
<p>${templates[lang].time}: ${new Date().toLocaleString()}</p>
`;