
写这篇文章的起因很简单上个月给一个水处理项目做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.xnpm 8.xGitVS 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 xmlnshttp://www.w3.org/2000/svg viewBox0 0 200 200 rect x10 y80 width100 height60 rx10 fill#cccccc stroke#333333 stroke-width2/ rect x50 y40 width20 height40 fill#888888 stroke#333333 stroke-width2/ circle cx55 cy140 r15 fill#666666 stroke#333333 stroke-width2/ circle cx65 cy140 r15 fill#666666 stroke#333333 stroke-width2/ text idpump_temp x30 y90 font-size12 fill#0000000.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不一致时就会变形。建议所有自定义图元统一使用viewBox0 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某些节点的颜色处理办法是给不想被动态改色的节点加上>