核心 SDK 详细设计文档 (Visual-Render-SDK)

一. 架构目标

构建一个渲染引擎,通过解析 JSON 协议,实现布局嵌套、图表渲染、数据源绑定及交互控制。支持通过"逃生通道"挂载业务定制组件。

二. 核心数据结构 (The Protocol)

所有的渲染都基于一份 config 对象。

字段 类型 说明
type String 节点类型:container​(布局) 或widget(组件)
component String 渲染标识,如FlexLayout​,EChartsRender​,TabContainer
children Array container有效,存放子节点配置
content Object widget​有效,包含templateId​(样式) 和sourceId(数据)
customComponent String 逃生通道 ​:若有值,则忽略content,直接加载此业务组件
reactiveParams Array<String> 声明需要监听的全局变量名,如['date']

示例:

json 复制代码
{
    "type": "widget",
    "key":"chart-01",
    "comp": "ChartWidget",
    "echartsOptions": {},
    "formOptions": {},
    "actions":{
        "type": "click",
        "execute": "openDialog",
        "controls":[
            {
                "comp":"ExportButton",
                "type": "action",
                "field": "name",
                "props":[

                ]
            }
        ],
        "config": {
            "datasourceKey": "data-source-1",
            "params": {
                "category": "{{data.name}}", 
                "date": "{{global.selectedDate}}"
            }
        }
    }
}

json 复制代码
{
    "type": "datasouce",
    "key": "datasource-1",
    "url": "",
    "params":[
        {
            "field":"x",
            "to":"replaceTextMap",
            "value": ""
        }
    ],
    "templateKey":"chart-01"
}

json 复制代码
{
    "type": "layout",
    "comp": "NineGridLayout",
    "deptId": "",
    "children": [
        {
            "id":"",
            "title":"xxx指标",
            "controls":[
                {
                    "comp":"FilterSelect",
                    "type": "filter",
                    "field": "name",
                    "props":[

                    ]
                }
            ],
            "datasourceKey": "data-source-1"
        }
    ]
}

三. 渲染逻辑实现 (Recursive Renderer)

1. 递归分发器 (VisualRenderer.vue)

这是 SDK 的入口。

  • 逻辑 :根据 type 判断。如果是 container,渲染布局组件并把 children 传给它;如果是 widget,渲染加载器。
  • 伪代码实现
xml 复制代码
<component :is="config.type === 'container' ? LayoutResolver : WidgetLoader" :config="config" />

2. 布局解析器 (LayoutResolver.vue)

  • 职责 :实现 FlexLayout, GridLayout, TabContainer
  • 逻辑 :如果是 TabContainer,需维护一个 activeKey。每个布局组件内部必须包含:
xml 复制代码
<VisualRenderer v-for="child in config.children" :config="child" />

四. 数据与交互中心 (Data & State)

1. 宿主注入 (Dependency Injection)

SDK 不得直接访问 Pinia。必须通过 provide/inject 获取宿主提供的能力。

  • request: 执行 API 的函数 (sourceId, params) => Promise
  • globalContext: 包含全局状态的对象,如 { date: '2023-10' }
  • bizComponents: 一个对象映射表,用于"逃生通道"查找业务组件

2. 响应式监听 (useGlobalReactive.js)

  • 目标:实现 A 项目需要日期联动,B 项目不需要。

  • 逻辑

    • 使用 watch 监听 globalContext
    • 判断当前 config.reactiveParams 是否包含改变的变量。
    • 若包含,调用 fetchData 刷新数据。

五. 逃生通道实现 (The Escape Hatch)

当 config.customComponent 有值时:

  • 从 inject('bizComponents') 中寻找对应的 Vue 组件
  • 使用 <component :is="foundComponent" /> 进行渲染
  • 将 config.props 透传给该组件

六. 交互面板 (Action Bar)

用于实现 Tab 右侧差异化控制按钮。

  • 分类

    • Filter: 下拉框/搜索框。修改 TabContainer 内部的 localState
    • Action: 按钮。点击时读取 localState,调用 request 执行下载或接口调用。

