You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Nuxt3页面切换后Three JS对象无法加载问题求助

问题:Three.js原子模型在Nuxt页面跳转返回后报错

我还有很多需要学习的内容,本来不想求助,但查了大量资料还是没解决。问题很简单,我的Three.js原子模型首次加载完全正常,但通过NuxtLink跳转到其他页面再返回时就会报错。

报错信息:

Uncaught (in promise) TypeError: Cannot read properties of null (reading 'width')
at new WebGLRenderer (three.module.js:26639:23)
at Atom.vue:64:1
at hook.__wdc.hook.__wdc (runtime-core.esm-bundler.js:2626:20)
at callWithErrorHandling (runtime-core.esm-bundler.js:155:22)
at callWithAsyncErrorHandling (runtime-core.esm-bundler.js:164:21)
at hook.__weh.hook.__weh (runtime-core.esm-bundler.js:2684:29)
at flushPostFlushCbs (runtime-core.esm-bundler.js:341:32)
at flushJobs (runtime-core.esm-bundler.js:395:9)

我猜是canvas没加载?试过用Options API替代Composition API,结果一样。也试过onActivated和KeepAlive,都没用。

以下是我的代码:

<script setup>
import * as THREE from 'three'

onActivated(() => {
// Canvas
const canvas = document.querySelector('canvas.webgl')
// Sizes
const sizes = {
    width: 300,
    height: 300
}
const pixelRatio = window.devicePixelRatio

/* Base */
// Scene
const scene = new THREE.Scene()

/* Props */
// Plane Background
const backgroundPlane = new THREE.PlaneGeometry(50, 50, 4, 4)
const backgroundMaterial = new THREE.MeshStandardMaterial({ emissive: "#000505", roughness:0.1, metalness: 1 })
const background = new THREE.Mesh(backgroundPlane, backgroundMaterial)
background.position.z = -5
scene.add(background)

// Atom Group
const atom = new THREE.Group()
scene.add(atom)
atom.position.z = -2

const protonSphere = new THREE.SphereGeometry(.3, 16, 16)
const protonMaterial = new THREE.MeshStandardMaterial({ emissive: "#080E54", roughness:0.4, metalness: 0.9 })
const proton = new THREE.Mesh(protonSphere, protonMaterial)
atom.add(proton)

const electronCloudSphere = new THREE.SphereGeometry(2, 32, 32)
const electronCloudMaterial = new THREE.MeshPhysicalMaterial({ opacity: .2, transparent: true, reflectivity:0, side:THREE.DoubleSide, roughness:0.5, metalness: 0, ior: 2, thickness: 2, transmission: 1 })
const electronCloud = new THREE.Mesh(electronCloudSphere, electronCloudMaterial)
atom.add(electronCloud)

const electronSphere = new THREE.SphereGeometry(.1, 16, 16)
const electronMaterial = new THREE.MeshStandardMaterial({ emissive: "#2FFFFF", roughness:0, metalness: 0.9 })
const electron = new THREE.Mesh(electronSphere, electronMaterial)
const radius = 1.5
let theta = Math.random() * Math.PI * 2
let phi = Math.random() * Math.PI
const x = radius * Math.sin(theta)
const z = radius * Math.cos(theta)
const y = radius * Math.cos(phi)
electron.position.set(x, y, z)
atom.add(electron)

const pointLight = new THREE.PointLight("#B7DBFF", 1, 5)
pointLight.position.z = 1
scene.add(pointLight)

/* Camera & Controls */
// Base camera
const camera = new THREE.PerspectiveCamera(75, sizes.width / sizes.height, 0.1, 100)
camera.position.z = 3
scene.add(camera)

/* Renderer */
const renderer = new THREE.WebGLRenderer({
    canvas,
    antialias: true,
})
renderer.setSize(sizes.width, sizes.height)
renderer.setPixelRatio(Math.min(pixelRatio, 2))

/* Animate */
const clock = new THREE.Clock()
let time = 0

const tick = () =>
{
    const elapsedTime = clock.getElapsedTime()
    const deltaTime = elapsedTime - time
    time = elapsedTime

    // Animations
    let shake = (Math.random() * 0.015)
    proton.rotateX(.5)
    proton.position.set(shake, shake, 0)
    atom.rotateX(.01 * Math.random())
    atom.rotateY(.02 * Math.random())
    atom.rotateZ(.04 * Math.random())

    // Render
    renderer.render(scene, camera)

    // Call tick again on the next frame
    window.requestAnimationFrame(tick)
}
tick()
})

</script>

<template>
    <div class="canvas-wrapper">
        <canvas id="atom" class="webgl"></canvas>
    </div>
</template>

问题根源

  1. DOM获取时机错误:onActivated触发时,组件模板可能还没完成渲染,document.querySelector('canvas.webgl')返回null,导致初始化WebGLRenderer时传入无效参数,触发报错。
  2. 资源未清理:页面跳转后,Three.js的渲染循环(requestAnimationFrame)仍在后台运行,返回时重新初始化会重复创建场景、渲染器等资源,引发内存泄漏和逻辑冲突。

