最近在调一个老项目,前端要整体迁到 Vue CLI 3,但核心的三维地球模块还被锁死在 Cesium 1.7。刚拿到这个组合时,我第一反应就是import Cesium from 'cesium',结果项目一跑起来,各种怪问题一个接一个。折腾了两天之后,我老老实实把 Cesium 1.7 当成“目录级静态资源”来处理,环境总算稳定了。这篇把完整的配置思路、踩坑点和排查顺序记录下来,给正在维护老版本三维 GIS 项目的朋友做个参考。
如果你也是类似处境:项目里已经有一套基于 Cesium 1.7 写好的 API、数据服务和 shader 改动,不能随便升级引擎,但又想把前端工程化切到 Vue CLI 3,那么这篇文章很适合你。我会尽量把“为什么这么做”讲清楚,而不是只给一段能跑的配置。
1. 为什么是这套组合,而不是直接升级到新版 Cesium
1.1 业务约束比技术选择更现实
这个三维模块不是新写的,而是从很早以前就在生产环境稳定运行的一套系统。底层的地形切片、影像服务、Entity 数据接口、还有二次封装的那一层工具函数,全部是围着 Cesium 1.7 写的。换新版本意味着所有涉及 WebGL 渲染、Entity 属性解析、相机控制、图层管理的代码都要重新过一遍回归清单。
Cesium 新版本这些年变化不小,尤其是内部模块化方案和部分 API 行为,升级之后很可能出现“页面不报错,但效果不对”的隐性 bug。对一套长期运行的项目来说,这种成本不是随便就能接受的。所以前端脚手架升级到 Vue CLI 3,只是为了把工程化构建、本地调试、部署流程统一起来,并不是要顺手换地图引擎。
1.2 Cesium 1.7 和 Vue CLI 3 之间存在明显的“代差”
Cesium 1.7 大概是 2016 年左右发布的版本。那个时期的前端生态里,主流做法还是直接用<script>标签引入一个全局 JS 文件,再配合 AMD 加载器处理内部依赖。Vue CLI 3 是 2018 年之后才普及的脚手架,底层是 webpack 4。Cesium 1.7 在设计之初并没有把“被现代打包器 import”作为主要使用场景。
这不是说 webpack 4 完全不能加载 AMD 模块,而是说这个老版本带了它自己的一套模块加载运行时。一旦项目里出现多入口、懒加载、代码分割,就很容易碰到define is not defined、Cesium is not defined这类模棱两可的报错。折腾到最后你会发现,最简单、最稳定的路线反而最朴素:把它当成传统 JS 库,用 script 标签全局引入,然后用window.Cesium访问。
2. 环境准备:老版本项目不能盲目上新工具
2.1 版本核对清单
老项目配新脚手架,第一步不是急着写代码,而是先把本机的 Node、npm、Vue CLI 版本对齐。Vue CLI 3 对 Node 的要求是 8.9 以上,但真正运行到 webpack 4 时,Node 版本太高反而会有兼容问题。我这里用的组合比较保守:
| 软件 | 推荐版本 | 说明 |
|---|---|---|
| Node.js | 10.24.x | 对 Vue CLI 3 和 webpack 4 最友好,避免高版本 Node 带来的算法兼容问题 |
| npm | 6.14.x | Node 10 自带版本,处理旧 lockfile 不容易出幺蛾子 |
| Vue CLI 3 | 3.12.x | 3.x 的最后一个稳定线,底层基于 webpack 4 |
| Cesium | 1.7.0 | 固定版本,避免 npm 安装时被自动升级 |
Node 14 以上跑 webpack 4 时,经常会出现error:0308010C:digital envelope routines::unsupported这类问题,本质是 OpenSSL 算法变了。用 Node 10 能绕开大部分坑,如果团队规定必须用高版本 Node,那就得额外配 NODE_OPTIONS 处理,多一事不如少一事。
2.2 创建项目并安装固定版本
安装命令本身很简单:
vue create vue-cesium-demo cd vue-cesium-demo npm install cesium@1.7.0 --save这里有几个容易忽略的细节。第一,cesium@1.7.0必须写死版本号,不能直接npm install cesium,否则拉下来的是几十个版本之后的新包,整个目录结构和加载方式都不一样了。第二,如果你们公司用的是私有 npm 镜像,而这个镜像早就把老版本清理掉了,那npm install可能直接失败。这时候不要纠结,直接从旧项目里拷贝一份Build/Cesium目录过来,后面所有步骤依然成立,因为本质上我们就是要这个目录。
安装完成后,可以到node_modules/cesium/下看一眼目录结构。重点确认是否存在Build/Cesium这个子目录。如果只有Source或别的结构,说明这个 npm 包不是标准的 Cesium 1.7 发布包。
3. 把 Cesium 1.7 当作“目录级静态资源”来搬
3.1 不要只拷一个 Cesium.js
Cesium 这个库很有特点,它不是单个 JS 文件就能跑起来的。构建发布之后,真正的运行时是一个完整的目录,里面除了主入口Cesium.js,还跟着一堆 Worker 脚本、默认图标、控件样式、第三方基础库。以 1.7 为例,Build/Cesium目录至少会包含这些内容:
| 目录/文件 | 作用 |
|---|---|
| Cesium.js | 主入口,暴露全局window.Cesium |
| Cesium.css | 控件和默认样式 |
| Assets/ | 默认纹理、图标、图片等资源 |
| Widgets/ | 时间轴、动画控制、图层选择等控件的 HTML/CSS |
| Workers/ | Web Worker 脚本,地形计算、几何处理会动态加载 |
| ThirdParty/ | 内部依赖的第三方库,按 AMD 方式加载 |
很多人第一次配置时只把Cesium.js复制到了静态目录,结果运行时发现地图控件出来了,但地球是黑的,或者控制台报一堆Assets/xxx 404、Workers/xxx 404。原因就是其他目录没跟着搬。
记住一个原则:整个 Build/Cesium 目录必须原样保留,目录层级也不要随意改写。Cesium 内部计算资源路径时,默认是假设 Cesium.js 和旁边的 Assets、Workers、Widgets 在同级目录下的。
3.2 手动拷贝与自动拷贝怎么选
两种方式都可以用。
第一种,手动拷贝到 Vue CLI 3 的public/cesium/目录。这种方式很直观,适合演示或快速验证:
public/ cesium/ Cesium.js Cesium.css Assets/ Widgets/ Workers/ ThirdParty/第二种,用copy-webpack-plugin在构建时自动从node_modules/cesium/Build/Cesium拷到输出目录。这种方式能保证目录完整,不容易漏文件,也方便以后通过 package.json 精确锁版本。
| 搬运方式 | 优势 | 缺点 |
|---|---|---|
| 手动拷贝到 public/cesium | 思路简单,不依赖额外插件 | 版本升级时容易忘,且容易只拷一部分文件 |
| CopyWebpackPlugin 自动拷贝 | 目录完整,版本可控,适合真实项目 | 配置有学习成本,插件版本不同写法有差异 |
我在正式项目里用的是自动拷贝。但对于只想先跑通 Demo 的人来说,手动拷贝public/cesium一步到位,后续配置也更少。
4. vue.config.js 和 index.html 要联动配置,不能只配一个
4.1 publicPath 决定浏览器以什么路径找资源
Vue CLI 3 里,publicPath会影响 index.html 中静态资源的引用前缀。默认值是/,也就是根路径。如果项目部署在服务器根目录下,这没问题。但如果部署在子目录下,或者用相对路径方式打开,那就要把 publicPath 改成./。
一个更稳妥的写法是区分开发环境和生产环境:
// vue.config.js module.exports = { publicPath: process.env.NODE_ENV === 'production' ? './' : '/' }开发环境用/,避免 Vue Router history 模式在子路由刷新时 404;生产环境用./,让 index.html 里的资源引用变成相对路径,不管丢到哪个子目录都能找到。
4.2 index.html 里用原生 script 标签引入
在 Vue CLI 3 中,public/index.html会经过 EJS 模板编译,<%= BASE_URL %>会被替换成publicPath的值。加入 Cesium 对应的样式和脚本:
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <title>三维地图管理平台</title> <link rel="stylesheet" href="<%= BASE_URL %>cesium/Cesium.css"> <script src="<%= BASE_URL %>cesium/Cesium.js"></script> </head> <body> <noscript>请先开启 JavaScript 再访问本系统。</noscript> <div id="app"></div> </body> </html>这里我把<script>标签放在了<head>里,而不是 body 底部。这样做的好处是:Vue 应用挂载之前,window.Cesium一定已经存在,组件里不需要考虑加载时序。Cesium.css也必须在业务代码之前加载,否则时间轴、图层选择器这些控件会出现“有功能但样式全乱”的效果。
如果采用手动拷贝方案,配置文件可以非常干净,vue.config.js只需要处理 publicPath 即可。如果采用自动拷贝方案,则需要在configureWebpack.plugins里加 CopyWebpackPlugin。
4.3 CopyWebpackPlugin 的完整配置
先安装合适版本的插件。Vue CLI 3 自带的 webpack 是 4,copy-webpack-plugin 用 5.x 最稳:
npm install copy-webpack-plugin@5.1.2 --save-dev然后配置:
// vue.config.js const path = require('path') const CopyWebpackPlugin = require('copy-webpack-plugin') module.exports = { publicPath: process.env.NODE_ENV === 'production' ? './' : '/', configureWebpack: { plugins: [ new CopyWebpackPlugin([ { from: path.join(__dirname, 'node_modules/cesium/Build/Cesium'), to: 'cesium', toType: 'dir' } ]) ] } }注意,这里 from 的路径是node_modules/cesium/Build/Cesium,to 是cesium。构建之后的效果是 dist 目录下生成cesium/,里面是完整目录。
如果你在某个项目里看到的是这种写法:
new CopyWebpackPlugin({ patterns: [ { from: '...', to: '...' } ] })那是 copy-webpack-plugin 6.x 以后的语法。Vue CLI 3 默认情况下不一定兼容,使用前先确认插件版本。vue.config.js修改之后必须重启 dev server,配置才会生效。
5. 在 Vue 单文件组件里创建并销毁 Viewer
5.1 基础封装代码
引入 Cesium 之后,最常见的使用方式是在一个独立组件里渲染三维地球。组件模板很简单,就是放一个容器 div:
<template> <div ref="cesiumContainer" class="cesium-container"></div> </template> <script> export default { name: 'CesiumViewer', viewer: null, mounted () { // 全局 Cesium 来自 index.html 中的 script 标签 this.viewer = new window.Cesium.Viewer(this.$refs.cesiumContainer, { animation: false, // 左下角动画控件 timeline: false, // 底部时间轴 baseLayerPicker: false, // 图层选择器 geocoder: false, // 搜索框 homeButton: false, // 回到默认视角 sceneModePicker: false, // 2D/3D 切换 navigationHelpButton: false, // 帮助按钮 infoBox: false, // 信息弹窗 selectionIndicator: false // 选中标记 }) // 设置一个初始视角 this.viewer.camera.setView({ destination: window.Cesium.Cartesian3.fromDegrees(120.15, 30.28, 10000.0) }) }, beforeDestroy () { if (this.viewer) { this.viewer.destroy() this.viewer = null } } } </script> <style scoped> .cesium-container { width: 100%; height: 100%; min-height: 400px; } </style>这里有几个点值得展开讲。
第一,1.7 版本的控件参数就是上面这些。新版本 Cesium 文档里的一些选项,比如baseLayer: false、baseMap,在 1.7 里不一定生效,配置时以 1.7 的 API 文档为准。
第二,this.viewer = null这种写法在 Vue 2 里不是响应式数据,这是刻意为之。Viewer 实例内部有大量 WebGL 对象和方法,放进 Vue 的响应式系统里既没必要,也可能带来性能损耗。直接把它挂在实例上,当成普通属性管理即可。
第三,容器的高度很关键。Cesium 的 canvas 默认继承父容器尺寸,如果父容器没有高度,画布会变成 0 高。所以.cesium-container要显式设置height: 100%或者固定高度。注意,如果放在 Vue Router 的嵌套路由里,通常还要让路由视图、父容器也都有高度,这个链条缺一个都不行。
5.2 SPA 路由切换和 keep-alive 的处理
单页应用里最容易被忽略的是 Viewer 的销毁。beforeDestroy里调用了viewer.destroy(),但如果你的路由组件被<keep-alive>包裹,组件不会真正卸载,beforeDestroy不会按时触发。结果就是一个页面来回切了几次之后,WebGL 上下文越来越多,内存一路往上走,最终页面越来越卡甚至白屏。
我的做法是:根据是否使用 keep-alive,决定在哪个生命周期里创建和销毁 Viewer。如果页面需要缓存,就改用activated和deactivated:
<script> export default { name: 'CesiumViewer', activated () { if (!this.viewer) { this.createViewer() } }, deactivated () { if (this.viewer) { this.viewer.destroy() this.viewer = null } } } </script>创建逻辑可以抽成一个方法,在 mounted 和 activated 里都调用。这样既能保证首次进入创建,也能在离开缓存页面后释放 WebGL 资源,再次进入时重新创建。
另外,Cesium 的 canvas 默认需要很多浏览器资源,开发调试时如果启动了很久的 dev server,偶尔出现“切了几次页面就黑屏”,优先怀疑不是配置问题,而是 WebGL 上下文达到浏览器上限,刷新页面即可恢复。
6. 高频报错与我的完整排查链路
6.1 报错:Cesium is not defined
这是最常见、也最容易误导人的一个报错。看到这个错误,很多人第一反应是 npm 包没装好,实际上更像是脚本没加载成功,或者加载顺序不对。
我的排查顺序是固定的:
- 打开浏览器 DevTools 的 Network 面板,刷新页面,直接搜
Cesium.js,看请求状态是不是 200。 - 如果状态是 404,说明路径不对。检查 index.html 里的引用路径和实际静态文件位置是否一致。比如把 Cesium 放到了
public/cesium/,但引用写成了<%= BASE_URL %>Cesium.js,就一定会 404。 - 如果状态是 200,但控制台还是报
Cesium is not defined,检查页面实际执行的打包代码是否引用了window.Cesium之外的名字。Cesium 1.7 挂载到全局时,正常应该可以直接用window.Cesium。 - 确认浏览器加载的是不是被插件拦截或缓存过的旧文件。老版本项目的调试现场经常会出现“代码改了,资源没刷新”的情况,DevTools 里勾选 Disable cache 再试一次。
我实际遇到的一次问题是 copy 插件配置错了,to写成了cesium/cesium。构建产物里出现了两层同名目录,浏览器请求cesium/Cesium.js时当然找不到。排查时直接在 dist 目录里看一眼文件路径,比反复猜配置高效得多。
6.2 报错:控件有,地球黑,或者 Assets/Widgets 404
如果页面能正常显示 Cesium 的旋转图标、时间轴这些控件,但地图区域一片黑,或者控制台刷出一堆Assets/xxx 404、Widgets/xxx 404,问题大概率不是主 JS 文件,而是 Cesium 的静态资源基础路径不对。
Cesium 运行时需要动态加载 Workers 和 Assets。它判断资源路径的方式依赖 Cesium.js 所在的 URL 位置。所以配置时一定要注意:不要把 Cesium.js 单独复制到别的地方,也不要让浏览器通过一个重定向后的 URL 加载它。最理想的情况是,浏览器地址栏里Cesium.js的完整路径,就是某个目录/Cesium.js,且这个目录下同级存在 Assets、Workers、Widgets。
我的排查步骤通常是:
- 看 Elements 面板里
<script>标签实际加载的 URL。 - 在浏览器直接访问这个 URL 的上级目录,确认 Assets 是否能列出来。
- 如果项目部署在子目录,检查生产环境的 publicPath 是否设成了
./,不能一边用绝对路径部署,一边期望相对路径资源能自动对上。
有些资料会提到设置window.CESIUM_BASE_URL。老版本对资源路径的推断相对脆弱,如果你确实需要强制指定,可以在 index.html 的 Cesium.js 之前加一行window.CESIUM_BASE_URL = 'cesium/',但前提是你要能确认这个全局变量在 1.7 版本里会生效。我的建议是优先保持目录结构干净,让 Cesium 自己推断。
6.3 报错:切换路由几次后页面白屏,控制台提示 WebGL context lost
这类问题通常不是第一次进页面时报的,而是“用着用着就崩了”,所以很多人不会往开发环境配置方向想。
在 Vue 单页应用里,如果路由切走时没有销毁 Viewer,Cesium 创建的 WebGL 上下文和 canvas 会继续挂在 DOM 上。再次进入路由时,又会 new 一个 Viewer,等于一个页面里同时存着多个 WebGL 上下文。浏览器对 WebGL 上下文数量有严格限制,超了之后要么报Context Lost,要么整个页面白屏。
我的排查链路很简单:
- 页面切走之后,先看 DOM 里是不是还残留着之前创建的 canvas。
- 在
beforeDestroy里打一个日志,确认组件是否真的卸载。 - 如果组件被 keep-alive 缓存,立刻改用
activated/deactivated方案。
这个问题处理完之后,内存曲线会明显平稳下来,连续切换十几遍页面都不会再白屏。
还有一个细节:Cesium 1.7 的 Viewer 销毁之后,某些已注册的定时器和事件监听不一定能立即回收干净。如果项目里对内存要求很严格,可以在viewer.destroy()之后再手动把容器里的 DOM 清空:
const container = this.$refs.cesiumContainer if (container) { container.innerHTML = '' }这一步不做也不至于马上出问题,但在长时间运行的监控大屏项目里,能少一点是一点。
这套环境配下来,真正复杂的地方不在于写了多少配置,而在于搞清楚 Cesium 1.7 的历史加载方式。它天生是一个“目录级”依赖,不是单纯的 npm 模块。只要心里有这个概念,遇到 404、is not defined、WebGL context lost 这些问题时,都能沿着“资源路径对不对、全局对象有没有、生命周期有没有泄露”这条线快速定位。老版本项目维护就是这样,没有太多惊喜,但每一步都值得记录。