Vue Scoped CSS 与动态创建 DOM 的兼容性问题
背景
在一个支持 2D (Canvas) 和 3D (Unity WebGL) 地图切换的管理后台页面中,需要两个地图共享同一个视口容器,底部/右侧操作面板保持不变。
切换机制设计为:每次切换时销毁当前视图,渲染目标视图。3D Unity 容器采用 CSS 隐藏而非 DOM 移除,以保持 WebGL context 不丢失。
问题现象
初次加载 3D 视图时,Unity canvas 始终显示为 300×150 (canvas 默认尺寸),容器高度只有 172px ,而非视口高度 869px。CSS 中明明设置了:
css
#unity-container {
position: absolute;
inset: 0;
}
#unity-canvas {
width: 100%;
height: 100%;
}
但这些样式完全没有生效。
根因分析
Vue 的 <style scoped> 在编译时会为所有选择器添加 [data-v-xxxxx] 属性选择器。编译后实际生成的 CSS 类似:
css
#unity-container[data-v-xxxxx] {
position: absolute;
inset: 0;
}
问题在于:#unity-container 是通过 document.createElement('div') 动态创建、再通过 viewport.appendChild() 插入 DOM 的。Vue 不会为这些动态创建的元素添加 data-v-xxxxx 属性,因此选择器匹配失败,样式不生效。
复现条件
只要满足以下所有条件就会触发:
- 组件使用
<style scoped> - DOM 元素通过
document.createElement()创建 - 样式规则定义在同组件的
<style scoped>中 - 样式依赖于 CSS class 或 id 选择器(而非仅 inline style)
解决方案
方案 A:将元素放入 template(推荐)
如果元素在组件生命周期内稳定存在(即使初始隐藏),应直接在 <template> 中声明。Vue 在编译模板时会为这些元素添加 data-v-xxxxx,scoped CSS 正常匹配。
vue
<template>
<div ref="viewportRef" class="map-viewport">
<div id="unity-container" class="unity-hidden">
<canvas id="unity-canvas"></canvas>
</div>
</div>
</template>
typescript
// 切换逻辑:纯 CSS class 控制显示/隐藏
function switchTo3D() {
unityContainer.classList.remove('unity-hidden');
}
function switchTo2D() {
unityContainer.classList.add('unity-hidden');
}
方案 B:动态元素使用 inline style(备选)
当元素必须动态创建(如某些第三方库的 canvas)时,通过 style.cssText 直接写入样式:
typescript
const canvas = document.createElement('canvas');
canvas.className = 'canvas-layer';
canvas.style.cssText = 'position:absolute;top:0;left:0;z-index:1';
这样不依赖选择器匹配,scoped CSS 的 [data-v-xxxxx] 限制不构成障碍。
延伸:Unity WebGL Context 保持
在 2D/3D 切换场景中还需处理另一个问题:Unity WebGL 的 rendering context 绑定在 <canvas> DOM 元素上。从 DOM 中移除再重新添加会导致 WebGL context 永久丢失,Unity 实例崩溃且无法恢复。
因此 3D 容器的隐藏不能使用 DOM 移除,必须使用 CSS off-screen 定位:
css
#unity-container {
position: absolute;
inset: 0; /* 3D 模式:填充视口 */
}
#unity-container.unity-hidden {
position: absolute;
top: -9999px;
left: -9999px;
width: 1px;
height: 1px;
overflow: hidden;
pointer-events: none;
}
总结
| 问题 | 原因 | 解决方案 |
|---|---|---|
| Scoped CSS 不生效 | 动态元素缺少 data-v-xxxxx 属性 |
Template 预置 / inline style |
| WebGL context 丢失 | 从 DOM 移除 canvas | CSS off-screen 隐藏 |
两条原则:
- Template 优先 :只要元素生命周期稳定,就放在
<template>中 - 不拆不丢:Unity/WebGL 容器不拆出 DOM,避免 context 丢失
相关资源
Vue SFC Scoped CSS 文档:https://vuejs.org/api/sfc-css-features.html#scoped-css
Unity WebGL 文档 - 应用挂起 https://docs.unity3d.com/Manual/webgl-suspend.html