返回

Meta2d.js 使用指北

做工业组态、设备监控这类可视化项目时,你大概率会面临一个选择题:是从零用 Canvas 手撸一套,还是找个现成的框架?如果你选了后者,Meta2d.js 值得认真看看。

Meta2d.js 是一个开源的 2D 可视化引擎,支持实时数据展示、动态交互和数据管理,基本覆盖了组态应用的核心需求。我在实际项目中用了一段时间,踩了不少坑,这篇文章就把上手过程和关键知识点梳理一遍,希望能帮你少走些弯路。

核心概念速览

在动手之前,先了解几个 Meta2d.js 的核心概念,后面的代码才看得明白:

概念说明类比
Pen(图元)画布上的基本图形单元,可以是矩形、圆形、图片等PPT 里的形状
Meta2d 实例画布的管理者,负责渲染、事件、数据通信等Canvas 的上下文管理器
图元库预置图形的集合,如流程图、类图、表单等组件库
socketFn接收实时数据后的回调函数,决定如何更新图元WebSocket 的 onmessage
ResultMap画布数据的 JSON 快照,可保存/加载/传输序列化后的状态

从安装到跑起来

安装依赖

Meta2d.js 采用了模块化的包结构,核心库必装,其他图元库按需引入:

完整安装命令
# 安装核心库
pnpm install @meta2d/core --save

# 可选功能模块
pnpm install @meta2d/activity-diagram --save
pnpm install @meta2d/chart-diagram --save
pnpm install @meta2d/class-diagram --save
pnpm install @meta2d/flow-diagram --save
pnpm install @meta2d/fta-diagram --save
pnpm install @meta2d/form-diagram --save
pnpm install @meta2d/sequence-diagram --save
pnpm install @meta2d/le5le-charts --save
pnpm install @meta2d/svg --save

这些图元库分别对应活动图、类图、流程图、时序图等常见图形,不用的就别装,减小打包体积。

在 Vue3 中跑通第一个 Demo

[!NOTE] 本文主要介绍 Meta2d.js 在 Vue3 中的使用方法,其他框架的使用方式类似。

要让 Meta2d.js 在 Vue3 项目中正常工作,首先得在 index.html 里引入一些外部资源。这一步容易被忽略,但跳过了后面会出各种奇怪的问题:

index.html 资源引入
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" href="/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Meta2d Vue3</title>
    <script src="//at.alicdn.com/t/c/font_4042197_vr5c62twlzh.js"></script>
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="/src/main.ts"></script>
    <script src="https://assets.le5lecdn.com/2d/canvas2svg.js"></script>
    <script defer src="/meta2d/marked.min.js"></script>
  </body>
</html>

准备工作做好了,接下来创建 Meta2d 实例。这里有个关键细节:Meta2d 在创建时会在全局挂载一个 meta2d 对象,后续可以直接通过它来操作画布。

<script setup lang="ts">
import { Meta2d } from '@meta2d/core'
import { onMounted, onUnmounted } from 'vue'

const meta2dOptions = {}

onMounted(() => {
  new Meta2d('meta2d', meta2dOptions)
  meta2d.on('contextmenu', (e) => console.log('右键菜单', e))
  meta2d.on('click', (e) => console.log('点击', e))
})

onUnmounted(() => {
  meta2d.destroy()
})
</script>

<template>
  <div id="meta2d" class="w-full h-full" />
</template>

[!WARNING] 组件卸载时务必调用 meta2d.destroy() 释放资源,否则会有内存泄漏的风险。Meta2d 内部维护了大量的事件监听和 Canvas 上下文,不销毁的话这些资源不会被 GC 回收。

到这里,一个最简单的 Meta2d 画布就跑起来了。接下来的重头戏是让它真正可用——支持拖拽图元、编辑属性、接收实时数据。

核心功能:拖拽与图元

组态应用的核心交互就是”拖拽”:用户从图元库里选中一个图元,拖到画布上,调整位置和大小。Meta2d.js 对这一流程提供了开箱即用的支持。

注册图元库

在创建 Meta2d 实例之后,需要把用到的图元库注册进去。这一步决定了画布上能使用哪些预置图形:

import { activityDiagram } from '@meta2d/activity-diagram'
import { classPens } from '@meta2d/class-diagram'
import { flowPens } from '@meta2d/flow-diagram'
import { formPens } from '@meta2d/form-diagram'
import { sequencePens, sequencePensbyCtx } from '@meta2d/sequence-diagram'

