Vue项目迁移报错:Uncaught TypeError: 无法读取null的offsetWidth属性
解决Vue项目中Three.js脚本因DOM未渲染导致的null错误
问题根源很明确:demo3.js被导入时立刻执行代码,此时Vue组件里的<canvas id="scene">还没渲染到页面上,document.querySelector("#scene")返回null,读取offsetWidth自然报错。下面是具体修复步骤:
1. 重构demo3.js,把代码封装成可调用的函数
把原来直接执行的代码改成导出的初始化函数,允许传入canvas元素,避免直接全局查询DOM:
let renderer, scene, camera, shape, mouse, resizeTm, onMouseMove; export function initThreeScene(canvas) { if (!canvas) return; const width = canvas.offsetWidth; const height = canvas.offsetHeight; renderer = new THREE.WebGLRenderer({ canvas: canvas, antialias: true, }); renderer.setPixelRatio(window.devicePixelRatio > 1 ? 2 : 1); renderer.setSize(width, height); renderer.setClearColor(0x161d31); scene = new THREE.Scene(); camera = new THREE.PerspectiveCamera(100, width / height, 0.1, 10000); camera.position.set(120, 0, 300); const light = new THREE.HemisphereLight(0xffffff, 0x0c056d, 0.6); scene.add(light); const directionalLight = new THREE.DirectionalLight(0x590d82, 0.5); directionalLight.position.set(100, 300, 400); scene.add(directionalLight); const light2 = directionalLight.clone(); light2.position.set(-100, 300, 400); scene.add(light2); const geometry = new THREE.IcosahedronGeometry(120, 4); for (let i = 0; i < geometry.vertices.length; i++) { const vector = geometry.vertices[i]; vector._o = vector.clone(); } const material = new THREE.MeshPhongMaterial({ emissive: 0x23f660, emissiveIntensity: 0.4, shininess: 0, }); shape = new THREE.Mesh(geometry, material); scene.add(shape); mouse = new THREE.Vector2(0.8, 0.5); function updateVertices(a) { for (let i = 0; i < geometry.vertices.length; i++) { const vector = geometry.vertices[i]; vector.copy(vector._o); const perlin = noise.simplex3( vector.x * 0.006 + a * 0.0002, vector.y * 0.006 + a * 0.0003, vector.z * 0.006 ); const ratio = perlin * 0.4 * (mouse.y + 0.3) + 0.9; vector.multiplyScalar(ratio); } geometry.verticesNeedUpdate = true; } function render(a) { requestAnimationFrame(render); updateVertices(a); renderer.render(scene, camera); } function onResize() { canvas.style.width = ""; canvas.style.height = ""; const newWidth = canvas.offsetWidth; const newHeight = canvas.offsetHeight; camera.aspect = newWidth / newHeight; camera.updateProjectionMatrix(); renderer.setSize(newWidth, newHeight); } onMouseMove = function(e) { TweenMax.to(mouse, 0.8, { y: e.clientY / height, x: e.clientX / width, ease: Power1.easeOut, }); } requestAnimationFrame(render); window.addEventListener("mousemove", onMouseMove); window.addEventListener("resize", function () { resizeTm = clearTimeout(resizeTm); resizeTm = setTimeout(onResize, 200); }); } export function destroyThreeScene() { // 清理事件监听 if (onMouseMove) { window.removeEventListener("mousemove", onMouseMove); } window.removeEventListener("resize", () => {}); clearTimeout(resizeTm); // 销毁Three.js资源 if (scene && shape) { scene.remove(shape); shape.geometry.dispose(); shape.material.dispose(); } if (renderer) { renderer.dispose(); } }
2. 修改HomeComponent.vue,在DOM渲染完成后初始化Three.js
- 给canvas加
ref属性,用Vue的方式获取DOM元素 - 在
mounted钩子(DOM渲染完成后触发)里调用初始化函数 - 在
beforeUnmount钩子中清理资源,避免内存泄漏
修改后的HomeComponent.vue关键部分:
<template> <!-- 其他代码不变 --> <main> <div class="content"> <!-- 添加ref属性 --> <canvas class="scene scene--full" id="scene" ref="sceneCanvas"></canvas> <!-- 其他代码不变 --> </div> </main> </template> <script> // CSS导入保持不变 import "../assets/assets/css/base.css"; import "../assets/app-assets/css-rtl/bootstrap.css"; import "../assets/app-assets/css-rtl/bootstrap-extended.css"; import "../assets/app-assets/css-rtl/colors.css"; import "../assets/app-assets/css-rtl/themes/dark-layout.css"; import "../assets/assets/css/menu.css"; import "../assets/app-assets/css-rtl/custom-rtl.css"; import "../assets/assets/css/style-rtl.css"; // JS导入调整,导入封装后的函数 import "../assets/assets/js/three.min.js"; import "../assets/assets/js/perlin.js"; import "../assets/assets/js/TweenMax.min.js"; import { initThreeScene, destroyThreeScene } from "../assets/assets/js/demo3.js"; import "../assets/assets/js/grid.js"; import "../assets/assets/js/imagesloaded.pkgd.min.js"; import "../assets/assets/js/anime.min.js"; import "../assets/assets/js/menu2.js"; import "../assets/assets/js/demo.js"; export default { name: "HomeComponent", mounted() { // DOM渲染完成后获取canvas,初始化Three场景 const canvas = this.$refs.sceneCanvas; initThreeScene(canvas); }, beforeUnmount() { // 组件卸载前清理资源 destroyThreeScene(); } }; </script>
3. 确保demo3.js没有全局执行代码
现在demo3.js所有逻辑都在导出的函数里,导入时不会直接执行,只有调用initThreeScene才会启动Three.js场景。
这样修改后,就能保证Three.js代码在DOM元素存在后再执行,解决null错误,同时遵循Vue的组件生命周期规范,避免内存泄漏问题。
内容的提问来源于stack exchange,提问作者Sammy
相关产品推荐
相关产品推荐

