1. 前端插件
插件化手册
  • FH2 插件开发手册
    • 产品简介
    • 典型场景
    • 发版说明
    • 快速开始
      • 快速开始总览
      • 第一个插件
      • 后端插件
      • 全栈闭环
    • AI辅助开发
      • AI 开发 Skill
      • 安装与复验
      • 第一个工程
      • 真实需求示例:现场问题上报
      • 改造已有工程
      • 调试与交付
    • 开发指南
      • 开发指南
      • 基础概念
        • 基础概念总览
        • 什么是插件开发
        • 平台架构
        • 前端宿主、扩展点与能力
        • 后端运行时、入口与权限
        • 开发流程
        • 安全与隔离
        • 支持范围与问题反馈
      • 开发指南
        • 开发指南总览
        • 前端插件
          • 前端插件深度指南
          • 前端工程结构
          • 插件清单(fh2-plugin.json)
          • 入口与生命周期
          • 界面与状态
          • 多宿主插件
          • 跨宿主通信服务
          • HTTP、WebSocket 与事件
          • 存储、Blob 与定时器
          • 工具链与本地调试页面
        • 后端插件
          • 后端开发深度指南
          • 后端插件的结构
          • 后端源清单 fh2-plugin.json
          • HTTP 接口处理函数
          • 消息订阅与事件处理函数
          • ctx 与能力注入
          • 数据存储与缓存
          • 流程编排
          • 触发方式
          • 连接外部系统
          • 流程编排练习(DAG)
        • 前后端协作
          • 前后端协作总览
          • 前后端共享契约
          • 端到端幂等
          • 事件、通知与状态
          • 统一错误模型
          • 链路标识与诊断日志
        • 完整练习
          • 环境准备
          • 第一个前端插件
          • 第一个后端插件
          • 第一个全栈插件
      • 能力总览
        • 能力地图
        • 能力可用性矩阵
        • 前端能力
          • 平台通用能力
            • 平台通用能力总览
            • 基础运行时:HTTP、事件、WebSocket 与存储
            • 导航、位置与通知
            • 弹窗、音频与剪贴板
            • 地图能力
            • 项目设备能力
            • WPMZ 与航线预览
            • 受控对象上传
          • 项目宿主
            • 项目宿主总览
            • 项目上下文
            • 项目侧边栏入口
            • 项目插件路由
            • 项目工作区
          • 消息宿主
            • 消息宿主总览
            • 消息告警弹层
            • 消息导航与通知
          • 计划库宿主
            • 计划库宿主总览
            • 计划库表单上下文
            • 计划库表单扩展
            • 校验、提交与 `biz_ext`
          • 驾驶舱
            • 驾驶舱插件能力总览
            • 驾驶舱扩展点
            • 上下文与遥测
            • 相机、检测与 AI
            • 场景几何与直播指针
            • 飞控、云台与任务
            • 驾驶舱平台事件
        • 后端能力
          • 后端能力地图
          • 后端能力总览
          • 配置、受管出网与集成连接
          • 事件与流程
          • 飞行任务与违停巡逻
          • 数据存储
          • 媒体与前端通知
          • 资源租约
          • 作业、目标轨迹与视频处理
      • 调试与测试
        • 调试与测试总览
        • 快速入门排错
        • 本地开发
        • Web IDE:打开工程与保存改动
        • 工作流编辑器:节点、接线与交付
        • 知识与 MCP:先判断需要哪一层事实
        • 前端本地调试
        • 后端运行时与流程调试
        • 模拟数据与测试数据
        • 集成测试与 E2E
        • 日志与链路追踪
        • 常见问题排查
      • 完整示例
        • 示例与案例总览
        • 项目侧边栏与工作区
        • 消息告警卡片
        • 计划表单任务清单
        • 驾驶舱动作与左侧面板
        • 最小后端插件
        • 最小全栈插件
        • 多宿主插件实战
        • 事故快处
        • 交通抓拍
        • 违停巡逻完整案例
      • 构建发布与安全
        • 构建、发布与安全
        • 质量门禁
        • 行业包构建与打包
        • 能力版本发布
        • 安装、升级与回滚
        • 安全基线
      • API参考
        • API 参考总览
        • 前端
          • 前端 API 参考
          • 前端插件清单结构
          • 前端 SDK API
          • 前端扩展点目录
          • 前端能力目录
          • 前端平台事件目录
          • 跨宿主通信服务 API
          • 工具链命令行参考(`fh2-plugin`)
        • 后端
          • 后端 API 参考
          • 后端插件源清单
          • 后端 HTTP 接口参考
          • 后端事件消费参考
          • 后端脚本上下文(`ctx`)
          • 后端数据存储
          • 流程编排语言
          • 流程触发器参考
          • 后端能力目录
          • 后端 HTTP API
        • 共享契约
          • 共享契约参考
          • 类型与契约目录
          • 错误模型
          • 版本兼容性
      • 附录
        • 错误码
        • 术语表
        • 常见问题
  1. 前端插件

界面与状态

插件的可视部分叫界面:一个独立的 Vue 3 bundle,负责画 UI 和响应用户操作。这一页讲界面怎么声明、怎么和入口代码交换状态、怎么挂载和清理。核心结论先记住:界面拿不到 ctx(上下文对象),它需要的一切数据都通过界面契约向入口要。

前置条件#