onMounted(() => {
  new Meta2d('meta2d', meta2dOptions)

  // 注册各类图元
  meta2d.register(flowPens())
  meta2d.register(activityDiagram())
  meta2d.register(classPens())
  meta2d.register(sequencePens())
  meta2d.registerCanvasDraw(sequencePensbyCtx())
  meta2d.registerCanvasDraw(formPens())
})

[!TIP] register() 用于注册通过路径(Path)绘制的图元,registerCanvasDraw() 用于注册需要直接操作 Canvas 2D Context 的图元。两者的区别在于渲染方式不同,实际使用时按照图元库文档来就行。

搭建图元库面板

光注册了图元还不够,还需要一个侧边栏让用户能看到并拖拽它们。关键在于 dragStart 函数——它通过 HTML5 的 Drag API 把图元数据传递给 Meta2d 画布:

function dragStart(e: DragEvent | MouseEvent, elem: GraphicItem) {
  if (!elem) return
  e.stopPropagation()

  if (e instanceof DragEvent) {
    // 桌面端:通过 dataTransfer 传递数据
    e.dataTransfer?.setData('Meta2d', JSON.stringify(elem.data))
  } else {
    // 平板端:点击添加图元
    meta2d.canvas.addCaches = [elem.data]
  }
}

模板中每个图元同时绑定 @dragstart@click.prevent,确保桌面端拖拽和平板端点击都能正常工作。

完整图元库面板组件
<script setup lang="ts">
const graphicGroups = [
  {
    name: '基本形状',
    show: true,
    list: [
      {
        name: '正方形',
        icon: 'l-rect',
        id: 1,
        data: { width: 100, height: 100, name: 'square' },
      },
      {
        name: '矩形',
        icon: 'l-rectangle',
        id: 2,
        data: { width: 200, height: 50, borderRadius: 0.1, name: 'rectangle' },
      },
    ],
  },
]

function dragStart(e, elem) {
  if (!elem) return
  e.stopPropagation()
  if (e instanceof DragEvent) {
    e.dataTransfer?.setData('Meta2d', JSON.stringify(elem.data))
  } else {
    meta2d.canvas.addCaches = [elem.data]
  }
}
</script>

<template>
  <div class="graphics-panel">
    <div class="panel-title">图元库</div>
    <div v-for="group in graphicGroups" :key="group.name" class="graphic-group">
      <div class="group-header">{{ group.name }}</div>
      <div class="group-content">
        <div
          v-for="elem in group.list"
          :key="elem.id"
          class="graphic-item"
          :draggable="true"
          @dragstart="dragStart($event, elem)"
          @click.prevent="dragStart($event, elem)"
        >
          <svg class="icon" aria-hidden="true">
            <use :xlink:href="`#${elem.icon}`" />
          </svg>
          <div class="graphic-name" :title="elem.name">{{ elem.name }}</div>
        </div>
      </div>
    </div>
  </div>
</template>

核心功能:属性编辑

光能把图元拖到画布上还不够,真正的组态应用需要让用户能编辑每个图元的属性——位置、大小、颜色、文字等等。这就需要监听图元的选中/取消选中事件,然后把属性显示到一个表单里。

管理图元选中状态

为了让属性面板知道当前选中的是什么,我封装了一个 useMeta2dSelection 组合式函数。它会根据选中图元的类型(普通图元、设备、测点、连线)自动判断并设置不同的编辑模式:

export enum SelectionMode {
  File = 'file',
  Pen = 'pen',
  Equip = 'equip',
  Point = 'point',
  Line = 'line',
}

export function useMeta2dSelection() {
  const selections = reactive<{
    mode: SelectionMode
    pen?: Pen
    pens?: Pen[]
  }>({
    mode: SelectionMode.File,
    pen: undefined,
    pens: [],
  })

  const select = (pens?: Pen[]) => {
    if (!pens || pens.length !== 1) {
      selections.mode = SelectionMode.File
      selections.pen = undefined
      selections.pens = pens?.length ? pens : []
      return
    }

    const pen = pens[0]
    const penName = pen.name || ''
    if (penName === 'image') selections.mode = SelectionMode.Equip
    else if (penName.startsWith('point-combine')) selections.mode = SelectionMode.Point
    else if (pen.type === 1) selections.mode = SelectionMode.Line
    else selections.mode = SelectionMode.Pen

    selections.pen = pen
    selections.pens = pens
  }

  return { selections, select }
}

属性面板与双向绑定

