简介:面向需要在Vue3项目中快速集成Cesium.js的开发者,这份开箱即用的Vite+Vue3+TypeScript+Cesium.js集成模板省去了手动配置构建工具、类型系统与三维地球环境的繁琐步骤。模板包含16个文件、压缩后仅21KB,其中4个json负责依赖与项目配置,3个ts承载类型声明和主要逻辑,2个vue文件对应应用入口与示例组件,另有样式、图标及README等辅助文件,整体目录结构紧凑清晰。配套示例演示了如何在Vue3组件中调用Cesium.js API创建基本地球场景,并实现视角切换等基础交互;同时预置axios、element-plus、vuex等常用依赖,方便后续扩展业务功能。内容预览中的.vscode配置与pnpm-lock.yaml也体现了对开发环境一致性的考虑。目前已有2686人学习/下载,对于想快速启动Cesium相关项目或参考工程化实践的开发者而言,是一个实用起点。
1. 为什么 Vite+Vue3+TypeScript+Cesium.js 集成模板总是卡在“第一步”
做某跨平台系统的三维地图模块时,我把整个下午耗在 Vite+Vue3+TypeScript+Cesium.js 这套集成模板的环境上。Vite 默认按模块拆包,Cesium 却要求在静态目录里能找到 Workers、Assets、Widgets;TypeScript 想给全局变量和模块声明类型,Cesium 的类型文件又和 Vite 的 alias 解析经常对不上。这份模板把这些问题预先踩平:拿下来装完依赖,createMap 一调用就能见三维球体,不用手动调构建配置。适合准备做三维 GIS 场景的 Vue 前端,也适合团队内部拿它当底座,只在上层堆业务。
2. 工程结构选型:Vite+Vue3+TS+Cesium 的组合边界
模板表面看只有几个文件,实际上它替你在三类边界上做了选择:Cesium 实例和 Vue 响应式系统之间的关系、TypeScript 类型该从哪来、Vite 构建管不住的那部分静态资源怎么安放。先把这三件事想清楚,后面复现时才不会变成纯抄代码。
2.1 目录划分:Cesium 实例必须活在 Vue 响应式系统之外
Cesium 的 Viewer 是一个“重对象”:内部有相机、场景、图层、WebGL 上下文,对象层级非常深。模板没有把 viewer 定义在组件 data 里,而是放在 src/utils/cesium.ts 里,用普通函数创建并返回,组件只负责容器和生命周期。
src/ ├── utils/ │ └── cesium.ts ├── components/ │ └── MapViewer.vue └── views/ └── MapPage.vue// src/utils/cesium.ts import * as Cesium from 'cesium' export function createMap(container: HTMLElement): Cesium.Viewer { const viewer = new Cesium.Viewer(container, { animation: false, baseLayerPicker: false, geocoder: false, timeline: false, infoBox: false, sceneMode: Cesium.SceneMode.SCENE3D, shouldAnimate: true }) return viewer }这段代码里,animation、baseLayerPicker、geocoder、timeline 全部关掉,是因为业务型模板一般自带控制和面板,不需要 Cesium 右上角那一排默认控件。sceneMode 显式写成 SCENE3D,避免某些版本把 2D/3D 切换按钮藏着掖着;shouldAnimate 保持 true,后面接动态轨迹、粒子效果时不需要改初始配置。
我见过很多人把 viewer 直接塞进 reactive 或 ref,页面最初能出球,拖拽几次就报一些完全不像 Cesium 该报的错误。原因是 Vue 会把对象包一层 Proxy,Cesium 内部用了大量 Object.defineProperty、私有属性、原生对象判等,代理之后属性查找顺序和 this 绑定都会变。模板的约定很简单:组件里用普通 let 保存实例,只有容器 div 交给 Vue 的 ref 管理。
<!-- src/components/MapViewer.vue --> <script setup lang="ts"> import { onMounted, onBeforeUnmount, ref } from 'vue' import * as Cesium from 'cesium' import { createMap } from '@/utils/cesium' const mapEl = ref<HTMLElement | null>(null) let viewer: Cesium.Viewer | null = null onMounted(() => { if (mapEl.value) { viewer = createMap(mapEl.value) } }) onBeforeUnmount(() => { viewer?.destroy() viewer = null }) </script> <template> <div ref="mapEl" class="map-container" /> </template> <style scoped> .map-container { width: 100%; height: 100vh; } </style>这里 onMounted 创建实例,onBeforeUnmount 里销毁并置空,是模板的第一条生命周期纪律。容器高度写成 100vh 而不是 auto,是因为 Cesium 在 0 高度的容器里会创建一个 0x0 的 canvas,页面看似在跑,实际黑屏,这是最隐蔽的一个坑。
2.2 类型声明:别装 @types/cesium,直接用包内自带声明
旧教程经常让人额外装一套 @types/cesium,那套类型停留在 1.62 时代,很多新 API 缺失。Cesium 从较早版本开始就自带 index.d.ts,模板里只需要保证 TypeScript 模块解析能读到。
模板中 tsconfig.json 的关键配置是这样:
{ "compilerOptions": { "target": "ES2020", "moduleResolution": "bundler", "strict": true, "types": ["vite/client"], "paths": { "@/*": ["./src/*"] } } }moduleResolution 设为 bundler,是给 Vite 场景用的;types 只保留 vite/client,避免把其他全局类型卷进来。Cesium 的导入方式用import * as Cesium from 'cesium',运行时拿的是完整的命名空间,编译时拿的也是包内声明文件。不要用import Cesium from 'cesium',Cesium 的导出形态会让 default 导入在某些场景下得到 undefined。
再补一个 src/env.d.ts:
/// <reference types="vite/client" /> declare const CESIUM_BASE_URL: stringCESIUM_BASE_URL 是后面在 vite.config.ts 里用 define 注入的全局常量。如果不做 declare,TypeScript 会在使用这个变量的文件里报“找不到名称”。这行声明就是告诉编辑器:这个全局存在,类型是字符串。
2.3 静态资源边界:Vite 打包管不住 Cesium 的 Worker
Cesium 不是纯 JS 模块,它运行时要动态加载 Workers、Assets、ThirdParty、Widgets 这些目录。Vite 的依赖预构建和打包器没法替你猜 worker 的文件路径,所以模板必须显式复制这些目录。
node_modules/cesium/Build/Cesium/ ├── Assets/ ├── ThirdParty/ ├── Widgets/ ├── Workers/ └── Cesium.js主入口 Cesium.js 可以交给 npm 模块解析,但其余四个目录要原样搬到可访问的静态路径下。模板里用的做法是 vite-plugin-static-copy,在 dev 和 build 两个阶段都会把文件复制到输出目录:
// vite.config.ts import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' import { viteStaticCopy } from 'vite-plugin-static-copy' export default defineConfig({ plugins: [ vue(), viteStaticCopy({ targets: [ { src: 'node_modules/cesium/Build/Cesium/Assets/**', dest: 'cesium/Assets' }, { src: 'node_modules/cesium/Build/Cesium/ThirdParty/**', dest: 'cesium/ThirdParty' }, { src: 'node_modules/cesium/Build/Cesium/Workers/**', dest: 'cesium/Workers' }, { src: 'node_modules/cesium/Build/Cesium/Widgets/**', dest: 'cesium/Widgets' } ] }) ], define: { CESIUM_BASE_URL: JSON.stringify('/cesium') } })这段配置的 dest 不是磁盘路径,而是 URL 路径,也就是说 dev server 上直接访问 /cesium/Workers/xxx.js 就能命中复制后的文件。define 里的 CESIUM_BASE_URL 会在构建时把所有出现该变量的地方替换成 '/cesium',Cesium 内部就用这个值拼接 worker 和纹理地址。
为什么模板不用 CDN?因为三维地图业务经常要部署到内网,Cesium 运行时还会按环境去加载大量字体、地形、影像,CDN 方案一旦离线就是白屏。npm 包内置,资源随构建产物走,是最不容易翻车的部署方式。另一个常见做法是把这些目录丢进 public,但那样每次升级 Cesium 都要手动同步,模板里用插件自动复制就是省这一步。
3. 复现模板:从空目录跑出第一个三维场景
这一章把模板的完整复现路径拆开:新建工程、锁依赖、配静态复制、挂组件、加验证点。跟着走一遍,你拿到的不是黑匣子,而是能改能拆的骨架。
3.1 初始化工程与依赖锁定
先创建 Vue + TypeScript 工程:
npm create vite@latest map-starter -- --template vue-ts cd map-starter npm install npm install cesium装完之后不要立刻升级依赖。Cesium 的大版本升级有时会调整 Build 目录内部结构、改名 worker 文件、改默认光照参数,模板锁的版本是已经测过的一套。实际操作里我一般会保留 package-lock.json,把 cesium 固定在当前这个大版本的小版本区间,等模板跑通后再选时间统一升级。
接着在 main.ts 引入 widgets.css:
import 'cesium/Build/Cesium/Widgets/widgets.css' import { createApp } from 'vue' import App from './App.vue' createApp(App).mount('#app')这行 CSS 不引入,Viewer 创建时控件会全部错位,导航按钮挤在左上角,时间轴没有底纹,页面样式一团乱。模板里把它放在 main.ts 是因为全局只需要一份。
3.2 配置 Vite 的环境变量与资源复制
复制第 2.3 节那段 vite.config.ts 到工程根目录。这里重点解释两个容易改错的地方。
第一个是 dest 路径。vite-plugin-static-copy 的 dest 相对于 outDir,默认是 dist 根目录,所以目标路径 URL 就是 '/cesium/...'。如果你把项目部署在 /gis/ 子路径下,不能继续写死 '/cesium',应该用服务端实际暴露的路径前缀,否则 build 之后资源请求全部 404。
第二个是 define 的行为。define 是文本替换,不是运行时变量。CESIUM_BASE_URL 写到 JSON.stringify('/cesium'),值必须带引号,如果写成 define: { CESIUM_BASE_URL: '/cesium' },替换后代码里会出现裸字符串,直接语法报错。
3.3 用 Vue 生命周期管理 Viewer
组件代码和 2.1 节基本一致,这里再展开一点处理策略:
<script setup lang="ts"> import { onMounted, onBeforeUnmount, ref } from 'vue' import * as Cesium from 'cesium' import { createMap } from '@/utils/cesium' const mapEl = ref<HTMLElement | null>(null) let viewer: Cesium.Viewer | null = null function initMap() { if (!mapEl.value) return viewer = createMap(mapEl.value) viewer.scene.globe.baseColor = Cesium.Color.fromCssColorString('#16222e') } onMounted(initMap) onBeforeUnmount(() => { viewer?.destroy() viewer = null }) </script> <template> <div ref="mapEl" class="map-container" /> </template> <style scoped> .map-container { width: 100%; height: 100vh; position: relative; } </style>initMap 作为普通函数而不是 onMounted 内联函数,好处是后续要增加参数、初始化图层时不需要重排生命周期代码。viewer.destroy() 会把 WebGL 上下文、事件监听器、实体集合一并清理,只把这个实例置空还不够,必须调用 destroy。
position: relative 是给以后叠加图层面板、点击信息弹窗准备的,Cesium 的 canvas 绝对定位后,业务浮层才能正确盖在上层。
3.4 基础验证:加一个点,确认坐标系通
模板跑通后不能只看有没有球,要加一个业务实体验证坐标转换链路:
const position = Cesium.Cartesian3.fromDegrees(116.39, 39.9, 200) viewer.entities.add({ position, point: { pixelSize: 10, color: Cesium.Color.RED }, label: { text: '模板验证点', font: '14px sans-serif', pixelOffset: new Cesium.Cartesian2(0, -18) } }) viewer.zoomTo(viewer.entities)Cartesian3.fromDegrees 接收经度、纬度、高度,单位是度,不是弧度。pixelSize 是像素直径,color 需要 Cesium.Color 类型,不能直接传 CSS 字符串。zoomTo 让相机飞到实体包围盒位置。如果这里红点能出现,说明全局常量、静态资源、类型声明、渲染管线全部正常;如果这一步失败,问题大概率还停在前面几节的配置上。
4. 常见问题与排查:模板落地时最容易翻车的五个场景
环境集成类模板真正值钱的部分不是正常路径,而是异常路径。这里五条都是我在实际项目里踩过的,按“现象 → 原因 → 解决”写清楚。
4.1 白屏加 404:Cesium 找不到自己的家目录
现象:页面白屏,导航控件和球体都没有,控制台 Network 里大量 /cesium/Workers/xxx.js 404。
原因:CESIUM_BASE_URL 没有指向复制后的静态目录。Vite 的 public 目录不会自动把 node_modules 里的资源吐出来;没有 staticCopy 配置时,Cesium 按默认相对路径找资源,入口代码能加载,但后续 Worker 全部失败,地球创建到一半就停止。
解决:按 2.3 节配置复制任务;然后直接访问 http://localhost:5173/cesium/Workers/xxx.js,确认返回 200 而不是 index.html。如果项目部署在子目录,把 define 里的 /cesium 改成目标环境实际前缀,比如 '/gis/cesium'。
4.2 TypeScript 报错“找不到模块 cesium”
现象:Vite 能跑,但编辑器或 vue-tsc 报 Cannot find module 'cesium',或者某个文件里 Cesium 命名空间全是红色。
原因:模块解析方式不是 bundler,或者装过 @types/cesium 与包内类型冲突。还有一种情况是只装了 vite 没装 cesium,因为有些脚手架会把 peerDependencies 悄悄跳过。
解决:先执行npm ls cesium,确认包真实存在;然后删掉 node_modules/@types/cesium 和 package.json 里对应依赖;最后检查 tsconfig.app.json 的 moduleResolution 是否为 bundler。模板里统一使用import * as Cesium from 'cesium',这样运行时和类型都拿到,不依赖 default 导出。
4.3 把 Viewer 放进响应式变量后应用崩溃
现象:地图能显示,但旋转相机、双击定位或加载 3D Tiles 时报错,堆栈在 getPixelSizeInMeters 之类的地方,错误信息读不懂。
原因:reactive 或 ref 会把对象包成 Proxy,Cesium 内部有大量属性描述符操作、私有字段判断和原生对象身份比较,代理之后这些判断全部失效。这不是偶发,是和响应式系统根本性冲突。
解决:用普通 let 保存 viewer,最多在业务需要 UI 联动时另搞一个 shallowRef 存储状态字段,不要把 Cesium 实例本身放进 ref。组件卸载前必须手动 destroy,否则再次进入页面的 WebGL 上下文会被浏览器限制卡死。
4.4 热更新之后旧 Viewer 没销毁,创建新的报重复初始化
现象:开发时保存代码,页面报 DeveloperError 或容器里出现多个 canvas 图层重叠,鼠标交互错乱。
原因:Vite HMR 替换组件模块时不一定触发组件的 onBeforeUnmount,旧 Viewer 仍占着容器和 WebGL 上下文;新实例再创建时找不到干净容器。
解决:模板在 utils/cesium.ts 模块顶层维护一个当前实例引用,并注册 hot.dispose 清理钩子:
let activeViewer: Cesium.Viewer | null = null export function registerViewer(viewer: Cesium.Viewer): Cesium.Viewer { activeViewer = viewer return viewer } if (import.meta.hot) { import.meta.hot.dispose(() => { activeViewer?.destroy() activeViewer = null }) }组件创建实例时先调用 registerViewer,HMR 触发模块替换时旧实例会被销毁,新模块再运行时容器是干净的。这里难点是 dev 和 prod 差异:import.meta.hot 在 build 时会被静态替换成 undefined,整个块不执行,所以不影响线上。
4.5 高分屏下地图边缘锯齿和文字发虚
现象:地图能跑,但线条边缘明显锯齿,label 文字模糊,把 canvas 调大也没用。
原因:Cesium 默认按 window.devicePixelRatio 缩放画布,在 3 倍屏上渲染像素数量剧增,性能下降后浏览器自动降采样;有些模板为了帧率开了低分辨率,但没开抗锯齿。
解决:实例创建后设置分辨率上限并打开 FXAA:
viewer.resolutionScale = Math.min(window.devicePixelRatio || 1, 2) viewer.scene.postProcessStages.fxaa.enabled = trueresolutionScale 是 Cesium 内部渲染分辨率和 CSS 分辨率之间的比例,锁在 2 既能避免 3 倍屏性能浪费,又能保住清晰度。FXAA 属于后处理抗锯齿,适合大范围地形边缘,比开启 MSAA 耗电低,模板默认开启。
5. 进阶验证:用三条基线判断模板是否真的“开箱即用”
拿到模板后先别急着堆业务,我用一个固定流程验证环境是否健康,三条基线过了才往上面加功能。
第一条是静态资源请求全部 200。打开 Network,过滤 cesium 关键字,至少能看到 Workers、Assets、Widgets 三类请求,状态码没有 404。如果只有入口 JS 而没有 worker 请求,说明某条路径被缓存或 CESIUM_BASE_URL 没生效。
第二条是空场景交互帧率。加一段统计代码,拖动相机时观察帧率:
let frames = 0 let start = performance.now() viewer.scene.postRender.addEventListener(() => { frames++ const now = performance.now() if (now - start >= 1000) { console.log(`fps: ${frames}`) frames = 0 start = now } })postRender 每场渲染结束触发一次,统计出一秒内走完多少帧。空白球体在 requestRenderMode 开启时,静止状态可能不渲染,这是正常的;拖动相机时有交互,帧率低于 30 基本要怀疑资源复制不完整或 WebGL 上下文有残留。
第三条是 build 产物离线可跑。执行 npm run build 后用 vite preview 起服务,再关掉 dev server,刷新页面确认球体和贴图都在。有些资源在 dev 模式能被 Vite 代理,build 后路径不匹配,预览一测就穿帮。
模板里还留了一个性能开关:requestRenderMode 设为 true,Cesium 只在场景变化或时钟更新的情况下重新渲染。配合 maximumRenderTimeChange 可以挡住动画时钟对帧率的持续拉高。业务场景里如果只是静态看模型,这个组合能省不少 GPU;等你要接粒子、轨迹动画时,再把 shouldAnimate 和时钟相关配置调回来。
从那以后,我每次拿到这类模板,都会强制走一遍“三维球能转、资源请求全 200、build 产物预演不报 404”这三条基线,再开始往上面堆业务,省下来的时间比调环境多得多。希望帮到你。
本文还有配套的精品资源,点击获取