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
相关产品推荐
相关产品推荐