有了选中状态管理,就可以构建属性面板了。核心逻辑是:选中图元时读取其属性填入表单,修改表单时通过 meta2d.setValue() 回写到图元上。

// 获取当前选中图元的属性
function getPenProperties() {
  const pen = selections.pen
  if (!pen) return

  form.text = pen.text || ''
  form.color = pen.color || ''
  form.background = pen.background || ''

  const rect = meta2d.getPenRect(pen)
  if (rect) {
    form.x = Math.round(rect.x) || 0
    form.y = Math.round(rect.y) || 0
    form.width = Math.round(rect.width) || 0
    form.height = Math.round(rect.height) || 0
  }
}

// 更新图元属性
function updateProperty(prop: string) {
  if (!selections.pen) return
  const update = { id: form.id }
  update[prop] = form[prop]
  meta2d.setValue(update, { render: true })
}

// 监听选中图元变化,自动刷新表单
watch(() => selections.pen?.id, getPenProperties)

[!TIP] meta2d.setValue() 的第二个参数 { render: true } 会触发画布重绘。如果你在批量更新多个属性,可以先不传这个参数,最后统一调用 meta2d.render() 来避免不必要的重复渲染。

完整属性面板组件
<script setup lang="ts">
import { Meta2d } from '@meta2d/core'
import { onMounted, onUnmounted, reactive, watch } from 'vue'
import { useMeta2dSelection } from './useMeta2dSelection'

const { selections, select } = useMeta2dSelection()

const form = reactive({
  id: '',
  x: 0, y: 0, width: 0, height: 0,
  text: '', color: '', background: '',
  fontSize: 12, fontFamily: 'Arial', lineWidth: 1,
})

function active(pens) { select(pens) }
function inactive() { select() }

function getPenProperties() {
  const pen = selections.pen
  if (!pen) return
  form.id = pen.id || ''
  form.text = pen.text || ''
  form.color = pen.color || ''
  form.background = pen.background || ''
  form.fontSize = pen.fontSize || 12
  form.fontFamily = pen.fontFamily || 'Arial'
  form.lineWidth = pen.lineWidth || 1
  const rect = meta2d.getPenRect(pen)
  if (rect) {
    form.x = Math.round(rect.x) || 0
    form.y = Math.round(rect.y) || 0
    form.width = Math.round(rect.width) || 0
    form.height = Math.round(rect.height) || 0
  }
}

function updateProperty(prop) {
  if (!selections.pen) return
  const update = { id: form.id }
  update[prop] = form[prop]
  meta2d.setValue(update, { render: true })
}

watch(() => selections.pen?.id, getPenProperties)

onMounted(() => {
  new Meta2d('meta2d', {})
  meta2d.on('active', active)
  meta2d.on('inactive', inactive)
})

onUnmounted(() => { meta2d.destroy() })
</script>

<template>
  <div class="editor-container">
    <div class="sidebar">
      <GraphicsPanel />
    </div>

    <div class="canvas-container">
      <div id="meta2d" class="canvas" />
    </div>

    <div v-if="selections.pen" class="properties-panel">
      <div class="panel-title">属性设置</div>

      <div class="property-group">
        <div class="group-title">位置和大小</div>
        <div class="property-row">
          <div class="property-label">X 坐标</div>
          <input v-model.number="form.x" type="number" @change="updateProperty('x')">
        </div>
        <div class="property-row">
          <div class="property-label">Y 坐标</div>
          <input v-model.number="form.y" type="number" @change="updateProperty('y')">
        </div>
        <div class="property-row">
          <div class="property-label">宽度</div>
          <input v-model.number="form.width" type="number" @change="updateProperty('width')">
        </div>
        <div class="property-row">
          <div class="property-label">高度</div>
          <input v-model.number="form.height" type="number" @change="updateProperty('height')">
        </div>
      </div>

      <div class="property-group">
        <div class="group-title">样式</div>
        <div class="property-row">
          <div class="property-label">边框颜色</div>
          <input v-model="form.color" type="color" @change="updateProperty('color')">
        </div>
        <div class="property-row">
          <div class="property-label">背景颜色</div>
          <input v-model="form.background" type="color" @change="updateProperty('background')">
        </div>
      </div>

      <div class="property-group">
        <div class="group-title">文本</div>
        <div class="property-row">
          <div class="property-label">内容</div>
          <input v-model="form.text" type="text" @change="updateProperty('text')">
        </div>
        <div class="property-row">
          <div class="property-label">字体大小</div>
          <input v-model.number="form.fontSize" type="number" @change="updateProperty('fontSize')">
        </div>
      </div>
    </div>
  </div>