七. 前端开发任务清单 (Implementation Checklist)

任务 1:核心骨架 (基础)

  • 实现 VisualRenderer 递归递归组件。
  • 实现 FlexLayout (支持 row/column) 和 GridLayout (支持 columns 配置)。

任务 2:物料加载器 (数据)

  • 实现 WidgetLoader:根据 sourceId 调用注入的 request 方法。
  • 实现 ECharts 通用渲染模板:接收 optiondata

任务 3:交互与权限 (进阶)

  • 实现 AuthGuard:在渲染每个节点前判断 config.acl
  • 实现 TabContainer 及其右侧动态 ControlRenderer

任务 4:逃生机制 (定制)

  • 实现动态组件查找逻辑,确保能挂载宿主项目传入的 .vue 文件。

八. 开发提示

  • 样式 ​:统一使用 CSS Variables (如 --primary-color),不要写死颜色。

  • 错误处理 ​:若 sourceId​ 请求失败,组件应渲染 ErrorPlaceholder 而不是白屏。

  • 性能 :ECharts 实例必须在 onUnmounted​ 时调用 dispose()

  • sdk升级问题

    • 当 A 项目由于时间紧,无法整体重构但又要新功能时:在 SDK 1.0 中紧急发布一个补丁版本(v1.1.0),仅把 2.0 中某些核心功能组件(比如那个新的九宫格或导出按钮)以"独立插件"的形式回填。(A 项目仍然用 1.0 的框架,但局部引用了 2.0 的功能。)
    • 多版本协议兼容层: 转换函数的作用是:抹平字段差异,填充默认值。

核心渲染分发器 (VisualRenderer.vue)

它是 SDK 的唯一出口。它不负责具体的渲染,只负责根据版本号进行路由分发

html 复制代码
<template>
  <!-- 根据版本号动态切换渲染器 -->
  <component 
    :is="currentRenderer" 
    v-bind="$attrs" 
    :config="processedConfig" 
  />
</template>

<script setup>
import { computed } from 'vue';
import RendererV1 from './v1/RendererV1.vue';
import RendererV2 from './v2/RendererV2.vue';
import { transformV1ToV2 } from './adapters/v1ToV2';

const props = defineProps({
  config: { type: Object, required: true }
});

// 1. 自动转换逻辑:如果是旧版本,且我们希望用新引擎跑,可以先转换
const processedConfig = computed(() => {
  const version = props.config.version || '1.0';
  // 如果是 1.0 的配置,但在 2.0 环境下运行,执行转换
  if (version === '1.0') {
    return transformV1ToV2(props.config);
  }
  return props.config;
});

// 2. 分发逻辑:决定使用哪个版本的 UI 渲染器
const currentRenderer = computed(() => {
  const version = processedConfig.value.version;
  if (version >= '2.0') return RendererV2;
  return RendererV1;
});
</script>
js 复制代码
/**
 * 将 V1 版本的协议转换为 V2 版本的标准协议
 * 场景示例:V1 中图表配置在 renderOptions,V2 中统一到了 content.style
 */
export function transformV1ToV2(oldConfig) {
  // 深度克隆,避免污染原始数据
  const newConfig = JSON.parse(JSON.stringify(oldConfig));
  
  // 1. 升级版本标识
  newConfig.version = '2.0-compat'; 

  // 2. 递归处理组件
  const walk = (node) => {
    if (!node) return;

    // 示例:V1 使用 'chartType', V2 统一映射到 'component'
    if (node.type === 'chart' && node.chartType) {
      node.component = node.chartType === 'line' ? 'EChartsLine' : 'EChartsBar';
      delete node.chartType;
    }

    // 示例:V1 的数据源配置在 dataSource,V2 要求在 content 目录下
    if (node.dataSource && !node.content) {
      node.content = {
        sourceId: node.dataSource.id || node.dataSource.url,
        params: node.dataSource.params || {}
      };
      delete node.dataSource;
    }

    // 递归处理子节点
    if (node.children && Array.isArray(node.children)) {
      node.children.forEach(walk);
    }
  };

  walk(newConfig);
  return newConfig;
}

