Cesium 入门(二):Geocoder 搜索框参数详解与天地图搜索接入
上一篇我们跑通了 Vue3 + Vite + Cesium + 天地图底图。这一篇来深挖 Cesium 自带的搜索框(Geocoder)------从 Viewer 配置、Widget 实例、ViewModel 状态,到接入天地图地理编码服务,把搜索功能彻底吃透。
一、先建立整体认知:四层结构
Cesium 的搜索能力不是单一组件,而是分了四层:
arduino
Viewer 构造参数 (geocoder: true/false/对象)
↓
Geocoder Widget(UI 层,负责输入框、下拉列表、DOM 事件)
↓
GeocoderViewModel(状态层,管理 searchText、autoComplete、flightDuration 等)
↓
GeocoderService(HTTP 层,负责真正的地理编码请求)
理解这个分层非常重要:options 只在初始化时被读取一次,用来创建 ViewModel;Widget 实例本身并不保存这些配置,真正的运行时状态都在 viewModel 上 。后续我们想动态改搜索行为,操作的是 myGeocoder.viewModel。
二、Viewer 的 geocoder 参数:最简单的开关
在 new Cesium.Viewer() 时传入 geocoder: true,Cesium 就会在地图右上角渲染一个默认搜索框:
js
viewer = new Cesium.Viewer('cesium-container', {
// ... 其他配置
geocoder: true, // 开启默认搜索框
})
如果不传值 ,geocoder 会走 Cesium 内部的默认值,等效于创建了一个默认配置的 Geocoder Widget:
js
// 不传时,Cesium 内部等价于
new Geocoder({
container: 'cesium-viewer-geocoderContainer',
scene: viewer.scene,
})
效果就是地图右上角出现搜索框:
此时我们在控制台打印:
js
console.log(viewer.geocoder) // Geocoder Widget 实例,第二层
console.log(viewer.geocoder.viewModel) // GeocoderViewModel,第三层

可以看到 viewModel 上有 autoComplete、flightDuration、destinationFound、searchText 等属性,这些都是运行时真正被读取的状态。
⚠️ 默认搜索框调用的是 Cesium ion 的地理编码服务,用的是内置默认 token,对中文地名支持很差,生产环境必须替换掉。
三、自定义搜索框:手动 new 一个 Geocoder
默认搜索框固定在右上角,样式也不受我们控制。如果我们想把它放到左上角、自定义输入框宽度、或者接入自己的搜索源,就要手动实例化 Cesium.Geocoder。
3.1 关闭默认搜索框,准备挂载点
js
viewer = new Cesium.Viewer('cesium-container', {
// ... 其他配置
geocoder: false, // 关闭默认搜索框
})
在页面上放一个用于挂载搜索框的 div:
vue
<template>
<div id="cesium-container">
<div id="my-geocoder"></div>
</div>
</template>
3.2 手动实例化 Geocoder
js
const myGeocoder = new Cesium.Geocoder({
container: 'my-geocoder', // 挂载到页面上的 div id
scene: viewer.scene, // 必须,搜索后飞行需要场景
autoComplete: false, // 初始化时关闭输入联想
flightDuration: 3, // 搜索成功后的飞行动画时长,单位秒
geocoderServices: [
new Cesium.IonGeocoderService({ scene: viewer.scene }),
],
destinationFound: function (vm, destination) {
console.log('搜索成功,坐标', destination)
viewer.camera.flyTo({ destination, duration: 3 })
},
})
window.myGeocoder = myGeocoder // 方便在控制台调试
运行后,左上角就出现了完全由我们控制的搜索框:

3.3 这些 options 到底去哪了?
注意一个关键细节:options 只用来初始化 ViewModel 。Widget 实例本身并不会保存 autoComplete、flightDuration 这些值。你打印 myGeocoder 时,会发现这些属性在 viewModel 上:
js
myGeocoder.viewModel.autoComplete // false
myGeocoder.viewModel.flightDuration // 3
Widget 层主要负责 UI 交互 :监听输入框的 focus、blur、keydown,显示/隐藏下拉列表,调用 ViewModel 的命令;而真正的业务状态、配置、搜索结果都保存在 ViewModel 上 。这也是为什么后面想动态改配置,必须操作 viewModel 而不是 Widget 实例。
四、接入天地图搜索:实现 GeocoderService
默认的 IonGeocoderService 对中文支持不好,而且依赖 Cesium ion 服务。我们有天地图密钥,完全可以自己实现一个 GeocoderService,让搜索框走天地图地名搜索 API。
4.1 天地图 GeocoderService 实现
新建 src/services/tianditu-geocoder-service.js:
js
import * as Cesium from 'cesium'
/**
* 天地图地名搜索服务,用于接入 Cesium 的 Geocoder 组件
* 文档:http://lbs.tianditu.gov.cn/server/search.html
*
* Cesium 要求实现 geocode(query),
* 返回 Promise<{displayName, destination}[]>,destination 为 Cartesian3 或 Rectangle
*
* 注意:天地图的 tk 必须是"浏览器端"类型密钥,服务端会校验 Referer
*/
export default class TiandituGeocoderService {
constructor(token) {
this._token = token
}
// Cesium GeocoderService 接口
async geocode(query) {
const postStr = {
keyWord: query, // 天地图要求的参数名是 keyWord,不是 query
mapBound: '-180,-90,180,90', // 全球范围
level: 12,
queryType: 1, // 普通搜索
start: 0,
count: 10,
}
const resource = new Cesium.Resource({
url: 'https://api.tianditu.gov.cn/v2/search', // 注意是 /v2/search,不是 /search
queryParameters: {
postStr: JSON.stringify(postStr),
type: 'query',
tk: this._token,
},
})
try {
const json = await resource.fetchJson()
const pois = json?.pois ?? []
return pois
.map((poi) => {
const [lon, lat] = (poi.lonlat ?? '').split(',').map(Number)
const address = poi.address || ''
return {
displayName: address ? `${poi.name}(${address})` : poi.name,
// 3000 米高度,保证飞行后能看到周边环境
destination: Cesium.Cartesian3.fromDegrees(lon, lat, 3000),
}
})
.filter((r) => r.displayName && !Number.isNaN(r.destination.x))
} catch (e) {
console.warn('天地图搜索失败:', e)
return []
}
}
}
4.2 几个实测踩坑点
在接入过程中,我踩了三个坑,这里一次性列出来:
| 坑 | 说明 |
|---|---|
| 端点 | 天地图地名搜索 V2.0 端点是 https://api.tianditu.gov.cn/v2/search,用旧版 /search 会直接 404 |
| 参数名 | 关键字参数必须是 keyWord,写成 query 会返回"缺少参数:keyWord" |
| Token 类型 | 天地图服务端会校验 Referer 和 User-Agent,必须使用浏览器端类型的 tk;curl 裸调会被拒绝,但在浏览器里 fetch 能正常通过 |
4.3 把天地图服务挂到自定义搜索框上
js
import TiandituGeocoderService from './services/tianditu-geocoder-service.js'
const TIANDITU_TOKEN = '*****' // 替换成你的天地图浏览器端密钥
const myGeocoder = new Cesium.Geocoder({
container: 'my-geocoder',
scene: viewer.scene,
autoComplete: true, // 打开输入联想,天地图也会返回候选结果
flightDuration: 3,
geocoderServices: [new TiandituGeocoderService(TIANDITU_TOKEN)],
destinationFound: function (vm, destination) {
console.log('搜索成功,坐标', destination)
viewer.camera.flyTo({ destination, duration: 3 })
},
})
输入"天安门",就能看到天地图返回的候选列表:

选择结果后,destinationFound 回调里打印出 Cartesian3 坐标:

控制台里也能看到天地图返回的原始 POI 数据结构:

最终相机会飞到目标位置,控制台还会打印"相机飞完了":