</template>

进阶:多画布与实时数据

当基础功能搭建完成后,你很可能会遇到两个进阶需求:同一页面管理多个画布,以及接入实时数据让画布”活”起来。

多画布的那个坑

[!WARNING] Meta2d.js 每次 new Meta2d() 都会把实例挂到全局 meta2d 对象上,后创建的会覆盖先创建的。如果你在主画布的弹窗里又开了一个次画布,全局 meta2d 就指向次画布了,主画布的操作全都会出问题。

解决方法是在创建次画布后,手动把全局引用恢复回来:

import type { Options } from '@meta2d/core'
import { Meta2d } from '@meta2d/core'

export function getExtraMeta2d(id: string, options?: Options) {
  const mainMeta2d = (globalThis as any).meta2d
  const newMeta2d = new Meta2d(id, options)
  ;(globalThis as any).meta2d = mainMeta2d
  return newMeta2d
}

用法很简单:主画布正常创建,次画布通过 getExtraMeta2d() 创建。函数内部会在创建完次画布后立即把全局 meta2d 恢复成主画布的引用,两个画布互不干扰。

三种实时数据通信方式

组态应用的灵魂在于”实时”——设备状态变了,画布上的图元要跟着变。Meta2d.js 支持三种数据通信方式,适用于不同的场景:

通信方式适用场景实时性复杂度
MQTTIoT 设备监控,设备量大、推送频繁
WebSocket通用 Web 应用,双向通信
HTTP 轮询数据更新频率低,不想维护长连接最低

三种方式都通过 meta2d.socketFn 回调来处理接收到的数据,核心模式是一样的:解析消息 -> 调用 meta2d.setValue() 更新图元。

[!IMPORTANT] socketFn 的返回值决定了后续行为:返回 false 只执行你的自定义回调;返回 true 则还会额外执行 Meta2d 核心库的默认处理逻辑。大多数场景下返回 false 就够了。

关于 socketFn 返回值的具体行为,可参考官方文档说明1

MQTT 通信适合物联网场景,设备数量多、数据推送频繁。Meta2d 内置了 MQTT 支持:

// 连接 MQTT
meta2d.connectMqtt({
  mqtt: 'ws://broker.example.com:8083/mqtt',
  mqttTopics: 'topic1/#,topic2',
  mqttOptions: {
    clientId: `meta2d_${Date.now()}`,
    username: 'username',
    password: 'password',
  },
})

// 自定义数据处理
meta2d.socketFn = (data: string) => {
  const message = JSON.parse(data)
  if (message.penId && message.value !== undefined) {
    meta2d.setValue({ id: message.penId, value: message.value })
  }
  return false
}

[!NOTE] 浏览器环境只能用 ws(s):// 协议连接 MQTT Broker,不能用 mqtt://。如果你从后端文档复制了 mqtt:// 开头的地址,记得改成 ws://

自定义 MQTT 客户端(使用 mqtt.js 库)
import mqtt from 'mqtt'
import { ref } from 'vue'

const MQTT_URL = 'ws://broker.example.com:8083/mqtt'
const MQTT_TOPICS = ['device/+/data', 'system/status']
const MQTT_OPTIONS = {
  username: 'username',
  password: 'password',
  clientId: `meta2d_client_${Date.now()}`,
}

export function useCustomMqtt() {
  const client = ref(null)
  const connectionStatus = ref('disconnected')

  function connect(messageHandler) {
    client.value = mqtt.connect(MQTT_URL, MQTT_OPTIONS)

    client.value.on('connect', () => {
      connectionStatus.value = 'connected'
      client.value.subscribe(MQTT_TOPICS, (err) => {
        if (err) console.error('订阅失败:', err)
      })
    })

    client.value.on('error', (err) => {
      connectionStatus.value = 'error'
    })

    client.value.on('message', (topic, payload) => {
      try {
        const message = JSON.parse(payload.toString())
        messageHandler(topic, message)
      } catch (e) {
        console.error('处理消息失败:', e)
      }
    })
  }

  function disconnect() {
    if (client.value?.connected) {
      client.value.end()
      connectionStatus.value = 'disconnected'
    }
  }

  function publish(topic, message, options = {}) {
    if (!client.value?.connected) return false
    const payload = typeof message === 'object' ? JSON.stringify(message) : message
    client.value.publish(topic, payload, options)
    return true
  }

  return { client, connectionStatus, connect, disconnect, publish }
}

