【Three.js】day124-data-render-decoupling

作者:mario 发布时间: 2026-09-07 阅读量:4 评论数:0

Day 124 · 数据层与渲染层解耦 — 为实时数据铺好管道(本周架构核心)

前三天把柱/面/点三种图表做出来了,但它们现在都"直接吃数据"——数据格式一变,图表代码就要改。今天做本周(甚至整个数字孪生主线)最关键的一件事:把数据层与渲染层解耦。解耦 = 渲染层只认"归一化数值快照",不认"能耗/温度/流速"这些具体业务字段;数据层负责"任意数据源 → 标准快照"。做完这件事,未来接 WS 实时数据时,渲染层零改动——这就是"为实时数据铺管道"的含义,也是架构能力的硬证据。


目录


一、为什么要解耦:现在埋的坑 vs 解耦的价值

1.1 不解耦的现状(昨天三天的写法)

柱状图构造:new BarChart3D(energies)   ← 认"能耗"这个业务字段
曲面构造:  new HeatSurface(temps)     ← 认"温度"
流场构造:  new FlowField(..., getVel) ← 认"流速"

问题:字段写死在图表里。明天 WS 推来的是 {"deviceId":..., "power":...},你要改 BarChart3D 的构造——每接一种数据改一次图。数据和渲染耦合 = 换数据源就改渲染代码 = 架构坏味道

1.2 解耦后的形态

数据源(静态 / 假实时 / 未来真 WS)
    │  → DataSource 统一接口
    ▼
标准快照(归一化数值,不认业务字段)
    │  → 快照映射(Data Mapper)
    ▼
渲染层图表(只认快照:values: number[], coords: [...])

解耦的价值三句话

  1. 换数据源渲染层零改动(今天验证)

  2. 图表可复用(同一图表吃不同数据)

  3. 可测试(快照是纯数据,单测友好)

1.3 边界纪律(Day 85 延续)

Vue 层(UI/控制)     → 只通过桥拿数据快照与事件
引擎层(渲染图表)     → 只认快照,不认业务字段
数据层(数据源+映射) → 任意数据 → 快照,引擎与 Vue 都不碰

今天的三层分工,是"数据驱动数字孪生"的最小骨架——Week 26+ 接 WS 时,只替换最左边的"数据源"。


二、三层架构:数据层 / 桥 / 渲染层

┌─────────────────────────────────────────────┐
│ Vue 层                                        │
│  ├─ SceneBridge(调引擎)                     │
│  ├─ SceneEvents(收事件)                     │
│  └─ UI(图例 / 控制 / tooltip)               │
├─────────────────────────────────────────────┤
│ 桥(薄契约层)                                │
│  ├─ 入向:setData(snapshot) / setQuality()   │
│  └─ 出向:onSelect(id) / onProgress()         │
├─────────────────────────────────────────────┤
│ 引擎层(纯 TS,不 import vue)                │
│  ├─ 渲染层图表:BarChart3D / HeatSurface /   │
│  │    ScatterPoints / FlowField             │
│  └─ 数据层:DataSource + DataMapper → 快照   │
└─────────────────────────────────────────────┘

引擎层的"数据层"与"渲染层"也分开(今天的新增)——数据层在引擎内部独立成模块,渲染层只从它拿快照。


三、数据源抽象:静态 → 假实时 → 真实时

3.1 统一接口

// src/core/data/data-source.ts(引擎内,纯逻辑)
/**
 * 数据源统一接口:任意来源都能"喂"出标准快照
 * 抽象的意义:静态/假实时/未来 WS 都实现同一接口,
 * 渲染层只认接口,不认实现。
 */
export interface DataSource {
  /** 订阅数据:每次数据更新回调一次快照 */
  subscribe(cb: (snapshot: DataSnapshot) => void): () => void;
  /** 一次性拉取当前快照 */
  get(): DataSnapshot;
}

3.2 标准快照类型

/**
 * 标准数据快照:渲染层唯一认的数据形态
 * 刻意"不认业务字段"——只有数值数组与坐标,
 * 能耗/温度/流速在进快照前已被 Mapper 归一化。
 */
export interface DataSnapshot {
  /** 系列名(图例用,如"能耗") */
  series: string;
  /** 离散数值(柱状图:每根柱的值) */
  values: number[];
  /** 连续场数值(曲面:二维网格采样) */
  field?: number[][];
  /** 空间点位(散点/流场:[x, y, z, value]) */
  points?: Array<[number, number, number, number]>;
  /** 矢量场(流场:每点速度函数,可选) */
  vectorField?: (x: number, y: number) => [number, number];
  /** 时间戳(时间驱动对齐用) */
  timestamp: number;
}

3.3 三种数据源实现

