使用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,避免模板字符串解析的问题,同时简化代码逻辑:
- 移除fetch脚本并插入模板字符串的逻辑,直接在Worker内部用
importScripts加载CDN上的依赖。 - 解决
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
相关产品推荐
相关产品推荐