修复方案

1. 用Vue Ref绑定Canvas元素

替代document.querySelector,确保组件能准确获取DOM元素:

<template>
  <div class="canvas-wrapper">
    <canvas ref="canvasRef" id="atom" class="webgl"></canvas>
  </div>
</template>

<script setup>
import * as THREE from 'three'
import { ref, onMounted, onActivated, onDeactivated } from 'vue'

const canvasRef = ref(null)
// 声明全局变量存储Three.js实例,方便后续清理
let scene, atom, renderer, camera, tickId

2. 拆分生命周期逻辑

  • onMounted:首次加载时初始化所有Three.js基础资源
  • onActivated:页面返回时重启渲染循环
  • onDeactivated:页面离开时停止渲染循环,避免内存泄漏
onMounted(() => {
  const canvas = canvasRef.value
  if (!canvas) return

  const sizes = { width: 300, height: 300 }
  const pixelRatio = window.devicePixelRatio

  // 初始化场景
  scene = new THREE.Scene()

  // 背景平面
  const backgroundPlane = new THREE.PlaneGeometry(50, 50, 4, 4)
  const backgroundMaterial = new THREE.MeshStandardMaterial({ emissive: "#000505", roughness:0.1, metalness: 1 })
  const background = new THREE.Mesh(backgroundPlane, backgroundMaterial)
  background.position.z = -5
  scene.add(background)

  // 原子组
  atom = new THREE.Group()
  scene.add(atom)
  atom.position.z = -2

  const protonSphere = new THREE.SphereGeometry(.3, 16, 16)
  const protonMaterial = new THREE.MeshStandardMaterial({ emissive: "#080E54", roughness:0.4, metalness: 0.9 })
  const proton = new THREE.Mesh(protonSphere, protonMaterial)
  atom.add(proton)

  const electronCloudSphere = new THREE.SphereGeometry(2, 32, 32)
  const electronCloudMaterial = new THREE.MeshPhysicalMaterial({ opacity: .2, transparent: true, reflectivity:0, side:THREE.DoubleSide, roughness:0.5, metalness: 0, ior: 2, thickness: 2, transmission: 1 })
  const electronCloud = new THREE.Mesh(electronCloudSphere, electronCloudMaterial)
  atom.add(electronCloud)

  const electronSphere = new THREE.SphereGeometry(.1, 16, 16)
  const electronMaterial = new THREE.MeshStandardMaterial({ emissive: "#2FFFFF", roughness:0, metalness: 0.9 })
  const electron = new THREE.Mesh(electronSphere, electronMaterial)
  const radius = 1.5
  let theta = Math.random() * Math.PI * 2
  let phi = Math.random() * Math.PI
  const x = radius * Math.sin(theta)
  const z = radius * Math.cos(theta)
  const y = radius * Math.cos(phi)
  electron.position.set(x, y, z)
  atom.add(electron)

  const pointLight = new THREE.PointLight("#B7DBFF", 1, 5)
  pointLight.position.z = 1
  scene.add(pointLight)

  // 相机
  camera = new THREE.PerspectiveCamera(75, sizes.width / sizes.height, 0.1, 100)
  camera.position.z = 3
  scene.add(camera)

  // 渲染器
  renderer = new THREE.WebGLRenderer({
    canvas,
    antialias: true,
  })
  renderer.setSize(sizes.width, sizes.height)
  renderer.setPixelRatio(Math.min(pixelRatio, 2))
})

// 页面激活时启动渲染循环
onActivated(() => {
  if (!scene || !renderer || !camera) return
  const clock = new THREE.Clock()
  let time = 0

  const tick = () => {
    const elapsedTime = clock.getElapsedTime()
    const deltaTime = elapsedTime - time
    time = elapsedTime

    // 动画逻辑
    let shake = (Math.random() * 0.015)
    atom.children[1].rotateX(.5)
    atom.children[1].position.set(shake, shake, 0)
    atom.rotateX(.01 * Math.random())
    atom.rotateY(.02 * Math.random())
    atom.rotateZ(.04 * Math.random())

    renderer.render(scene, camera)
    tickId = window.requestAnimationFrame(tick)
  }
  tick()
})

// 页面离开时停止渲染循环
onDeactivated(() => {
  if (tickId) {
    window.cancelAnimationFrame(tickId)
  }
})

3. 确保组件被KeepAlive包裹

在Nuxt的根页面或父组件中添加KeepAlive,让onActivated和onDeactivated钩子正常触发:

<!-- app.vue 或父页面组件 -->
<template>
  <KeepAlive>
    <NuxtPage />
  </KeepAlive>
</template>

内容的提问来源于stack exchange,提问作者Kyrony

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.18 02:20:37