VR/AR开发实战中WebXR与Unity引擎的关键技术对比与选型建议
我的写法,亲测靠谱
VR/AR 在前端里真不是炫技玩具——去年我接手一个房产展厅项目,客户要求用 WebXR 做轻量级样板间漫游,不装 App、不扫码、点开即进。结果上线前一周,iOS Safari 上黑屏、安卓 Chrome 里模型抖动、用户戴 VR 头显后手柄追踪失灵……折腾了四天,最后发现:90% 的问题不是 XR API 本身难,而是我们没把“Web 环境的脆弱性”当回事。
我现在的做法很土,但稳定:所有 XR 逻辑都包裹在 try/catch + 状态兜底 + 用户可退 三层保险里。核心代码就这几行,放在 useXRSession 自定义 Hook 里(React):
function useXRSession() {
const [isInXR, setIsInXR] = useState(false);
const [xrSession, setXrSession] = useState(null);
const enterXR = async () => {
if (!navigator.xr) return;
try {
// 关键:加 'immersive-ar' 或 'immersive-vr',别只写 'ar'
const session = await navigator.xr.requestSession('immersive-ar', {
requiredFeatures: ['local-floor', 'dom-overlay'],
optionalFeatures: ['hand-tracking', 'light-estimation'],
// ⚠️ 注意:这里必须传对象,不能传空数组或 undefined
});
// 必须立刻 attach 到 canvas,否则 iOS 直接报错
const canvas = document.querySelector('#xr-canvas');
if (canvas && session.renderState) {
session.renderState.baseLayer = new XRWebGLLayer(session, gl);
}
setXrSession(session);
setIsInXR(true);
// 退出监听,别漏掉!
session.addEventListener('end', () => {
setIsInXR(false);
setXrSession(null);
});
// 错误兜底:session 被系统强制终止时触发(比如切后台太久)
session.addEventListener('select', handleSelect);
} catch (err) {
console.warn('XR 启动失败', err.name, err.message);
// 这里我直接降级到 3D 模式(Three.js 全景图),用户无感
fallbackTo3DView();
}
};
return { isInXR, xrSession, enterXR };
}
为什么这样写?因为 WebXR 不是“启动成功就一劳永逸”的东西。我踩过最惨的一次:没监听 end 事件,用户切微信再切回来,session 已失效,但页面还显示“已进入 VR”,点击没反应,客服电话被打爆……
这几种错误写法,别再踩坑了
以下是我团队里新人常写的几种“看着能跑,上线就崩”的写法,列出来省得你再花一天 debug:
- 错误 1:requestSession 不传参数,或者只传字符串
❌navigator.xr.requestSession('ar')
✅ 正确必须传完整配置对象,哪怕空:navigator.xr.requestSession('immersive-ar', {})。iOS Safari 对参数校验极严,漏一个 key 就拒绝。 - 错误 2:Canvas 没提前设好尺寸,或用了 CSS 缩放
❌<canvas style="width: 100%; height: 100vh; transform: scale(0.8)"></canvas>
✅ XR 渲染层依赖 canvas 的width/height属性值(不是 CSS),且必须是整数。我一律用 JS 动态设置:canvas.width = window.innerWidth; canvas.height = window.innerHeight;,并监听 resize 重置。 - 错误 3:在 XR session 运行中直接操作 DOM 样式
❌ 给按钮加display: none或改z-index—— 在部分 Android 设备上会触发 XR session 异常中断。
✅ 所有 UI 控制走dom-overlay配置项 +document.body.appendChild(overlayDiv),overlay 内容用position: absolute定位,不碰 body 结构。 - 错误 4:手柄姿态数据没做空值保护
❌frame.getPose(inputSource.gripSpace, refSpace).transform.matrix直接取矩阵,但 gripSpace 在刚连接手柄时可能为 null。
✅ 我加了一层判断:if (!inputSource.gripSpace || !frame.getPose(...)) return;,不然 Three.js 会报Invalid matrix并卡死。
实际项目中的坑
这些不是文档里写的,是我在 jztheme.com 的房产项目里被逼出来的:
1. iOS 上的“首次授权弹窗”陷阱
苹果要求 AR 必须由用户手势触发(如点击按钮),不能 onload 自动 requestSession。但我们有个自动播放引导动画的需求——结果用户还没点,动画播完,按钮消失了,用户不知道要干嘛。解决办法:动画结束后,按钮用 opacity: 0.01 + pointer-events: auto 保持可点击,视觉上“消失”,但依然能响应 touchstart。
2. 模型加载和 XR 启动不能串行
曾有人写:先等 GLB 加载完,再调 requestSession —— 结果用户点了按钮,转圈 3 秒没反应,以为卡了。现在我改成:点击即 requestSession,同时并行加载模型;session ready 后,用 session.updateRenderState({ baseLayer: layer }) 接管渲染,模型加载完成再挂载。用户感知就是“点一下,秒进”。
3. 光照估计在低端机上大概率失败light-estimation feature 在千元机上返回的 ambientIntensity 常是 0。我直接 fallback 到预设亮度值,并加个开关让用户手动调:“环境光太暗?点这里提亮”。比硬报错友好太多。
4. 安卓 WebView 的兼容性黑洞
某些厂商定制 WebView(比如华为某型号)根本不支持 navigator.xr,但又不抛错,typeof navigator.xr === 'undefined' 返回 false。最终靠 UA + 特征探测组合判断:!!navigator.xr && 'requestSession' in navigator.xr,再加一层 try { await navigator.xr.requestSession('inline') } catch 实锤。
最后一点实在建议
别追求“全平台完美支持”。我们上线时明确标注:“推荐使用 Chrome for Android 或 Safari for iOS”,其他平台降级为 360° 全景图。用户投诉少了 70%,开发时间省下一半。技术不是越全越好,而是让最多人用得顺。
以上是我踩坑后的总结,希望对你有帮助。这个技巧的拓展用法还有很多,比如如何用 WebXR 做多人协同标注、怎么压缩 GLB 保证首帧不卡顿……后续会继续分享这类博客。有更好的方案欢迎评论区交流。

暂无评论