Nuxt3页面切换后Three JS对象无法加载问题求助
我还有很多需要学习的内容,本来不想求助,但查了大量资料还是没解决。问题很简单,我的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>
问题根源
- DOM获取时机错误:
onActivated触发时,组件模板可能还没完成渲染,document.querySelector('canvas.webgl')返回null,导致初始化WebGLRenderer时传入无效参数,触发报错。 - 资源未清理:页面跳转后,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

