mars3d在vue3.0生态上开发了两个gis相关开源项目 mars3d-vue-example 和 mars3d-vue-project,在这两个项目中widget都是非常重要的一个模块。通过widget可以在复杂的场景下非常清晰的管理功能模块之间的互斥关系,管理内存,完成不同的功能模块之间的解耦,并且可以实现功能模块之间的交互。下面就来介绍一下,widget模块的使用。
前置依赖: 使用widget模块,则必须依赖vuex,通过vuex实现widget管理、交互等。
widget相关的代码一般会被放置在src/widgets
目录下,按照功能或者业务来新建子目录管理widget代码,一般每个目录下的 index.vue 就是此widget的入口,通常也会包括一个map.ts文件用于编写地图相关代码。在某些特殊的场景下,也会将多个vue文件放置在同一个widget目录下,方便共享map.ts的命名空间,后面会针对这些特殊场景具体介绍。下面是一个标准的widget目录位置和结构
创建好widget之后,下面就应该来配置widget,标准写法是在入口main.ts 同级新建widget-store.ts 文件,下面这个示例演示了这个文件的基本结构和一个widget的配置
import { defineAsyncComponent, markRaw } from "vue"
import { WidgetState } from "@mars/common/store/widget"
import { StoreOptions } from "vuex"
const store: StoreOptions<WidgetState> = {
state: {
// 自定义widget的默认值
defaultOption: {},
// widgets 配置列表
widgets: [
{
// required widget使用的异步组件
component: markRaw(defineAsyncComponent(() => import("@mars/widgets/demo/menu/index.vue"))),
// required widget的唯一标识
name: "menu",
// 是否可以被自动关闭 default true
autoDisable: false,
// 此widget打开时,是否自动关闭其他可关闭的widget default true
disableOther: false,
/*
* widget分组, widget打开时会强制关闭所有同组的其他widget
* 优先级高于 disableOther autoDisable
*/
group: "demo"
}
],
// 默认打开的widget
openAtStart: ["menu"]
}
}
export default store
WidgetState接口定义如下
// 为 store state 声明类型
export interface DefaultOption {
autoDisable?: boolean
disableOther?: boolean | string[]
group?: string // group相同的widget一定是互斥的
meta?: any // 额外参数 不会在每次关闭后清除
}
export interface Widget {
name: string // 唯一标识
key?: string // 作为vue diff 环节的key,用于控制组件重载
component?: any // widget关联的异步组件
autoDisable?: boolean // 是否能够被自动关闭
disableOther?: boolean | string[] // 是否自动关闭其他widget,或通过数组指定需要被关闭的widget
group?: string // group相同的widget一定是互斥的
visible?: boolean // 显示隐藏
data?: any // 额外传参 会在每次关闭后清除
meta?: any // 额外参数 不会在每次关闭后清除
}
export interface WidgetState {
widgets: Widget[] // widget具体配置
openAtStart: string[] // 默认加载的widget
defaultOption?: DefaultOption // 支持配置默认参数
}
还需要在main.ts中初始化配置
import { createApp } from "vue"
import Application from "./App.vue"
import { injectState, key } from "@mars/common/store/widget"
import { store as testStore, key as testKey } from "@mars/common/store/test"
import store from "./widget-store"
const app = createApp(Application)
// 此处传入key是为了让widget的store有自己的独立命名空间,不影响项目中的其他store
app.use(injectState(store), key)
// 项目中的其他store
app.use(testStore, testKey)
app.mount("#app")
完成上述配置工作之后,就可以编写具体的widget代码了,widget本身其实就是一个vue组件,原则上只负责ui相关的展示和逻辑,地图相关的代码新建map.ts文件进行逻辑上的解耦,widget提供了api来完成组件和map的交互。下面是一个标准的 widget组件的基本结构。
<template>
<mars-button class="small-btn" @click="onDraw">绘制</mars-button>
</template>
<script setup lang="ts">
import { onUnmounted, ref } from "vue"
import useLifecycle from "@mars/common/uses/use-lifecycle"
import * as mapWork from "./map"
// 激活map.ts生命周期
useLifecycle(mapWork)
const onClick = () => {
// 调用 map.ts 中的方法
mapWork.onDraw()
}
// 监听 map.ts 中抛出的事件,基于 mars3d.BaseClass 类实现
mapWork.eventTarget.on("drawExtent", function (event: any) {
// 此处用于处理事件
console.log(event)
})
onUnmounted(() => {
// 销毁操作
})
</script>
<style lang="less"></style>
上面代码中的 useLifecycle 是一个比较重要的api,他接受map.ts 的导出模块作为参数,建立了vue和map.ts生命周期的关联。可用的map.ts 可用的生命周期为 onMounted 和 onUnmounted,map.ts 标准结构如下。
import * as mars3d from "mars3d"
let map: mars3d.Map // 地图对象
// 事件对象,用于抛出事件给vue
export const eventTarget = new mars3d.BaseClass()
// 初始化当前业务
export function onMounted(mapInstance: mars3d.Map): void {
map = mapInstance // 记录map
}
// 释放当前业务
export function onUnmounted(): void {
map.graphicLayer.clear()
eventTarget = null
map = null
}
// 绘制矩形(演示map.js与index.vue的交互)
export function onDraw(): void {
map.graphicLayer.clear()
// 绘制矩形
map.graphicLayer.startDraw({
type: "rectangle",
style: {
fill: true,
color: "rgba(255,255,0,0.2)",
outline: true,
outlineWidth: 2,
outlineColor: "rgba(255,255,0,1)"
},
success: function (graphic: mars3d.graphic.RectangleEntity) {
const rectangle = graphic.getRectangle({ isFormat: true })
// 抛出事件可以在vue中进行监听
eventTarget.fire("drawExtent", { extent: JSON.stringify(rectangle) })
}
})
}
到此我们就初步了解了widget的使用,继续来看一下widget在各种不同的场景下的使用。具体包括和marsui搭配使用、参数配置、动态传参、api、多个根组件场景的处理、widget间的动态交互等。
在学习具体业务场景的使用之前,要先明确一个概念,widget本身并不会负责ui方面的任何事情,总体来说,widget只做了三件事,1. 对ui部分和map部分进行解耦,关联生命周期。2. 对widget各种行为的封装,通过简单的配置和api,完成widget各种交互。 3. 支持对prop的配置和动态修改。当widget的这些特性结合到marsui中的pannel或者dialog使用时,就能够轻松的应对一些复杂场景。
- 搭配marsui使用
widget的入口是一个vue单文件组件,在这个组件的模板中我们可以写任何的html或者没有html,在gis的场景中经常需要一些操作面板,这些就可以在组件的模板中来编写这些代码,这个时候又免不了需要去封装一些组件,幸运的是mars-ui恰好解决了这个问题。在火星科技的vue项目中,mars-dialog 和 mars-pannel 通常都会搭配widget一起使用来实现这种操作面板的场景。下面是一个简单的示例
<template>
<!-- 这是一个宽 370px 位于左上角的固定面板 -->
<mars-pannel width="370" left="10" top="10">
<a-space>
<mars-button>按钮1</mars-button>
<mars-button>按钮2</mars-button>
<mars-button>按钮3</mars-button>
</a-space>
</mars-pannel>
</template>
<template>
<!-- 这是一个宽 370px 位于左上角的可移动可缩放的面板 -->
<mars-dialog width="370" left="10" top="10">
<a-space>
<mars-button>按钮1</mars-button>
<mars-button>按钮2</mars-button>
<mars-button>按钮3</mars-button>
</a-space>
</mars-dialog>
</template>
有一个细节,上面的示例并没有去传入visible参数,那么pannel或者dialog是如何显示的呢,这里就涉及到组件的属性继承。widget已经在外部传入了visible属性,因为两个示例都只存在一个根组件,所以就自动继承的visible属性。我们也可以手动的进行属性继承,后面会这对这种场景具体分析。
- 参数配置
WidgetState 接口定义了widget相关的配置参数,具体可参考上一篇文档,(下文中提到的defaultOption、 meta、data等概念也请参看上一篇文档)。这里主要来介绍一下在widget中对ui控件的一些配置。首先在defaultOption中可以为所有的widget配置prop,写法如下:
// widget-store.ts
// 这里配置的参数会被合并到所有组件的prop中,组件中的相同属性将会被覆盖
defaultOption: {
meta: {
props: {
top: 50,
bottom: 50,
left: 50
}
}
}
除了defaultOption 也可以在每一个widget配置中传入自己的个性化参数,写法如下:
// widget-store.ts
// 这里配置的参数会被合并到本组件的prop中,组件和defaultOption中的相同属性都将会被覆盖
{
component: markRaw(defineAsyncComponent(() => import("your-widget.vue"))),
name: "your-widget",
meta: {
props: {
top: 50,
bottom: 50,
left: 50
}
}
}
以上都是通过配置的方式来设置组件的prop值,widget还支持通过动态调用的方式,进行prop的设置:
import { useWidget } from "@mars/common/store/widget"
const { activate, disable, getWidget } = useWidget()
// 激活 your-widget
activate({
name: "your-widget",
data: {
// 为这个组件设置prop,此处属性的优先级,高于所有配置中的属性
props: {
top: 50,
bottom: 50,
left: 50
}
}
})
})
- API
上面的示例中用到了 activate 这个api,用来激活widget并传递data,他通过useWidget 函数导出,useWidget提供了多个api来操作widget。
export const useWidget: () => {
// 本页面widget配置数组
widgets: ComputedRef<Widget[]>
// 默认开启的widget
openAtStart: ComputedRef<string[]>
// 获取指定的widget
getWidget: (name: string) => Widget
// 出发对应widget的onUpdate
updateWidget: (name: string, ...args: any[]) => viod
// 获取widget的当前激活状态
isActivate: (name: string) => boolean
// 激活指定 widget模块
activate: (widget: string | Widget, reload = true) => void
// 释放指定的widget
disable: (name: string) => void
// 关闭释放所有widget ,hasAll传true值强制释放所有widget(默认autoDisable为false的widet不会释放)
disableAll: (hasAll?: boolean) => void
}
- 多个根组件的场景,假设一个widget的组件像下面这样。
<template>
<mars-pannel :width="305" left="15" top="170" bottom="40">
</mars-pannel>
<mars-pannel :width="340" right="10" top="10" bottom="40">
</mars-pannel>
</template>
此时会发现,这个widget激活之后无法在页面中显示,这是因为这里有两个根组件,此时vue无法自动继承prop,所以需要我们手动的来完成这个继承的操作,所以需要改成下面这样。
<template>
<mars-pannel :width="305" left="15" top="170" bottom="40" v-bind="attrs">
</mars-pannel>
<mars-pannel :width="340" right="10" top="10" bottom="40" v-bind="attrs">
</mars-pannel>
</template>
<script setup lang="ts">
import { useAttrs } from "vue"
const attrs = useAttrs()
</script>
这种方式就可以实现具有多个面板的widget,我们也可以根据自身的需求来确定让哪些面板来继承外部的props。甚至在只有一个面板时我们也可以对外部的props做一些自定义的处理。只需要再加上inheritAttrs: false 来禁用继承。
// 这里禁用了属性继承,并且将内联的prop优先级提升到最高。
<template>
<mars-pannel v-bind="attrs" :width="305" left="15" top="170" bottom="40">
</mars-pannel>
</template>
<script setup lang="ts">
import { useAttrs } from "vue"
const attrs = useAttrs()
</script>
<script lang="ts">
export default {
inheritAttrs: false
}
</script>
- widget间的交互
一些复杂的场景中可能会需要两个widget之间有一些数据的传递,这里是通过updateWidget来实现的。
// widget1
<template>
<mars-pannel width="370" left="10" top="10">
<mars-button @click="change">按钮1</mars-button>
</mars-pannel>
</template>
<script setup lang="ts">
import { useWidget } from "@mars/common/store/widget"
const { updateWidget } = useWidget()
const change = () => {
updateWidget("widget2", {
/* 传递参数,这里的参数将会被处理成响应式数据,如果需要传递复杂对象,请使用markRaw */
})
}
</script>
// widget2
<script setup lang="ts">
import { useWidget } from "@mars/common/store/widget"
const { getWidget } = useWidget()
const widget = getWidget("widget2") // 获取自身的widget
widget.onUpdate((...args: any[]) => {
// 监听 updateWidget 的调用
console.log(...args)
})
</script>
接下来就针对实际需求看一下widget本身的代码是如何编写的。我们将一起来完成一个widget的编写,其中会介绍到我们在开发项目时会用到的一些技巧以及遵循的规范。由于gis项目本身的特殊性,如果不想后续当业务变的复杂之后造成一些难以排查的bug或者性能问题,最好也和我们一样遵循这些简单的规范。
功能:我们接下来要实现的是底图切换的面板,具体功能如下图
下面我们来一步步的实现
- 创建文件
我们在widgets目录下新建一个目录 src/widgets/basic/manage-basemap
这里我们要求widget都统一存放在widgets下。然后创建 index.vue
文件作为入口。由于将要实现的功能,包括一些地图相关的操作,所以我们还需要创建 map.ts
文件用于编写这些代码。这两个文件基础的代码已经在前面的两篇中有详细的说明,这里不做赘述。
- 获取底图
获取地图这个操作本身很简单 map.getBasemap(true)
,但是需要考虑的是,我们需要通过map进行调用,所以这个操作需要放在 map.ts 文件中。这也是我们项目中的的另一个规范:只要是与mars3d相关的代码,我们都会放在 map.ts 中。看起来这个规范过于严苛,但是确实能够很好的帮我们将ui部分与地图部分进行接耦。我们初步的代码就变成了这样。
// map.ts
export function onMounted(mapInstance: mars3d.Map): void {
map = mapInstance // 记录map
const baseMaps = map.getBasemaps(true) // 获取底图
const hasTerrain = map.hasTerrain // 是否显示地形
}
- 接下来又面临一个问题,此时我们将地图与ui强制分离之后,ui部分该如何获取到 baseMaps 呢?此时就用到了我们之前说过的 eventTarget 了。代码很简单就直接放在下面了。
// map.ts
export const eventTarget = new mars3d.BaseClass()
export function onMounted(mapInstance: mars3d.Map): void {
map = mapInstance // 记录map
const baseMaps = map.getBasemaps(true) // 获取底图
const hasTerrain = map.hasTerrain // 是否显示地形
eventTarget.fire("mapLoaded", { baseMaps, hasTerrain }) // 触发了自定义事件
}
// index.vue
import { onUnmounted, ref, markRaw } from "vue"
import useLifecycle from "@mars/common/uses/use-lifecycle"
import * as mapWork from "./map"
const baseMaps = ref<any[]>([]) // 底图列表
const active = ref("") // 当前高亮的底图
const chkHasTerrain = ref(false) // 是否显示地形
mapWork.eventTarget.on("mapLoaded", initData)
onUnmounted(() => {
mapWork.eventTarget.off("mapLoaded", initData)
})
function initData(e: any) {
baseMaps.value = e.baseMaps.map((m: any) => {
if (m.isAdded && m.show) {
active.value = m.uuid
}
return {
name: m.name,
uuid: m.uuid,
options: markRaw(m.options)
}
})
chkHasTerrain.value = e.hasTerrain || false
}
上面的代码很好理解,不过有些细节还是要注意一下,请关注一下26行,可以看到这里是用了一个特殊的函数 markRaw。这是vue提供的一个函数,用于表示此对象不需要被vue进行响应式处理。这里的目的是mars3d本身会产生一些复杂的对象,比如map对象,矢量数据、图层等,这些复杂对象如果再次被vue进行一次响应式处理,将会带来极大的性能开销。
- 现在我们来实现,点击之后切换底图和切换地形,直接放代码。
// map.ts
export function changeBaseMaps(id: string) {
map.basemap = id
}
export function changeTerrain(value: boolean) {
map.hasTerrain = value
}
// index.vue
function changeBaseMaps(item: any) {
mapWork.changeBaseMaps((active.value = item.uuid))
}
function changeTerrain() {
mapWork.changeTerrain(chkHasTerrain.value)
}
- 目前就剩下最后一步,支持外部更新高亮底图,用于实现通过其他方式切换底图,自动更新高亮的图块的功能。这里就要使用到widget的一个api
widget.onUpdate
来实现。其实就像相当于一个事件的订阅者。代码如下。
// index.vue
const { getWidget } = useWidget()
const widget = getWidget("manage-map" /* widget的name */)
widget.onUpdate((e) => {
changeBaseMaps(e)
})
// 外部改变底图时
const { updateWidget } = useWidget()
const changeBaseMap = (item) => {
updateWidget("manage-map", item)
}
over~~~~