Three.js 架构实践:用 MVC 模式组织你的 3D 应用

大多数 Three.js 教程都把所有逻辑写在一个 index.js 里:创建场景、相机、渲染器、添加网格、动画循环......Demo 阶段没问题,但当项目里有几十个对象、复杂交互和状态管理时,单文件就会变成难以维护的 "屎山"。本文带你用经典的 MVC 模式重构 Three.js 项目,让数据、渲染和交互各司其职。

一、为什么 Three.js 需要架构

Three.js 本身不强制任何代码组织方式。一个典型的入门项目长这样:

typescript

运行

ini 复制代码
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, w / h, 0.1, 1000);
const renderer = new THREE.WebGLRenderer();
const cube = new THREE.Mesh(
  new THREE.BoxGeometry(1, 1, 1),
  new THREE.MeshBasicMaterial({ color: 0x00ff00 })
);
scene.add(cube);

function animate() {
  requestAnimationFrame(animate);
  cube.rotation.x += 0.01;
  cube.rotation.y += 0.01;
  renderer.render(scene, camera);
}
animate();

简单、直观,但问题也很明显:

  • 数据和渲染耦合:立方体的颜色、尺寸、旋转速度直接写在创建 Mesh 的代码里,想改属性就得碰渲染逻辑。
  • 难以测试:要验证 "鼠标移动时物体旋转" 的逻辑,必须启动浏览器、模拟 WebGL 环境。
  • 扩展困难:新增一种对象类型,就得在同一个文件里继续堆代码,很快就失控了。

MVC(Model-View-Controller)的核心思想是分离关注点

表格

层级 职责 依赖
Model(模型) 持有应用状态和业务数据,不依赖 Three.js
View(视图) 把 Model 数据转成 Three.js 可渲染的对象 依赖 Model 和 Three.js
Controller(控制器) 接收用户输入,更新 Model,协调各部分 依赖 Model

数据流是单向的:用户输入 → Controller → Model(触发事件) → View 更新渲染

二、项目骨架搭建

2.1 基础模型类

所有 Model 都需要事件分发能力,我们先做一个基类:

typescript

运行

typescript 复制代码
// src/models/BaseModel.ts
export class BaseModel<Events = Record<string, unknown>> {
  private listeners: Map<keyof Events, Set<(e: any) => void>> = new Map();

  addEventListener<K extends keyof Events>(type: K, callback: (e: Events[K]) => void) {
    if (!this.listeners.has(type)) this.listeners.set(type, new Set());
    this.listeners.get(type)!.add(callback);
  }

  dispatchEvent<K extends keyof Events>(event: Events[K] & { type: K }) {
    this.listeners.get(event.type)?.forEach(cb => cb(event));
  }
}

2.2 场景模型

SceneModel 管理场景中所有 3D 对象的集合:

typescript

运行

typescript 复制代码
// src/models/SceneModel.ts
import { BaseModel } from './BaseModel';
import type { SceneObjectModel } from './SceneObjectModel';

export class SceneModel extends BaseModel {
  private _objects = new Map<string, SceneObjectModel>();

  get(id: string) { return this._objects.get(id); }
  getAll() { return Array.from(this._objects.values()); }

  add(object: SceneObjectModel, parent?: SceneObjectModel) {
    this._objects.set(object.id, object);
    this.dispatchEvent({ type: 'object-added', object, parent });
  }
}

2.3 相机、光照、环境模型

typescript

运行

scala 复制代码
// src/models/CameraModel.ts
import { BaseModel } from './BaseModel';

export class CameraModel extends BaseModel {
  fov = 50;
  near = 0.1;
  far = 1000;
  position = { x: 0, y: 0, z: 5 };
}

typescript

运行

ini 复制代码
// src/models/LightsModel.ts
import { BaseModel } from './BaseModel';

export class LightsModel extends BaseModel {
  ambientColor = '#ffffff';
  ambientIntensity = 0.5;
  directionalColor = '#ffffff';
  directionalIntensity = 1;
}

typescript

运行

scala 复制代码
// src/models/EnvironmentModel.ts
import { BaseModel } from './BaseModel';

export class EnvironmentModel extends BaseModel {
  background = '#1a1a2e';
}

2.4 视图层

SceneView 负责创建 Three.js 的 SceneWebGLRenderer,并把光照、环境、相机都初始化好:

typescript

运行

