提交 198eb1d0 作者: test

feat: 优化 Mapbox 组件设计

1. 采用统一队列处理所有逻辑层到视图层通信
2. 地图配置项新增 onSourceRequestHandle 实现监听 setGeoJSONSourceForRequest 触发的请求回执
3. 新增 setGeoJSONSourceForRequest 过滤器配置,允许过滤掉接口得到的部分数据后再返回到逻辑层,优化配置项统一请求
上级 c376cf1e
import type mapboxgl from 'mapbox-gl'
import type { MapboxConfig, MapboxInstance } from './index'
import qs from 'qs'
import type { GeoJSONSourceDataUrlParams, MapboxConfig, MapboxInstance } from './index'
import { API_URL, API_URL_PREFIX } from '/@/utils/net'
import { isProdMode } from '@/utils/env'
import { getToken } from '@/utils/auth'
// 组件名称
export const name = 'Mapbox'
......@@ -49,16 +52,22 @@ export function useMapbox<T extends MapboxInstance, P extends MapboxConfig>(
removeSource: (id: string) => getInstance()?.removeSource(id),
setGeoJSONSourceForRequest: (
id: string,
url: string,
map: (
config: string | GeoJSONSourceDataUrlParams,
handler?: (
data: Recordable,
) => GeoJSON.Feature<GeoJSON.Geometry> | GeoJSON.FeatureCollection<GeoJSON.Geometry>,
) => getInstance()?.setGeoJSONSourceForRequest(id, url, map),
filters?: string[],
) =>
getInstance()?.setGeoJSONSourceForRequest(id, buildGeoJSONSourceDataUrl(config), handler, [
'jsonObject',
'liveVos',
...(filters || []),
]),
setGeoJSONSourceData: (id: string, data: mapboxgl.GeoJSONSource['setData'], filter?: string) =>
getInstance()?.setGeoJSONSourceData(id, data, filter),
setVectorTileSourceTiles: (id: string, tiles: string[]) =>
getInstance()?.setVectorTileSourceTiles(id, tiles),
addLayer: (layer: mapboxgl.Layer) => getInstance()?.addLayer(layer),
addLayer: (layer: mapboxgl.Layer, beforeId?: string) => getInstance()?.addLayer(layer, beforeId),
removeLayer: (id: string) => getInstance()?.removeLayer(id),
setPaintProperty: (layerId: string, name: string, value: any) =>
getInstance()?.setPaintProperty(layerId, name, value),
......@@ -70,13 +79,14 @@ export function useMapbox<T extends MapboxInstance, P extends MapboxConfig>(
]
}
export function buildGeoJSONSourceDataUrl(config: { url: string; params: Recordable }): string {
const { url, params } = config
const keys = Object.keys(params)
if (keys.length === 0) {
return url
export function buildGeoJSONSourceDataUrl(config: GeoJSONSourceDataUrlParams | string): string {
const baseURL = `${API_URL}${API_URL_PREFIX}`
const defaultParams = {
Authorization: getToken(),
}
const query = keys.map((key) => `${key}=${params[key]}`).join('&')
return `${url}?${query}`
const url = typeof config === 'string' ? config : config.url
const params = typeof config === 'string' ? defaultParams : { ...defaultParams, ...config.params }
const query = qs.stringify(params, { addQueryPrefix: true })
return `${baseURL}${url}${query}`
}
......@@ -826,11 +826,18 @@ export interface MapboxConfig {
// =============== 自定义事件 =================
/**
* 注意: 此处暴露的 map 对象的类型定义只是为了方便 IDE 提示, 实际上 map 对象上的方法和属性并不完整,依赖于 Mapbox 自定义组件的内部实现
* @param map 组件实例
* 地图加载完成事件
* @param data 事件数据, 由视图层在 Map 加载完成后传入的可序列化 JSON 数据
*/
onLoaded?: (map: MapboxInstance, data: Recordable) => void
onLoaded?: (data: Recordable) => void
/**
* 监听请求数据源的请求回执
* @param id 数据源 ID
* @param url 请求地址
* @param data 请求回来的数据(默认情况下将过滤掉一些大字段或数据集,例如:jsonObject、liveVos)
*/
onSourceRequestHandle?: <T = Recordable>(data: { id: string; url: string; data: T }) => void
}
/**
......@@ -879,14 +886,16 @@ export interface MapboxInstance {
* @param id 数据源 ID
* @param url 数据源 URL,通常是一个后端的数据查询接口(色斑图查询接口、站点数据查询接口)
* @param map 数据映射函数,用于处理后端返回的数据,将其转换为 GeoJSON 数据
* @param filters 过滤对象,用于过滤响应 onSourceRequestHandle 事件时的数据,默认过滤掉 jsonObject、liveVos 字段
* @link https://docs.mapbox.com/mapbox-gl-js/api/map/#map#addsource
*/
setGeoJSONSourceForRequest: (
id: string,
url: string,
map: (
url: string | GeoJSONSourceDataUrlParams,
map?: (
data: Recordable,
) => GeoJSON.Feature<GeoJSON.Geometry> | GeoJSON.FeatureCollection<GeoJSON.Geometry> | any,
filters?: string[],
) => void
/**
* 设置 GeoJSONSource 数据源 data
......@@ -941,6 +950,11 @@ export interface MapboxInstance {
setLayoutProperty: (layerId: string, name: string, value: any) => void
}
export interface GeoJSONSourceDataUrlParams {
url: string
params: Recordable
}
/**
* 站点数据类型定义
*/
......
......@@ -18,7 +18,8 @@
data() {
return {
id: nanoid(),
onLoaded: noop,
onLoadedEvent: noop,
onSourceRequestHandleEvent: noop,
options: {},
// FIXED: 定义为 undefined,避免在 Hook 中通过计算属性取值是出现两次变化的问题
loaded: undefined,
......@@ -26,30 +27,20 @@
// 以下是一些 mapbox 的方法所需参数,方便传递到 renderjs 中
events: {},
eventOptions: undefined,
eventOptionsQueue: [],
addSourceOptions: undefined,
addSourceOptionsQueue: [],
addLayerOptions: undefined,
addLayerOptionsQueue: [],
removeOptions: undefined,
removeOptionsQueue: [],
setGeoJSONSourceForRequestOptions: undefined,
setGeoJSONSourceForRequestOptionsQueue: [],
setGeoJSONSourceDataOptions: undefined,
setGeoJSONSourceDataOptionsQueue: [],
setVectorTileSourceTilesOptions: undefined,
setVectorTileSourceTilesOptionsQueue: [],
setFilterOptions: undefined,
setFilterOptionsQueue: [],
setPaintPropertyOptions: undefined,
setPaintPropertyOptionsQueue: [],
setLayoutPropertyOptions: undefined,
setLayoutPropertyOptionsQueue: [],
flyToOptions: undefined,
flyToOptionsQueue: [],
// change options 锁,结合 event,配合数组实现先进先出队列控制
changeLock: {},
changeLock: false,
changeOptionsQueue: [],
}
},
computed: {
......@@ -70,21 +61,27 @@
console.log('✨ Map Loaded', data)
// 触发 onLoaded 事件
this.onLoaded?.(this, data)
this.onLoadedEvent?.(data)
},
onSourceRequestHandle(data) {
console.log('✨ Request Handle', data)
// 触发 onSourceRequestHandle 事件
this.onSourceRequestHandleEvent?.(data)
},
// 尝试触发事件, 通过 changeLock 控制
tryChange(event) {
if (this[`${event.type}OptionsQueue`]?.length && !this.changeLock[event.type]) {
this.changeLock[event.type] = true
this[`${event.type}Options`] = this[`${event.type}OptionsQueue`].shift()
tryTriggerChange() {
if (!this.changeLock && this.changeOptionsQueue.length) {
const item = this.changeOptionsQueue.shift()
this[`${item.fn}Options`] = item
this.changeLock = true
}
},
// 监听地图配置从逻辑层到视图层的响应式变化事件
onMapOptionsChangeEvent(event) {
console.log('✨ Map Event', event)
this.changeLock[event.type] = false
this.tryChange(event)
this.changeLock = false
this.tryTriggerChange()
},
setConfig(config) {
this.options = {
......@@ -94,21 +91,25 @@
attribution: config?.attribution,
}
this.onLoaded = config?.onLoaded || noop
this.onLoadedEvent = config?.onLoaded || noop
this.onSourceRequestHandleEvent = config?.onSourceRequestHandle || noop
},
// 重新实现 mapbox 的一些方法,方便传递到 renderjs 中
customEvent(event, type, layer, ...args) {
// 加入到 event 事件队列中
this.eventOptionsQueue.push({
// 加入到 change 事件队列中
this.changeOptionsQueue.push({
fn: event,
event,
type,
layer,
...args,
})
this.$nextTick(() => {
// 尝试触发 event 事件
this.tryChange({ type: 'event' })
this.tryTriggerChange()
})
return `${event}-${type}-${layer}`
},
......@@ -138,124 +139,138 @@
// 2. addSource/removeSource 方法
addSource(id, source) {
// 加入到 change 事件队列中
this.addSourceOptionsQueue.push({
this.changeOptionsQueue.push({
fn: 'addSource',
id,
source,
})
// 尝试触发 change 事件
this.tryChange({ type: 'addSource' })
this.tryTriggerChange()
},
removeSource(id) {
// 加入到 change 事件队列中
this.removeOptionsQueue.push({
this.changeOptionsQueue.push({
fn: 'removeSource',
id,
type: 'source',
})
// 尝试触发 change 事件
this.tryChange({ type: 'remove' })
this.tryTriggerChange()
},
setGeoJSONSourceForRequest(id, url, handler) {
setGeoJSONSourceForRequest(id, url, handler, filters) {
// 加入到 change 事件队列中
this.setGeoJSONSourceForRequestOptionsQueue.push({
this.changeOptionsQueue.push({
fn: 'setGeoJSONSourceForRequest',
id,
url,
handler: handler.toString(),
filters,
})
// 尝试触发 change 事件
this.tryChange({ type: 'setGeoJSONSourceForRequest' })
this.tryTriggerChange()
},
setGeoJSONSourceData(id, data, filter) {
// 加入到 change 事件队列中
this.setGeoJSONSourceDataOptionsQueue.push({
this.changeOptionsQueue.push({
fn: 'setGeoJSONSourceData',
id,
data,
filter,
})
// 尝试触发 change 事件
this.tryChange({ type: 'setGeoJSONSourceData' })
this.tryTriggerChange()
},
setVectorTileSourceTiles(id, tiles) {
// 加入到 change 事件队列中
this.setVectorTileSourceTilesOptionsQueue.push({
this.changeOptionsQueue.push({
fn: 'setVectorTileSourceTiles',
id,
tiles,
})
// 尝试触发 change 事件
this.tryChange({ type: 'setVectorTileSourceTiles' })
this.tryTriggerChange()
},
// 3. addLayer/removeLayer 方法
addLayer(layer, beforeId) {
// 加入到 change 事件队列中
this.addLayerOptionsQueue.push({
this.changeOptionsQueue.push({
fn: 'addLayer',
layer,
beforeId,
})
// 尝试触发 change 事件
this.tryChange({ type: 'addLayer' })
this.tryTriggerChange()
},
removeLayer(id) {
// 加入到 change 事件队列中
this.removeOptionsQueue.push({
this.changeOptionsQueue.push({
fn: 'removeLayer',
id,
type: 'layer',
})
// 尝试触发 change 事件
this.tryChange({ type: 'remove' })
this.tryTriggerChange()
},
// 4. setFilter 方法
setFilter(layerId, filter) {
// 加入到 change 事件队列中
this.setFilterOptionsQueue.push({
this.changeOptionsQueue.push({
fn: 'removeLayer',
layerId,
filter,
})
// 尝试触发 change 事件
this.tryChange({ type: 'setFilter' })
this.tryTriggerChange()
},
// 5. setPaintProperty 方法
setPaintProperty(layerId, paintProperty, value) {
// 加入到 change 事件队列中
this.setPaintPropertyOptionsQueue.push({
this.changeOptionsQueue.push({
fn: 'setPaintProperty',
layerId,
paintProperty,
value,
})
// 尝试触发 change 事件
this.tryChange({ type: 'setPaintProperty' })
this.tryTriggerChange()
},
// 6. setLayoutProperty 方法
setLayoutProperty(layerId, layoutProperty, value) {
// 加入到 change 事件队列中
this.setLayoutPropertyOptionsQueue.push({
this.changeOptionsQueue.push({
fn: 'setLayoutProperty',
layerId,
layoutProperty,
value,
})
// 尝试触发 change 事件
this.tryChange({ type: 'setLayoutProperty' })
this.tryTriggerChange()
},
// 7. flyTo 方法
flyTo(options) {
// 加入到 change 事件队列中
this.flyToOptionsQueue.push(options)
this.changeOptionsQueue.push({
fn: 'flyTo',
...options,
})
// 尝试触发 change 事件
this.tryChange({ type: 'flyTo' })
this.tryTriggerChange()
},
},
}
......
import axios from 'axios'
import { merge } from 'lodash-es'
import { merge, omit } from 'lodash-es'
import { HandlerUtil, defaultStyle, loadMapControl, loadMapboxLibs } from '/@/components/Map/Mapbox'
// renderjs 官方文档
......@@ -8,8 +8,12 @@ import { HandlerUtil, defaultStyle, loadMapControl, loadMapboxLibs } from '/@/co
// https://juejin.cn/post/7049185827582115870
async function request(url, handler) {
const res = await axios.get(url)
const { data } = await axios.get(url)
if (data.status !== 200) {
throw new Error(data.message)
}
const { body } = data
if (handler) {
// TODO: 待优化
// 1. 考虑是否为函数
......@@ -20,10 +24,10 @@ async function request(url, handler) {
// eslint-disable-next-line no-eval
const executableFunction = eval(`(${handler})`)
return executableFunction(res.data)
return { data: executableFunction(body), raw: body }
}
return res.data
return { data: body, raw: body }
}
const defaultEmptyGeoJSON = {
......@@ -149,19 +153,19 @@ export default {
}
},
changeFlyToOptions(options) {
this.emitChangeEvent('flyTo')
this.emitChangeEvent(options.fn)
if (this.checkOnChangeValidity(options)) {
this.map.flyTo(options)
}
},
changeAddSourceOptions(options) {
this.emitChangeEvent('addSource')
this.emitChangeEvent(options.fn)
if (this.checkOnChangeValidity(options)) {
this.map.addSource(options.id, options.source)
}
},
changeSetGeoJSONSourceForRequestOptions(options) {
this.emitChangeEvent('setGeoJSONSourceForRequest')
this.emitChangeEvent(options.fn)
if (this.checkOnChangeValidity(options)) {
if (this.map.getSource(options.id)) {
// 清空数据
......@@ -175,31 +179,37 @@ export default {
}
// 请求数据
request(options.url, options.handler).then((data) => {
request(options.url, options.handler).then(({ data, raw }) => {
this.map.getSource(options.id).setData(data)
this.$ownerInstance.callMethod('onSourceRequestHandle', {
id: options.id,
url: options.url,
data: omit(raw, options.filters || []),
})
})
}
},
changeSetGeoJSONSourceDataOptions(options) {
this.emitChangeEvent('setGeoJSONSourceData')
this.emitChangeEvent(options.fn)
if (this.checkOnChangeValidity(options)) {
this.map.getSource(options.id).setData(options.data)
}
},
changeSetVectorTileSourceTilesOptions(options) {
this.emitChangeEvent('setVectorTileSourceTiles')
this.emitChangeEvent(options.fn)
if (this.checkOnChangeValidity(options)) {
this.map.getSource(options.id).setTiles(options.tiles)
}
},
changeAddLayerOptions(options) {
this.emitChangeEvent('addLayer')
this.emitChangeEvent(options.fn)
if (this.checkOnChangeValidity(options)) {
this.map.addLayer(options.layer, options.beforeId)
console.log('options', options)
}
},
changeRemoveOptions(options) {
this.emitChangeEvent('remove')
this.emitChangeEvent(options.fn)
if (this.checkOnChangeValidity(options)) {
if (options.type === 'layer') {
this.map.removeLayer(options.id)
......@@ -209,25 +219,25 @@ export default {
}
},
changeSetFilterOptions(options) {
this.emitChangeEvent('filter')
this.emitChangeEvent(options.fn)
if (this.checkOnChangeValidity(options)) {
this.map.setFilter(options.layer, options.filter)
}
},
changeSetLayoutPropertyOptions(options) {
this.emitChangeEvent('setLayoutProperty')
this.emitChangeEvent(options.fn)
if (this.checkOnChangeValidity(options)) {
this.map.setLayoutProperty(options.layer, options.name, options.value)
}
},
changeSetPaintPropertyOptions(options) {
this.emitChangeEvent('setPaintProperty')
this.emitChangeEvent(options.fn)
if (this.checkOnChangeValidity(options)) {
this.map.setPaintProperty(options.layer, options.name, options.value)
}
},
changeEventOptions(options) {
this.emitChangeEvent('event')
this.emitChangeEvent(options.fn)
if (this.checkOnChangeValidity(options)) {
if (options.event === 'on') {
if (options.type === 'click') {
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论