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

Next.js 14集成Three.js遇ReferenceError: document未定义求助

问题原因与修复方案

触发错误的核心原因

你代码里的THREE.Scene、WebGLRenderer、地球模型等对象,全是在组件函数外部的全局作用域创建的。哪怕加了"use client",Next.js 14处理Client Component时,模块初始化阶段仍会在服务器端执行部分代码,这时候document对象根本不存在——而new THREE.WebGLRenderer()会直接尝试创建canvas元素,必然触发ReferenceError。

另外你在全局定义的animate函数,虽然没立刻执行,但创建renderer的步骤已经在服务器端运行,这就是报错的根源。

修复逻辑

把所有依赖浏览器环境的Three.js逻辑,全部移到组件内部的useEffect钩子中(useEffect的回调只会在客户端执行),确保只有在浏览器环境下才初始化Three.js相关对象。

修改后的完整代码

"use client";
export const dynamic = "auto";
import { ISSPosition, fetchISSPosition } from "@/lib/api";
import { convertCoordsToSphere } from "@/lib/functions";
import React, { useRef, useEffect, useState } from "react";
import * as THREE from "three";

const ThreeScene = () => {
  const [issPosition, setIssPosition] = useState<ISSPosition>();
  const containerRef = useRef<HTMLDivElement>(null);
  // 用useRef存储Three.js核心对象,避免组件重渲染重复创建
  const sceneRef = useRef<THREE.Scene | null>(null);
  const rendererRef = useRef<THREE.WebGLRenderer | null>(null);
  const earthRef = useRef<THREE.Mesh | null>(null);
  const cameraRef = useRef<THREE.PerspectiveCamera | null>(null);
  const issPointRef = useRef<THREE.Mesh | null>(null);
  const animationIdRef = useRef<number | null>(null);

  useEffect(() => {
    // 仅在客户端执行初始化逻辑
    if (typeof window === "undefined") return;

    const earthRadius = 10;
    // 初始化场景
    const scene = new THREE.Scene();
    sceneRef.current = scene;

    // 补全你的雾效设置代码
    // ...

    // 初始化相机
    const camera = new THREE.PerspectiveCamera(75, 1, 0.1, 1000);
    camera.position.set(0, 0, 30);
    cameraRef.current = camera;

    // 初始化渲染器(此时document已存在,不会报错)
    const renderer = new THREE.WebGLRenderer({
      alpha: true,
      antialias: true,
    });
    rendererRef.current = renderer;

    // 补全你的灯光设置代码
    // ...

    // 创建地球模型
    const geometry = new THREE.SphereGeometry(earthRadius, 64, 64);
    const earthMat = new THREE.MeshPhongMaterial({
      shininess: 350,
      map: new THREE.TextureLoader().load("/earth_texture.jpg"),
      bumpMap: new THREE.TextureLoader().load("/earth_texture.jpg"),
    });
    const earth = new THREE.Mesh(geometry, earthMat);
    earth.receiveShadow = true;
    earth.rotation.x = 0.3;
    scene.add(earth);
    earthRef.current = earth;

    // 补全你的ISS模型初始化代码,创建后赋值给issPointRef.current
    // ...

    // 将渲染器挂载到DOM
    containerRef.current?.appendChild(renderer.domElement);

    // 动画循环
    const animate = () => {
      animationIdRef.current = requestAnimationFrame(animate);
      if (earthRef.current) {
        earthRef.current.rotation.y -= 0.001;
      }
      if (rendererRef.current && sceneRef.current && cameraRef.current) {
        rendererRef.current.render(sceneRef.current, cameraRef.current);
      }
    };
    animate();

    // 窗口resize处理
    const handleResize = () => {
      if (rendererRef.current) {
        let size = window.innerWidth * 0.8 > 800 ? 800 : window.innerWidth * 0.8;
        rendererRef.current.setSize(size, size);
        if (cameraRef.current) {
          cameraRef.current.aspect = 1;
          cameraRef.current.updateProjectionMatrix();
        }
      }
    };
    handleResize();
    window.addEventListener("resize", handleResize);

    // 清理函数,避免内存泄漏
    return () => {
      if (animationIdRef.current) {
        cancelAnimationFrame(animationIdRef.current);
      }
      if (rendererRef.current) {
        rendererRef.current.dispose();
      }
      window.removeEventListener("resize", handleResize);
    };
  }, []);

  useEffect(() => {
    if (!issPosition || !sceneRef.current || !earthRef.current || !issPointRef.current) return;

    const updateIssPosition = () => {
      const pointCoords = convertCoordsToSphere(
        Number(issPosition.iss_position.latitude),
        -Number(issPosition.iss_position.longitude),
        10
      );
      issPointRef.current.position.copy(pointCoords.applyMatrix4(earthRef.current.matrix));
      // 避免重复添加ISS模型到场景
      if (!sceneRef.current.children.includes(issPointRef.current)) {
        sceneRef.current.add(issPointRef.current);
      }
    };
    updateIssPosition();
  }, [issPosition]);

  // 定时获取ISS位置
  useEffect(() => {
    const fetchISSData = async () => {
      setIssPosition(await fetchISSPosition());
    };
    const intervalId = setInterval(fetchISSData, 5000);
    return () => clearInterval(intervalId);
  }, []);

  if (typeof window === "undefined") return null;

  return (
    <>
      <div className="m-auto max-w-fit" ref={containerRef}></div>
      {issPosition ? (
        <div className="flex flex-col gap-2 mb-8 w-fit justify-center items-center mx-auto">
          <span>Latitude: {issPosition.iss_position.latitude}</span>
          <span>Longitude: {issPosition.iss_position.longitude}</span>
        </div>
      ) : (
        <div className="flex gap-4 justify-center">
          <p className="mb-4">Locating ISS</p>
          <i className="border-b- border-primary w-4 h-4 rounded-full animate-spin"></i>
        </div>
      )}
    </>
  );
};

export default ThreeScene;

额外注意事项

  • 用useRef存储Three.js核心对象,防止组件重渲染时重复创建实例
  • 动画循环记得用cancelAnimationFrame清理,避免内存泄漏
  • 窗口resize时要更新相机的aspect并调用updateProjectionMatrix,保证画面比例正确

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.27 22:08:17