Skip to content

组件指令系统开发文档

1. 概述

本文档针对 VideoMatrix(融屏矩阵)和 CMap(地图组件)的指令系统实现进行介绍。通过指令系统实现了组件与宿主应用的交互,使宿主应用能够控制组件的行为和状态。

2. 指令系统架构

2.1 核心概念

组件指令系统由三个核心部分组成:

  1. 指令定义:在组件的 index.ts 文件中定义组件支持的指令,包括指令名称、描述、参数和执行方法
  2. 方法实现:在组件的 index.vue 文件中通过 defineExpose 暴露实现指令的方法
  3. 指令注册:在组件的 index.ts 文件中通过 commands 属性注册组件支持的指令

2.2 指令类型定义

src/package/index.d.ts 中定义了指令的基本类型:

typescript
// 组件指令类型
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 属性注册组件支持的指令:

typescript
export const SomeComponentConfig: ConfigType = {
  key: 'ComponentKey',
  chartKey: 'VComponentKey',
  conKey: 'VCComponentKey',
  title: '组件标题',
  chartFrame: ChartFrameEnum.COMMON,
  commands: ComponentCommands // 注册组件指令
}

3. VideoMatrix 组件指令实现

3.1 指令定义 (index.ts)

typescript
// 定义组件指令
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)

typescript
// 暴露组件方法,供指令系统调用
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)

typescript
// 定义组件指令
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 中:

typescript
/**
 * 组件必须暴露的方法接口
 */
export interface ComponentExpose {
  /**
   * 获取组件当前状态
   * 此方法返回组件的当前状态信息,包括:
   * - state: 结构化的状态数据,可用于程序化处理
   * - renderText: 可读性好的文本描述,展示给用户
   */
  getState: () => {
    state: any;
    renderText: string;
  };
  
  // 组件可以根据需要暴露其他方法
  [key: string]: any;
}

4.2.1 组件实例接口

为了提高类型安全性和开发体验,我们应该为每个组件创建一个扩展自 ComponentExpose 的接口,用于约束组件实例的类型。这样在指令系统中使用组件实例时,可以获得更好的类型提示和错误检查。

创建组件实例接口的步骤:

  1. 在组件的 index.ts 文件中,创建一个接口,继承 ComponentExpose
  2. 在接口中定义组件通过 defineExpose 暴露的所有方法
  3. 在指令的 execute 方法中使用该接口替代 any 类型

示例:VideoMatrix 组件实例接口

typescript
/**
 * 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 组件实例接口

typescript
/**
 * 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
  }
}

使用组件实例接口的好处:

  1. 类型安全:避免使用 any 类型,减少运行时错误
  2. 代码提示:IDE 可以提供方法名和参数的自动补全
  3. 文档化:接口定义本身就是一种文档,帮助开发者理解组件暴露的方法
  4. 重构支持:当组件方法发生变化时,可以立即发现类型不匹配问题
typescript
// 暴露组件方法,供指令系统调用
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 完整流程

  1. 注册阶段:

    • 组件在配置中注册支持的指令
    • 指令通过 commands 属性暴露给宿主应用
  2. 调用阶段:

    • 宿主应用获取组件实例
    • 宿主应用通过指令名称和参数调用组件指令
    • 指令的 execute 函数接收组件实例和参数
    • 组件内部执行相应的方法(通过 defineExpose 暴露)
    • 返回执行结果(成功/失败)
  3. 错误处理:

    • 所有指令执行都包含 try-catch 错误处理
    • 执行失败时记录错误并返回 false

5.2 示例调用

javascript
// 宿主应用调用组件指令示例
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 中实现了资源路径处理函数:

typescript
// 资源路径处理函数
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 函数来处理资源路径:

typescript
// 资源路径处理函数
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.onOpenMap.onViewZoom
  • 保持命名的一致性和可读性
  • 方法名应与指令名保持语义一致性

7.2 参数类型

  • 为指令参数定义明确的类型
  • 在类型定义中提供参数的详细说明
  • 使用接口或类型别名定义复杂参数结构

7.3 异步处理

  • 所有指令执行函数都是异步的,返回 Promise
  • 使用 async/await 简化异步代码
  • 确保异步操作有适当的超时处理

7.4 错误处理

  • 捕获并记录所有可能的错误
  • 提供有意义的错误信息
  • 确保错误不会导致组件崩溃

7.5 状态管理

  • 实现 getState 方法,提供组件当前状态的快照
  • 状态信息应包含可读的文本描述和结构化数据
  • 确保状态信息不包含敏感数据

8. 总结

VideoMatrix 和 CMap 组件通过指令系统实现了与宿主应用的交互。指令系统的核心是在 index.ts 中定义指令,在 index.vue 中通过 defineExpose 暴露实现方法,并在组件配置中注册指令。这种架构使宿主应用能够灵活控制组件的行为,实现更丰富的交互体验。

同时,实现了资源路径处理机制,确保组件在远程加载时能够正确加载静态资源,支持开发环境和生产环境的无缝切换。

基于 MIT 许可发布