组件指令系统开发文档
1. 概述
本文档针对 VideoMatrix(融屏矩阵)和 CMap(地图组件)的指令系统实现进行介绍。通过指令系统实现了组件与宿主应用的交互,使宿主应用能够控制组件的行为和状态。
2. 指令系统架构
2.1 核心概念
组件指令系统由三个核心部分组成:
- 指令定义:在组件的
index.ts文件中定义组件支持的指令,包括指令名称、描述、参数和执行方法 - 方法实现:在组件的
index.vue文件中通过defineExpose暴露实现指令的方法 - 指令注册:在组件的
index.ts文件中通过commands属性注册组件支持的指令
2.2 指令类型定义
在 src/package/index.d.ts 中定义了指令的基本类型:
// 组件指令类型
export interface CommandsType {
// 指令操作符
[key: string]: {
// 指令名称 `[组件类别].[操作]`
name: string,
// 指令描述
description: string,
// 指令参数
params: Input[] | Select[] | null,
// 指令实行的方法
execute: <T extends ComponentExpose>(instance: T, params?: any) => Promise<boolean>
}
}
export enum InputEnum {
text= 'text',
number= 'number',
select= 'select'
}
type Select = {
key: string
label: string
type: InputEnum.select
value: string | number
required: boolean
options: {label: string; value: string}[]
}
type Input = {
key: string
label: string
type: InputEnum.input | InputEnum.number
value: string | number
required: boolean
}每个指令包含三个关键属性:
name: 指令的唯一标识符,通常采用[组件类别].[操作]的命名格式description: 指令的功能描述execute: 执行指令的函数,接收组件实例和参数,返回执行结果
2.3 组件配置中的指令注册
在组件的配置中,通过 commands 属性注册组件支持的指令:
export const SomeComponentConfig: ConfigType = {
key: 'ComponentKey',
chartKey: 'VComponentKey',
conKey: 'VCComponentKey',
title: '组件标题',
chartFrame: ChartFrameEnum.COMMON,
commands: ComponentCommands // 注册组件指令
}3. VideoMatrix 组件指令实现
3.1 指令定义 (index.ts)
// 定义组件指令
export const ComponentCommands: CommandsType = {
// 打开动态表格弹窗
onOpen: {
name: 'Modal.onOpen',
description: '打开弹窗',
params: [
{ label: '经度', key: 'jd', value: '', required: true },
{ label: '纬度', key: 'wd', value: '', required: true }
],
// 执行组件内的方法
execute: async (instance: any, params: {jd: string|number, wd: string|number}) => {
try {
await instance.open({
jd: params.jd,
wd: params.wd
})
return true
} catch (error) {
console.error(error)
return false
}
}
},
onClose: {
name: 'Modal.onClose',
description: '关闭弹窗',
params: null,
// 执行组件内的方法
execute: async (instance: any) => {
try {
const result = await instance.close()
return true
} catch (error) {
console.error(error)
return false
}
}
}
}3.2 方法实现 (index.vue)
// 暴露组件方法,供指令系统调用
defineExpose<ComponentExpose>({
// 关闭弹窗方法
close,
// 打开弹窗方法
open,
// 获取组件状态
getState: () => {
return {
state: {
jd: params.value.jd,
wd: params.value.wd,
groupIndex: groupIndex.value,
gridNum: gridNum.value,
videoDataCount: videoData.value.length,
isOpen: show.value
},
renderText: `
当前经纬度: 经度${params.value.jd}/纬度${params.value.wd}\n
当前组: ${groupIndex.value}\n
当前宫格数:${gridNum.value}\n
摄像头路数:${videoData.value.length} \n
`
}
}
})4. CMap 组件指令实现
4.1 指令定义 (index.ts)
// 定义组件指令
export const ComponentCommands = {
// 切换地图底图
onTier: {
name: 'Map.onTier',
description: '切换地图底图',
// 参数
params: [
{
key: 'tier',
label: '地图底图',
type: InputEnum.select,
value: '',
required: true,
options: []
}
],
// 执行组件内的方法
execute: async (instance: any, params: RasterType) => {
try {
const result = await instance.changeMapTier(params)
return true
} catch (error) {
console.error(error)
return false
}
}
},
// 调整地图视角
onViewPitch: {
name: 'Map.onViewPitch',
description: '调整地图视角',
// 参数
params: [
{
key: 'pitch',
label: '地图视角',
type: InputEnum.number,
value: '',
required: true
}
],
execute: async (instance: any, params: CesiumViewOptions) => {
try {
const result = await instance.changePitch(params)
return true
} catch (error) {
console.error(error)
return false
}
}
},
// 调整地图缩放层级
onViewZoom: {
name: 'Map.onViewZoom',
description: '调整地图缩放层级',
// 参数
params: [
{
key: 'zoom',
label: '地图缩放',
type: InputEnum.number,
value: '',
required: true
}
],
execute: async (instance: any, params: CesiumViewZoomOptions) => {
try {
const result = await instance.changeZoom(params)
return true
} catch (error) {
console.error(error)
return false
}
}
},
// 全局数据更新
updateScope: {
name: 'Global.updateScope',
description: '全局数据更新',
// 参数
params: [
{
key: 'scope',
label: '全局数据',
type: InputEnum.select,
value: '',
required: true,
options: []
}
],
execute: async (instance: any, params: GlobalScope) => {
try {
const result = await instance.setScope(params)
return true
} catch (error) {
console.error(error)
return false
}
}
}
}4.2 方法实现 (index.vue)
在组件的 index.vue 文件中,通过 Vue 的 defineExpose 方法暴露组件内部方法,供指令系统调用。这些方法实现了 ComponentExpose 接口,该接口定义在 src/package/index.d.ts 中:
/**
* 组件必须暴露的方法接口
*/
export interface ComponentExpose {
/**
* 获取组件当前状态
* 此方法返回组件的当前状态信息,包括:
* - state: 结构化的状态数据,可用于程序化处理
* - renderText: 可读性好的文本描述,展示给用户
*/
getState: () => {
state: any;
renderText: string;
};
// 组件可以根据需要暴露其他方法
[key: string]: any;
}4.2.1 组件实例接口
为了提高类型安全性和开发体验,我们应该为每个组件创建一个扩展自 ComponentExpose 的接口,用于约束组件实例的类型。这样在指令系统中使用组件实例时,可以获得更好的类型提示和错误检查。
创建组件实例接口的步骤:
- 在组件的
index.ts文件中,创建一个接口,继承ComponentExpose - 在接口中定义组件通过
defineExpose暴露的所有方法 - 在指令的
execute方法中使用该接口替代any类型
示例:VideoMatrix 组件实例接口
/**
* VideoMatrix组件实例接口,定义组件暴露的方法
*/
interface VideoMatrixInstance extends ComponentExpose {
/**
* 打开弹窗方法
* @param option 包含经纬度的参数对象
*/
open(option: { jd: string|number, wd: string|number }): Promise<boolean>
/**
* 关闭弹窗方法
*/
close(): void
}
// 在指令的 execute 方法中使用该接口
execute: async (instance: VideoMatrixInstance, params: {jd: string|number, wd: string|number}) => {
try {
await instance.open({
jd: params.jd,
wd: params.wd
})
return true
} catch (error) {
console.error(error)
return false
}
}示例:CMap 组件实例接口
/**
* CMap组件实例接口,定义组件暴露的方法
*/
interface CMapInstance extends ComponentExpose {
/**
* 切换地图底图类型
* @param rasterType 底图类型
*/
changeMapTier(rasterType: RasterType): void
/**
* 修改地图缩放级别
* @param options 缩放选项
*/
changeMapZoom(options: CesiumViewZoomOptions): void
/**
* 修改地图视角
* @param options 视角选项
*/
changeMapPitch(options: CesiumViewOptions): void
/**
* 设置全局范围
* @param params 全局范围参数
*/
setScope(params: GlobalScope): void
}
// 在指令的 execute 方法中使用该接口
execute: async (instance: CMapInstance, params: RasterType) => {
try {
await instance.changeMapTier(params)
return true
} catch (error) {
console.error(error)
return false
}
}使用组件实例接口的好处:
- 类型安全:避免使用
any类型,减少运行时错误 - 代码提示:IDE 可以提供方法名和参数的自动补全
- 文档化:接口定义本身就是一种文档,帮助开发者理解组件暴露的方法
- 重构支持:当组件方法发生变化时,可以立即发现类型不匹配问题
// 暴露组件方法,供指令系统调用
defineExpose<ComponentExpose>({
// 切换地图底图
changeMapTier: (rasterType: RasterType) => {
return mapRef.value?.changeMapTier(rasterType)
},
// 调整地图缩放
changeMapZoom: (options: CesiumViewZoomOptions) => {
return mapRef.value?.changeMapZoom(options)
},
// 调整地图视角
changePitch: (options: CesiumViewOptions) => {
return mapRef.value?.changePitch(options)
},
// 直接设置变量
setScope: (scope: GlobalScope) => {
return mapRef.value?.setScope(scope)
},
// 获取组件状态
getState: () => {
return {
state: {
// 地图组件状态信息
},
renderText: `地图组件状态信息`
}
}
})5. 指令调用流程
5.1 完整流程
注册阶段:
- 组件在配置中注册支持的指令
- 指令通过
commands属性暴露给宿主应用
调用阶段:
- 宿主应用获取组件实例
- 宿主应用通过指令名称和参数调用组件指令
- 指令的
execute函数接收组件实例和参数 - 组件内部执行相应的方法(通过
defineExpose暴露) - 返回执行结果(成功/失败)
错误处理:
- 所有指令执行都包含 try-catch 错误处理
- 执行失败时记录错误并返回 false
5.2 示例调用
// 宿主应用调用组件指令示例
const componentInstance = getComponentInstance('VideoMatrix');
const result = await executeCommand(
componentInstance,
'onOpen',
{ jd: 120.1, wd: 30.2 }
);
if (result) {
console.log('指令执行成功');
} else {
console.error('指令执行失败');
}6. 资源路径处理
为了确保组件在远程加载时能够正确加载静态资源,实现了资源路径处理机制:
6.1 资源路径工具函数
在 src/package/public/assetUtils.ts 中实现了资源路径处理函数:
// 资源路径处理函数
export function fixAssetPath(relativePath: string): string {
// 如果已经是绝对路径或数据URL,则直接返回
if (relativePath.startsWith('http') || relativePath.startsWith('data:')) {
return relativePath;
}
// 开发环境下直接使用相对路径
if (process.env.NODE_ENV === 'development') {
return relativePath;
}
// 获取组件基础路径
const basePath = getComponentBasePath();
// 处理相对路径
let assetPath = relativePath;
// 移除开头的 ./ 或 ../
if (assetPath.startsWith('./')) {
assetPath = assetPath.substring(2);
} else if (assetPath.startsWith('../')) {
// 对于 ../component/ComponentName/version/assets/file.png 格式的路径
// 直接提取 assets/file.png 部分
const parts = assetPath.split('/');
assetPath = parts.slice(-2).join('/');
}
// 提取文件名部分
const lastSlashIndex = assetPath.lastIndexOf('/');
if (lastSlashIndex !== -1) {
const directory = assetPath.substring(0, lastSlashIndex + 1);
const filename = assetPath.substring(lastSlashIndex + 1);
// 获取带哈希的文件名
const hashedFilename = getHashedFilename(filename);
// 拼接完整路径
return `${basePath}/${directory}${hashedFilename}`;
}
// 如果没有目录部分,直接处理文件名
const hashedFilename = getHashedFilename(assetPath);
return `${basePath}/${hashedFilename}`;
}6.2 组件中的使用
在组件中,使用封装的 getImagePath 函数来处理资源路径:
// 资源路径处理函数
const getImagePath = (filename: string) => {
// 开发环境下使用导入的资源
if (process.env.NODE_ENV === 'development') {
switch (filename) {
case 'checked1.png': return checked1Img;
case 'checked4.png': return checked4Img;
// ...其他资源
default: return '';
}
}
// 生产环境下使用动态路径
return fixAssetPath(`assets/${filename}`);
}7. 最佳实践
7.1 命名规范
- 指令名称采用
[组件类别].[操作]格式,如Modal.onOpen、Map.onViewZoom - 保持命名的一致性和可读性
- 方法名应与指令名保持语义一致性
7.2 参数类型
- 为指令参数定义明确的类型
- 在类型定义中提供参数的详细说明
- 使用接口或类型别名定义复杂参数结构
7.3 异步处理
- 所有指令执行函数都是异步的,返回 Promise
- 使用 async/await 简化异步代码
- 确保异步操作有适当的超时处理
7.4 错误处理
- 捕获并记录所有可能的错误
- 提供有意义的错误信息
- 确保错误不会导致组件崩溃
7.5 状态管理
- 实现
getState方法,提供组件当前状态的快照 - 状态信息应包含可读的文本描述和结构化数据
- 确保状态信息不包含敏感数据
8. 总结
VideoMatrix 和 CMap 组件通过指令系统实现了与宿主应用的交互。指令系统的核心是在 index.ts 中定义指令,在 index.vue 中通过 defineExpose 暴露实现方法,并在组件配置中注册指令。这种架构使宿主应用能够灵活控制组件的行为,实现更丰富的交互体验。
同时,实现了资源路径处理机制,确保组件在远程加载时能够正确加载静态资源,支持开发环境和生产环境的无缝切换。