低代码:如何实现不同组件间的级联通信(进阶版)
前言
上文里,我们使用事件触发器,实现组件间的级联通信,这种通信方式缺点如下所示:
- 事件触发时,需指明事件参数
- 随着项目的扩大,代码会变得越来越难以维护
基于上述考虑,我参考了另一开源代码的方案,重构了组件间的级联方案,大致方案步骤如下:
- 存储页面正在编辑的所有组件至
chartEditStore
内; - 当前组件
A
,添加事件eventA
时,选择联动组件B
; - 在事件
eventA
内,将组件A
选择的值绑定到组件B
的接口请求参数内; - 联动组件
B
接收到变化后的请求参数,重新请求接口,便能实现组件间的联动刷新效果
编辑效果如下图所示:
左侧组件为A
,右侧组件为B
存储编辑的组件
在组件拖拽或双击时,将其添加到数组componentList
内
创建chartEditStore.ts
,在内部增加addComponentList
方法,存储新增的组件列表
/**
* * 新增组件列表
* @param componentInstance 新图表实例
* @param isHead 是否头部插入
* @param isHistory 是否进行记录
* @returns
*/
addComponentList(
componentInstance:
| CreateComponentType
| CreateComponentGroupType
| Array<CreateComponentType | CreateComponentGroupType>,
isHead = false,
isHistory = false
): void {
if (componentInstance instanceof Array) {
componentInstance.forEach(item => {
this.addComponentList(item, isHead, isHistory)
})
return
}
if (isHistory) {
chartHistoryStore.createAddHistory([componentInstance])
}
if (isHead) {
this.componentList.unshift(componentInstance)
return
}
this.componentList.push(componentInstance)
}
在getters
增加getComponentList
方法,获取到所有的组件列表信息
getComponentList(): Array<CreateComponentType | CreateComponentGroupType> {
return this.componentList
}
选择联动组件
点击增加联动绑定
在组件A
内的点击事件内,增加联动绑定功能,如下代码所示:
<template>
<div
:class="['right-item', conActive == index ? 'active' : '']"
v-for="(item, index) in dirList.children"
:key="index"
@click="onClickDown(item, index)"
>
{{ item.name }}
</div>
</template>
<script lang="ts" setup>
import { useChartEditStore } from '@/store/modules/chartEditStore/chartEditStore'
import { useChartInteract } from '@/hooks'
const onClickDown = (item: any, index: number) => {
const { key } = item
// 存储到联动数据
useChartInteract(
props.chartConfig,
useChartEditStore,
{ [ComponentInteractParamsEnum.DATA]: key },
InteractEventOn.CLICK
)
}
</script>
在useChartInteract
方法内,我们将变化后的key
值赋值到B
组件的请求参数Params
内,它的各个参数的含义如下所示:
props.chartConfig
:组件A
的配置信息,我们将该组件所有的事件信息配置在该参数的events
属性内,我们将联动事件定义为interactEvents
,它的默认初始值[]
,但在组件A
内,我们给这其添加了click
事件,代码如下所示:
// 定义组件触发回调事件
export const interactActions: InteractActionsType[] = [
{
interactType: InteractEventOn.CLICK,
interactName: '点击',
componentEmitEvents: {
[ComponentInteractEventEnum.DATA]: [
{
value: ComponentInteractParamsEnum.DATA,
label: '类型'
}
]
}
}
]
useChartEditStore
:存储当前编辑页面所有的组件列表{ [ComponentInteractParamsEnum.DATA]: key }
:ComponentInteractParamsEnum.DATA
的值为字符串data
,该参数可简单理解为{'data': key}
,后期组件B
的请求参数重新赋值,来源于该对象的key
值InteractEventOn.CLICK
:即为事件类型,这里默认指字符串click
useChartInteract
代码的具体功能,我们在第三节的内容里进行分析
增加组件交互配置功能
在右侧事件编辑面板,增加组件交互配置功能,部分代码如下
<template>
<n-collapse-item title="组件交互" name="1" v-if="interactActions.length">
...
<setting-item-box name="触发事件" :alone="true">
<n-input-group v-if="interactActions">
<n-select
class="select-type-options"
v-model:value="item.interactOn"
size="tiny"
:options="interactActions"
/>
</n-input-group>
</setting-item-box>
<setting-item-box :alone="true">
...
<n-text>绑定</n-text>
...
<n-select
class="select-type-options"
value-field="id"
label-field="title"
size="tiny"
filterable
placeholder="仅展示符合条件的组件"
v-model:value="item.interactComponentId"
:options="fnEventsOptions()"
/>
</setting-item-box>
</n-collapse-item>
</template>
interactActions
即我们之前配置在组件A
内的联动事件信息,我们只配置了一个click
事件,故在触发事件的下拉选择框内,我们只看到了点击
类型,如下图所示:
fnEventsOptions
过滤非自己等信息,只显示其他组件,代码如下所示:
// 绑定组件列表
const fnEventsOptions = (): Array<SelectOption | SelectGroupOption> => {
const filterOptionList = chartEditStore.componentList.filter(item => {
// 排除自己
const isNotSelf = item.id !== targetData.value.id
// 排除静态组件
const isNotStatic = item.chartConfig.chartFrame !== ChartFrameEnum.STATIC
// 排除分组
const isNotGroup = !item.isGroup
return isNotSelf && isNotStatic && isNotGroup
})
return filterOptionList
选择了绑定组件B
后,我们将它的componentId
存储到interactComponentId
内,代码为v-model:value="item.interactComponentId"
编辑页面,除了组件A
,便是组件B
,故绑定下拉列表里,只有组件B
选项
绑定值至联动组件的请求参数内
让我们一起看下useChartInteract
内的代码
// Params 参数修改触发 api 更新图表请求
export const useChartInteract = (
chartConfig: CreateComponentType,
useChartEditStore: ChartEditStoreType,
param: { [T: string]: any },
interactEventOn: string
) => {
const chartEditStore = useChartEditStore()
const { interactEvents } = chartConfig.events
const fnOnEvent = interactEvents.filter(item => {
return item.interactOn === interactEventOn
})
if (fnOnEvent.length === 0) return
fnOnEvent.forEach(item => {
const index = chartEditStore.fetchTargetIndex(item.interactComponentId)
if (index === -1) return
const component = chartEditStore.componentList[index]
const { Params, Header } = toRefs(component!.request.requestParams)
// 处理管理组件请求参数
Object.keys(item.interactFn).forEach(key => {
if (Params.value[key]) {
Params.value[key] = param[item.interactFn[key]] || Params.value[key]
}
if (Header.value[key]) {
Header.value[key] = param[item.interactFn[key]]
}
})
})
}
我们从组件A
的chartConfig
的events
属性内,拿到interactEvents
事件数组,遍历该数组,找到类型为click(即interactEventOn参数的值)
的事件fnOnEvent
对fnOnEvent
进行遍历,从chartEditStore
找到组件ID为interactComponentId
的组件,通过toRefs
从组件内的requestParams
得到当前组件B
的参数配置信息,使用toRefs
获得到的Params
、Header
具有响应式效果,即值变化,会触发请求
将函数参数param
对象{'data': key}
中的key
值赋值给组件B
的Params
内对应的参数上,组件B
的接口请求配置,如下图所示:
参数变化,重新请求接口
在组件B
内增加如下方法
<template>
<v-chart ref="vChartRef" :init-options="initOptions" :theme="themeColor" :option="option.value" :manual-update="isPreview()" autoresize>
</v-chart>
</template>
<script setup lang="ts">
import VChart from 'vue-echarts'
import { useChartDataFetch } from '@/hooks'
const { vChartRef } = useChartDataFetch(props.chartConfig, useChartEditStore)
</script>
我们通过useChartDataFetch
方法实现接口请求,接下来,看看这个方法做了什么呢?
/**
* setdata 数据监听与更改
* @param targetComponent
* @param useChartEditStore 若直接引会报错,只能动态传递
* @param updateCallback 自定义更新函数
*/
export const useChartDataFetch = (
targetComponent: CreateComponentType,
useChartEditStore: ChartEditStoreType,
updateCallback?: (...args: any) => any
) => {
// eCharts 组件配合 vChart 库更新方式
const echartsUpdateHandle = (dataset: any) => {
if (chartFrame === ChartFrameEnum.ECHARTS) {
if (vChartRef.value) {
setOption(vChartRef.value, { dataset: dataset })
}
}
}
const requestIntervalFn = () => {
const chartEditStore = useChartEditStore()
// 全局数据
const {
requestOriginUrl,
requestIntervalUnit: globalUnit,
requestInterval: globalRequestInterval
} = toRefs(chartEditStore.getRequestGlobalConfig)
// 目标组件
const {
requestDataType,
requestUrl,
requestIntervalUnit: targetUnit,
requestInterval: targetInterval
} = toRefs(targetComponent.request)
// 非请求类型
if (requestDataType.value !== RequestDataTypeEnum.AJAX) return
try {
// 处理地址
// @ts-ignore
if (requestUrl?.value) {
// requestOriginUrl 允许为空
const completePath = requestOriginUrl && requestOriginUrl.value + requestUrl.value
if (!completePath) return
clearInterval(fetchInterval)
const fetchFn = async () => {
const res = await customizeHttp(toRaw(targetComponent.request), toRaw(chartEditStore.getRequestGlobalConfig), toRaw(targetComponent.id))
if (res) {
try {
const filter = targetComponent.filter
const { data } = res
echartsUpdateHandle(newFunctionHandle(data, res, filter))
// 更新回调函数
if (updateCallback) {
updateCallback(newFunctionHandle(data, res, filter))
}
} catch (error) {
console.error(error)
}
}
}
// 普通初始化与组件交互处理监听
watch(
() => targetComponent.request,
() => {
fetchFn()
},
{
immediate: true,
deep: true
}
)
// 定时时间
const time = targetInterval && targetInterval.value ? targetInterval.value : globalRequestInterval.value
// 单位
const unit = targetInterval && targetInterval.value ? targetUnit.value : globalUnit.value
// 开启轮询
if (time) fetchInterval = setInterval(fetchFn, intervalUnitHandle(time, unit))
}
// eslint-disable-next-line no-empty
} catch (error) {
console.log(error)
}
}
if (isPreview()) {
// 判断是否是数据池类型
targetComponent.request.requestDataType === RequestDataTypeEnum.Pond
? addGlobalDataInterface(targetComponent, useChartEditStore, updateCallback || echartsUpdateHandle)
: requestIntervalFn()
}
return { vChartRef }
}
代码有些长,让我们自动忽略数据池类型
判断,上面的方法定义了requestIntervalFn
方法,并在最后执行了它,当轮询时间time
为0时,只在图表初始化时请求,若time
非0,则会根据设置的time
时间轮询请求接口
在requestIntervalFn
方法内,定义了fetchFn
方法,在该方法内部customizeHttp
处理接口请求,监听targetComponent.request
变化,即组件B
的参数变化,重新执行fetchFn
方法,实现接口再次调用
最终,实现的效果图如下所示:
可以看到每次点击,会看到两次接口请求,第一次请求类型为preflight
,即浏览器的预检请求,属于options请求。该请求会在浏览器认为即将要执行的请求可能会对服务器造成不可预知的影响时,由浏览器自动发出。
由于接口地址跨域,所以报了CORS
错误信息,但这些并不重要,我们可以清楚地看到随着组件A
的点击,请求的level
参数值一直在变化。
若大家有更好的办法,欢迎在评论区内留言~~