// 1. 静态数据源(现成数组)
export class StaticSource implements DataSource {
  constructor(private data: DataSnapshot) {}
  subscribe() { return () => {}; }        // 静态,不推送
  get() { return this.data; }
}

// 2. 假实时数据源(模拟定时推送——测试/演示用,Day 126 验收主力)
export class MockRealtimeSource implements DataSource {
  private timer?: number;
  constructor(private generator: () => DataSnapshot, private intervalMs = 2000) {}
  subscribe(cb: (s: DataSnapshot) => void) {
    this.timer = window.setInterval(() => cb(this.generator()), this.intervalMs);
    cb(this.generator());                  // 立即推一帧
    return () => window.clearInterval(this.timer);
  }
  get() { return this.generator(); }
}

// 3. 未来真 WS 源(Week 26 实现,今天只留占位注释)
// export class WsSource implements DataSource { ... }

红线MockRealtimeSource 只在测试/演示分支启用(用户规则:开发/生产绝不埋模拟数据)——通过配置开关注入,而不是写死在渲染路径里。


四、快照映射:任意数据变成"标准快照"

// src/core/data/data-mapper.ts(引擎内)
/**
 * 业务数据 → 标准快照 的映射器
 * 这是"解耦"的转换枢纽:把能耗/温度/流速等具体字段,
 * 归一化成渲染层认的 values/field/points。
 * 换业务字段只改这里,渲染层永远不动。
 */
export function mapEnergyToSnapshot(devices: Array<{ id: string; energy: number; status: string }>): DataSnapshot {
  return {
    series: "能耗",
    values: devices.map((d) => d.energy),   // 业务字段在这被"抹平"
    timestamp: Date.now(),
  };
}

export function mapTempFieldToSnapshot(grid: number[][]): DataSnapshot {
  return {
    series: "温度",
    field: grid,
    timestamp: Date.now(),
  };
}

Mapper 的价值:所有"业务耦合"收敛在 Mapper 一层。将来数据结构变了,只改 Mapper;渲染层、桥、Vue 全都无感知


五、渲染层契约:图表只认快照

改造前三天图表,构造只接受快照而非具体字段:

// src/core/three/viz/index.ts(统一导出图表工厂)
import { BarChart3D } from "./bar-chart";
import { HeatSurface } from "./heat-surface";
import { ScatterPoints, FlowField } from "./scatter-points"; // 合并简化示意
import type { DataSnapshot } from "../../data/data-source";

/**
 * 图表工厂:按快照类型分派到对应图表
 * 渲染层唯一入口——只认 DataSnapshot,不认任何业务字段
 */
export function createViz(snapshot: DataSnapshot, scene: THREE.Scene): THREE.Object3D | null {
  if (snapshot.values)      return new BarChart3D(snapshot.values, 4, Math.ceil(snapshot.values.length / 4)).mesh;
  if (snapshot.field)       return new HeatSurface(snapshot.field!).mesh;
  if (snapshot.points)      return new ScatterPoints(snapshot.points!).points;
  return null;
}
// 引擎装配:数据源 → Mapper → 渲染层(一条解耦流水线)
const source: DataSource = new MockRealtimeSource(
  () => mapEnergyToSnapshot(fetchDevices()),   // 换真 WS 时只换 source
  2000
);
source.subscribe((snapshot) => {
  // 快照到达 → 更新对应图表(脏检查:timestamp 没变跳过)
  chart.update(snapshot.values);
});

六、桥契约扩展:Vue ↔ 引擎的数据通道

6.1 入向桥(Vue → 引擎)

// src/bridge/scene-bridge.ts(Day 104 契约,新增数据接口——只扩不破)
export interface SceneBridge {
  // 原有:初始化/控制(保持不变)
  init(canvas: HTMLCanvasElement): Promise<void>;
  setQuality(level: QualityLevel): void;

  // 新增:数据注入(Vue 把外部数据喂进引擎)
  /** 注入标准快照:Vue 拿到的数据经 Mapper 后传进来 */
  setData(snapshot: DataSnapshot): void;
  /** 切换数据源(演示用:静态/假实时切换) */
  setSource(source: "static" | "realtime"): void;
}

6.2 出向桥(引擎 → Vue)

// 新增:数据相关事件
export interface SceneEvents {
  // 原有:onReady / onProgress
  /** 选中某个数据对象(柱/点),Vue 显示详情/联动 2D */
  (e: "select", id: number | null): void;
}

6.3 桥的"窄口"原则(Day 85 回流)

✓ 桥只传"快照 + 选中 id"这种最小形态
✗ 绝不传 Three 对象 / 内部节点
→ Vue 零 Three 依赖这条红线本周依然成立

