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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.04 08:36:25