清单 views 里已声明界面 ID 和 ./views/<id>.js 入口(见插件清单)。
入口代码能拿到对应宿主的生成类型。
fh2-plugin-sdk 是插件开发 SDK;下文模块名用于说明,实际导入路径保留生成工程配置。模块名不表示已发布的 npm 包名。

三件套的结构#

每个界面在 src/views/<view-id>/ 下有三个文件,职责固定:
src/views/counter-panel/
├── contract.ts   # 契约:状态形状 + 方法签名,入口和界面都 import 它
├── index.ts      # 默认导出 defineView(...),宿主加载的入口
├── mount.ts      # mount 函数:建订阅、挂 Vue、返回 dispose
└── Panel.vue     # 纯 UI 组件

第一步:定义契约#

契约是入口与界面之间的通信通道的"说明书":
// src/views/counter-panel/contract.ts —— 入口和界面都 import 这个文件
import { definePluginView } from 'fh2-plugin-sdk'

export interface CounterState {
  readonly count: number
}

interface CounterContract {
  state: CounterState
  methods: {
    increment: () => CounterState
  }
}

export const counterView = definePluginView<CounterContract>('counter-panel')
契约的类型约束:
state 和方法参数、返回值必须能跨边界传输(可序列化的普通值,加上 Blob、Date、Map 等少数类型)。函数、DOM 节点、Vue 响应式对象都不行。
每个方法最多一个参数。
方法名不能用保留名:state、dispose、then、catch、finally、constructor、prototype、__proto__。

第二步:入口提供状态和方法#

状态由入口持有,用 createViewState 创建:
// src/hosts/cockpit/activate.ts(节选)
import { createViewState } from 'fh2-plugin-sdk'
import { counterView } from '../../views/counter-panel/contract'
import type { CounterState } from '../../views/counter-panel/contract'

const state = createViewState<CounterState>({ count: 0 })

await counterView.handle(ctx, {
  state, // 要求有 subscribe 方法;createViewState 直接满足
  methods: {
    increment: () => {
      state.update(current => ({ count: current.count + 1 }))
      return state.get()
    },
  },
})
createViewState 返回四个方法:
方法作用
get()读当前值
update(fn)传 current => 新值;fn 返回新对象(不可变更新)。返回原对象时不会通知订阅者
map(fn)把完整状态映射成某个界面需要的形状
subscribe(fn)订阅;注册时会立即收到一次当前值

第三步:界面挂载#

界面的默认导出必须是 defineView(...) 返回的函数,宿主加载 bundle 后调用它的 mount:
// src/views/counter-panel/index.ts
export { default } from './mount'
// src/views/counter-panel/mount.ts
import { createApp, reactive } from 'vue'
import { defineView } from 'fh2-plugin-sdk'
import CounterPanel from './Panel.vue'
import { counterView } from './contract'

export default defineView({
  mount (ctx) {
    // ctx 里有:viewId、mountElement、styleTarget、overlayElement、port、locale、direction、theme
    const state = reactive({ count: 0 })

    // 通过 port 连回入口;方法调用全部返回 Promise
    const client = counterView.client(ctx.port)
    const subscription = client.state.subscribe(next => { state.count = next.count })

    const app = createApp(CounterPanel, {
      state,
      onIncrement: () => client.increment(),
    })
    app.mount(ctx.mountElement)

    return {
      dispose () {
        subscription.dispose()
        client.dispose()
        app.unmount()
      },
    }
  },
})
打开界面时,client.state.subscribe 会先收到一次最新状态,所以界面不会闪空白。关闭时 dispose 负责停订阅、断通道、卸 Vue 应用。

两个容易踩的 DOM 规则#

浮层组件(Modal、Select 下拉、Teleport)挂到 ctx.overlayElement,不要挂 document.body。它是平台准备的浮层容器,会随界面一起清理。
CSS-in-JS 挂到 ctx.styleTarget。界面的 DOM 容器由宿主根据运行条件决定,styleTarget 指向当前界面正确的样式容器。

需要区分实例时:initialState#

同一个界面 ID 有时需要按"内容身份"区分实例(比如两个不同任务的详情面板)。做法:在契约类型里加 initialState 字段(值必须是 JSON 安全的),入口用 counterView.get(ctx, initialState) 生成带身份的界面句柄。身份相同会复用已打开的界面,身份不同会重新挂载。

日常命令与预期#

预期:构建后每个界面是 dist/views/ 下的独立 bundle;界面打开立即显示最新状态;关闭后订阅、通道、Vue 应用全部释放。

常见问题#

现象原因与处理
界面一片空白三处 ID 要一致:清单 views 的键、definePluginView('...') 的参数、默认导出是否为 defineView(...) 的函数
报 VIEW_LOAD_FAILED默认导出不是函数。defineView({ mount ... }) 的返回值才是默认导出
点了按钮界面没反应入口忘了先 counterView.handle(ctx, ...) 再提交扩展;或方法名两边不一致
状态变了但界面不更新update 里返回了原对象(就地修改),平台按相同值跳过通知;改成返回新对象
跨边界方法调用报 PLUGIN_VALUE_INVALID契约里传了函数、DOM、Vue 响应式对象或能力句柄;改成只传满足 PluginValue 的数据
弹层被裁剪或关闭界面后残影残留浮层挂到了 document.body;改挂 ctx.overlayElement
修改于 2026-10-22 03:39:50
上一页
入口与生命周期
下一页
多宿主插件
Built with