typescript 复制代码
// src/views/SceneView.ts
import * as THREE from 'three';
import type { SceneModel } from '../models/SceneModel';
import type { LightsModel } from '../models/LightsModel';
import type { EnvironmentModel } from '../models/EnvironmentModel';
import type { CameraView } from './CameraView';

export class SceneView {
  private scene: THREE.Scene;
  private renderer: THREE.WebGLRenderer;
  private sceneModel: SceneModel;
  private cameraView: CameraView;

  constructor(
    sceneModel: SceneModel,
    lightsModel: LightsModel,
    environmentModel: EnvironmentModel,
    cameraView: CameraView
  ) {
    this.sceneModel = sceneModel;
    this.cameraView = cameraView;
    this.setupRenderer();
    this.setupScene(environmentModel);
    this.setupLights(lightsModel);
  }

  private setupRenderer() {
    this.renderer = new THREE.WebGLRenderer({ antialias: true });
    this.renderer.setSize(window.innerWidth, window.innerHeight);
    this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
    document.body.appendChild(this.renderer.domElement);
  }

  private setupScene(env: EnvironmentModel) {
    this.scene = new THREE.Scene();
    this.scene.background = new THREE.Color(env.background);
  }

  private setupLights(lights: LightsModel) {
    const ambient = new THREE.AmbientLight(lights.ambientColor, lights.ambientIntensity);
    const directional = new THREE.DirectionalLight(lights.directionalColor, lights.directionalIntensity);
    directional.position.set(5, 5, 5);
    this.scene.add(ambient, directional);
  }

  get domElement() { return this.renderer.domElement; }

  update() {
    this.renderer.render(this.scene, this.cameraView.camera);
  }
}

CameraViewCameraModel 的数据转成实际的 PerspectiveCamera

typescript

运行

typescript 复制代码
// src/views/CameraView.ts
import * as THREE from 'three';
import type { CameraModel } from '../models/CameraModel';

export class CameraView {
  public camera: THREE.PerspectiveCamera;

  constructor(model: CameraModel) {
    this.camera = new THREE.PerspectiveCamera(
      model.fov, window.innerWidth / window.innerHeight, model.near, model.far
    );
    this.camera.position.set(model.position.x, model.position.y, model.position.z);
  }
}

2.5 渲染控制器

RenderController 封装 requestAnimationFrame 循环,每帧调用传入的更新函数:

typescript

运行

typescript 复制代码
// src/controllers/RenderController.ts
export class RenderController {
  private update: () => void;
  private running = false;

  constructor(update: () => void) {
    this.update = update;
  }

  start() {
    this.running = true;
    this.loop();
  }

  private loop = () => {
    if (!this.running) return;
    this.update();
    requestAnimationFrame(this.loop);
  };
}

2.6 应用入口

把所有部分组装起来:

typescript

运行

kotlin 复制代码
// src/index.ts
import { SceneModel } from './models/SceneModel';
import { CameraModel } from './models/CameraModel';
import { LightsModel } from './models/LightsModel';
import { EnvironmentModel } from './models/EnvironmentModel';
import { SceneView } from './views/SceneView';
import { CameraView } from './views/CameraView';
import { RenderController } from './controllers/RenderController';

class Application {
  private sceneModel: SceneModel;
  private cameraModel: CameraModel;
  private lightsModel: LightsModel;
  private environmentModel: EnvironmentModel;
  private sceneView: SceneView;
  private cameraView: CameraView;
  private renderController: RenderController;

  constructor() {
    // 1. 创建所有 Model
    this.sceneModel = new SceneModel();
    this.cameraModel = new CameraModel();
    this.lightsModel = new LightsModel();
    this.environmentModel = new EnvironmentModel();

    // 2. 创建所有 View(依赖 Model)
    this.cameraView = new CameraView(this.cameraModel);
    this.sceneView = new SceneView(
      this.sceneModel, this.lightsModel, this.environmentModel, this.cameraView
    );

    // 3. 创建 Controller
    this.renderController = new RenderController(this.update);
  }

  private update = () => this.sceneView.update();

  start() { this.renderController.start(); }
}

new Application().start();

三、添加 3D 对象:模型与视图分离

3.1 事件定义

Model 变化时通过事件通知 View,先定义事件类型:

typescript

运行

typescript 复制代码
// src/types/events.ts
import type { Euler, Vector2 } from 'three';
import type { SceneObjectModel } from '../models/SceneObjectModel';