目录结构建议

html 复制代码
src/
├── sdk/                # 核心 SDK (多项目共用)
│   ├── renderer/       # 递归渲染引擎
│   └── components/     # 基础物料库 (图表/表格/容器)
├── designer/           # 配置界面 (仅管理员可见)
│   ├── setters/        # 各类属性编辑器 (JSON Schema Form)
│   └── canvas/         # 拖拽画布/结构树
└── views/              # 各项目具体的业务页面 (引用 renderer)

九. 插件化架构

插件化架构的核心是将 SDK 从一个"​巨型功能库 ​"转变为一个"​轻量级调度中心"。

在 SDK 内部,我们不再通过 if (config.xxx)​ 来堆砌逻辑,而是建立一套生命周期钩子(Hooks) ,将功能逻辑像"外挂"一样按需挂载。

插件化架构的三个维度

逻辑插件化:基于 Hook 的"功能注入"

不要在 WidgetLoader.vue 中写业务,它只负责调用。

javascript 复制代码
// WidgetLoader.vue 核心逻辑
import { useDataFetcher } from './hooks/useDataFetcher';
import { useGlobalReactive } from './plugins/globalReactive';
import { useLocalInteraction } from './plugins/localInteraction';

export default {
  setup(props) {
    // 1. 核心功能:取数逻辑(每个组件必有)
    const { data, refresh } = useDataFetcher(props.config);

    // 2. 插件功能:全局响应(只有配置了 reactiveParams 才会真正生效)
    // 逻辑在独立文件中,SDK 主逻辑不关心它是如何监听 date 的
    useGlobalReactive(props.config, refresh);

    // 3. 插件功能:组件联动(如点击 A 刷新 B)
    useLocalInteraction(props.config, refresh);

    return { data };
  }
}

物料插件化:基于"组件映射表"的解耦

SDK 内部不 import​ 具体的业务图表,只维护一个 ComponentMap

  • SDK 内部 :只有一个 BaseChart.vue
  • 宿主项目 :在 app.use(SDK, { components: { MySpecialChart } }) 时注入。
  • 效果:如果 A 项目要一个"极其复杂的 3D 飞线图",你直接在 A 项目里写,SDK 根本不需要知道它的存在。

协议插件化:自定义字段解析

允许 SDK 注册"协议处理器"。

javascript 复制代码
// 比如你想增加一个"水印"功能,但不想改 SDK 源码
VisualSDK.use({
  name: 'watermark-plugin',
  // 当解析到包含 'watermark': true 的 JSON 节点时触发
  onRender(node, el) {
    if (node.watermark) {
      applyWatermark(el);
    }
  }
});

为什么要这么做?

特性 传统模式 (硬编码) 插件化模式 (解耦)
功能隔离 改了日期联动,可能会把权限逻辑改坏。 日期联动逻辑在globalReactive.js​,权限在auth.js,互不干扰。
代码体积 所有功能都打进包里,B 项目不需要也要加载。 可以实现 Tree-shaking,没用到的插件逻辑不打包。
多人协作 大家都在WidgetLoader里改代码,冲突不断。 每个人负责不同的 Hook 文件。
应对强势方 "这个功能 SDK 不支持,我得改源码"。 "我在项目里写个插件/组件注入进去就行了"。

💡 针对你的场景:如何落地插件化?

  • 抽离 Hook ​:把 ​API 请求 ​、​全局状态监听 ​、事件广播 分别写成 src/sdk/hooks 下的独立文件。

  • 定义 Contract(契约)

    • useDataFetcher 必须返回 dataloading
    • useGlobalReactive 必须接受 configcallback
  • 保持 Renderer 纯净RecursiveRenderer.vue 里的代码不应超过 100 行,它只负责递归,不处理任何业务。