七、验证:换数据源渲染层零改动

7.1 验收实验(今天最重要的事)

实验目标:证明"换数据源渲染层零改动"
步骤:
  1. 用 StaticSource 渲染柱状图 → 正常
  2. 改成 MockRealtimeSource(同一 Mapper)→ 渲染层代码一行没改
  3. 换一套业务数据(能耗 → 温度),只新增一个 Mapper → 渲染层零改动

7.2 判定标准

✓ 渲染层(BarChart3D 等)文件 0 改动
✓ 桥契约(SceneBridge)0 改动
✓ Vue 组件 0 改动
✗ 只有"数据源 + Mapper"新增/替换
→ 解耦成功;反之解耦失败,回去重构

7.3 测试(用户规则:主要功能写测试)

// src/core/data/data-source.test.ts(vitest)
import { StaticSource, MockRealtimeSource } from "./data-source";
import { mapEnergyToSnapshot } from "./data-mapper";

it("静态源返回原快照", () => {
  const s = new StaticSource({ series: "t", values: [1, 2, 3], timestamp: 0 });
  expect(s.get().values).toEqual([1, 2, 3]);
});

it("Mapper 把业务字段抹平成 values", () => {
  const snap = mapEnergyToSnapshot([
    { id: "a", energy: 10, status: "ok" },
    { id: "b", energy: 20, status: "warn" },
  ]);
  expect(snap.values).toEqual([10, 20]);
  expect(snap.series).toBe("能耗");
});

it("假实时源按间隔推送并返回退订函数", async () => {
  const s = new MockRealtimeSource(() => ({ series: "t", values: [1], timestamp: Date.now() }), 50);
  let count = 0;
  const off = s.subscribe(() => count++);
  await new Promise((r) => setTimeout(r, 120));
  off();
  expect(count).toBeGreaterThanOrEqual(2); // 立即 1 帧 + 定时
});

八、常见坑点

坑 1:Mapper 放错层(Vue 里做映射)

症状:Vue 组件里写 snapshot.values = devices.map(...)修法:映射放引擎的数据层,Vue 只传原始数据或直接传快照(第四、六节)。

坑 2:图表构造还认业务字段

症状:new BarChart3D(energies) 残留。修法:统一改 createViz(snapshot),业务字段只出现在 Mapper(第五节)。

坑 3:模拟数据写死在渲染路径

症状:MockRealtimeSource 直接 new 在引擎装配里。修法:通过配置/桥切换,测试演示才启用(用户规则:开发生产无模拟)(第三节)。

坑 4:桥里传了 Three 对象

症状:bridge.setData(mesh)修法:桥只传快照/选中 id(窄口原则)(第六节)。

坑 5:脏检查漏了 timestamp

症状:数据每帧都 update,白耗 CPU。修法:快照带 timestamp,相同就跳过(第五节)。

坑 6:解耦了但没人验证"换源零改动"

症状:解耦是"口头的"。修法:跑第七节的验收实验,把结果写进笔记


九、自测挑战

T1 · 数据源三件套(40 分钟)

实现 StaticSource / MockRealtimeSource / DataSnapshot,跑通第三节的单测。

T2 · 两个 Mapper(40 分钟)

mapEnergyToSnapshotmapTempFieldToSnapshot,确认"业务字段 → values/field"抹平。

T3 · 换源实验(60 分钟)

把昨天的柱状图接到新管线:静态源 → 假实时源 → 换 Mapper(能耗→温度),全程渲染层与 Vue 零改动。录屏证明"数据在动、代码没改"。

T4 · 桥契约扩展(40 分钟)

SceneBridgesetData / setSource,在 SceneEventsselect 事件,确认"只扩不破"(原有接口签名不变)。


十、总结

环节

要点

解耦价值

换数据源渲染层零改动;图表可复用;可单测

三层

Vue(UI) / 桥(薄契约) / 引擎(数据层+渲染层)

数据源

统一接口:静态 / 假实时 / 未来 WS 三实现

Mapper

业务字段 → 标准快照,业务耦合收敛在这一层

渲染契约

图表只认 DataSnapshot,工厂按类型分派

setData / select 新增,窄口原则不破

本周架构核心完成:一套"数据源 → Mapper → 快照 → 渲染层"的管道,让 3D 图表与数据来源彻底解耦。未来接 WS 时,替换最左边的数据源即可——这正是数字孪生 Demo 从"静态演示"走向"实时驱动"的关键台阶。明天让这些图表"可交互":拾取、tooltip、下钻联动


明日预告:Day 125 交互与钻取——raycaster 拾取柱/点/曲面、tooltip 显示详情、选中联动 2D 图表。让数据看板从"能看"变成"能查"。

评论