内网项目逼着我放弃 CDN
手上这个服务是部署在内网的,外面访问不到,Cesium 官方那套在线服务全都用不了。所以我得把 Cesium 整个搬到本地来。
版本我用的是 Cesium-1.128 这个压缩包,2025 年 4 月发布的。
为什么要专门下一个包回来?官网确实直接给了 CDN 引入:head 里一行 script 指向 1.128 版本 Build/Cesium 下面的 Cesium.js,再一行 link 指向同版本 Build/Cesium/Widgets 下的 widgets.css,两行写完就能开跑。
有人就想,那我把这两个文件下载下来,改成本地引入不就行了。我试了,下下来本地一引,直接报错:
Cesium.js:1 Uncaught SyntaxError: Invalid regular expression: /[×-ת؀-Û¿Ý-ݿࢠ-ࣿ]/: Range out of order in character class (at Cesium.js:1:1711082)
at new RegExp (<anonymous>)
原因很简单,Cesium.js 里面还引用着很多别的 js 文件,只搬一个入口文件进去肯定撑不起来。
Build 目录下该引哪一份
正确的引用方式是把下载好的压缩包整个解压。
我是直接在 html 文件里测的,所以只盯 Build 这一个文件夹。我猜 packages 文件夹应该是 npm 包,用框架开发的话该关注的是那个文件夹。
点开 Build 文件夹:
里面 Cesium 文件夹是压缩后的代码,CesiumUnminified 文件夹是未压缩的代码,这两份都是完整的 Cesium。
引入的时候跟官方 CDN 的写法是一样的,只要把基础地址改掉就行。我的引法是:
<script src="./assets/cesium-1.128/Build/CesiumUnminified/Cesium.js"></script>
<link
href="./assets/cesium-1.128/Build/CesiumUnminified/Widgets/widgets.css"
rel="stylesheet"
/>
到这步引入就算完事了,剩下都是代码层面的。
我的案例代码,参数基本全关
先贴我自己的这份测试代码,一个 div 撑满视口,配置项几乎一个个关掉。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title></title>
<script src="./assets/cesium-1.128/Build/CesiumUnminified/Cesium.js"></script>
<link
href="./assets/cesium-1.128/Build/CesiumUnminified/Widgets/widgets.css"
rel="stylesheet"
/>
</head>
<style>
* {
padding: 0;
margin: 0;
}
#cesiumContainer {
width: 100%;
height: 100vh;
}
</style>
<body>
<div id="cesiumContainer"></div>
<script type="module">
// 禁用Ion服务
Cesium.Ion.defaultAccessToken = undefined;
let viewer;
async function initCesium() {
viewer = new Cesium.Viewer('cesiumContainer', {
animation: false,
baseLayerPicker: false,
fullscreenButton: false,
vrButton: false,
geocoder: false,
homeButton: false,
infoBox: false,
sceneModePicker: false,
selectionIndicator: false,
timeline: false,
navigationHelpButton: false,
skyBox: false,
skyAtmosphere: false,
contextOptions: {
webgl: {
alpha: true,
failIfMajorPerformanceCaveat: false,
},
},
imageryProvider: false, //让控制台不报错 默认状态会加载cesium的底图
});
viewer.cesiumWidget.creditContainer.style.display = 'none';
// 添加底图
// const provider = await Cesium.SingleTileImageryProvider.fromUrl(
// '127.0.0.1:8004/blue.jpg'
// );
// viewer.scene.imageryLayers.addImageryProvider(provider);
viewer.scene.imageryLayers.addImageryProvider(
new Cesium.TileMapServiceImageryProvider({
url: '127.0.0.1:8004/map/{z}/{x}/{reverseY}.jpg',
fileExtension: 'jpg',
tilingScheme: new Cesium.GeographicTilingScheme(),
})
// new Cesium.UrlTemplateImageryProvider({
// url: '192.168.5.228:8004/map/{z}/{x}/{reverseY}.jpg',
// fileExtension: 'jpg',
// tilingScheme: new Cesium.GeographicTilingScheme(),
// })
);
}
initCesium();
</script>
</body>
</html>
几个配置说清楚。
Cesium.Ion.defaultAccessToken:既然是离线使用,这个 Ion 的默认访问令牌就没有用处了,我直接设成 undefined。设成 undefined 是为了避免一些请求,在内网环境下那些请求发出去就报错,所以干脆置空来躲掉。
那些官方自带的组件就别想了,全设成 false,具体每一项可以看官方文档里 Cesium.Viewer 的 ConstructorOptions 那节。
baseLayerPicker 设成 false,也是为了避开不必要的报错。
imageryProvider 设成 false 这个要重点说。它在新版本里其实已经被移除了,用 ts 的话大概率给你标个错。但确实还能正常用。为什么移除还在用?主要就是不写这一项的话,Cesium 会默认去加载它自己的地图服务,内网里必然连不上,控制台就一直报错。
代码里还有一段注释掉的,我单独说一下。
const provider = await Cesium.SingleTileImageryProvider.fromUrl(
'127.0.0.1:8004/blue.jpg'
);
viewer.scene.imageryLayers.addImageryProvider(provider);
值得说的是,SingleTileImageryProvider 这个用法我去问 AI、上网搜,给的答案都是错的。AI 回我的是这么写的:
function initCesium() {
viewer = new Cesium.Viewer('cesiumContainer', {
// ... existing code ...
imageryProvider: new Cesium.SingleTileImageryProvider({
url: './assets/img/blue.jpg',
rectangle: Cesium.Rectangle.fromDegrees(-180.0, -90.0, 180.0, 90.0) // 添加地理范围
})
});
// ... existing code ...
}
我照着跑,页面一片空白。
新版已经改成异步用法了,得用 await Cesium.SingleTileImageryProvider.fromUrl() 来加载图片。
地图影像数据才是麻烦事,服务本身几行搞定
部署这块反而简单,最麻烦的是地图影像数据。这里要感谢 xingke 提供的地图影像数据。
为了方便大家下载,我已经给大家整理好了资源。资源链接放在下面,自取就好,如果链接失效,可以在评论区留言,我看到了会及时更新。
是两个压缩包,一个是地图影像的,一个是地形的。
下来之后就可以选服务类型了。我这边拿 nest.js 起了个本地测试服务,很简单:去官网直接用命令创建一个 nest 项目,然后在项目根目录建一个 public 目录,把压缩包直接解压到 public 下面,最后在项目根目录的 src/main.ts 里把 public 的路径和跨域设置一下。
// src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { join } from 'path';
import { NestExpressApplication } from '@nestjs/platform-express';
import { TransformInterceptor } from './core/interceptor/transform.interceptor';
async function bootstrap() {
const app = await NestFactory.create<NestExpressApplication>(AppModule);
// 配置 CORS
app.enableCors({
origin: true, // 允许所有来源访问,生产环境建议设置具体的域名
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE,OPTIONS',
credentials: true,
});
app.useStaticAssets(join(__dirname, '..', 'public'));
app.useGlobalInterceptors(new TransformInterceptor());
await app.listen(8004, '127.0.0.1');
}
bootstrap();
前面引底图那份代码里的地址,替换成你自己的地址就可以了,注意要补上对应的协议前缀。
tilingScheme 只有两种可选
顺便把 TileMapServiceImageryProvider 里的 tilingScheme 说清楚,1.128 版本目前就只有这两种可选。
GeographicTilingScheme,地理坐标切片方案:
- 基于地理坐标系(经纬度)
- 适用于全球范围的数据展示
- 经度范围:-180° 到 180°
- 纬度范围:-90° 到 90°
- 在高纬度地区变形较小
WebMercatorTilingScheme,墨卡托投影切片方案:
- 基于墨卡托投影
- 是最常用的网络地图切片方案
- 适用于 Google Maps、OpenStreetMap 等常见地图服务
- 在高纬度地区会产生较大变形
- 不能完全显示极地区域
这两种切片方案的主要区别就在投影方式和极端地区的表现上,我自己的影像数据配的是 GeographicTilingScheme。