JavaScript is required

多平台架构

目的与范围

本文解释 relation-graph 的多平台架构,重点说明单一的、与框架无关的核心引擎,如何通过适配器组件集成到 Vue 2、Vue 3、React、Svelte 与 Web Components 中。内容涵盖共享核心模式、数据提供者特化、组件导出结构、响应式集成策略,以及事件桥接机制,这些机制共同保证了不同 UI 框架下的一致能力表现。

如需了解核心类继承层级,请参见类继承层级。如需查看更细的平台集成细节,请参见 Vue 2 与 Vue 3 集成React 集成Svelte 集成Web Components


框架无关的核心设计

relation-graph 之所以能支持多平台,是因为它将图逻辑引擎与 UI 框架关注点明确分离。核心引擎(RelationGraphFinal 及其整条继承链)完全由纯 TypeScript 编写,对任何 UI 框架都没有依赖。

核心独立策略

graph TB
    subgraph "核心包:relation-graph-models"
        Core["RelationGraphFinal
(框架无关核心)"] Base["RelationGraphBase
事件系统基础"] Layouts["布局算法
RGForceLayout、RGTreeLayout 等"] Utils["工具类
RGGraphMath、RGNodesAnalytic"] end subgraph "平台适配器" Vue2["Vue 2 适配器
packages/platforms/vue2"] Vue3["Vue 3 适配器
packages/platforms/vue3"] React["React 适配器
packages/platforms/react"] Svelte["Svelte 适配器
packages/platforms/svelte"] WC["Web Components
packages/platforms/web-components"] end Core --> Base Core --> Layouts Core --> Utils Vue2 -->|"实例化 + setReactiveData"| Core Vue3 -->|"实例化 + setReactiveDataVue3"| Core React -->|"实例化 + setUpdateViewHook"| Core Svelte -->|"实例化 + RGDataProvider4Svelte"| Core WC -->|"基于 Svelte 构建"| Svelte Vue2 -->|setEventEmitHook| Core Vue3 -->|setEventEmitHook| Core React -->|Context Provider| Core Svelte -->|RGHooks| Core

核心独立性的主要特征:

方面 实现
不引入框架依赖 核心模型从不导入 Vue / React / Svelte 库
基于 Hook 的集成 核心暴露 setUpdateViewHook()setEventEmitHook()
平台检测 通过 isReact 标志启用条件行为
数据分离 核心处理的是普通对象,而不是响应式代理对象
共享布局 所有平台使用完全相同的布局算法
共享工具 所有平台共用 RGGraphMath、RGNodesAnalytic 等工具

包结构与分发

代码库为每个平台维护独立包,但共享同一套核心引擎。每个平台都会作为独立 npm 包发布,并保持版本号同步。

NPM 包分发

平台 NPM 包名 源码目录 构建产物
Vue 2 @relation-graph/vue2 packages/platforms/vue2 lib/vue2/
Vue 3 @relation-graph/vue packages/platforms/vue3 lib/vue3/
React @relation-graph/react packages/platforms/react lib/react/
Svelte @relation-graph/svelte packages/platforms/svelte lib/svelte/
Web Components @relation-graph/web-components (基于 Svelte 构建) lib/web-components/

所有包共享同一个版本号(当前为 3.0.2),该版本定义在根目录 package.json 中。发布过程中,构建系统会将版本号同步到所有平台包。


数据提供者特化

每个平台都需要一个专用数据提供者,用于把核心内部的数据结构桥接到该框架的响应式系统中。这些数据提供者都继承自基础类 RGDataProvider

数据提供者层级

graph TB
    Base["RGDataProvider
(基础类)
packages/relation-graph-models"] Vue2Provider["RGDataProvider4Vue2
使用 Vue.set() 实现响应式"] Vue3Provider["RGDataProvider4Vue3
使用 reactive() 与 ref()"] ReactProvider["RGDataProvider4React
触发 updateViewHook()"] SvelteProvider["RGDataProvider4Svelte
使用 writable() stores"] Base --> Vue2Provider Base --> Vue3Provider Base --> ReactProvider Base --> SvelteProvider Vue2Provider -->|"导出于"| Vue2Index["packages/platforms/vue2/src/index.ts"] Vue3Provider -->|"导出于"| Vue3Index["packages/platforms/vue3/src/index.ts"] ReactProvider -->|"导出于"| ReactIndex["packages/platforms/react/src/index.ts"] SvelteProvider -->|"导出于"| SvelteIndex["packages/platforms/svelte/src/index.ts"]

各平台数据提供者特性

平台 数据提供者 关键方法 响应式机制
Vue 2 由框架通过 Vue.observable() 处理 直接变更 由 Vue 2 响应式自动处理
Vue 3 由框架通过 reactive() 处理 直接变更 由 Vue 3 响应式自动处理
React 通过核心中的 hooks updateViewHook() 手动强制更新
Svelte RGDataProvider4Svelte Store 变更 Svelte stores(writable()

数据提供者的特化保证了:核心引擎中的数据变更,能够以各个平台最符合其习惯的方式触发 UI 更新。


适配器模式

每个平台都实现了一个适配器组件,用来包裹核心引擎,并在框架约定与核心 API 之间完成翻译。

适配器组件职责

graph LR
    User["用户应用"]
    Adapter["平台适配器
Vue2 / Vue3 / React 组件"] Core["RelationGraphFinal
核心引擎"] subgraph "适配器职责" A1["1. 实例化核心"] A2["2. 桥接响应式"] A3["3. 映射事件"] A4["4. 管理生命周期"] A5["5. 提供 DOM 引用"] end User -->|props / options| Adapter Adapter --> A1 Adapter --> A2 Adapter --> A3 Adapter --> A4 Adapter --> A5 A1 --> Core A2 --> Core A3 --> Core A4 --> Core A5 --> Core Core -->|methods / state| Adapter Adapter -->|响应式更新| User

各平台适配器文件

平台 适配器组件 核心实例化位置 构建工具
Vue 2 packages/platforms/vue2/src/core4vue/RelationGraph.vue 第 255 行 Vite
Vue 3 packages/platforms/vue3/src/relation-graph/src/core4vue3/RelationGraph.vue 第 77 行 Vite
React packages/platforms/react/src/relation-graph/RelationGraph.tsx 通过 Context Provider Rollup
Svelte packages/platforms/svelte/src/relation-graph/RelationGraph.svelte 直接实例化 Vite
Web Components (由 Svelte 编译而来) 通过 Svelte 构建 Vite

各平台的响应式集成

多平台支持的核心挑战,在于如何把各框架的响应式系统与核心引擎的数据结构连接起来。每个平台都用不同机制来探测并传播数据变化。

Vue 2 响应式集成

Vue 2 使用 Vue.observable() 创建响应式数据对象,核心引擎随后可以直接对这些对象进行变更。

sequenceDiagram
    participant Adapter as "Vue2 适配器
index.vue" participant Core as "RelationGraphFinal" participant Data as "响应式数据
Vue.observable()" participant DOM as "Vue 模板" Adapter->>Data: 创建响应式对象
graphData、graph Adapter->>Core: new RelationGraphFinal(options, listeners) Adapter->>Core: setReactiveData(graphData, graph) Note over Core: 核心保存对这些
响应式对象的引用 Core->>Data: 变更 graphData.nodes[...] Data-->>DOM: 自动重新渲染 Core->>Adapter: $emit(eventName, args) Adapter->>DOM: 事件继续向外传播

Vue 2 实现细节:

// 使用 Vue 2 响应式初始化数据
data() {
    return {
        graphData: {
            rootNode: null,
            nodes: [],
            links: [],
            elementLines: []
        },
        graph: {
            options: createDefaultConfig({}),
            allLineColors: []
        }
    };
}

核心引擎通过 setReactiveData() 获取这些响应式对象的引用,并直接变更它们,从而自动触发 Vue 2 的响应式更新。


Vue 3 响应式集成

Vue 3 使用 reactive() 包装对象,同时使用 markRaw() 包装核心实例,以防止 Vue 将核心引擎本身也转成响应式代理。

sequenceDiagram
    participant Adapter as "Vue3 适配器
index.vue" participant Core as "RelationGraphFinal" participant Data as "响应式数据
reactive()" participant Instance as "markRaw(instance)" participant DOM as "Vue 模板" Adapter->>Data: reactive(graphData) Adapter->>Data: reactive(graph) Adapter->>Core: new RelationGraphFinal(options, listeners) Adapter->>Instance: markRaw(rgInstance) Note over Instance: 防止 Vue 将
核心方法包装为代理 Adapter->>Core: setReactiveDataVue3(graphData, graph) Core->>Data: 变更 graphData.nodes[...] Data-->>DOM: 自动重新渲染 Core->>Adapter: emit(eventName, args) Adapter->>DOM: 事件继续向外传播

Vue 3 实现细节:

// 响应式数据对象
const graphData = reactive<RGGraphData>({
    rootNode: undefined,
    nodes: [],
    links: [],
    elementLines: []
});

const graph = reactive<RGGraphReactiveData>({
    instance: undefined,
    options: createDefaultConfig({}),
    allLineColors: []
});

// 使用 markRaw 包裹核心实例,防止被响应式化
graph.instance = markRaw(rgInstance);

markRaw() 这层包装非常关键。否则 Vue 3 的响应式系统会把核心引擎所有方法都包进响应式代理中,既会带来严重的性能下降,也可能破坏方法引用。


React 集成

React 使用 Context API 将核心实例提供给子组件,并通过手动更新 hook 触发重新渲染。

sequenceDiagram
    participant StoreProvider as "RelationGraphStoreProvider"
    participant Context as "RelationGraphStoreContext"
    participant Adapter as "RelationGraph 组件"
    participant Core as "RelationGraphFinal"
    participant Hook as "updateViewHook"
    participant React as "React 重新渲染"

    StoreProvider->>Core: new RelationGraphFinal(options, listeners)
    StoreProvider->>Hook: setUpdateViewHook(forceUpdate)
    Note over Hook: forceUpdate 会触发
组件重新渲染 StoreProvider->>Context: 提供核心实例 Adapter->>Context: useContext(RelationGraphStoreContext) Note over Adapter: 组件获得
核心实例 Core->>Core: 发生数据变更 Core->>Hook: updateViewHook() Hook->>React: 强制组件更新 React-->>Adapter: 使用新数据重新渲染

React 实现细节:

React 适配器依赖核心引擎中的 setUpdateViewHook() 方法:

// 位于 RelationGraphBase.ts
protected updateViewHook: () => void = () => {
    // do nothing
};

setUpdateViewHook(hook: () => void) {
    this.isReact = true;
    this.updateViewHook = hook;
}

当核心引擎需要触发 UI 更新时,它会调用 this.updateViewHook(),从而进入 React 的强制更新机制:

protected _doSomethingAfterDataUpdated() {
    devLog('_dataUpdated:', this._dataUpdatingNext);
    this.updateShouldRenderGraphData();
    this.updateEasyView();
    this.updateViewHook(); // 触发 React 重新渲染
}

事件系统桥接

核心引擎维护了一套三层事件系统,用于桥接到各个框架自身的原生事件机制。

事件流架构

graph TB
    UserAction["用户交互
点击、拖拽等"] CoreEvent["核心事件处理器
RelationGraphWith7Event"] EventDispatch["emitEvent()
RelationGraphBase"] subgraph "三层事件处理" Tier1["1. 默认处理器
listeners.onNodeClick"] Tier2["2. 自定义处理器
eventHandlers[]"] Tier3["3. 框架 Hook
_hook()"] end Vue2Emit["Vue2: $emit(eventName)"] Vue3Emit["Vue3: emit(eventName)"] ReactContext["React: 事件 props"] UserAction --> CoreEvent CoreEvent --> EventDispatch EventDispatch --> Tier1 EventDispatch --> Tier2 EventDispatch --> Tier3 Tier3 -->|Vue 2| Vue2Emit Tier3 -->|Vue 3| Vue3Emit Tier3 -->|React| ReactContext

事件 Hook 安装

每个平台都会在初始化时安装自己的事件 Hook:

Vue 2:

relationGraph.setEventEmitHook((eventName: RGEventNames, ...eventArgs: any[]) => {
    this.$emit(eventName, ...eventArgs);
});

Vue 3:

rgInstance.setEventEmitHook((eventName: RGEventNames, ...eventArgs: any[]) => {
    emit(eventName, ...eventArgs);
});

React: React 直接通过 listeners 参数把事件监听器传入核心构造函数,因此不经过 Hook 系统。


组件依赖注入

每个平台都使用自己原生的依赖注入模式,把核心实例提供给子组件。

Vue 2:基于函数的 Provide / Inject

graph TB
    Parent["Vue2 适配器
index.vue"] Provide["provide()
getGraphInstance: this.getInstance"] Child1["RGCanvas.vue"] Child2["RGNode.vue"] Child3["RGLine.vue"] Inject["inject: ['getGraphInstance']"] Core["RelationGraphFinal
核心实例"] Parent --> Provide Provide --> Child1 Provide --> Child2 Provide --> Child3 Child1 --> Inject Child2 --> Inject Child3 --> Inject Inject --> Core

Vue 2 实现:

provide() {
    return {
        getGraphInstance: this.getInstance
    };
}

methods: {
    getInstance() {
        return this.relationGraph;
    }
}

Vue 3:基于 Symbol Key 的 Provide / Inject

graph TB
    Parent["Vue3 适配器
index.vue"] Symbol["getGraphInstanceKey
Symbol key"] Provide["provide(key, () => graph.instance)"] Child1["RGCanvas.vue"] Child2["RGNode.vue"] Child3["RGLine.vue"] Inject["inject(getGraphInstanceKey)"] Core["RelationGraphFinal
核心实例"] Parent --> Symbol Symbol --> Provide Provide --> Child1 Provide --> Child2 Provide --> Child3 Child1 --> Inject Child2 --> Inject Child3 --> Inject Inject --> Core

Vue 3 实现:

// 使用 Symbol key 来提升类型安全性
provide(getGraphInstanceKey, () => graph.instance)

Vue 3 使用 constants 文件中的 Symbol 键,可以获得更好的类型安全性,并避免命名冲突。


React:Context API

graph TB
    Provider["RelationGraphStoreProvider"]
    Context["RelationGraphStoreContext"]
    Core["RelationGraphFinal
核心实例"] Adapter["RelationGraph.tsx"] Child1["RGCanvas"] Child2["RGNode"] Child3["RGLine"] Hook["useContext()"] Provider -->|创建| Core Provider -->|通过下文提供| Context Context --> Adapter Context --> Child1 Context --> Child2 Context --> Child3 Adapter --> Hook Child1 --> Hook Child2 --> Hook Child3 --> Hook Hook -->|返回| Core

React 实现:

const RelationGraph: React.FC<RelationGraphCompJsxProps> = (props) => {
    const graphInstance = useContext(RelationGraphStoreContext);
    // graphInstance 就是核心引擎
};

React 的 Context API 在应用层通过 Provider 包装核心实例,再通过 useContext() 将其提供给所有子组件。


生命周期管理

每个平台处理组件生命周期的方式不同,但它们都遵循相同的核心初始化顺序。

统一初始化顺序

sequenceDiagram
    participant Adapter as "平台适配器"
    participant Core as "RelationGraphFinal"
    participant Dom as "DOM 引用"
    participant Layout as "布局系统"

    Note over Adapter: 组件挂载
    Adapter->>Core: new RelationGraphFinal(options, listeners)
    Adapter->>Core: setReactiveData*() / setUpdateViewHook()
    Adapter->>Core: setEventEmitHook()
    Adapter->>Dom: 获取 DOM 引用
    Adapter->>Core: setDom(domElement)
    Adapter->>Core: ready()
    Note over Core: 初始化序列
    Core->>Core: initDom()
    Core->>Core: resetViewSize()
    Core->>Core: initImageTool()
    Core->>Core: updateViewBoxInfo()
    Core->>Core: addFullscreenListener()

    Note over Adapter: 用户调用 setJsonData()
    Adapter->>Core: setJsonData(data)
    Core->>Layout: doLayout()
    Core->>Adapter: 触发 UI 更新

    Note over Adapter: 组件卸载
    Adapter->>Core: beforeUnmount()
    Core->>Core: options.instanceDestroyed = true
    Core->>Core: removeFullscreenListener()

各平台生命周期 Hook

平台 挂载 Hook 卸载 Hook DOM 引用
Vue 2 mounted() beforeDestroy() this.$refs.seeksRelationGraph
Vue 3 onMounted() onBeforeUnmount() seeksRelationGraph$.value
React useEffect(() => {...}, []) useEffect(() => () => {...}, []) seeksRelationGraph$.current

平台检测与条件行为

核心引擎内部只保留了极少量的平台检测逻辑,用于在必要时启用条件行为。

React 检测标志

graph LR
    SetHook["setUpdateViewHook(hook)"]
    Flag["isReact = true"]
    DataUpdate["_dataUpdated()"]
    Condition{"isReact?"}
    UpdateHook["updateViewHook()"]
    Skip["跳过(Vue 自动处理)"]

    SetHook --> Flag
    DataUpdate --> Condition
    Condition -->|是| UpdateHook
    Condition -->|否| Skip

当调用 setUpdateViewHook() 时,会设置 isReact 标志,从而让核心在后续更新中触发 React 的手动更新机制:

setUpdateViewHook(hook: () => void) {
    this.isReact = true;
    this.updateViewHook = hook;
}

这是核心中唯一的平台特定条件分支。Vue 平台不需要它,因为 Vue 的响应式系统会自动感知变更。


总结:多平台策略

relation-graph 的多平台架构通过以下方式实现了框架独立性:

  1. 纯 TypeScript 核心:核心引擎不依赖任何 UI 框架
  2. 适配器模式:轻量包装层负责在框架约定与核心 API 之间做转换
  3. 响应式桥接
    • Vue 2:setReactiveData() + Vue.observable()
    • Vue 3:setReactiveDataVue3() + reactive()markRaw()
    • React:setUpdateViewHook() + 手动强制更新
  4. 事件 Hook 系统setEventEmitHook() 将核心事件桥接到各框架的事件系统
  5. 依赖注入:每个平台使用自己的原生模式(provide/inject、Context API)
  6. 最小平台检测:仅用一个 isReact 标志来控制条件更新

这套架构让一个经过充分验证的核心引擎,能够驱动三种不同的 UI 框架,同时仍然遵守各框架自身的惯用模式。