五、运行时操控:GeocoderViewModel 状态模型
搜索框初始化之后,我们常常需要在运行时动态控制它。比如:
- 根据用户权限动态开启/关闭输入联想
- 根据页面状态修改飞行时长
- 从外部回填搜索文本
- 监听搜索完成事件
这些都要通过 viewModel 来操作:
js
const vm = myGeocoder.viewModel
// 1. 动态开启/关闭输入联想
vm.autoComplete = true
// 改成 false 后,下拉联想会立即消失
// 2. 动态修改飞行时长
vm.flightDuration = 5
// 3. 外部回填输入框文本
vm.searchText = '北京'
// 4. 查看当前是否正在搜索(只读响应式属性)
console.log(vm.isSearching) // false,开始搜索时变为 true
// 5. 查看联想结果列表
console.log(vm.searchSuggestions)
// 6. 监听相机飞行完成事件
vm.complete.addEventListener(() => {
console.log('相机飞完了')
})
为什么要这么做?
因为 options 只在构造函数里被读取一次 ,用来创建 ViewModel。之后所有的交互------输入、联想、搜索、飞行------都由 ViewModel 来管理。Widget 只负责把 DOM 事件转发给 ViewModel,并把 ViewModel 的状态反映到界面上。
所以记住这个口诀:
初始化看 options,运行时改 viewModel。
六、完整可运行代码
6.1 src/services/tianditu-geocoder-service.js
js
import * as Cesium from 'cesium'
export default class TiandituGeocoderService {
constructor(token) {
this._token = token
}
async geocode(query) {
const postStr = {
keyWord: query,
mapBound: '-180,-90,180,90',
level: 12,
queryType: 1,
start: 0,
count: 10,
}
const resource = new Cesium.Resource({
url: 'https://api.tianditu.gov.cn/v2/search',
queryParameters: {
postStr: JSON.stringify(postStr),
type: 'query',
tk: this._token,
},
})
try {
const json = await resource.fetchJson()
const pois = json?.pois ?? []
return pois
.map((poi) => {
const [lon, lat] = (poi.lonlat ?? '').split(',').map(Number)
const address = poi.address || ''
return {
displayName: address ? `${poi.name}(${address})` : poi.name,
destination: Cesium.Cartesian3.fromDegrees(lon, lat, 3000),
}
})
.filter((r) => r.displayName && !Number.isNaN(r.destination.x))
} catch (e) {
console.warn('天地图搜索失败:', e)
return []
}
}
}
6.2 src/App.vue
vue
<script setup>
import * as Cesium from 'cesium'
import { onBeforeUnmount, onMounted } from 'vue'
import TiandituGeocoderService from './services/tianditu-geocoder-service.js'
const TIANDITU_TOKEN = '*****' // 替换为你的天地图浏览器端密钥
let viewer = null
let myGeocoder = null
function createTiandituLayer(layerName) {
return new Cesium.WebMapTileServiceImageryProvider({
url: `https://t{s}.tianditu.gov.cn/${layerName}_w/wmts?service=wmts&request=GetTile&version=1.0.0&LAYER=${layerName}&tileMatrixSet=w&TileMatrix={TileMatrix}&TileRow={TileRow}&TileCol={TileCol}&style=default&format=tiles&tk=${TIANDITU_TOKEN}`,
layer: layerName,
style: 'default',
format: 'tiles',
tileMatrixSetID: 'w',
subdomains: ['0', '1', '2', '3', '4', '5', '6', '7'],
maximumLevel: 18,
})
}
onMounted(() => {
viewer = new Cesium.Viewer('cesium-container', {
baseLayer: new Cesium.ImageryLayer(createTiandituLayer('img')),
animation: false,
timeline: false,
geocoder: false, // 关闭默认搜索框
baseLayerPicker: false,
homeButton: false,
sceneModePicker: false,
navigationHelpButton: false,
fullscreenButton: false,
})
viewer.imageryLayers.addImageryProvider(createTiandituLayer('cia'))
// 手动创建自定义搜索框
myGeocoder = new Cesium.Geocoder({
container: 'my-geocoder',
scene: viewer.scene,
autoComplete: true,
flightDuration: 3,
geocoderServices: [new TiandituGeocoderService(TIANDITU_TOKEN)],
destinationFound: function (vm, destination) {
console.log('搜索成功,坐标', destination)
viewer.camera.flyTo({ destination, duration: 3 })
},
})
// 动态控制示例(可删)
const vm = myGeocoder.viewModel
vm.complete.addEventListener(() => {
console.log('相机飞完了')
})
viewer.camera.flyTo({
destination: Cesium.Cartesian3.fromDegrees(116.39, 39.9, 50000),
})
})
onBeforeUnmount(() => {
myGeocoder?.destroy()
viewer?.destroy()
viewer = null
})
</script>
<template>
<div id="cesium-container">
<div id="my-geocoder"></div>
</div>
</template>
<style scoped>
#cesium-container {
width: 100%;
height: 100%;
position: relative;
}
#my-geocoder {
position: absolute;
top: 10px;
left: 10px;
z-index: 999;
}
:deep(.cesium-geocoder-input) {
width: 240px;
}
</style>
七、常见踩坑速查
| 现象 | 原因及解决 |
|---|---|
| 默认搜索框搜中文地名没结果 | 默认走 Cesium ion 地理编码服务,对中文支持差;换成天地图等国产服务 |
new Cesium.IonGeocoderService() 报错 options.scene is required |
该版本构造函数必须传 scene,写成 new Cesium.IonGeocoderService({ scene: viewer.scene }) |
| 天地图搜索报 404 | 端点要用 /v2/search,不是 /search |
| 天地图返回"缺少参数:keyWord" | 参数名是 keyWord,不是 query |
| curl 能调通但浏览器报权限错误 | 天地图服务端校验 Referer,浏览器端 tk 需要在浏览器环境使用;确保 token 类型是"浏览器端" |
修改 myGeocoder.autoComplete 无效 |
状态在 viewModel 上,要改 myGeocoder.viewModel.autoComplete |
| 路由切换后搜索框事件还在 | 记得在 onBeforeUnmount 里调 myGeocoder.destroy() |
写在最后
这一篇我们从 Viewer 的 geocoder 参数出发,逐层拆到了 Widget、ViewModel、GeocoderService,并最终把天地图搜索接入了自定义搜索框。
核心收获就两条:
- UI 想自定义,手动
new Cesium.Geocoder(),自己挂 DOM; - 运行时想控制,全部找
viewModel。
本文是《Cesium 入门》系列第 2 篇,示例代码可直接复制运行。发布前请把 TIANDITU_TOKEN 替换为你自己的天地图浏览器端密钥。