插件化架构是你对抗强势需求方的"盾牌"。

当他们提出奇葩需求时,你的回复从 "我要改 SDK 核心逻辑(风险大)" 变成了 "我给这个项目单独注入一个逻辑钩子(风险受控)"

十. 关键代码伪码实现

SDK拿到宿主项目提供的全局状态

如果 SDK 也要修改宿主的狀態?

可以在 app.use​ 的配置項中再注入一個 Action 回調函數

  • SDK 内部使用 getGlobalField 的组件实现
js 复制代码
import { inject, computed } from 'vue';

// 定義一個 Hook 方便 SDK 內部組件獲取宿主狀態
export function useVisualContext() {
  // 注入宿主提供的全局上下文
  const context = inject('VISUAL_GLOBAL_CONTEXT', {});

  // 提供輔助方法,確保數據獲取時有默認值,防止崩潰
  const getGlobalField = (field, defaultValue = null) => {
    return computed(() => {
      // 宿主傳入的可能是個函數 (Getter),也可能是個響應式對象
      const data = typeof context === 'function' ? context() : context;
      return data[field] ?? defaultValue;
    });
  };

  return { getGlobalField };
}
js 复制代码
<!-- WidgetLoader.vue -->
<script setup>
import { watch, onMounted } from 'vue';
import { useVisualContext } from '../hooks/useVisualContext';
import { useVisualApi } from '../hooks/useVisualApi';

const props = defineProps(['config']); // 包含 sourceId 等信息
const { getGlobalField } = useVisualContext();
const { fetchData } = useVisualApi();

// 1. 获取响应式的全局 date 状态
// getGlobalField 返回的是一个 computedRef
const globalDate = getGlobalField('date', '2023-01-01');

// 2. 封装请求逻辑
const refreshData = async () => {
  const params = {
    date: globalDate.value, // 自动获取最新的 date 值
    ...props.config.params  // 合并组件自身的参数
  };
  
  console.log(`🚀 正在为组件 ${props.config.id} 请求数据, 日期为: ${params.date}`);
  const data = await fetchData(props.config.sourceId, params);
  // ... 处理返回的数据并渲染图表
};

// 3. 监听 globalDate 的变化
// 当宿主项目中的 Pinia 状态改变时,这里会自动触发
watch(globalDate, (newDate, oldDate) => {
  if (newDate !== oldDate) {
    refreshData();
  }
});

// 4. 初始化加载
onMounted(() => {
  refreshData();
});
</script>
  • 宿主项目(Host Project)的配合
js 复制代码
// 宿主项目 main.js
import { useDateStore } from '@/stores/date';
import VisualSDK from '@company/visual-sdk';

const app = createApp(App);
const pinia = createPinia();
app.use(pinia);

app.use(VisualSDK, {
  globalContext: () => {
    const dateStore = useDateStore();
    const userStore = useUserStore();
    return {
	  token: userStore.token,
      deptId: userStore.userInfo.deptId,
      role: userStore.role,
      theme: userStore.currentTheme,
      date: dateStore.currentDate // 返回 Pinia 中的日期
    };
  }
});

SDK 内部的"条件监听"实现

WidgetLoader.vue​ 中,利用 Vue 3 的 watch 动态判断是否需要执行刷新逻辑。

js 复制代码
// WidgetLoader.vue
import { watch } from 'vue';
import { useVisualContext } from '../hooks/useVisualContext';

const props = defineProps(['config']);
const { getGlobalField } = useVisualContext();

// 1. 依然获取响应式的 globalDate
const globalDate = getGlobalField('date');

// 2. 监听逻辑
watch(globalDate, (newVal) => {
  // 核心判断:检查当前组件配置中是否包含了 'date'
  const reactiveParams = props.config.content.reactiveParams || [];
  
  if (reactiveParams.includes('date')) {
    console.log('A项目:监测到 date 变化,开始刷新数据...');
    refreshData();
  } else {
    // B项目:虽然 globalDate 变了,但配置里没说要理它
    console.log('B项目:监测到 date 变化,但配置要求忽略。');
  }
});