WebSocket 通信是最常见的方案,适合大多数 Web 应用场景:

// 连接 WebSocket
meta2d.connectWebsocket('ws://your-server.com/ws')

// 自定义数据处理(和 MQTT 一样通过 socketFn)
meta2d.socketFn = (data: string) => {
  const message = JSON.parse(data)
  if (message.penId && message.value !== undefined) {
    meta2d.setValue({ id: message.penId, value: message.value })
  }
  return false
}

HTTP 轮询虽然实时性不如前两者,但胜在简单可靠,不需要额外的服务端支持。特别适合数据更新频率不高的场景:

// 单个接口轮询
meta2d.store.data.http = 'https://api.example.com/device/status'
meta2d.store.data.httpTimeInterval = 3000 // 3 秒轮询
meta2d.store.data.httpHeaders = { Authorization: 'Bearer xxx' }
meta2d.connectHttp()
多接口轮询配置(1.0.26+ 支持)
// 配置多个 HTTP 轮询
meta2d.store.data.https = [
  {
    http: 'https://api.example.com/device/status',
    method: 'GET',
    httpTimeInterval: 3000,
    httpHeaders: {},
  },
  {
    http: 'https://api.example.com/alarm/list',
    method: 'GET',
    httpTimeInterval: 5000,
    httpHeaders: {},
  },
]
meta2d.connectHttp()

工程化实践

当项目复杂度上来之后,把 Meta2d 的各个功能模块拆分清楚就变得很重要了。下面分享几个在实际项目中总结出的实践。

组件化封装

与其让 Meta2d 的初始化代码散落在各个页面,不如封装成一个通用的 Vue 组件。通过 props 接收配置和初始数据,通过 emit 暴露事件,父组件用起来会清爽很多:

Meta2d 通用画布组件
<script setup lang="ts">
import type { Pen } from '@meta2d/core'
import { Meta2d } from '@meta2d/core'
import { onMounted, onUnmounted, ref, watch } from 'vue'

const props = defineProps({
  options: { type: Object, default: () => ({}) },
  initialData: { type: Object, default: null },
  canvasId: { type: String, default: 'meta2d' },
})

const emit = defineEmits(['active', 'inactive', 'click', 'contextmenu'])

const meta2dInstance = ref(null)

function initMeta2d() {
  meta2dInstance.value = new Meta2d(props.canvasId, props.options)

  meta2dInstance.value.on('active', (pens: Pen[]) => emit('active', pens))
  meta2dInstance.value.on('inactive', () => emit('inactive'))
  meta2dInstance.value.on('click', (e: MouseEvent) => emit('click', e))
  meta2dInstance.value.on('contextmenu', (e: MouseEvent) => emit('contextmenu', e))

  if (props.initialData) {
    meta2dInstance.value.open(props.initialData)
  }
}

watch(() => props.initialData, (newData) => {
  if (meta2dInstance.value && newData) {
    meta2dInstance.value.open(newData)
  }
}, { deep: true })

defineExpose({
  getInstance: () => meta2dInstance.value,
})

onMounted(() => initMeta2d())
onUnmounted(() => meta2dInstance.value?.destroy())
</script>

<template>
  <div :id="canvasId" class="meta2d-canvas" />
</template>

<style scoped>
.meta2d-canvas {
  width: 100%;
  height: 100%;
}
</style>

数据持久化

画布上精心搭建的组态画面,当然需要保存下来。核心 API 就两个:meta2d.data() 导出 JSON,meta2d.open(json) 加载 JSON。

// 导出画布数据
const data = meta2d.data()

// 加载画布数据
meta2d.open(data)

// 导出为图片
meta2d.toPng('my-canvas')     // PNG
meta2d.toSVG('my-canvas')     // SVG
完整数据持久化工具函数
export function useMeta2dData() {
  function saveData(filename = 'meta2d-data.json') {
    const data = meta2d.data()
    const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' })
    const url = URL.createObjectURL(blob)
    const link = document.createElement('a')
    link.href = url
    link.download = filename
    link.click()
    URL.revokeObjectURL(url)
  }

  function loadFromFile(file: File): Promise<boolean> {
    return new Promise((resolve, reject) => {
      const reader = new FileReader()
      reader.onload = (e) => {
        try {
          const data = JSON.parse(e.target.result as string)
          meta2d.open(data)
          resolve(true)
        } catch (err) {
          reject(err)
        }
      }
      reader.onerror = reject
      reader.readAsText(file)
    })
  }

  function exportImage(filename = 'meta2d-canvas', type: 'png' | 'svg' = 'png') {
    type === 'svg' ? meta2d.toSVG(filename) : meta2d.toPng(filename)
  }

  return { saveData, loadFromFile, exportImage }
}