export type SceneModelEvents = {
  'object-added': { object: SceneObjectModel; parent?: SceneObjectModel };
};

export type SceneObjectEvents = {
  'rotation-changed': { rotation: Euler };
};

export type InputModelEvents = {
  'input-changed': { position: Vector2 };
};

3.2 场景对象基类模型

所有 3D 对象都有位置、旋转、缩放和材质属性,抽到基类里:

typescript

运行

typescript 复制代码
// src/models/SceneObjectModel.ts
import { Euler, Vector3 } from 'three';
import { BaseModel } from './BaseModel';
import type { SceneObjectEvents } from '../types/events';
import type { MapColorPropertiesToColorRepresentations } from 'three/src/materials/Material';

export class SceneObjectModel<T = Partial<MapColorPropertiesToColorRepresentations<{}>>>
  extends BaseModel<SceneObjectEvents> {
  public readonly id: string;
  _position = new Vector3(0, 0, 0);
  _rotation = new Euler(0, 0, 0);
  _scale = new Vector3(1, 1, 1);
  _material: Partial<MapColorPropertiesToColorRepresentations<T>>;

  constructor(id: string, material: Partial<MapColorPropertiesToColorRepresentations<T>> = {}) {
    super();
    this.id = id;
    this._material = { ...material };
  }

  get position() { return this._position; }
  get rotation() { return this._rotation; }
  get scale() { return this._scale; }
  get material() { return this._material; }
}

3.3 具体模型

立方体模型 ------ 线框材质,初始绕 Y 轴旋转 45°:

typescript

运行

typescript 复制代码
// src/models/CubeModel.ts
import { MathUtils } from 'three';
import type { MeshBasicMaterialProperties } from 'three';
import { SceneObjectModel } from './SceneObjectModel';

export class CubeModel extends SceneObjectModel<MeshBasicMaterialProperties> {
  private _size = 1.5;

  constructor(id: string) {
    super(id, { color: '#44aaff', wireframe: true });
    this.rotation.y = MathUtils.degToRad(45);
  }

  get size() { return this._size; }
}

环面纽结模型 ------ 金属材质,会反射 HDR 环境光:

typescript

运行

typescript 复制代码
// src/models/TorusModel.ts
import type { MeshStandardMaterial } from 'three';
import { SceneObjectModel } from './SceneObjectModel';

export class TorusModel extends SceneObjectModel<MeshStandardMaterial> {
  private _radius = 0.3;
  private _tube = 0.08;
  private _tubularSegments = 128;
  private _radialSegments = 16;

  constructor(id: string) {
    super(id, { color: '#4e8ab3', metalness: 1, roughness: 0.1 });
  }

  get radius() { return this._radius; }
  get tube() { return this._tube; }
  get tubularSegments() { return this._tubularSegments; }
  get radialSegments() { return this._radialSegments; }
}

3.4 视图基类

SceneObjectView 是抽象基类,子类实现 createMesh,基类负责同步变换:

typescript

运行

kotlin 复制代码
// src/views/SceneObjectView.ts
import type { Mesh } from 'three';
import type { SceneObjectModel } from '../models/SceneObjectModel';

export abstract class SceneObjectView<T extends SceneObjectModel = SceneObjectModel> {
  private _mesh!: Mesh;
  protected _model: T;

  constructor(model: T) {
    this._model = model;
    this.createMesh();
    this.syncTransform();
    this.addEventListeners();
  }

  protected abstract createMesh(): void;

  get mesh() { return this._mesh; }
  set mesh(mesh: Mesh) { this._mesh = mesh; }
  get model() { return this._model; }

  private syncTransform() {
    this.mesh.position.copy(this._model.position);
    this.mesh.rotation.copy(this._model.rotation);
    this.mesh.scale.copy(this._model.scale);
  }

  private addEventListeners() {
    this._model.addEventListener('rotation-changed', ({ rotation }) => {
      this.mesh.rotation.set(rotation.x, rotation.y, rotation.z, rotation.order);
    });
  }
}

3.5 具体视图

立方体视图 ------ 从 Model 读取尺寸和材质,创建 Mesh:

typescript

运行

scala 复制代码
// src/views/CubeView.ts
import { BoxGeometry, MeshBasicMaterial, Mesh } from 'three';
import { SceneObjectView } from './SceneObjectView';
import type { CubeModel } from '../models/CubeModel';

