前端扩展点实战
本文系统介绍 BPMAX 插件前端的主要扩展能力,重点是把插件代码挂接到平台前端页面和流程编辑器中。
学习目标
- 理解 BPMAX 前端插件入口的职责
- 掌握路由、UI 插槽、流程配置组件和国际化的注册方式
- 能为业务插件选择合适的前端扩展点
插件前端入口文件
main.ts 的职责
插件前端通常以入口文件作为注册中心。这个文件不负责启动一个新的 Vue 应用,而是把插件能力注册给平台系统。
你通常会在这里做这些事情:
- 注册页面路由
- 注册 UI 插槽
- 注册流程配置组件
- 合并国际化语言包
一个入口文件可以先组织成下面这样:
import HelloPage from './pages/HelloPage.vue';
import DetailExtraPanel from './components/DetailExtraPanel.vue';
import TaskStepConfig from './components/TaskStepConfig.vue';
import zhCN from './languages/zhCN';
console.log('plugin_example frontend loaded');
BPMAX.router.addRoute({
path: '/plugin/hello-demo',
component: HelloPage,
});
BPMAX.ui.register('Slot.Page.Detail.ExtraPanel', DetailExtraPanel);
BPMAX.flow.stepComponent({
field: 'plugin_task_integration',
component: TaskStepConfig,
defaultValue: {
enabled: false,
platform_id: '',
mapping_function: '',
},
});
BPMAX.mergeLocaleMessage('zhCN', zhCN);什么时候只需要前端入口
如果插件只是:
- 增加一个独立页面
- 在已有页面插一个展示组件
- 给流程编辑器增加纯前端配置界面
那么很多时候只写前端入口也能先跑通。
什么时候必须联动后端接口
如果插件需要:
- 调用第三方接口
- 读写平台数据
- 执行业务校验
- 保存插件自己的配置或状态
那么前端入口只能解决“挂载位置”,还需要联动插件后端。
扩展方式一:注册独立页面
适用场景
适合:
- 插件管理页面
- 配置页面
- 报表页面
- 调试页面
BPMAX.router.addRoute
最小用法如下:
import HelloPage from './HelloPage.vue';
BPMAX.router.addRoute({
path: '/plugin/hello-demo',
component: HelloPage,
});页面路径命名建议
- 统一使用
/plugin/前缀 - 路径名尽量和插件英文名相关
- 避免使用平台已有页面路径
页面组件可以先写成:
<template>
<section class="plugin-page">
<h2>插件管理页</h2>
<p>这里可以展示插件配置、状态和调试信息。</p>
</section>
</template>扩展方式二:注册 UI 插槽
适用场景
适合:
- 给现有页面增加按钮
- 增加详情区块
- 扩展表格操作列
- 扩展配置区域
BPMAX.ui.register
最小用法如下:
BPMAX.ui.register('Slot.Page.Detail.ExtraPanel', DetailExtraPanel);或者把组件挂到某个业务插槽中:
BPMAX.ui.register('Slot.Table.RowActions', TableRowAction);常见插槽类型
常见插槽通常覆盖以下区域:
- 页面详情区
- 列表操作区
- 表单扩展区
- 流程操作区
这类方式的优点是侵入性低,不需要修改平台页面本身。
一个最小插槽组件可以写成:
<script setup lang="ts">
const props = defineProps<{
row?: Record<string, any>;
}>();
const handleClick = () => {
console.log('current row', props.row);
};
</script>
<template>
<button type="button" @click="handleClick">同步任务</button>
</template>插槽组件的输入输出
不同插槽会给组件传入不同上下文,因此写插槽组件时要先确认:
- 插槽位置在哪里
- 平台会传什么参数
- 组件应该只展示,还是可以发起交互
如果插槽需要调用后端接口,可以直接在组件中发请求:
async function syncTask(rowId: string) {
const res = await BPMAX.app.post('/api/plugin_task_integration/sync', {
row_id: rowId,
});
console.log(res);
}扩展方式三:注册流程元配置组件
适用场景
适合给流程整体增加配置,例如:
- 流程级开关
- 流程级外部系统参数
- 流程级自动化规则
BPMAX.flow.metaComponent
最小用法如下:
BPMAX.flow.metaComponent({
field: 'meta_config_field_name',
component: ConfigComponent,
defaultValue: {
enabled: false,
platform_id: '',
},
});对应的配置组件可以先保持简单:
<script setup lang="ts">
const model = defineModel<any>({
default: {
enabled: false,
platform_id: '',
},
});
</script>
<template>
<div>
<label>
<span>启用能力</span>
<input v-model="model.enabled" type="checkbox" />
</label>
<label>
<span>平台实例</span>
<input v-model="model.platform_id" type="text" />
</label>
</div>
</template>默认值设计
建议 defaultValue 直接给完整结构,而不是只给布尔值或空字符串。这样可以减少:
- 旧数据兼容问题
- 表单渲染判空问题
- 版本升级后的结构不一致问题
配置结构设计建议
建议配置对象遵循:
- 字段稳定
- 命名可读
- 预留扩展空间
例如优先使用:
{
enabled: false,
platform_id: '',
mapping_function: '',
}而不是把多个含义塞进一个字符串。
扩展方式四:注册流程环节配置组件
适用场景
适合给某个流程环节增加配置,例如:
- 当前环节是否启用某能力
- 当前环节执行什么外部动作
- 当前环节的特殊审批策略
BPMAX.flow.stepComponent
最小用法如下:
BPMAX.flow.stepComponent({
field: 'plugin_task_integration',
component: TaskStepConfig,
defaultValue: {
enabled: false,
mapping_function: '',
platform_id: '',
},
});环节级配置组件示例如下:
<script setup lang="ts">
const model = defineModel<any>({
default: {
enabled: false,
mapping_function: '',
platform_id: '',
},
});
</script>
<template>
<div>
<label>
<span>启用</span>
<input v-model="model.enabled" type="checkbox" />
</label>
<label>
<span>映射函数</span>
<input v-model="model.mapping_function" type="text" />
</label>
</div>
</template>与 metaComponent 的区别
metaComponent:流程整体只配置一次stepComponent:每个环节可以独立配置
如果能力与单个节点行为强相关,应优先选择 stepComponent。
编辑器组件复用建议
流程配置相关组件可以复用平台流程编辑器中的标准组件。这样做的好处是:
- 风格一致
- 输入结构一致
- 减少重复造轮子
扩展方式五:注册国际化语言包
BPMAX.mergeLocaleMessage
典型写法如下:
import en from '../languages/en/index';
import zhCN from '../languages/zhCN/index';
BPMAX.mergeLocaleMessage('en', en);
BPMAX.mergeLocaleMessage('zhCN', zhCN);zhCN 语言包可以先按下面结构组织:
export default {
'plugin_example.page_title': '插件管理页',
'plugin_example.sync_button': '同步任务',
};多语言目录组织建议
推荐结构:
languages/
en/
zhCN/
jaJP/每个插件维护自己的语言命名空间,避免与平台或其他插件冲突。
可复用的平台能力
全局对象 BPMAX
插件运行时可直接使用平台暴露的全局对象。前端入口与页面组件中,最常用的是下面这些能力:
BPMAX.app:当前应用实例,可访问i18n、请求能力等运行时上下文BPMAX.route:当前路由信息BPMAX.router:路由注册与跳转能力BPMAX.ui:页面插槽注册能力BPMAX.flow:流程元配置、环节配置等扩展能力BPMAX.Vue:运行时 Vue 对象BPMAX.getSetting(name):读取插件配置BPMAX.addSetting(name, setting):保存插件配置BPMAX.mergeLocaleMessage(lang, message):合并插件语言包BPMAX.getEnv():读取插件运行环境变量BPMAX.eventBus:插件事件总线
可以把前端入口整理成下面这样:
import HelloPage from './pages/HelloPage.vue';
import DetailExtraPanel from './components/DetailExtraPanel.vue';
import TaskStepConfig from './components/TaskStepConfig.vue';
import zhCN from './languages/zhCN';
BPMAX.router.addRoute({
path: '/plugin/hello-demo',
component: HelloPage,
});
BPMAX.ui.register('Slot.Page.Detail.ExtraPanel', DetailExtraPanel);
BPMAX.flow.stepComponent({
field: 'plugin_task_demo',
component: TaskStepConfig,
defaultValue: {
enabled: false,
notify_users: [],
},
});
BPMAX.mergeLocaleMessage('zhCN', zhCN);运行时配置读写
当插件需要保存自己的前端配置时,可以直接调用运行时 API:
async function loadPluginSetting() {
const setting = await BPMAX.getSetting('plugin_example');
return {
enabled: !!setting.enabled,
api_base_url: setting.api_base_url || '',
};
}
async function savePluginSetting(formData: {
enabled: boolean;
api_base_url: string;
}) {
await BPMAX.addSetting('plugin_example', formData);
}运行环境读取
const env = BPMAX.getEnv();
console.log(env.API_HOST);
console.log(env.RUNTIME_ENV);
console.log(env.PLUGIN_NAME);Vite 全局依赖注入
为什么要注入全局变量
插件前端不是独立站点,而是运行在平台页面里的一个扩展包。
因此,像 Vue、Element UI、echarts、部分平台控件和组合式方法,通常由平台运行时统一提供,插件构建时不再重复打包。
这样做有三个直接收益:
- 避免把平台已经提供的运行时再次打进插件包
- 减少插件产物体积
- 保证插件与平台使用同一份基础依赖和控件实例
external 与 globals 的作用
在 vite.config.ts 里,核心就是两步:
- 在
external中声明哪些依赖不要打包 - 在
output.globals中声明这些依赖运行时对应哪个全局变量
最小示例如下:
import path from 'node:path';
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue2';
export default defineConfig({
plugins: [vue()],
build: {
lib: {
entry: path.resolve(__dirname, './src/main.ts'),
name: 'plugin-example',
},
rollupOptions: {
external: [
'vue',
'element-ui',
'echarts',
'BPMAX',
'@bpmax/UseAxios',
'@bpmax/UserPicker',
],
output: {
globals: {
vue: '$__VUE',
'element-ui': '$__ELEMENT_UI',
echarts: '$__ECHARTS',
BPMAX: '$__BPMAX',
'@bpmax/UseAxios': '$__UseAxios',
'@bpmax/UserPicker': '$__UserPicker',
},
},
},
},
});如何理解这段配置
以上配置的含义是:
- 源码里如果引用了
vue,构建结果不会再把 Vue 打进去 - 运行到平台页面时,这个依赖会从全局变量
$__VUE取值 - 组件、工具、组合式方法也可以按同样方式映射到全局变量
什么时候要放进 external
推荐放进 external 的依赖:
- 平台已统一提供的基础库,例如
vue、element-ui、echarts - 平台已暴露的运行时对象,例如
BPMAX - 平台已暴露的前端控件、工具方法、组合式方法
不建议放进 external 的依赖:
- 你的插件私有工具库
- 只在当前插件内部使用、平台没有提供的第三方包
- 与插件业务强绑定、需要自己控制版本的库
推荐写法:显式读取全局变量
对于平台已暴露的组件和工具,教程更推荐直接按全局变量使用,而不是写相对路径导入。
例如:
const { useAxios } = $__UseAxios;
const { Message, MessageBox } = $__ELEMENT_UI;
const UserPicker = $__UserPicker;
const UserGroupPicker = $__UserGroupPicker;
const PdfReader = $__PdfReader;这样做的好处是:
- 依赖来源清晰,能一眼看出这是平台运行时提供的能力
- 不会把平台内部目录结构暴露到插件代码里
- 文档和实际加载机制一致
平台已暴露的全局变量与控件
基础运行时
常见基础全局变量包括:
window.BPMAXwindow.$__BPMAXwindow.$__VUEwindow.$__ROUTERwindow.$__APPwindow.$__ROUTEwindow.$__VUE_ROUTERwindow.$__VUEXwindow.$__Piniawindow.$__VUEUSEwindow.$__VueI18n
通用库
window.$__ELEMENT_UIwindow.$__FORM_CREATEwindow.$__ECHARTSwindow.$__LODASHwindow.$__MOMENTwindow.$__SOCKETIO
平台前端控件
运行时已经暴露了一批常用控件,可在插件中直接复用:
window.$__UserPickerwindow.$__UserGroupPickerwindow.$__PdfReaderwindow.$__AttachmentReaderwindow.$__CodeEditorwindow.$__ConsoleCodeEditorwindow.$__FileUploadwindow.$__ImageUploadwindow.$__ImageUploadMultiplewindow.$__ImagePreviewwindow.$__VisibilityControlwindow.$__HomeNavwindow.$__LayoutHeaderwindow.$__LayoutFooterwindow.$__LayoutListwindow.$__BreadCrumbwindow.$__SectionItemwindow.$__SideColumnSectionwindow.$__MainColumnSectionwindow.$__SimpleMessageTemplateDialogwindow.$__IconStatusProject
流程设计器与表单设计器相关能力
window.$__FormDesignerUtilswindow.$__FormDesignerConfigwindow.$__FormDesignerUseFormCreatewindow.$__FormDesignerUseFormCreateMapwindow.$__FormDesignerIFormCreatewindow.$__FormDesignerProjectSelectorNewwindow.$__FormDesignerUserSelectwindow.$__FormDesignerGroupSelectwindow.$__useFormCreateMapwindow.$__useStepEditorContextwindow.$__UseStepEditorManagerwindow.$__FlowEditorAsideRightConfigwindow.$__GridLayoutGridLayoutwindow.$__GridLayoutGridLayoutAsidewindow.$__GridLayoutWidget
组合式方法与工具方法
window.$__UseAxioswindow.$__UseStorewindow.$__UseWorkspacewindow.$__UseVueRouterwindow.$__UseTabBindRoutewindow.$__UsePluginEventBuswindow.$__UseDataSetwindow.$__UseDataSourcewindow.$__UseCrudwindow.$__UseVconsolewindow.$__UtilsLocationwindow.$__ProjectTableHelperwindow.$__ParserUtilwindow.$__ProjectMixinswindow.$__ObjectEventswindow.$__reflectwindow.$__FeishuHelperwindow.$__getMimeTypeByFilewindow.$__isLocalUploadTypewindow.$__getLocalUploadUrl
全局控件使用示例
示例一:直接使用 Element UI
const { Message, MessageBox, Loading } = $__ELEMENT_UI;
async function handleSync() {
const loading = Loading.service({
text: '正在同步',
});
try {
await MessageBox.confirm('确认立即同步吗?', '提示', {
type: 'warning',
});
Message.success('同步已提交');
} finally {
loading.close();
}
}示例二:使用平台提供的 useAxios
const { useAxios } = $__UseAxios;
const axios = useAxios();
export async function queryTaskList() {
return axios.$get('/plugin_example/tasks', {
params: {
page: 1,
page_size: 20,
},
});
}示例三:复用人员选择器
<script setup lang="ts">
const UserPicker = $__UserPicker;
const { ref } = $__VUE;
const visible = ref(false);
const selectedUsers = ref<any[]>([]);
function handleConfirm(list: any[]) {
selectedUsers.value = list;
visible.value = false;
}
</script>
<template>
<div>
<el-button type="primary" @click="visible = true">选择人员</el-button>
<component
:is="UserPicker"
:visible.sync="visible"
@confirm="handleConfirm"
@cancel="visible = false"
/>
</div>
</template>示例四:复用附件与预览控件
<script setup lang="ts">
const AttachmentReader = $__AttachmentReader;
const PdfReader = $__PdfReader;
const fileUrl = 'https://example.com/demo.pdf';
</script>
<template>
<section>
<component :is="AttachmentReader" :url="fileUrl" />
<component :is="PdfReader" :url="fileUrl" style="height: 600px" />
</section>
</template>示例五:流程配置组件里复用设计器控件
<script setup lang="ts">
const UserSelect = $__FormDesignerUserSelect;
const GroupSelect = $__FormDesignerGroupSelect;
const model = defineModel<any>({
default: {
reviewers: [],
groups: [],
},
});
</script>
<template>
<div>
<component :is="UserSelect" v-model="model.reviewers" />
<component :is="GroupSelect" v-model="model.groups" />
</div>
</template>平台组件与样式约定
流程配置型插件建议优先复用平台已有编辑器组件,而不是自己再造一套表单体系。
建议优先顺序如下:
- 优先复用平台已经暴露的选择器、上传器、编辑器、预览器
- 需要表单配置时优先复用
FormDesigner相关能力 - 只有平台没有现成能力时,再引入插件私有组件
这样可以减少:
- 视觉风格不一致
- 数据结构不一致
- 同类控件交互体验分裂
常见扩展类型
页面型扩展
特点:
- 以路由注册为主
- 提供独立页面或配置页面
UI 插槽型扩展
特点:
- 挂接到现有页面局部区域
- 适合按钮、区块、操作栏和状态展示
流程配置型扩展
特点:
- 挂接到流程级或环节级配置
- 与流程执行逻辑绑定较深
常见问题
组件注册后未显示
- 插槽名写错
- 当前页面没有这个插槽
- 插件入口没有执行
流程配置默认值异常
defaultValue结构和组件期望结构不一致- 历史数据缺少新字段
国际化 key 冲突
建议统一加插件前缀,例如:
plugin_example.xxxplugin_task_integration.xxx