自定义图元

内置图元满足不了业务需求时,就需要自己造了。Meta2d.js 支持通过自定义渲染函数来绘制任意图形。核心是定义图元的 data 结构和可选的 onRender 回调:

const customDevicePen = {
  name: 'customDevice',
  icon: 'l-custom-device',
  data: {
    text: '自定义设备',
    width: 120,
    height: 80,
    name: 'customDevice',
    deviceId: '',
    status: 'normal',
    color: '#1890ff',
    background: '#e6f7ff',
  },
}

// 注册到 Meta2d
meta2d.register([customDevicePen])
带自定义渲染的完整示例
export function createCustomPens() {
  const customDevicePen = {
    name: 'customDevice',
    icon: 'l-custom-device',
    data: {
      text: '自定义设备',
      width: 120,
      height: 80,
      name: 'customDevice',
      deviceId: '',
      status: 'normal',
      lineWidth: 2,
      color: '#1890ff',
      background: '#e6f7ff',
      statusStyles: {
        normal: { background: '#e6f7ff', color: '#1890ff' },
        warning: { background: '#fff7e6', color: '#fa8c16' },
        error: { background: '#fff1f0', color: '#f5222d' },
      },
    },
  }

  const customMetricPen = {
    name: 'customMetric',
    icon: 'l-custom-metric',
    data: {
      text: '0',
      width: 80,
      height: 40,
      name: 'customMetric',
      metricId: '',
      unit: '',
      fontSize: 16,
      fontWeight: 'bold',
      onRender: (ctx, pen) => {
        ctx.fillStyle = pen.background || '#f0f0f0'
        ctx.strokeStyle = pen.color || '#d9d9d9'

        const { x, y, width: w, height: h } = pen.calculative.worldRect
        const r = 4

        ctx.beginPath()
        ctx.moveTo(x + r, y)
        ctx.lineTo(x + w - r, y)
        ctx.arcTo(x + w, y, x + w, y + r, r)
        ctx.lineTo(x + w, y + h - r)
        ctx.arcTo(x + w, y + h, x + w - r, y + h, r)
        ctx.lineTo(x + r, y + h)
        ctx.arcTo(x, y + h, x, y + h - r, r)
        ctx.lineTo(x, y + r)
        ctx.arcTo(x, y, x + r, y, r)
        ctx.closePath()
        ctx.fill()
        ctx.stroke()

        ctx.fillStyle = pen.fontColor || '#000'
        ctx.textAlign = 'center'
        ctx.textBaseline = 'middle'
        ctx.font = `${pen.fontWeight || ''} ${pen.fontSize || 16}px ${pen.fontFamily || 'Arial'}`
        const text = pen.unit ? `${pen.text || '0'} ${pen.unit}` : (pen.text || '0')
        ctx.fillText(text, x + w / 2, y + h / 2)

        return true
      },
    },
  }

  return [customDevicePen, customMetricPen]
}

Demo 项目与总结

在学习和实践 Meta2d.js 的过程中,我搭了一个 Vue3 的演示项目,把上面提到的功能都集成了进去,有兴趣的话可以看看:

回顾整个使用过程,Meta2d.js 给我的感觉是”够用且灵活”。它不像一些重量级框架那样大而全,但在 2D 组态这个垂直领域,该有的功能基本都有——图元拖拽、属性编辑、多种数据通信方式、自定义图元、多画布管理。上手成本不算高,但要用好确实需要花些时间去理解它的设计思路,特别是全局 meta2d 对象的管理和 socketFn 回调的工作机制。

如果你正在做组态监控、流程图编辑器或者其他 2D 可视化项目,Meta2d.js 是一个值得尝试的选择。

Footnotes

  1. socketFn 的回调机制:当 Meta2d 通过 MQTT、WebSocket 或 HTTP 接收到数据时,会先调用用户通过 meta2d.socketFn 注册的自定义回调函数,并将原始消息字符串作为参数传入。如果回调返回 true,Meta2d 还会继续执行内置的默认数据处理逻辑(即尝试将数据解析为 {id, ...props} 格式并自动调用 setValue 更新对应图元);返回 false 则跳过默认处理,完全由用户自行控制数据流向。