export class CubeView extends SceneObjectView<CubeModel> {
  protected createMesh() {
    const geometry = new BoxGeometry(this.model.size, this.model.size, this.model.size);
    const material = new MeshBasicMaterial({
      color: this.model.material.color,
      wireframe: this.model.material.wireframe,
    });
    this.mesh = new Mesh(geometry, material);
  }
}

环面纽结视图 ------ 同样的模式,不同的几何体和材质:

typescript

运行

scala 复制代码
// src/views/TorusView.ts
import { TorusKnotGeometry, MeshStandardMaterial, Mesh } from 'three';
import { SceneObjectView } from './SceneObjectView';
import type { TorusModel } from '../models/TorusModel';

export class TorusView extends SceneObjectView<TorusModel> {
  protected createMesh() {
    const geometry = new TorusKnotGeometry(
      this.model.radius, this.model.tube,
      this.model.tubularSegments, this.model.radialSegments
    );
    const material = new MeshStandardMaterial({
      color: this.model.material.color,
      metalness: this.model.material.metalness,
      roughness: this.model.material.roughness,
    });
    this.mesh = new Mesh(geometry, material);
  }
}

3.6 串联:SceneView 监听对象添加

SceneView 监听 SceneModelobject-added 事件,根据 Model 类型创建对应的 View,并处理父子层级:

typescript

运行

csharp 复制代码
// src/views/SceneView.ts(补充部分)
import { CubeView } from './CubeView';
import { TorusView } from './TorusView';
import { CubeModel } from '../models/CubeModel';
import { TorusModel } from '../models/TorusModel';
import type { SceneObjectView } from './SceneObjectView';

export class SceneView {
  private objectViews = new Map<string, SceneObjectView>();

  constructor(/* ... */) {
    // ... 原有初始化
    this.addEventListeners();
  }

  private addEventListeners() {
    this.sceneModel.addEventListener('object-added', (event) => {
      let view: SceneObjectView;

      if (event.object instanceof CubeModel) {
        view = new CubeView(event.object);
      } else if (event.object instanceof TorusModel) {
        view = new TorusView(event.object);
      } else {
        return;
      }

      this.objectViews.set(event.object.id, view);
      const parentView = event.parent && this.objectViews.get(event.parent.id);
      const parent = parentView?.mesh || this.scene;
      parent.add(view.mesh);
    });
  }
}

最后在 Application 里创建对象:

typescript

运行

typescript 复制代码
// src/index.ts(补充)
import { CubeModel } from './models/CubeModel';
import { TorusModel } from './models/TorusModel';

class Application {
  constructor() {
    // ... 原有初始化
    this.spawnInitialObjects();
  }

  private spawnInitialObjects() {
    const cube = new CubeModel('cube-1');
    const torus = new TorusModel('torus-1');
    this.sceneModel.add(cube);
    this.sceneModel.add(torus, cube); // torus 作为 cube 的子对象
  }
}

此时场景中应该能看到一个线框立方体,里面嵌套着一个金属质感的环面纽结。

数据流 :Model 持有数据 → SceneModel.add() 触发 object-added 事件 → SceneView 收到事件,创建对应 View → View 从 Model 读取数据生成 Mesh → 添加到场景。Model 完全不知道 View 的存在。

四、加入交互:让物体动起来

MVC 的真正威力在于:用户输入不直接操作 View,而是通过 Controller 更新 Model,Model 触发事件,View 响应事件更新渲染。

4.1 输入模型

InputModel 存储归一化的指针坐标(-1 到 1),不涉及任何 DOM 操作:

typescript

运行

scala 复制代码
// src/models/InputModel.ts
import { Vector2 } from 'three';
import { BaseModel } from './BaseModel';
import type { InputModelEvents } from '../types/events';

export class InputModel extends BaseModel<InputModelEvents> {
  private _position = new Vector2(0, 0);

  update(x: number, y: number) {
    this._position.set(x, y);
    this.dispatchEvent({ type: 'input-changed', position: this._position });
  }
}

4.2 输入控制器

InputController 监听画布上的指针事件,把坐标归一化后写入 InputModel

typescript

运行

kotlin 复制代码
// src/controllers/InputController.ts
import type { InputModel } from '../models/InputModel';

export class InputController {
  private inputModel: InputModel;
  private domElement: HTMLElement;