实现一个"局部逃生"的动态组件加载器

動態組件分發 (WidgetLoader.vue)

html 复制代码
<template>
  <div class="widget-container">
    <!-- 方案 A: 逃生通道 - 如果配置了 customComponent,直接渲染手寫組件 -->
    <component 
      :is="customComponentInstance" 
      v-if="customComponentInstance"
      v-bind="config.props" 
      :context="globalContext"
    />

    <!-- 方案 B: 標準通道 - 走 ECharts/Table 等預設模板 -->
    <StandardChartRender 
      v-else-if="config.type === 'chart'" 
      :config="config" 
    />
    
    <StandardTableRender 
      v-else-if="config.type === 'table'" 
      :config="config" 
    />
  </div>
</template>

<script setup>
import { computed, inject } from 'vue';

const props = defineProps(['config']);
// 注入宿主項目註冊的所有自定義組件表
const bizComponents = inject('BIZ_COMPONENTS', {});
const globalContext = inject('GLOBAL_CONTEXT', {});

const customComponentInstance = computed(() => {
  const name = props.config.customComponent;
  if (!name) return null;
  
  // 從注入的組件庫中尋找,找不到則報錯提示
  const comp = bizComponents[name];
  if (!comp) {
    console.error(`[SDK] 找不到逃生組件: ${name},請確認是否已在主項目註冊。`);
  }
  return comp;
});
</script>

宿主項目(業務層)如何「遞刀子」

在業務項目中,你寫好那個「逆反需求」的組件,然後在初始化 SDK 時傳進去。

步驟 1:寫一個手寫組件 UrgentRequirement.vue

html 复制代码
<template>
  <div class="my-crazy-css">
    <h3>需求方非要的奇葩功能</h3>
    <button @click="doSomethingCrazy">點擊執行逆反邏輯</button>
  </div>
</template>

步驟 2:在 main.js中註冊給 SDK

javascript

js 复制代码
import UrgentRequirement from '@/biz-custom/UrgentRequirement.vue';

app.use(VisualSDK, {
  // 這裡就是「逃生艙」名單
  customComponents: {
    UrgentRequirement // 鍵名與 JSON 中的 customComponent 對應
  }
});

步骤3: JSON 配置如何調用

當遇到搞不定的需求,JSON 直接這麼寫:

json 复制代码
{
  "id": "widget_001",
  "type": "custom", 
  "customComponent": "UrgentRequirement", // 指定逃生組件名
  "props": {
    "someData": "可以傳入自定義參數"
  }
}
相关推荐
AI砖家2 小时前
Claude Code Superpowers 安装使用指南:让 AI 编程从“业余”走向“工程化”
前端·人工智能·python·ai编程·代码规范
李白的天不白2 小时前
webpack 与axios 版本冲突问题
前端·webpack·node.js
Java后端的Ai之路2 小时前
模型调好了怎么给老板看?用这玩意儿5分钟出Demo,连前端都不用学:Gradio 6全栈实战指南
前端·机器学习·gradio
木斯佳2 小时前
前端八股文面经大全:中科星图前端日常实习(2026-04-29)·面经深度解析
前端
heRs BART3 小时前
spring-boot-starter和spring-boot-starter-web的关联
前端
龙猫里的小梅啊3 小时前
CSS(七)CSS列表控制
前端·css
浩冉学编程3 小时前
微信小程序中基于java后端实现官方的文本内容安全识别msgSecCheck
java·前端·安全·微信小程序·小程序·微信公众平台·内容安全审核
李李李勃谦3 小时前
鸿蒙PC配色方案工具:取色、配色生成与 CSS 导出
前端·css·华为·harmonyos
Jul1en_3 小时前
Claude 迁移 Codex 工作流迁移与更新
java·服务器·前端·后端·ai编程