提交 198eb1d0 作者: test

feat: 优化 Mapbox 组件设计

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