准备
- node 和 git -项目开发环境
- layui – UI组件库
- jquery – jquery库
- video.js – video.js库
- axios – http请求库
- crypto-js – 加解密库
- echarts – 可视化图表库
目录文件及重要文件说明
目录文件
/src/main/webapp/js
├── api api请求管理
├── config 项目配置 oss 配置等
├── const 项目中的常量
├── diagramly
│ ├── sidebar
│ │ └── thingskit 项目新增的元件库
├── grapheditor 图形编辑器有关文件
├── plugin 项目中使用的插件
│ ├── ace
│ ├── axios
│ ├── crypto-js
│ ├── echarts
│ ├── layui
│ │ ├── css
│ │ │ └── modules
│ │ │ ├── laydate
│ │ │ │ └── default
│ │ │ └── layer
│ │ │ └── default
│ │ └── font
│ └── videos
重要文件说明
元件库开发
文件位置
.
├── src
│ └── main
│ └── webapp
│ └── js
| └── diagramly
│ └── Devel.js 加载元件库文件
│ └── sidebar
| └── Sidebar.js 各图形库加载图形
| └── thingskit 自定义元件库文件
| └── Sidebar-Engine.js 元件库文件
├── etc
└── build
└── build.xml 打包自定义元件库
源码介绍
创建元件库
创建需要自定义新增的元件库、自定义属性用于确定组件类型件组件和数据绑定面板关联。
// Sidebar.Engine.js
// src/main/webapp/js/diagramly/sidebar/thingskit/Sidebar-Engine.js
(function () {
// Adds Atlassian shapes
// conduit 管道
Sidebar.prototype.addConduitPalette = function () {
// 组件归类 决定数据面板中组件有哪些操作面板
const { COMPONENT_TYPE } = this.enumCellBasicAttribute
const { DEFAULT } = this.enumComponentType
// 图形库信息
const gn = 'mxgraph.engine';// 图形库id 后续注册时需要使用
const dt = 'engine'; //图 形库id 后续注册时需要使用
const label = '发动机'
const width = 66;
const height = 74;
const staticPath = `${Proxy_Prefix}/img/lib/thingskit/`
const prefix = 'image;image=img/lib/thingskit/'
const defaultStyle = ';imageAspect=0;'
this.setCurrentSearchEntryLibrary(dt);
// !!!自定义属性 当前设置该组件库为默认类型
const cellAttribute = {
[COMPONENT_TYPE]: DEFAULT
}
const graphPathLib = [
{ name: '3-D 发动机.svg', path: 'engine/3-D 发动机.svg' },
]
const lib = graphPathLib.map(item => {
item.staticPath = staticPath + item.path
return item
})
const fns = graphPathLib.map(item => {
return this.addEntry(this.getTagsForStencil(gn, item.name, dt).join(' '), mxUtils.bind(this, function () {
const cell = new mxCell('', new mxGeometry(0, 0, width, height), `${prefix}${item.path}${defaultStyle}`);
cell.setVertex(true)
this.setCellAttributes(cell, cellAttribute)
return this.createVertexTemplateFromCells([cell], cell.geometry.width, cell.geometry.height, item.name);
}));
})
this.setVariableImageLib(dt, label, lib)
this.addPaletteFunctions(dt, label, false, fns);
this.setCurrentSearchEntryLibrary();
};
})();
- 将元件库导入
// src/main/webapp/js/diagramly/Devel.js
// Devel.js
// 将Sidebar-Engine.js引入
mxscript(drawDevUrl + 'js/diagramly/sidebar/thingskit/Sidebar-Engine.js');
- 将元件库加入到需要打包的文件队列中
<!-- build.xml -->
<!-- /etc/build/build.xml -->
<!-- 将Sidebar-Engine.js添加至需要打包的文件中去 -->
<jscomp compilationLevel="simple" debug="false" forceRecompile="true" output="${basedir}/sidebar.min.js">
...
<file name="./thingskit/Sidebar-Engine.js" />
</jscomp>
- 将元件库注册到侧边栏
// Sidebar.js
// src/main/webapp/js/diagramly/sidebar/Sidebar.js
// 将图形库id加入Siderbar中
Sidebar.prototype.configuration = [
...
{ id: 'engine' } // 图形库id
]
// 添加到更多图形中去
Sidebar.prototype.init = function () {
...
var thingskitEntries = [
// 注册至到更多图形中
{ title: "发动机", id: 'engine', image: IMAGE_PATH + '/thingskit/发动机.png' }
];
}
// 添加到侧边栏中
Sidebar.prototype.initPalettes = function () {
...
// 发动机
this.addEnginePalette();
}
数据绑定面板开发
文件位置
/src/main/webapp/js
├── grapheditor
│ ├── Format.js
源码介绍
控制元件拥有哪些数据绑定面板,通过 this.setComponentPermission 方法设置各个类型的元件有哪些数据绑定面板。
// Sidebar.js
// src/main/webapp/js/diagramly/sidebar/Sidebar.js
Sidebar.prototype.init = function () {
const { LINE_CHART_EXPAND, BAR_CHART_EXPAND, DASHBOARD_CHART_EXPAND, DYNAMIC_EFFECT, DATA_SOURCE, VAR_IMAGE, INTERACTION, VIDEO: VIDEO_PANEL, SWITCH_STATE_SETTING, ONLY_SINGLE_EVENT, RUNNING_AND_STOP } = this.enumPermissionPanel
const { LINE, LINE_CHART, REAL_TIME, TITLE, VARIABLE, DEFAULT, BAR_CHART, VIDEO, SWITCH, PARAMS_SETTING_BUTTON, DASHBOARD_CHART, IMAGE } = this.enumComponentType
// 水流类型元件拥有水流效果面板与数据东西面板
this.setComponentPermission(LINE, [RUNNING_AND_STOP, DYNAMIC_EFFECT])
// 默认类型的元件拥有数据动效面板
this.setComponentPermission(DEFAULT, [DYNAMIC_EFFECT])
this.setComponentPermission(REAL_TIME, [DYNAMIC_EFFECT])
// 标题类型的元件拥有数据交互面板与动态数据面板
this.setComponentPermission(TITLE, [INTERACTION, DYNAMIC_EFFECT])
this.setComponentPermission(VAR_IMAGE, [INTERACTION, VAR_IMAGE])
this.setComponentPermission(VARIABLE, [DATA_SOURCE, INTERACTION, DYNAMIC_EFFECT])
this.setComponentPermission(BAR_CHART, [DATA_SOURCE, BAR_CHART_EXPAND])
this.setComponentPermission(LINE_CHART, [DATA_SOURCE, LINE_CHART_EXPAND])
this.setComponentPermission(DASHBOARD_CHART, [DATA_SOURCE, DASHBOARD_CHART_EXPAND])
this.setComponentPermission(VIDEO, [VIDEO_PANEL])
this.setComponentPermission(SWITCH, [DATA_SOURCE, SWITCH_STATE_SETTING])
this.setComponentPermission(PARAMS_SETTING_BUTTON, [DATA_SOURCE, ONLY_SINGLE_EVENT])
this.setComponentPermission(IMAGE, [DATA_SOURCE])
}
Format.js 根据元件拥有的权限渲染数据绑定面板,如上图开关元件与变量元件数据绑定面板的差异化渲染。
// 数据绑定面板的开发源码均在此方法中
// 开发使用layui与jquery库
// src/main/webapp/js/grapheditor/Format.js
DataFormatPanel.prototype.addDataFont = function () {
// 该方法用于根据元件类型渲染不同的面板
async function initNode() {
const basicAttr = sidebarInstance.enumCellBasicAttribute
const permissionKey = sidebarInstance.enumPermissionPanel
const { LINE_CHART, BAR_CHART, DASHBOARD_CHART } = Sidebar.prototype.enumComponentType
const renderMapping = {
// 元件拥有数据源权限
[permissionKey.DATA_SOURCE]: createDataSourcePanel,
// 元件拥有图表数据绑定面板
[permissionKey.LINE_CHART_EXPAND]: createChartBindPanel.bind(this, LINE_CHART),
[permissionKey.BAR_CHART_EXPAND]: createChartBindPanel.bind(this, BAR_CHART),
// 元件拥有数据源拓展面板
[permissionKey.DASHBOARD_CHART_EXPAND]: createChartBindPanel.bind(this, DASHBOARD_CHART),
// 元件拥有数据交互面板
[permissionKey.INTERACTION]: createInteractionPanel,
// 元件拥有数据动效面板
[permissionKey.DYNAMIC_EFFECT]: createDynamicEffectPanel,
// 元件库拥有变量图片选项
[permissionKey.VAR_IMAGE]: createVarImagePanel,
// 元件库拥有视频配置面板
[permissionKey.VIDEO]: createVideoBindPanel,
// 元件库拥有开关状态切换面板
[permissionKey.SWITCH_STATE_SETTING]: createSwitchStateSettingPanel,
}
// 根据元件的权限渲染数据绑定面板
function permissionRender() {
try {
const cell = vertices[0]
const permission = graph.getAttributeForCell(cell, basicAttr.COMPONENT_TYPE)
const needDisplayPanel = sidebarInstance.getComponentPermission(permission)
for (const key of needDisplayPanel) {
renderMapping[key]()
}
if (needDisplayPanel.length) createSubmitPanel()
UseLayUi.nextTick(() => form.render())
} catch (e) {
throw Error('component permission setting has some problem, please check your component permission bind on "Sidebar.prototype.init" method setComponentPermission')
}
}
}
// 该方法用于创建提交按钮以及根据元件类型做提交参数转换处理
function createSubmitPanel() {
...
const value = getValueOnSubmit(field)
}
// 根据元件类型做上传参数转换
function getValueOnSubmit(field) {
const basicAttr = sidebarInstance.enumCellBasicAttribute
const componentType = sidebarInstance.enumComponentType
...
const renderMapping = {
[componentType.BAR_CHART]: getChartSubmitValue,
[componentType.DEFAULT]: getSubmitValue,
[componentType.VIDEO]: getVideoSubmitValue,
[componentType.SWITCH]: getSwitchSubmitValue,
}
...
// 处理方法
function getChartSubmitValue() {}
function getSubmitValue() {}
function getVideoSubmitValue() {}
function getSwitchSubmitValue() {}
}
WebSocket接收数据,前端根据页面组件做发布订阅处理。
// src/main/webapp/js/grapheditor/Format.js
// 事件中心 发布订阅
class EventCenter {
...
}
// socket 相关
class Ws {
....
}
// 调度中心
class DispatchCenter {
// ...
// 初始化方法
async init(editorUi, currentPage) {
// 创建事件中心
this.createEventBus()
// 获取页面所有节点
this.saveContentInfo(editorUi, currentPage)
// 建立socket连接
this.connectSocket()
// 获取页面中所有绑定数据源的组件记录
await this.getContentDataNode()
// 实例化各个处理模块 各个处理模块
this.dataSourceHandlerInstance = new HandleDataSource(this)
this.dataInteractionInstance = new HandleDataInteraction(this)
this.dynamicEffectInstance = new HandleDynamicEffect(this)
// 更新队列
this.updateQueueInstance = new UpdateQueue(this)
this.sendSubscribeMessage()
}
// 创建订阅消息模型 将 消息分类汇总过滤,重复数据源只订阅一条消息
generateSubscribeMessage() {
// ...
}
// 根据socket接受消息 调用不同的处理逻辑
socketOnMessage() {
// ...
// 查找当前订阅id中,绑定的相同数据源
const subList = this.subscribeIdMapping.get(subscriptionId)
subList.forEach(item => {
// ...
// 根据绑定的数据源判断调用的处理逻辑 例如调用 HandleDataSource 实例中的updateCommonDataSource 方法
this.dataSourceHandlerInstance.updateCommonDataSource(message, item)
}
}
}
// 处理数据源
class HandleDataSource {
...
// 根据元件类型分发处理逻辑
updateCommonDataSource(message, record) {
const { nodeId, attr } = record
const node = this.getNodeByCmdId(nodeId)
node && this.updatePage(() => {
const { data } = message
const type = this.getComponentType(node)
// ...
// 元件类型为图片时
if (type === this.componentType.IMAGE) {
this.handleImageComponent(message, record)
return
}
}, node)
// ...
}
// 图片处理逻辑
handleImageComponent(message, record) {
const { data = {} } = message
const { nodeId, attr } = record
const node = this.getNodeByCmdId(nodeId)
const [[_timespan, receiveValue] = []] = data[attr] || []
this.updatePage(() => {
// 更新节点中的图片
node.setAttribute('label', `<img class="basic-component__image" alt="图片" src="${receiveValue}" />`)
}, node)
}
}
// 处理数据交互
class HandleDataInteraction {
...
}
// 处理数据动效
class HandleDynamicEffect {
...
}
// 建立更新队列
class UpdateQueue {
...
}
const.js
文件位置
/src/main/webapp/js
├── const
│ ├── const.js // 定义项目中的常量
│ ├── persistentStorage.js // 加解密storage,与后台管理系统一致
源码介绍
- const.js 常量名
GLOBAL_TOKEN 访问令牌:
用于请求中携带
来源于后台管理系统中存储在storage中的值,storage中的值涉及在生产环境和开发环境中是否加解密问题,加解密方式应与后台管理系统中 一致。
GLOBAL_PLATFORM_INFO 平台信息
用于项目中需要使用平台信息的场景,例如首屏加载时的loading,显示不同平台的平台名。
GLOBAL_WS_URL socket地址
用于项目中socket连接
hasSavePermission 检查是否权限
用于检查组态中时候拥有写入的权限
- persistentStorage.js 加解密storage相关
配置文件
OSS打包配置
文件位置
/src/main/webapp/js
├── config
│ ├── config.js
源码介绍
/**
* @description 加载OSS文件时使用的oss文件路径
*/
const OSS_Prefix = 'https://oss.xxx.com/'
/**
* @description 是否使用OSS文件 开启时生产环境中使用oss服务的文件将从oss服务器中加载。
*/
const Enable_OSS = false
/**
* @description 代理配置项
*/
const Proxy_Prefix = window.location.pathname.startsWith('/') ? window.location.pathname.replace(/\/$/, '') : window.location.pathname
// index.html
// 例如在index.html中使用Enable_OSS判断是否开启OSS模式,开启则通过 OSS_Prefix 配置的oss服务器地址中加载文件
const appMinSrc = Enable_OSS ? `${OSS_Prefix}app.min.js?v=${releaseVersion}` : `js/app.min.js?v=${releaseVersion}`
项目打包配置
gulpfile.js执行流程
项目基于draw.io二次开发,使用ant进行打包,打包时不能编译es6+语法,如async,await, class等语法,使用新语法时会导致打包失败,因此先使用glup先编译,在通过ant打包。
// 1.打包前先复制需要编译的文件源码
function copyFile() {}
// 2.编译文件
function complieFormat() {}
// 3.引入的文件增加版本号(用于打包部署后服务器上文件不更新)
function generatoreVersion(){}
// 4.构建war包
async function buildWar() {}
// 5.复制需要上传到oss服务器中的文件
function copyFileUsageOssServer() {}
// 6.还原文件
function reductionFile() {}
// 7.清除复制的文件
function clean() {}
开发环境搭建及源码运行
打包部署
准备工作
- [JDK](Download Java for macOS)安装与环境变量配置(项目使用JDK11) 安装文档
- [Apache Ant](Apache Ant – Welcome)安装与环境变量配置(项目使用Ant version 1.10.12 版本) 安装文档 ,安装Apache Ant 1 确保将JAVA_HOME环境变量设置到安装JDK的文件夹。 2 下载的二进制文件从 http%3A%2F%2Fant.apache.org 3,创建一个名为ANT_HOME,一个新的环境变量指向Ant的安装文件夹,在 c%3Aapache-ant-1.8.2-bin 文件夹。 5 附加的路径Apache Ant批处理文件添加到PATH环境变量中。 在我们的例子是 c%3Aapache-ant-1.8.2-binbin文件夹。))
- [Node.js](Node.js (nodejs.org))安装(项目使用v16.15.0版本)
运行打包
# 安装依赖
npm install
# 运行打包 运行后开始打包 预计耗时1min
npm run build
打包成功后控制台输出结果与包存放位置,oss文件存放需要上传至oss服务器的js文件。
参考文档
- [mxGraph](API Specification (jgraph.github.io)) – mxGraph 库 Api 文档
- [Draw.io常见问题](diagrams.net Frequently Asked Questions) – Draw.io常见问题
- [掘金二开文章一](mxgraph配合draw.io再次使用总结 – 掘金 (juejin.cn)) – 掘金二次开发参考文章
- [掘金二开文章二](DrawIO 二开 —— 是时候给你的 ProcessOn 充值终身 VIP 了 – 掘金 (juejin.cn)) – 掘金二次开发参考文章
- [mxGraph二开文章一](mxGraph入门 (gitee.io)) – mxGraph二次开发参考文章
- [mxGraph二开文章二](mxGraph教程 – 李理的博客 (fancyerii.github.io)) – mxGraph二次开发参考文章
FAQ
- 打包过程中发生失败时,可能回导致需要gulp编译的文件被覆盖。应检查需要编译的文件是否已经被gulp编译。
左侧文件则被gulp编译,失败时则到dist目录中找到未被编译的文件或者通过git进行回滚,在解决打包失败的原因后,再次打包。 - 打包后index.html会发生变更,引入文件的版本号修改,导致index.html被git追踪,可以通过将index.html加入git忽略文件中去。
- 组态启动后的端口因与后台管理端的端口保持一致。