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

使用Qwik与Three.js加载3D模型时遇Unexpected Identifier $错误求助

解决Qwik+Three.js Web Worker中"Unexpected Identifier $"错误

我用Qwik和Three.js写了一段加载3D模型的代码,运行时抛出"Unexpected Identifier $"错误,怀疑和Web Worker的作用域有关,代码如下:

import { component$, useOnDocument, $ } from '@builder.io/qwik';
import * as THREE from 'three';
import type { GLTF } from 'three/examples/jsm/loaders/GLTFLoader.js';

export const Robot = component$(() => {
  useOnDocument('qinit', $(() => {

... (other code)

    async function loadModel() {
      const threeJS = await fetchScript('https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js');
      const gltfLoaderJS = await fetchScript('https://cdn.jsdelivr.net/gh/mrdoob/three.js@r128/examples/jsm/loaders/GLTFLoader.js');

      const workerCode = `
        const threeJS = \`${threeJS}\`;
        const gltfLoaderJS = \`${gltfLoaderJS}\`;

        eval(threeJS);
        eval(gltfLoaderJS);

        const loader = new THREE.GLTFLoader();

        self.onmessage = function(event) {
          const { url } = event.data;
          loader.load(url, function(gltf) {
            self.postMessage({ type: 'model', data: gltf });
          }, undefined, function(error) {
            self.postMessage({ type: 'error', data: error });
          });
        };
      `;

      const blob = new Blob([workerCode], { type: 'application/javascript' });
      const worker = new Worker(URL.createObjectURL(blob));
      worker.postMessage({ url: '/bane10-20.gltf' });

      worker.onmessage = function(event: MessageEvent) {
        const { type, data } = event.data;

        if (type === 'model') {
          const gltf: GLTF = data;
          const model = gltf.scene;

          model.traverse((object: THREE.Object3D) => {
            if ((object as THREE.Mesh).isMesh) {
              const mesh = object as THREE.Mesh;
              if (!mesh.material) {
                mesh.material = new THREE.MeshStandardMaterial({ color: 0x555555 });
              }
            }
          });

          scene.add(model);
          document.body.removeChild(loadingDiv);
        } else if (type === 'error') {
          console.error('An error occurred while loading the GLTF model:', data);
          document.body.removeChild(loadingDiv);
        }
      };
    }

    function animate() {
      if (renderer) {
        frameId = requestAnimationFrame(animate);
        renderer.render(scene, camera);
      }
    }

    function onResize() {
      if (renderer && camera) {
        const width = container.clientWidth;
        const height = container.clientHeight;
        renderer.setSize(width, height);
        camera.aspect = width / height;
        camera.updateProjectionMatrix();
      }
    }

    window.addEventListener('resize', onResize);

    init();

    return () => {
      if (frameId) cancelAnimationFrame(frameId);
      if (renderer) {
        renderer.dispose();
        if (renderer.domElement.parentElement) {
          renderer.domElement.parentElement.removeChild(renderer.domElement);
        }
      }
      document.body.removeChild(loadingDiv);
      window.removeEventListener('resize', onResize);
    };
  }));

  return <div></div>;
});

错误原因

这个错误的核心是模板字符串解析冲突:你通过模板字符串把fetch到的Three.js压缩代码插入到Worker脚本中,但Three.js压缩后的代码包含大量$开头的变量(比如$a、$b),这些$在模板字符串中会被当成模板变量标识符,导致浏览器解析Worker代码时认为这些$是未定义的变量,从而抛出语法错误。

另外,直接在Worker中eval整个Three.js脚本也不是最佳实践,不仅有安全风险,还容易引发作用域问题。


解决方案

改用Worker原生的importScripts方法加载Three.js和GLTFLoader,避免模板字符串解析的问题,同时简化代码逻辑:

  1. 移除fetch脚本并插入模板字符串的逻辑,直接在Worker内部用importScripts加载CDN上的依赖。
  2. 解决postMessage无法传递Three.js复杂对象的问题,提取可序列化的模型数据在主线程重建。

修改后的代码:

import { component$, useOnDocument, $ } from '@builder.io/qwik';
import * as THREE from 'three';
import type { GLTF } from 'three/examples/jsm/loaders/GLTFLoader.js';

export const Robot = component$(() => {
  useOnDocument('qinit', $(() => {
    // 假设init函数、scene、camera、renderer、container、loadingDiv、frameId已定义
    ... (other code)

    function loadModel() {
      const workerCode = `
        // 在Worker中直接加载依赖脚本
        importScripts(
          'https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js',
          'https://cdn.jsdelivr.net/gh/mrdoob/three.js@r128/examples/jsm/loaders/GLTFLoader.js'
        );

        const loader = new THREE.GLTFLoader();

        self.onmessage = function(event) {
          const { url } = event.data;
          loader.load(url, function(gltf) {
            // 提取可序列化的模型数据(无法直接传递THREE对象)
            const modelData = {
              geometries: gltf.scene.children.map(child => {
                if (child.isMesh) {
                  return {
                    type: child.type,
                    geometry: child.geometry.toJSON(),
                    material: child.material.toJSON()
                  };
                }
                return null;
              }).filter(Boolean)
            };
            self.postMessage({ type: 'model', data: modelData });
          }, undefined, function(error) {
            self.postMessage({ type: 'error', data: error.message });
          });
        };
      `;

      const blob = new Blob([workerCode], { type: 'application/javascript' });
      const worker = new Worker(URL.createObjectURL(blob));
      worker.postMessage({ url: '/bane10-20.gltf' });

      worker.onmessage = function(event: MessageEvent) {
        const { type, data } = event.data;

        if (type === 'model') {
          // 在主线程重建Three.js模型
          const model = new THREE.Group();
          data.geometries.forEach(item => {
            const geometry = THREE.BufferGeometry.fromJSON(item.geometry);
            const material = THREE.MeshStandardMaterial.fromJSON(item.material);
            const mesh = new THREE.Mesh(geometry, material);
            model.add(mesh);
          });

          scene.add(model);
          document.body.removeChild(loadingDiv);
        } else if (type === 'error') {
          console.error('加载GLTF模型出错:', data);
          document.body.removeChild(loadingDiv);
        }
      };
    }

    function animate() {
      if (renderer) {
        frameId = requestAnimationFrame(animate);
        renderer.render(scene, camera);
      }
    }

    function onResize() {
      if (renderer && camera) {
        const width = container.clientWidth;
        const height = container.clientHeight;
        renderer.setSize(width, height);
        camera.aspect = width / height;
        camera.updateProjectionMatrix();
      }
    }

    window.addEventListener('resize', onResize);

    init();

    return () => {
      if (frameId) cancelAnimationFrame(frameId);
      if (renderer) {
        renderer.dispose();
        renderer.domElement.parentElement?.removeChild(renderer.domElement);
      }
      document.body.removeChild(loadingDiv);
      window.removeEventListener('resize', onResize);
    };
  }));

  return <div></div>;
});

注意事项

  • GLTF对象传递限制:postMessage无法直接传递Three.js的复杂对象(如THREE.Scene、THREE.Mesh),因为它们包含不可序列化的WebGL资源,必须提取JSON格式的几何体和材质数据在主线程重建。
  • Qwik的$函数使用:useOnDocument的回调用$包裹是正确的,Qwik需要通过$标记可序列化的函数,确保 hydration 时正确执行。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.20 13:03:19