ThingsKit开发指南-前段组态开发指南(组态开发指南(v1.2.0之前版本))

准备

目录文件及重要文件说明

目录文件

/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

重要文件说明

元件库开发

ThingsKit开发指南-前段组态开发指南(组态开发指南(v1.2.0之前版本))
文件位置
.
├── 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();
	};
})();
  1. 将元件库导入
// src/main/webapp/js/diagramly/Devel.js
// Devel.js
// 将Sidebar-Engine.js引入
mxscript(drawDevUrl + 'js/diagramly/sidebar/thingskit/Sidebar-Engine.js');
  1. 将元件库加入到需要打包的文件队列中
 <!-- 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>
  1. 将元件库注册到侧边栏
// 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();
}

数据绑定面板开发

ThingsKit开发指南-前段组态开发指南(组态开发指南(v1.2.0之前版本))
ThingsKit开发指南-前段组态开发指南(组态开发指南(v1.2.0之前版本))
文件位置
/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,与后台管理系统一致
源码介绍
  1. const.js 常量名
GLOBAL_TOKEN 访问令牌:
	用于请求中携带
	来源于后台管理系统中存储在storage中的值,storage中的值涉及在生产环境和开发环境中是否加解密问题,加解密方式应与后台管理系统中		一致。
	
GLOBAL_PLATFORM_INFO 平台信息
	用于项目中需要使用平台信息的场景,例如首屏加载时的loading,显示不同平台的平台名。
	
GLOBAL_WS_URL socket地址
	用于项目中socket连接
	
hasSavePermission 检查是否权限
	用于检查组态中时候拥有写入的权限
  1. 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() {}

开发环境搭建及源码运行

打包部署

准备工作

  1. [JDK](Download Java for macOS)安装与环境变量配置(项目使用JDK11)  安装文档
  2. [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文件夹。))
  3. [Node.js](Node.js (nodejs.org))安装(项目使用v16.15.0版本)

运行打包

# 安装依赖
npm install

# 运行打包 运行后开始打包 预计耗时1min
npm run build

打包成功后控制台输出结果与包存放位置,oss文件存放需要上传至oss服务器的js文件。

ThingsKit开发指南-前段组态开发指南(组态开发指南(v1.2.0之前版本))
ThingsKit开发指南-前段组态开发指南(组态开发指南(v1.2.0之前版本))

参考文档

FAQ

  1. 打包过程中发生失败时,可能回导致需要gulp编译的文件被覆盖。应检查需要编译的文件是否已经被gulp编译。
    左侧文件则被gulp编译,失败时则到dist目录中找到未被编译的文件或者通过git进行回滚,在解决打包失败的原因后,再次打包。
    ThingsKit开发指南-前段组态开发指南(组态开发指南(v1.2.0之前版本))ThingsKit开发指南-前段组态开发指南(组态开发指南(v1.2.0之前版本))
  2. 打包后index.html会发生变更,引入文件的版本号修改,导致index.html被git追踪,可以通过将index.html加入git忽略文件中去。
    ThingsKit开发指南-前段组态开发指南(组态开发指南(v1.2.0之前版本))
  3. 组态启动后的端口因与后台管理端的端口保持一致。