  constructor(inputModel: InputModel, domElement: HTMLElement) {
    this.inputModel = inputModel;
    this.domElement = domElement;
    this.setupListeners();
  }

  private handlePointer = (e: PointerEvent) => {
    if (!e.isPrimary || !(e.currentTarget instanceof HTMLElement)) return;
    const rect = e.currentTarget.getBoundingClientRect();
    const x = ((e.clientX - rect.left) / rect.width) * 2 - 1;
    const y = -(((e.clientY - rect.top) / rect.height) * 2 - 1);
    this.inputModel.update(x, y);
  };

  private setupListeners() {
    this.domElement.addEventListener('pointermove', this.handlePointer);
    this.domElement.addEventListener('pointerdown', this.handlePointer);
  }
}

4.3 变换控制器

TransformController 把输入坐标翻译成物体旋转 ------ 水平移动控制立方体 Y 轴旋转,垂直移动控制环面 X 轴旋转:

typescript

运行

typescript 复制代码
// src/controllers/TransformController.ts
import type { SceneModel } from '../models/SceneModel';
import type { InputModel } from '../models/InputModel';
import { CubeModel } from '../models/CubeModel';
import { TorusModel } from '../models/TorusModel';

export class TransformController {
  constructor(sceneModel: SceneModel, inputModel: InputModel) {
    inputModel.addEventListener('input-changed', ({ position }) => {
      sceneModel.getAll().forEach((obj) => {
        const rotation = obj.rotation;
        if (obj instanceof CubeModel) {
          rotation.y = position.x * Math.PI;
        } else if (obj instanceof TorusModel) {
          rotation.x = position.y * Math.PI;
        }
        obj.dispatchEvent({ type: 'rotation-changed', rotation });
      });
    });
  }
}

4.4 完整组装

Application 中把三个控制器都接上:

typescript

运行

kotlin 复制代码
// src/index.ts(最终版)
import { InputModel } from './models/InputModel';
import { InputController } from './controllers/InputController';
import { TransformController } from './controllers/TransformController';

class Application {
  private inputModel: InputModel;
  private inputController: InputController;
  private transformController: TransformController;

  constructor() {
    // ... Model 创建
    this.inputModel = new InputModel();

    // ... View 创建

    // Controller 创建
    this.renderController = new RenderController(this.update);
    this.inputController = new InputController(this.inputModel, this.sceneView.domElement);
    this.transformController = new TransformController(this.sceneModel, this.inputModel);

    this.spawnInitialObjects();
  }
}

一次鼠标移动的完整数据流

plaintext

css 复制代码
PointerEvent → InputController → InputModel(触发 input-changed)
  → TransformController → CubeModel/TorusModel(触发 rotation-changed)
    → SceneObjectView(更新 mesh.rotation)

每一环只知道自己对接的接口,不关心整条链路。旋转逻辑里没有任何 Three.js 导入,渲染代码里也看不到指针事件。

五、最终文件结构

plaintext

bash 复制代码
src/
├── controllers/
│   ├── InputController.ts      # 指针输入 → InputModel
│   ├── RenderController.ts     # 动画循环
│   └── TransformController.ts  # 输入 → 物体旋转
├── models/
│   ├── BaseModel.ts            # 事件基类
│   ├── CameraModel.ts
│   ├── CubeModel.ts
│   ├── EnvironmentModel.ts
│   ├── InputModel.ts
│   ├── LightsModel.ts
│   ├── SceneModel.ts
│   ├── SceneObjectModel.ts     # 3D 对象基类
│   └── TorusModel.ts
├── types/
│   └── events.ts               # 事件类型定义
├── views/
│   ├── CameraView.ts
│   ├── CubeView.ts
│   ├── SceneObjectView.ts      # 对象视图基类
│   ├── SceneView.ts
│   └── TorusView.ts
└── index.ts                    # 应用入口

六、性能基准测试

在 Steam Deck(SteamOS 3.7.19,Chrome 146,WebGL 2)上,使用 InstancedMesh 对不同数量的对象进行 10 秒性能采样:

表格

指标 1000 对象 25000 对象 50000 对象 100000 对象
FPS(平均 / 最低 / 最高) 120 59 33 19
输入→渲染延迟(平均) 0.47ms 0.87ms 1.45ms 2.2ms
输入→渲染延迟(峰值) 1.1ms 9.9ms 18.6ms 23.2ms
内存占用 17 MB 18 MB 19 MB 21 MB

