写这篇文章的起因很简单:上个月给一个水处理项目做FUXA组态界面,我当着客户的面拖了几个标准阀门到画布上,现场工程师看了一眼就摆手说“这图标不是我们厂里的泵啊,能不能换成我们设备那种外形?”当时我就明白,FUXA这类开源Web组态平台,真正卡住落地体验的往往不是功能逻辑,而是系统默认内置的那批SVG图元资源。内置资源能满足演示,但一到真实工业现场就捉襟见肘。
FUXA本身是一套基于Node.js + Angular的开源SCADA/HMI组态系统,可以用纯浏览器完成画面设计、数据绑定和实时监控。它最核心的画布机制就是SVG——所有图元、管线路由、文本框,本质上都是SVG节点。这篇博文面向的读者是打算基于FUXA源码做二次开发的开发者、系统集成商或者工厂信息化部门的技术人员。我会从源码架构讲起,把“往FUXA里添加自定义SVG资源”这件事拆成环境准备、文件放置、资源注册、面板加载、数据联动五个环节,过程中穿插我在真实项目里踩过的坑和排查思路。
1. 项目概述:为什么自定义SVG资源是FUXA定制的命门
1.1 FUXA是什么,它凭什么能当组态软件用
FUXA全称是Fast Unified eXchange Architektur(我记不住官方全称也无所谓,大家知道它是开源的Web组态平台就够了)。它具备典型的SCADA系统三件套:数据采集(支持MQTT、Modbus、OPC UA、Bacnet等协议)、画面组态(浏览器端拖拽式编辑器)、运行时监控(实时刷新、告警联动)。整个系统分server和client两个进程,server负责REST API和数据网关,client是Angular单页应用,承载编辑器与运行画面。
这套架构有一个天然优势:画布本身就是SVG。SVG是一种基于XML的矢量图形格式,它可以用文本描述整个工业画面,这意味着我们可以把图元、颜色、位置、绑定表达式全部当成数据来处理。相比传统组态软件那种封闭的私有图形格式,SVG的开放性让二次开发有了非常高的可操作性。但从另一个角度看,开放性也意味着初始资源库不可能覆盖所有行业需求——FUXA官方维护了一套通用图元,本质上只能覆盖泵、阀门、电机、仪表盘、管道这些常见对象。
1.2 为什么说“自定义资源”是组态项目绕不开的刚需
做组态项目的都知道,画面好看不好看、像不像现场,直接影响甲方验收体验。一个化工项目的客户可能希望反应釜、离心机、储罐的图标能跟DCS界面里看到的形状接近;一个水厂项目可能希望显示卧式离心泵和潜污泵的侧视外形差异。这些需求靠官方内置的那几十个图元根本满足不了。
有一种观点说“不需要改源码,可以在运行时手动上传SVG”。没错,FUXA的数据编辑器里确实提供了上传SVG或在画布中粘贴SVG的能力。但这里有个很实际的问题:运行时上传的SVG只是作为一个独立的画布对象存在,它不会出现在编辑器的资源面板里,也不会被团队成员在新建工程时默认加载。每个工程师都要各自维护一份SVG文件,风格不一致、更新不同步。而通过改源码把自定义SVG注册进默认资源库,效果就完全不同:团队里任何人新建工程,编辑器左侧图形库直接就能拖出企业统一定制的设备图元,风格、尺寸规范都是约束好的,维护成本大大降低。
1.3 源码定制和纯路由配置的本质区别
这里想强调一个概念。很多开发新人第一次接触FUXA,会去找一个类似“资源配置文件”的东西,幻想在某个JSON里加一行路径就能让SVG出现在面板上。实际实现并不是这么简单。FUXA源码中,编辑器面板的资源列表本质上是由前端代码里的一个资源注册集合决定的,工程画布在保存时会把SVG字符串和绑定关系序列化进工程文件。你要让新增的SVG成为“一等公民”,必须在前端源码中找到这个注册集合,把新资源挂进去。这也是“FUXA源码添加自定义资源-svg”这个标题下,最核心的一个思路转换:别把这件事当成配参数,要把它当成扩展代码。
2. 源码架构:SVG资源在FUXA里的完整生命周期
2.1 先画出源码的目录地图
以我从GitHub拉取的版本为例,FUXA根目录下面主要分两个部分:
fuxa/ ├─ server/ │ ├─ src/ │ │ ├─ api/ // REST API控制器 │ │ ├─ modules/ // 核心业务模块,包含设备、工程、用户等 │ │ ├─ database/ // 数据库与文件存储适配层 │ │ └─ index.js // 服务入口 │ └─ public/ // 前端构建产物输出目录 └─ client/ ├─ src/ │ ├─ app/ │ │ ├─ modules/ // Angular业务模块 │ │ │ ├─ dashboard/ // 运行时监控模块 │ │ │ └─ projects/ // 工程编辑器模块 │ │ ├─ services/ // 公共数据服务 │ │ └─ ... ├─ angular.json // Angular工程配置 └─ package.json跟自定义SVG资源关系最密切的是client/src/app/modules/projects目录,下面通常会有工程画布、图元编辑器、资源面板这几块逻辑。你不需要全部看懂,但至少要能在里面找到“资源面板”对应的组件,因为后面注册自定义资源的代码就加在那一块。
2.2 SVG资源的三段式流转
一个SVG图元从文件到画布,在FUXA里的流转路径大致是:资源注册集合 → 资源面板列表 → 画布SVG节点。这是理解整个定制过程的主线。
第一段,资源注册集合。编辑器左侧的图元库数据,在源码里头由一处数组或者配置对象统一管理。数组里的每条资源记录至少包含三个关键字段:唯一标识、显示名称、SVG内容来源。自定义资源要做的第一件事,就是在数组里增加一条记录。
第二段,资源面板读取。当用户切到“工程编辑”模式,左侧面板组件根据这个注册集合渲染出可拖拽的图标列表。注意有些版本还会做分类目录,比如“基础图元”“工业图元”“自定义图元”,新增资源时如果没指定正确的类型,面板可能直接过滤掉。
第三段,画布渲染。当用户把图标拖进画布,编辑器会把该项资源对应的SVG字符串取出来,作为<svg>节点的子内容插入画布,同时挂上拖拽、缩放、绑定数据的辅助逻辑。到这一步,SVG就从一个静态文件变成画布里的活图元了。
2.3 资源列表为什么不建议直接改数据库
很多人在FUXA里找不到数据库建表语句,因为它的设计很轻量:工程文件通过文件系统(比如本地目录下的json文件)持久化,设备配置、变量配置也有对等的存储方式。于是有人想,既然资源列表也存在某个json里,那我直接往json里塞一条svg路径不就行了?
我实测下来的结论是:不行,至少不能只这么做。原因有两个。第一,前端编辑器的资源面板是从前端编译产物里读取的静态配置,它不会实时去解析数据库或者文件系统里的资源列表;第二,就算你放弃了前端静态注册,改成让后端动态下发资源列表,整个拖拽逻辑里的资源类型判断、行为联动都是在前端代码里写死的,缺了类型判断,拖出来的图元很可能没有绑定行为。所以最稳妥的方案就是改前端源码的注册集合,让新增资源“生来”就和内置资源行为一致。
3. 环境准备与源码构建流程
3.1 开发环境的依赖清单与版本选择
源码定制第一件事是把工程拉下来跑起来。FUXA的后端运行在Node.js环境,前端是Angular。这里重点提醒一个坑:Node版本不能一味追求新。FUXA不同版本对应的Angular版本不一样,Angular对Node版本有明确要求,比如老版本通常要求Node 14.x或16.x,如果你直接在Node 20的环境下npm install,常见报错就是node-sass编译失败、或者node-gyp找不到Python。
我的建议是先用nvm装一个Node 16.20.x,再配合使用最新稳定版的npm。另外,Angular CLI要用和项目package.json里一致的大版本,避免ng serve时出现CLI和项目Angular版本不匹配的情况。
依赖清单大致如下:
- Node.js 16.20.x
- npm 8.x
- Git
- VS Code或任何顺手的前端IDE
- 可选:Docker(方便后面做环境隔离)
3.2 获取源码并跑通前后端联调
从仓库把代码clone下来之后,分别在server和client两个目录下执行npm install。这个步骤耗时比较久,建议同时看看server目录下是否有config.js或者.env.example这类配置文件,FUXA默认存储目录、端口号、数据库路径都从这里读,很多启动失败都源于配置不对。
后端跑起来很简单,进入server目录,执行:
node src/index.js正常情况下控制台会打印监听端口和已加载的协议插件。前端开发模式需要另开一个终端,进入client目录:
npm startAngular CLI启动后通常监听4200端口,同时它会做一个开发代理,把/api开头的请求转发到后端的端口(比如1880)。这一步能通,说明前后端联调链路没问题,后面就能放心改代码了。
我习惯把前端源码改动后重新构建出来,再放到server的public目录下做整体验证,因为这才是生产环境真正运行的形态。构建命令是:
npm run build构建产物默认输出到client/dist下,把它拷到server/public即可。每次改完前端源码,都要重新构建再拷贝,这个流程虽然麻烦但很必要,能避免“本地开发好了、生产一跑就找不到资源”这种低级事故。
4. 核心实操:给FUXA添加自定义SVG资源的全流程
4.1 准备符合工业规范和FUXA预期的SVG文件
不是随便拿个SVG就能当组态图元。FUXA画布是一个按坐标定位的SVG容器,图元拖进去之后会被缩放、旋转、平移,还会被绑定实时数据。我在实践里踩过几次坑,总结出三条选型标准。
第一,SVG文件里必须声明viewBox,而且最好统一业务图元的viewBox尺寸。比如你定了一套泵阀图元,就尽量都设计成0 0 200 200。这样在编辑器里缩放时比例才会一致,不会出现有的图元一拖进去就占满全屏的情况。第二,尽量使用简单的fill和stroke,不要用太多滤镜、遮罩、复杂渐变。FUXA在运行时会对部分图元做颜色覆盖、状态闪烁,如果SVG内部使用了大量复杂滤镜,动态改色时会非常难看甚至失效。第三,SVG内部的文本内容要单独用<text>标签标识,并且尽量给关键节点加id或class属性,后面做数据绑定更新的时候能直接通过DOM选择器定位到它。
举个例子,一个简单的卧式泵图元可以长这样:
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200"> <rect x="10" y="80" width="100" height="60" rx="10" fill="#cccccc" stroke="#333333" stroke-width="2"/> <rect x="50" y="40" width="20" height="40" fill="#888888" stroke="#333333" stroke-width="2"/> <circle cx="55" cy="140" r="15" fill="#666666" stroke="#333333" stroke-width="2"/> <circle cx="65" cy="140" r="15" fill="#666666" stroke="#333333" stroke-width="2"/> <text id="pump_temp" x="30" y="90" font-size="12" fill="#000000">0.0</text> </svg>这个图元我故意画得简陋,但结构是清晰的:外壳是一个圆角矩形,上面是电机,下面两个圆是泵体支撑轮,pump_temp文本节点可以用来显示实时温度。
4.2 在源码中放置SVG文件并纳入Angular资源管线
SVG文件不能只扔进项目就完事,Angular构建时只会打包angular.json里配置过的资源目录。FUXA项目的资产目录一般在client/src/assets,我看过一些旧版本甚至直接在client/src/assets/svg下面管理图元文件。我的做法是在client/src/assets下新建一个子目录,专门放企业自定义图元:
client/src/assets/ └─ custom-svg/ ├─ pump_horizontal.svg ├─ valve_ball.svg └─ tank_quadrate.svg同时确认angular.json里的assets配置包含了这个目录,Angular的assets配置支持通配符,你可以显式声明:
{ "glob": "**/*.svg", "input": "src/assets/custom-svg/", "output": "/assets/custom-svg/" }这部分如果不配置,直接后果是本地开发能看,但构建部署后404。
4.3 前端注册自定义资源到图元面板
这是整个操作里最关键的一步。我以新版FUXA常见的结构为例,编辑器资源面板一般会读取一个资源集合,类似于:
// 文件位置大约在 client/src/app/modules/projects/.../resource.list.ts export const CUSTOM_RESOURCES = [ { id: 'custom_pump_horizontal', name: '卧式离心泵', type: 'svg', category: 'custom', svgPath: 'assets/custom-svg/pump_horizontal.svg' }, { id: 'custom_valve_ball', name: '球阀', type: 'svg', category: 'custom', svgPath: 'assets/custom-svg/valve_ball.svg' } ];你需要做的,是把这套对象数组和编辑器面板组件用的资源列表合并。不同版本FUXA会把内置资源定义在不同的文件里,有的叫svg-list,有的叫itemFactory,有的直接放在组件构造器里。最有效的查找办法是在client/src下全局搜索一个你已经认识的内置图元名称,比如那套默认阀门图元对应的name字符串,然后顺藤摸瓜找到注册集合的位置。
找到之后,把你的CUSTOM_RESOURCES数组追加进去,注意保持一致的对象结构。如果内置资源用svg字段直接存SVG字符串,你也可以选择把SVG内容以字符串形式写进数组,而不走svgPath。两种方式都可行,但用svgPath的方式维护成本更低,SVG文件可以单独用编辑器调整,不用改代码。
4.4 让资源面板正确读取并渲染新图元
这一步容易踩坑。就算你往注册集合里加了数据,资源面板内部可能会有分类过滤逻辑。有些版本的编辑器面板在渲染时会根据category字段做分组,比如只显示basic和industrial两个分组。如果你新加的category: 'custom'没有被面板的过滤条件包含,那面板上根本看不见它。
解决办法是在面板组件里把自定义资源类型也加入渲染条件,或者更稳妥一点,直接用已有的category值,比如内置资源里已经有industrial分类,我们就把自定义资源也归到industrial下面,只通过name前缀区分。我个人更推荐“不动面板组件、复用已有分类”的方案,因为这样代码侵入最小,后续FUXA升级时合并代码也轻松。
如果你确实需要新增一个分类页签,也不难,找到资源面板的模板文件,增加一个<mat-tab>或者一个分组容器,绑定筛选条件即可。只是要注意,这个改动会牵扯模板和逻辑两处,改起来稍多一点。
4.5 给SVG图元注入实时数据联动能力
仅仅让图元能拖出来,还只是完成了50%。组态图元的真正价值在于能和实时数据绑定。FUXA运行时画面里,每个设备对象都会和某个变量关联,变量值变化时会去更新画布上对应元素的显示。
我现在的做法是,在自定义SVG内部给想要动态刷新的节点加上id,同时在项目配置里识别这些ID。例如运行时的画面刷新逻辑中,找到pump_temp这个文本节点,然后用最新的温度值覆盖contextValue属性或者直接改textContent。
// 运行时数据更新伪代码,具体位置取决于FUXA版本 const valueElement = this.svgRoot.querySelector('#pump_temp'); if (valueElement) { valueElement.textContent = latestValue.toFixed(2); }这里要特别提醒,在Angular环境下操作SVG内部的DOM节点,最好放在ngAfterViewInit或者其他画布初始化完成后的生命周期里,避免DOM还没渲染就去查节点。另外,如果SVG内部节点ID是全局唯一的,查询要限定在当前画布根节点内,别直接用document.getElementById,因为一个画布里可能拖了多个同类型设备,ID会冲突。
4.6 构建、部署和现场验证
前端代码改完,执行:
npm run build把client/dist下的内容覆盖到server/public,然后重启后端服务。打开浏览器进入编辑模式,切到图元面板,应该能在对应分类下看到你新注册的卧式离心泵、球阀这些图元。拖一个到画布上,验证缩放、旋转、对齐这些基础操作正常;再把它绑定到一个模拟变量上,切到运行模式,如果数据能实时刷新到SVG内部的文本节点,说明整条链路已经通了。
5. 常见问题与排查思路速查
5.1 图元面板看不到新加的资源,怎么排查
优先级最高的三个检查点:第一,确认注册集合代码确实编译进了最新的构建包,有时候你改了client/src下的代码,但忘了重新构建,页面加载的还是旧的server/public资源;第二,确认category和现有面板筛选条件一致,或者你新增的分组已经正确绑定;第三,确认Angular构建时没有因为路径大小写报错,Linux服务器对大小写敏感,如果你的SVG路径写的是Pump_horizontal.svg但文件名是pump_horizontal.svg,构建产物里路径匹配不上就会404。
我实际遇到过最诡异的一个现象是:本地开发模式能看到,构建后看不到。后来发现是assets配置里的output路径和代码里写的svgPath不一致,导致构建产物里文件被拷贝到了别的子目录。解决方法是统一把svgPath写成相对src的完整路径,并且在构建后手动检查server/public/assets/custom-svg/下有没有对应文件。
5.2 SVG拖入画布后位置偏移或者尺寸异常
这个问题分两种。一种是图元拖进去后在画布左上角,而且整体被压缩或者放大得不成比例,多半是因为SVG文件没有声明viewBox。FUXA画布在计算图元尺寸时,如果拿不到viewBox,就只能读取SVG的width/height属性,而width/height和viewBox不一致时就会变形。建议所有自定义图元统一使用viewBox="0 0 200 200",顺便可以把内部的图形按这个坐标系设计。
另一种情况是SVG内部存在大量空白区域,比如你的图形实际只占20%,四周都是透明画布。解决办法是打开SVG编辑器,执行“裁剪画布”或者“适配内容”操作,让图形边缘紧贴viewBox边界。这一步很多人忽略,但它对后续对齐吸附功能影响很大。
5.3 数据不刷新或者ref错误
运行模式下图元拖出来了,但绑定的数据不刷新。先从三个方向排查:变量对象是否添加成功、运行时数据绑定表达式是否写对、SVG内部的待更新节点ID是否与查询逻辑匹配。很多时候问题不在FUXA,而在你自己写的SVG里没有给节点设置合适的ID属性。我建议在写完SVG后,用一个简单页面做DOM检测,确认querySelector('#pump_temp')能选中节点再由FUXA加载。
如果多个同类图元拖进画布,一定要记得限制查询范围。用当前画布组件的根节点去querySelector,不要用全局document。
5.4 中文字体与样式覆盖问题
还有一类玄学问题:自定义SVG图元里写的中文乱码、或者运行时被FUXA的主题色覆盖。中文乱码通常是SVG文件编码问题,确保文件以UTF-8无BOM格式保存。被覆盖颜色通常是因为FUXA运行时会根据状态去强制设置SVG某些节点的颜色,处理办法是给不想被动态改色的节点加上>
西门子S7-200 PLC工业洗衣机控制系统设计详解
做电气这些年,接触过不少拿来练手的经典项目,要说哪个最值得推荐给刚入门PLC的朋友,我第一个提名工业洗衣机控制系统。它的工艺流程明确,输入输出点不多,却把开关量控制里最常见的自锁、互锁、定时器、计数器、顺序控制…
内网穿透的几种方式—免费与收费(钉钉、Frp、花生壳、nat123)
我需要你提供具体的项目标题,才能据此生成完整的博文。请按这个格式发给我:项目标题: [项目标题] 项目正文: [一些零散的描述,没有可以不填] 关键词: [关键词1, 关键词2, ...] 摘要描述: [一句话简介]比如你前面提到过类似“内网穿透的几种方…
光子晶体光纤传感:单芯、双芯与定向耦合结构的建模与实验检测
最近把光子晶体光纤的三种典型结构——单芯传输、双芯耦合、定向耦合——从模型建立到实验检测完整跑了一遍。不夸张地说,这个故事比我想象中曲折很多:仿真里灵敏度做得漂亮,一上实验台就被光谱噪声教做人;结构参数差那么零点几个…
海外文献学术搜索:发现、获取、跟踪的完整链路指南
提到海外文献学术搜索,很多人的第一反应是“用Google Scholar还是百度学术”。真正踩过坑的人才知道,检索入口只是其中最不足挂齿的一环。过去几年我带过不少做综述和开题的研究生,发现大多数人卡住的地方惊人的一致:不是不会打开…
云服务器kibana环境搭建
下载地址: https://www.elastic.co/cn/downloads/past-releases/kibana-7-8-0https://www.elastic.co/cn/downloads/past-releases/kibana-7-8-0https://www.elastic.co/cn/downloads/past-releases/kibana-7-8-0 linux安装包解压 Kibana 的 Linux 安装过程主要分为解压、配置…
Vite+Vue3 Iconify离线方案:三段式构建时图标预编译
1. 项目概述:为什么必须让 Iconify 在离线环境下稳如磐石 最近帮一个政务类内部系统做前端重构,客户提了个看似简单但实则棘手的需求:“所有图标必须在断网状态下正常显示,连本地开发时关掉 Wi-Fi 都不能出错。”我第一反应是——…