前端框架集成指南
Three.js 作为原生 JavaScript 库,可以与任何前端框架(React、Vue、Angular 等)集成。本指南涵盖主流框架的集成方案、最佳实践和常见问题。
系统架构
code
┌─────────────────────────────────────────────────────────────────────────┐
│ 前端框架 + Three.js 架构 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ 框架层 (UI/状态) │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │ │
│ │ │ 组件树 │ │ 状态管理 │ │ 路由 │ │ UI 交互事件 │ │ │
│ │ └────┬─────┘ └────┬─────┘ └────┬─────┘ └──────┬───────┘ │ │
│ │ │ │ │ │ │ │
│ └───────┼─────────────┼────────────┼───────────────┼──────────┘ │
│ │ │ │ │ │
│ ▼ ▼ ▼ ▼ │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ Three.js 层 (渲染) │ │
│ │ │ │
│ │ ┌─────────┐ ┌─────────┐ ┌──────────┐ ┌──────────────┐ │ │
│ │ │ Scene │ │ Camera │ │ Renderer │ │ Controls │ │ │
│ │ ├─────────┤ ├─────────┤ ├──────────┤ ├──────────────┤ │ │
│ │ │ Lights │ │ Objects │ │ Animations│ │ Raycaster │ │ │
│ │ │ Meshes │ │ Materials│ │ PostFX │ │ Loaders │ │ │
│ │ └─────────┘ └─────────┘ └──────────┘ └──────────────┘ │ │
│ │ │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
│ 数据流: │
│ 框架状态 → Props/Store → Three.js 对象属性 → 渲染循环 │
│ 用户交互 → 事件 → 框架状态更新 → Three.js 同步 │
│ │
└─────────────────────────────────────────────────────────────────────────┘
核心设计原则:
1. Three.js 对象不应存储在框架的响应式系统中(避免性能问题)
2. 使用 ref/引用持有 Three.js 对象,避免不必要的重渲染
3. 渲染循环独立于框架的更新周期
4. 通过命令式 API 操作 Three.js,通过声明式 API 描述 UI概述
为什么需要框架集成?
| 场景 | 纯 Three.js | 框架 + Three.js |
|---|---|---|
| 单页 Demo | ✅ 足够 | 过度工程 |
| 复杂应用 | ❌ 状态管理困难 | ✅ 结构清晰 |
| 团队协作 | ❌ 代码组织难 | ✅ 组件化 |
| 与其他功能结合 | ❌ 手动 DOM 操作 | ✅ 无缝集成 |
| 路由/权限等 | ❌ 需自己实现 | ✅ 框架支持 |
三种集成方式
- 手动集成:直接在组件生命周期中管理 Three.js
- 声明式库:使用 @react-three/fiber、TresJS 等
- 混合模式:部分使用库,部分手动
通用集成原则
核心要点
javascript
// ⚠️ 不要这样做:
// 将 Three.js 对象放入响应式状态会导致严重性能问题
// React - 错误示例 ❌
const [scene, setScene] = useState(new THREE.Scene())
// 每次 setState 都会尝试比较整个场景图...
// Vue - 错误示例 ❌
const scene = ref(new THREE.Scene())
// Vue 的深度代理会遍历所有对象属性...
// ✅ 正确做法:用 ref 引用非响应式对象正确的引用模式
javascript
// React 方式
import { useRef, useEffect } from 'react'
import * as THREE from 'three'
function CanvasComponent() {
const containerRef = useRef(null)
const sceneRef = useRef(null) // 非 useState!
const rendererRef = useRef(null)
useEffect(() => {
// 初始化(只执行一次)
const scene = new THREE.Scene()
sceneRef.current = scene
const renderer = new THREE.WebGLRenderer({ antialias: true })
renderer.setSize(containerRef.current.clientWidth, containerRef.current.clientHeight)
containerRef.current.appendChild(renderer.domElement)
rendererRef.current = renderer
// 动画循环
let animId
function animate() {
animId = requestAnimationFrame(animate)
renderer.render(scene, camera)
}
animate()
// 清理
return () => {
cancelAnimationFrame(animId)
renderer.dispose()
if (containerRef.current) {
containerRef.current.removeChild(renderer.domElement)
}
}
}, []) // 空依赖数组 = 只执行一次
return <div ref={containerRef} style={{ width: '100%', height: '100%' }} />
}尺寸自适应
javascript
function useResizeObserver(containerRef, callback) {
useEffect(() => {
const container = containerRef.current
if (!container) return
const observer = new ResizeObserver((entries) => {
for (const entry of entries) {
callback(entry.contentRect.width, entry.contentRect.height)
}
})
observer.observe(container)
return () => observer.disconnect()
}, [])
}React 集成
方式一:纯手写(推荐入门)
jsx
// components/ThreeCanvas.jsx
import { useRef, useEffect, useCallback } from 'react'
import * as THREE from 'three'
export default function ThreeCanvas({ onReady }) {
const containerRef = useRef(null)
const threeObjects = useRef({})
const initScene = useCallback(() => {
const container = containerRef.current
const width = container.clientWidth
const height = container.clientHeight
// 场景
const scene = new THREE.Scene()
scene.background = new THREE.Color(0x1a1a2e)
threeObjects.current.scene = scene
// 相机
const camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000)
camera.position.set(5, 5, 5)
threeObjects.current.camera = camera
// 渲染器
const renderer = new THREE.WebGLRenderer({ antialias: true })
renderer.setSize(width, height)
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
container.appendChild(renderer.domElement)
threeObjects.current.renderer = renderer
// 光照
const ambientLight = new THREE.AmbientLight(0xffffff, 0.4)
scene.add(ambientLight)
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8)
directionalLight.position.set(10, 20, 10)
scene.add(directionalLight)
// 控制器
import('three/examples/jsm/controls/OrbitControls.js').then(({ OrbitControls }) => {
const controls = new OrbitControls(camera, renderer.domElement)
controls.enableDamping = true
threeObjects.current.controls = controls
if (onReady) onReady(threeObjects.current)
})
// 动画循环
let animId
function animate() {
animId = requestAnimationFrame(animate)
if (threeObjects.current.controls) {
threeObjects.current.controls.update()
}
renderer.render(scene, camera)
}
animate()
// 自适应尺寸
const resizeObserver = new ResizeObserver((entries) => {
const { width: w, height: h } = entries[0].contentRect
camera.aspect = w / h
camera.updateProjectionMatrix()
renderer.setSize(w, h)
})
resizeObserver.observe(container)
return () => {
cancelAnimationFrame(animId)
resizeObserver.disconnect()
renderer.dispose()
container.removeChild(renderer.domElement)
}
}, [onReady])
useEffect(() => {
const cleanup = initScene()
return cleanup
}, [initScene])
return (
<div
ref={containerRef}
style={{ width: '100%', height: '100%', position: 'relative' }}
/>
)
}jsx
// App.jsx
import { useCallback, useRef } from 'react'
import ThreeCanvas from './components/ThreeCanvas'
export default function App() {
const sceneRef = useRef(null)
const handleSceneReady = useCallback((objects) => {
sceneRef.current = objects
// 添加一个测试立方体
const geometry = new THREE.BoxGeometry(2, 2, 2)
const material = new THREE.MeshStandardMaterial({
color: 0x4488ff,
metalness: 0.3,
roughness: 0.4
})
const cube = new THREE.Mesh(geometry, material)
objects.scene.add(cube)
// 可选:添加动画
objects.userData = objects.userData || {}
objects.userData.animatedMeshes = [cube]
// 启动自定义动画
startAnimation(objects)
}, [])
function startAnimation(objects) {
function update() {
if (objects.userData?.animatedMeshes) {
objects.userData.animatedMeshes.forEach(mesh => {
mesh.rotation.x += 0.005
mesh.rotation.y += 0.01
})
}
requestAnimationFrame(update)
}
update()
}
return (
<div style={{ display: 'flex', height: '100vh' }}>
{/* 左侧控制面板 */}
<aside style={{ width: 280, padding: 20, background: '#16213e', color: '#fff' }}>
<h2>控制面板</h2>
{/* UI 控件 */}
</aside>
{/* 右侧 3D 画布 */}
<main style={{ flex: 1 }}>
<ThreeCanvas onReady={handleSceneReady} />
</main>
</div>
)
}方式二:@react-three/fiber(推荐生产)
bash
npm install three @types/three @react-three/fiber @react-three/dreijsx
// components/Scene.jsx
import { Canvas } from '@react-three/fiber'
import { OrbitControls, Environment, ContactShadows } from '@react-three/drei'
import { Suspense } from 'react'
import AnimatedBox from './AnimatedBox'
import ParticleField from './ParticleField'
export default function Scene() {
return (
<Canvas
shadows
camera={{ position: [5, 5, 5], fov: 50 }}
gl={{ antialias: true }}
style={{ background: '#1a1a2e' }}
>
<Suspense fallback={null}>
{/* 光照 */}
<ambientLight intensity={0.4} />
<directionalLight
position={[10, 20, 10]}
intensity={0.8}
castShadow
shadow-mapSize={[2048, 2048]}
/>
{/* 场景内容 */}
<AnimatedBox position={[0, 1, 0]} color="#4488ff" />
<ParticleField count={2000} />
{/* 环境 */}
<Environment preset="city" />
<ContactShadows
position={[0, -0.5, 0]}
opacity={0.6}
scale={15}
blur={2}
/>
{/* 控制器 */}
<OrbitControls enableDamping dampingFactor={0.05} />
</Suspense>
</Canvas>
)
}jsx
// components/AnimatedBox.jsx
import { useRef } from 'react'
import { useFrame } from '@react-three/fiber'
import { MeshDistortMaterial } from '@react-three/drei'
import * as THREE from 'three'
export default function AnimatedBox({ position = [0, 0, 0], color = '#ff4444' }) {
const meshRef = useRef()
useFrame((state, delta) => {
if (!meshRef.current) return
meshRef.current.rotation.x += delta * 0.5
meshRef.current.rotation.y += delta * 0.8
meshRef.current.position.y = position[1] + Math.sin(state.clock.elapsedTime) * 0.3
})
return (
<mesh ref={meshRef} position={position} castShadow>
<boxGeometry args={[1.5, 1.5, 1.5]} />
<MeshDistortMaterial
color={color}
speed={2}
distort={0.3}
radius={1}
/>
</mesh>
)
}jsx
// components/ParticleField.jsx
import { useRef, useMemo } from 'react'
import { useFrame } from '@react-three/fiber'
import * as THREE from 'three'
export default function ParticleField({ count = 1000, spread = 20 }) {
const pointsRef = useRef()
const positions = useMemo(() => {
const pos = new Float32Array(count * 3)
for (let i = 0; i < count; i++) {
pos[i * 3] = (Math.random() - 0.5) * spread
pos[i * 3 + 1] = (Math.random() - 0.5) * spread
pos[i * 3 + 2] = (Math.random() - 0.5) * spread
}
return pos
}, [count, spread])
useFrame((state) => {
if (!pointsRef.current) return
pointsRef.current.rotation.y = state.clock.elapsedTime * 0.03
})
return (
<points ref={pointsRef}>
<bufferGeometry>
<bufferAttribute
attach="attributes-position"
count={count}
array={positions}
itemSize={3}
/>
</bufferGeometry>
<pointsMaterial
size={0.08}
color="#88ccff"
transparent
opacity={0.7}
sizeAttenuation
/>
</points>
)
}React 状态与 Three.js 通信
jsx
import { create } from 'zustand'
// Zustand store
const useSceneStore = create((set, get) => ({
selectedObject: null,
objects: [],
showGrid: true,
wireframeMode: false,
backgroundColor: '#1a1a2e',
selectObject: (id) => set({ selectedObject: id }),
addObject: (obj) => set(state => ({ objects: [...state.objects, obj] })),
toggleGrid: () => set(state => ({ showGrid: !state.showGrid })),
toggleWireframe: () => set(state => ({ wireframeMode: !state.wireframeMode })),
setBackgroundColor: (color) => set({ backgroundColor: color }),
}))
// 在 R3F 组件中读取 store
function ReactiveScene() {
const backgroundColor = useSceneStore(s => s.backgroundColor)
const wireframeMode = useSceneStore(s => s.wireframeMode)
return (
<Canvas style={{ background: backgroundColor }}>
<ReactiveContent wireframe={wireframeMode} />
</Canvas>
)
}
function ReactiveContent({ wireframe }) {
const materialRef = useRef()
useFrame(() => {
if (materialRef.current) {
materialRef.current.wireframe = wireframe
}
})
return (
<mesh>
<boxGeometry />
<meshStandardMaterial ref={materialRef} color="#4488ff" />
</mesh>
)
}
// 从 UI 控制 Three.js
function ControlPanel() {
const toggleWireframe = useSceneStore(s => s.toggleWireframe)
const setBackgroundColor = useSceneStore(s => s.setBackgroundColor)
return (
<div className="panel">
<button onClick={toggleWireframe}>切换线框</button>
<input
type="color"
onChange={(e) => setBackgroundColor(e.target.value)}
/>
</div>
)
}Vue 集成
方式一:Composition API(Vue 3 推荐)
Vue SFC
<!-- components/ThreeCanvas.vue -->
<template>
<div ref="containerRef" class="canvas-container"></div>
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount, shallowRef } from 'vue'
import * as THREE from 'three'
const props = defineProps({
backgroundColor: { type: String, default: '#1a1a2e' },
showAxes: { type: Boolean, default: false },
enableControls: { type: Boolean, default: true }
})
const emit = defineEmits(['ready'])
const containerRef = ref(null)
// 用 shallowRef 避免 Vue 的深度代理
const scene = shallowRef(null)
const camera = shallowRef(null)
const renderer = shallowRef(null)
let controls = null
let animationId = null
onMounted(async () => {
await initThree()
})
onBeforeUnmount(() => {
cleanup()
})
async function initThree() {
const container = containerRef.value
const width = container.clientWidth
const height = container.clientHeight
// 场景
const s = new THREE.Scene()
s.background = new THREE.Color(props.backgroundColor)
scene.value = s
// 相机
const c = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000)
c.position.set(5, 5, 5)
camera.value = c
// 渲染器
const r = new THREE.WebGLRenderer({ antialias: true })
r.setSize(width, height)
r.setPixelRatio(Math.min(window.devicePixelRatio, 2))
r.shadowMap.enabled = true
r.shadowMap.type = THREE.PCFSoftShadowMap
container.appendChild(r.domElement)
renderer.value = r
// 光照
s.add(new THREE.AmbientLight(0xffffff, 0.4))
const dirLight = new THREE.DirectionalLight(0xffffff, 0.8)
dirLight.position.set(10, 20, 10)
dirLight.castShadow = true
s.add(dirLight)
// 坐标轴辅助
if (props.showAxes) {
s.add(new THREE.AxesHelper(5))
}
// 控制器
if (props.enableControls) {
const { OrbitControls } = await import(
'three/examples/jsm/controls/OrbitControls.js'
)
controls = new OrbitControls(c, r.domElement)
controls.enableDamping = true
controls.dampingFactor = 0.05
}
// 自适应
const observer = new ResizeObserver(entries => {
const { width: w, height: h } = entries[0].contentRect
c.aspect = w / h
c.updateProjectionMatrix()
r.setSize(w, h)
})
observer.observe(container)
// 动画循环
function animate() {
animationId = requestAnimationFrame(animate)
if (controls) controls.update()
r.render(s, c)
}
animate()
emit('ready', { scene: s, camera: c, renderer: r, controls })
}
function cleanup() {
if (animationId) cancelAnimationFrame(animationId)
if (renderer.value) {
renderer.value.dispose()
const container = containerRef.value
if (container && renderer.value.domElement) {
container.removeChild(renderer.value.domElement)
}
}
if (controls) controls.dispose()
}
// 暴露方法给父组件
defineExpose({
getScene: () => scene.value,
getCamera: () => camera.value,
getRenderer: () => renderer.value
})
</script>
<style scoped>
.canvas-container {
width: 100%;
height: 100%;
overflow: hidden;
}
</style>Vue SFC
<!-- views/Dashboard.vue -->
<template>
<div class="dashboard">
<aside class="sidebar">
<h3>控制面板</h3>
<label>
<input type="checkbox" v-model="showGrid" /> 显示网格
</label>
<label>
<input type="checkbox" v-model="wireframe" /> 线框模式
</label>
<label>
背景颜色
<input type="color" v-model="bgColor" />
</label>
<button @click="addRandomCube">添加方块</button>
<p>对象数量: {{ objectCount }}</p>
</aside>
<main class="viewport">
<ThreeCanvas
:background-color="bgColor"
:show-axes="showGrid"
@ready="handleReady"
/>
</main>
</div>
</template>
<script setup>
import { ref, watch } from 'vue'
import ThreeCanvas from '@/components/ThreeCanvas.vue'
const showGrid = ref(false)
const wireframe = ref(false)
const bgColor = ref('#1a1a2e')
const objectCount = ref(0)
let threeContext = null
function handleReady(ctx) {
threeContext = ctx
addDefaultObject()
}
function addDefaultObject() {
if (!threeContext) return
const { scene } = threeContext
const geometry = new THREE.BoxGeometry(2, 2, 2)
const material = new THREE.MeshStandardMaterial({
color: 0x4488ff,
metalness: 0.3,
roughness: 0.4
})
const cube = new THREE.Mesh(geometry, material)
cube.castShadow = true
cube.receiveShadow = true
scene.add(cube)
objectCount.value++
// 动画
function animate() {
cube.rotation.x += 0.005
cube.rotation.y += 0.01
requestAnimationFrame(animate)
}
animate()
}
function addRandomCube() {
if (!threeContext) return
const { scene } = threeContext
const geo = new THREE.BoxGeometry(1, 1, 1)
const mat = new THREE.MeshStandardMaterial({
color: Math.random() * 0xffffff
})
const mesh = new THREE.Mesh(geo, mat)
mesh.position.set(
(Math.random() - 0.5) * 10,
Math.random() * 3 + 1,
(Math.random() - 0.5) * 10
)
scene.add(mesh)
objectCount.value++
}
watch(wireframe, (val) => {
if (!threeContext) return
threeContext.scene.traverse(child => {
if (child.isMesh && child.material) {
child.material.wireframe = val
}
})
})
</script>
<style scoped>
.dashboard {
display: flex;
height: 100vh;
}
.sidebar {
width: 280px;
padding: 20px;
background: #16213e;
color: #fff;
display: flex;
flex-direction: column;
gap: 12px;
}
.viewport {
flex: 1;
}
</style>方式二:TresJS(Vue 3 声明式)
bash
npm install tres @tresjs/core threeVue SFC
<template>
<TresCanvas
window-size
shadows
:camera="{ position: [5, 5, 5], fov: 50 }"
style="{ background: '#1a1a2e' }"
>
<TresAmbientLight :intensity="0.4" />
<TresDirectionalLight
:position="[10, 20, 10]"
:intensity="0.8"
cast-shadow
/>
<TresMesh
:position-y="1"
cast-shadow
@pointer-enter="isHovered = true"
@pointer-leave="isHovered = false"
>
<TresBoxGeometry :args="[2, 2, 2]" />
<TresMeshStandardMaterial
:color="isHovered ? '#ff6644' : '#4488ff'"
:metalness="0.3"
:roughness="0.4"
/>
</TresMesh>
<TresOrbitControls enable-damping :damping-factor="0.05" />
</TresCanvas>
</template>
<script setup>
import { ref } from 'vue'
const isHovered = ref(false)
</script>Angular 集成
typescript
// services/three.service.ts
import { Injectable, ElementRef, OnDestroy } from '@angular/core'
import * as THREE from 'three'
@Injectable({ providedIn: 'root' })
export class ThreeService implements OnDestroy {
private scene!: THREE.Scene
private camera!: THREE.PerspectiveCamera
private renderer!: THREE.WebGLRenderer
private animationId: number | null = null
private controls: any = null
async init(
container: ElementRef<HTMLDivElement>,
options?: { bgColor?: string; showAxes?: boolean }
) {
const el = container.nativeElement
const width = el.clientWidth
const height = el.clientHeight
this.scene = new THREE.Scene()
this.scene.background = new THREE.Color(options?.bgColor ?? '#1a1a2e')
this.camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000)
this.camera.position.set(5, 5, 5)
this.renderer = new THREE.WebGLRenderer({ antialias: true })
this.renderer.setSize(width, height)
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
this.renderer.shadowMap.enabled = true
el.appendChild(this.renderer.domElement)
this.scene.add(new THREE.AmbientLight(0xffffff, 0.4))
const dirLight = new THREE.DirectionalLight(0xffffff, 0.8)
dirLight.position.set(10, 20, 10)
dirLight.castShadow = true
this.scene.add(dirLight)
if (options?.showAxes) {
this.scene.add(new THREE.AxesHelper(5))
}
const { OrbitControls } = await import(
'three/examples/jsm/controls/OrbitControls.js'
)
this.controls = new OrbitControls(this.camera, this.renderer.domElement)
this.controls.enableDamping = true
const observer = new ResizeObserver((entries) => {
const { width: w, height: h } = entries[0].contentRect
this.camera.aspect = w / h
this.camera.updateProjectionMatrix()
this.renderer.setSize(w, h)
})
observer.observe(el)
this.animate()
}
private animate() {
this.animationId = requestAnimationFrame(() => this.animate())
if (this.controls) this.controls.update()
this.renderer.render(this.scene, this.camera)
}
getScene() { return this.scene }
getCamera() { return this.camera }
getRenderer() { return this.renderer }
ngOnDestroy() {
if (this.animationId) cancelAnimationFrame(this.animationId)
this.renderer.dispose()
if (this.controls) this.controls.dispose()
}
}typescript
// components/canvas-container.component.ts
import { Component, OnInit, ViewChild, AfterViewInit } from '@angular/core'
import { ThreeService } from '../services/three.service'
@Component({
selector: 'app-canvas-container',
template: '<div #canvasContainer style="width:100%;height:100%"></div>',
styles: [':host { display: block; width: 100%; height: 100% }']
})
export class CanvasContainerComponent implements AfterViewInit {
@ViewChild('canvasContainer') container!: ElementRef<HTMLDivElement>
constructor(private threeService: ThreeService) {}
ngAfterViewInit() {
this.threeService.init(this.container, { bgColor: '#1a1a2e', showAxes: true })
}
}框架对比与选型
| 特性 | 手动集成 | @react-three/fiber | TresJS | Angular 手写 |
|---|---|---|---|---|
| 学习成本 | 低(懂 Three.js 即可) | 中 | 低 | 中 |
| 代码量 | 多 | 少 | 少 | 多 |
| 灵活性 | 最高 | 高 | 高 | 高 |
| 类型安全 | 取决于 TS 配置 | 好 | 好 | 好 |
| 生态插件 | 无 | drei/troika 等 | tresjs/ecosystem | 无 |
| 社区活跃度 | N/A | 非常高 | 增长中 | N/A |
| 适合场景 | 简单项目 | React 项目 | Vue 3 项目 | Angular 项目 |
选型建议:
- React 项目:优先考虑
@react-three/fiber+@react-three/drei - Vue 3 项目:优先考虑
TresJS - 简单需求或非主流框架:手写集成
- 需要精细控制:手写集成(即使使用框架库)
状态管理
推荐模式:单向数据流
code
UI Action → Store 更新 → 订阅者接收 → 更新 Three.js 对象javascript
// React + Zustand 示例
const useAppStore = create((set) => ({
// UI 状态
isPlaying: true,
selectedId: null,
// 3D 场景配置
config: {
ambientIntensity: 0.4,
sunPosition: [10, 20, 10],
fogEnabled: false,
fogColor: '#1a1a2e',
fogNear: 10,
fogFar: 50
},
// 数据驱动列表
dataPoints: [],
togglePlay: () => set(s => ({ isPlaying: !s.isPlaying })),
setSelected: (id) => set({ selectedId: id }),
updateConfig: (patch) => set(s => ({ config: { ...s.config, ...patch } })),
setDataPoints: (data) => set({ dataPoints: data })
}))
// 在 R3F 组件中使用
function SceneConfig() {
const config = useAppStore(s => s.config)
const fogEnabled = config.fogEnabled
return (
<>
<fog attach="fog" args={[config.fogColor, config.fogNear, config.fogFar]}
visible={fogEnabled} />
<ambientLight intensity={config.ambientIntensity} />
<directionalLight position={config.sunPosition} intensity={0.8} />
</>
)
}TypeScript 支持
类型定义
typescript
// types/three-extended.d.ts
import * as THREE from 'three'
declare module 'three' {
interface Object3D {
userData: {
instanceId?: number
dataType?: string
originalColor?: THREE.Color
[key: string]: any
}
}
}
// 类型安全的 Three.js 工具函数
type Vector3Like = THREE.Vector3 | [number, number, number] | { x: number; y: number; z: number }
function toVector3(v: Vector3Like): THREE.Vector3 {
if (v instanceof THREE.Vector3) return v.clone()
if (Array.isArray(v)) return new THREE.Vector3(...v)
return new THREE.Vector3(v.x, v.y, v.z)
}
interface SceneOptions {
backgroundColor?: string
fog?: {
color: string
near: number
far: number
}
shadowMap?: boolean
}
class SceneManager {
private scene: THREE.Scene
private camera: THREE.PerspectiveCamera
private renderer: THREE.WebGLRenderer
constructor(private container: HTMLElement, options: SceneOptions = {}) {
this.scene = new THREE.Scene()
this.init(options)
}
private init(options: SceneOptions) {
if (options.backgroundColor) {
this.scene.background = new THREE.Color(options.backgroundColor)
}
if (options.fog) {
this.scene.fog = new THREE.Fog(
options.fog.color,
options.fog.near,
options.fog.far
)
}
}
addObject<T extends THREE.Object3D>(
object: T,
position?: Vector3Like
): T {
if (position) {
object.position.copy(toVector3(position))
}
this.scene.add(object)
return object
}
removeObject(object: THREE.Object3D): void {
this.scene.remove(object)
if (object instanceof THREE.Mesh) {
object.geometry.dispose()
if (Array.isArray(object.material)) {
object.material.forEach(m => m.dispose())
} else {
object.material.dispose()
}
}
}
}打包优化
Vite 配置
javascript
// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
build: {
target: 'esnext',
rollupOptions: {
output: {
manualChunks: {
three: ['three'],
'three-examples': [
'three/examples/jsm/controls/OrbitControls',
'three/examples/jsm/loaders/GLTFLoader',
'three/examples/jsm/postprocessing/EffectComposer'
]
}
}
}
},
optimizeDeps: {
include: ['three']
}
})webpack 配置
javascript
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.(glb|gltf)$/,
type: 'asset/resource'
}
]
},
optimization: {
splitChunks: {
cacheGroups: {
threeVendors: {
test: /[\\/]node_modules[\\/](three|@react-three)[\\/]/,
name: 'three-vendors',
chunks: 'all'
}
}
}
}
}按需导入
javascript
// ✅ 推荐:按需导入
import { Scene, PerspectiveCamera, WebGLRenderer } from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
// ❌ 不推荐:全量导入
import * as THREE from 'three' // 会增加 bundle 大小常见问题
Q: 组件重新渲染导致 Three.js 闪烁?
确保 Three.js 对象不在 useState 中,使用 useRef 或 shallowRef:
javascript
// React
const sceneRef = useRef(null) // ✅
// const [scene] = useState(new Scene()) // ❌
// Vue
const scene = shallowRef(null) // ✅
// const scene = ref(new Scene()) // ❌Q: 如何处理窗口大小变化?
使用 ResizeObserver 而非 window.resize 事件:
javascript
useEffect(() => {
const observer = new ResizeObserver(([entry]) => {
const { width, height } = entry.contentRect
camera.aspect = width / height
camera.updateProjectionMatrix()
renderer.setSize(width, height)
})
observer.observe(containerRef.current)
return () => observer.disconnect()
}, [])Q: 内存泄漏如何排查?
javascript
// 清理清单
function dispose() {
// 1. 取消动画帧
cancelAnimationFrame(animationId)
// 2. 停止控制器
controls?.dispose()
// 3. 释放几何体和材质
scene.traverse(obj => {
if (obj.geometry) obj.geometry.dispose()
if (obj.material) {
if (Array.isArray(obj.material)) {
obj.material.forEach(m => m.dispose())
} else {
obj.material.dispose()
}
}
})
// 4. 释放渲染器
renderer.dispose()
// 5. 移除 DOM 元素
container.removeChild(renderer.domElement)
// 6. 释放纹理
textures.forEach(t => t.dispose())
}Q: SSR(服务端渲染)兼容性?
Three.js 依赖 window 和 document,在 SSR 环境下会报错。解决方案:
jsx
// Next.js / Nuxt
import dynamic from 'next/dynamic'
const ThreeCanvas = dynamic(
() => import('./ThreeCanvas'),
{ ssr: false }
)
// 或者动态 import
const [mounted, setMounted] = useState(false)
useEffect(() => setMounted(true), [])
if (!mounted) return null
return <ThreeCanvas />