结论:MVC 抽象层带来的性能开销可以忽略不计。即使到 10 万个对象,平均输入延迟仍低于 2.2ms,内存从 17MB 增长到 21MB,扩展性良好。

七、超越基础 MVC

7.1 集中式事件总线

当前 Model 和 View 直接绑定事件监听器。项目变大后,可以引入一个中央事件分发器(Event Bus) ,所有消息都走这个枢纽。Controller 和 Model 向总线发送更新,View 只订阅自己需要的数据。这样避免了组件之间直接订阅形成的复杂关系网,调试起来更清晰。

7.2 MVVM:当你需要 React/Vue 面板

如果 3D 场景上方有大量 HTML 配置面板(用 React 或 Vue 构建),传统 MVC 的 Controller 和响应式 UI 之间容易出现状态重复。此时可以转向 MVVM(Model-View-ViewModel) :ViewModel 把 3D 数据转换成 DOM 容易消费的格式,天然适配响应式框架。

7.3 MVP:当性能是第一优先级

如果场景中有成千上万个对象,每帧都重新计算位置可能导致掉帧。MVP(Model-View-Presenter) 让 View 完全被动,Presenter 统一计算所有实体的变换,然后批量更新 WebGL。当场景极其庞大、稳定帧率是绝对优先事项时,这个模式是必要的。

7.4 SOLID 原则的重要性

MVC 只是分层,SOLID 原则保证分层不被破坏。看上面的代码,有两处明显的耦合:

typescript

运行

csharp 复制代码
// TransformController 里
if (obj instanceof CubeModel) { /* ... */ }
else if (obj instanceof TorusModel) { /* ... */ }

// SceneView 里
if (event.object instanceof CubeModel) { view = new CubeView(event.object); }
else if (event.object instanceof TorusModel) { view = new TorusView(event.object); }

这些 instanceof 链违反了开闭原则 (新增对象类型必须修改这两个类)和依赖倒置原则(高层模块依赖了具体实现)。Demo 里这样写是为了让数据流更直观,但实际项目中应该:

  • 把旋转逻辑放到各自的 Model 里(多态替代条件判断)
  • 视图工厂(View Factory) 根据 Model 类型创建对应 View

八、总结

优点

  • 可预测性:数据与渲染循环严格隔离,修改应用状态不会意外破坏 WebGL 上下文。
  • 可测试性:Controller 和 Model 可以在纯 Node 环境里跑单元测试,不需要 mock Canvas 或 WebGL。
  • 性能:事件驱动架构开销极低,从 1000 到 10 万个实体,内存和延迟都保持在很低水平。

缺点

  • 样板代码多 :让第一个物体出现在屏幕上,需要写 Model、View、事件监听器,比直接 scene.add(mesh) 慢得多。
  • 数据重复:位置、旋转等属性需要在 Model 和 Three.js 对象之间同步,维护两份数据。
  • 需要纪律:随着应用增长,必须严格遵守 MVC 和 SOLID。一旦让逻辑溜进 View,或者让 Controller 绕过 Model 直接操作渲染,整个架构就失去了意义。

MVC 只是组织 Three.js 代码的一种方式。它适合中大型项目、需要可测试性和团队协作的场景。如果只是做一个简单的交互动效,单文件可能更高效 ------ 架构永远是为需求服务的,不是为了架构而架构。

相关推荐
光影少年4 小时前
react navite本地存储:AsyncStorage、MMKV、文件存储
前端·react native·react.js
曹牧4 小时前
C#:问号
前端·数据库·c#
王林不想说话5 小时前
JavaScript 从入门到进阶:一篇覆盖核心知识、运行机制与工程实践
javascript
windliang5 小时前
Claude Code 源码分析(十一):一段会话怎样保存、恢复与续写
前端·javascript·人工智能
掘金一周5 小时前
掘友们,你们现在下班了都玩啥游戏?| 沸点周刊 8.13
前端·人工智能·后端
HjhIron5 小时前
别再傻等后端接口了!React + Vite + Mock 打造真正的前后端分离开发体验
前端
用户921080262865 小时前
从 CSS 到 Vue Scoped:一篇讲透前端样式的工程化认知
前端
扫一屋6 小时前
Element Plus 源码级深度定制:不 Fork、不改包,用 Vite 插件实现组件行为运行时改造
前端
BigTopOne7 小时前
KOOM 学习计划
前端