在 UniApp 环境下,Cesium 属于高频操作 DOM 和大量计算的库,而 App 端逻辑层(JSCore/V8)与视图层(WebView)是分离的。为了实现高性能渲染,通常有两种成熟方案。
方案对比
| 维度 | 方案一:WebView 桥接 (H5 混合) | 方案二:renderjs 视图层穿透 |
|---|---|---|
| 适用场景 | 复杂的大型 GIS 系统、已有 H5 项目平移 | 轻量级 GIS、需要原生组件覆盖、对交互流畅度要求极高 |
| 开发难度 | 低(分离开发,通过 URL/PostMessage 交互) | 高(需处理逻辑层与 renderjs 层的频繁通信) |
| 调试体验 | 极佳(可直接在浏览器调试地图逻辑) | 一般(依赖真机或内置浏览器模拟) |
| 通信成本 | 较高(序列化开销大,且单向传输) | 较低(支持事件监听,局部更新快) |
方案一:WebView 桥接模式(工程解耦版)
核心思想: 将 Cesium 独立为一个 H5 项目发布,App 端通过 <web-view> 组件加载。
1. 逻辑层(App 侧)发送指令
由于 WebView 的 src 变化会引起页面刷新,建议采用 URL 哈希(Hash)或查询参数 传递简单指令。
javascript
<web-view
ref="webViewRef"
:webview-styles="webviewStyles"
:style="{ width: '100%'}"
:src="src"
:cache-mode="'default'"
@onPostMessage="handleMessage"
@message="handleMessage">
</web-view>
1. 浏览器想 App 传值:
javascript
uni.postMessage({data:{action:message}});
2. App 接受通信:
javascript
handleMessage(e){
console.log("woshinide1", JSON.stringify(e));
}
3. App 想浏览器传值: 直接把传参写到 src 路径里就行,浏览器解析 URL 就可以获取。
方案 2:通过 RenderJS(重点:恶心玩法来了)
重点来了,最恶心的 RenderJS 玩法来了。
注意: RenderJS 不支持 Vue3 语法糖,还是用 Vue2 玩吧。
1. 静态资源引入
首先在 App 的 static 目录下放入 Cesium 静态文件。

2. 异步加载 Cesium(解决加载阻塞)
然后再 RenderJS 动态引入就行。但由于 Cesium 很大,直接引入会影响生命周期(不是卡死,是它会抢在生命周期引入之前加载)。为此我写了一个异步代码
utils/cesiumLoader.js
javascript
/**
* Cesium 加载器 - 确保全局只有一个加载实例
*/
let loadPromise = null;
export function loadCesium() {
// 如果已经有加载任务在进行了,直接返回之前的 Promise
if (loadPromise) return loadPromise;
loadPromise = new Promise((resolve, reject) => {
// 1. 如果 window 中已经有了,直接 resolve
if (typeof window !== 'undefined' && window.Cesium) {
resolve(window.Cesium);
return;
}
// 2. 动态创建 CSS
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = './static/Cesium/Widgets/widgets.css';
document.head.appendChild(link);
// 3. 动态创建 Script
const script = document.createElement('script');
script.type = 'text/javascript';
script.src = './static/Cesium/Cesium.js';
script.onload = () => {
console.log('Cesium 库加载成功');
resolve(window.Cesium);
};
script.onerror = (err) => {
loadPromise = null; // 加载失败清除状态,允许重试
console.error('Cesium 脚本加载失败');
reject(err);
};
document.head.appendChild(script);
});
return loadPromise;
}
在 App.vue 中预加载
javascript
import { loadCesium } from '@/utils/cesiumLoader';
export default {
onLaunch: function() {
// 预加载一遍
loadCesium();
}
};
3. 开始恶心玩法:通信问题
这一步还是最简单玩法。开始恶心玩法通信问题:由于 RenderJS 逻辑层和视图层分离,想在视图层渲染,需要从逻辑层穿透到视图层调用,每个方法相当于对外开发了一个接口,用方法就是调接口,麻烦得很。
我尝试过通过 Bus.js 通信等等,发现都无法将 viewer 抛出。查看官方文档发现一个非常恶心的玩法:
子组件 map.vue 完整代码:
javascript
<template>
<view class="content">
<view
id="viewMapContainer"
:action="action"
:change:action="cesium.handleAction"
></view>
</view>
</template>
<script>
export default {
// 逻辑层必须声明 props,模板才能识别
props: {
action: {
type: Object,
default: () => ({}),
},
},
methods: {
onMapSuccess(res) {
if (res.type === 'EXPORT_IMAGE') {
// 处理回传的图片数据
}
}
},
};
</script>
<script module="cesium" lang="renderjs">
import { loadCesium } from '@/utils/cesiumLoader';
let viewer = null; // 局部变量持久化 viewer
export default {
props: ['action'],
async mounted() {
await loadCesium();
// 初始化 viewer 的逻辑写在这里...
},
methods: {
handleAction(newValue) {
if (!newValue || !viewer) return;
// 通过监听 action 的变化来模拟方法调用
if (newValue.type === 'export') {
this.exportImage();
}
},
exportImage() {
viewer.render();
const canvas = viewer.scene.canvas;
const base64Data = canvas.toDataURL('image/png');
// 将数据传回逻辑层(普通 script)
this.$ownerInstance.callMethod('onMapSuccess', {
type: 'EXPORT_IMAGE',
data: base64Data
});
}
}
};
</script>
<style>
.content {
position: relative;
width: 100vw;
height: 100vh;
}
#viewMapContainer {
position: absolute;
left: 0;
top: 0;
width: 100vw;
height: 100vh;
z-index: 1;
}
</style>
父组件 index.vue 完整代码:
javascript
<template>
<view class="content">
<mapVue ref="mapRef" :action="currentAction" />
<view @click="exportImage">
</view>
</template>
<script module="index">
export default {
data() {
return {
currentAction: {},
};
},
components: {
mapVue,
},
async mounted() {
},
methods: {
//导出图
async exportImage() {
this.currentAction = {
type: 'export',
data: {},
_t: Date.now()
};
},
},
};
</script>