外部页面
1 说明
需求场景说明
外部资源通过平台菜单加载出来,作为应用的一部分展示,外部资源可以是独立的一套子系统,或者是一个页面或者一个页面组件。
技术说明
外部页面和 frame 组件功能类似,只是底层实现逻辑不一样。
frame 组件是通过 iframe 技术将定制化外部页面加载到我们的系统,但是存在部分问题:定制化页面内部的弹框只能在 iframe 生效,如果外部应用页面想要自己内部跳转页面或者自己弹出层页面逻辑,效果会和主系统割裂开,所有页面组件都需要在共工平台上配置。frame 组件和主系统通信方式使用 postMessage.
外部页面则是通过微前端的思路展开,外部应用主体是基于无界微前端思路,会将外部地址通过 webComponents 组件代理 iframe 方法同步在 shadowbox 中,这样外部页面就会和主系统的 dom 在同一个体系上,定制化页面内部自己的路由跳转和弹出框都可以自己内部控制,与主系统的通过通过我们内部封装的 sdk 来调用,使用方法和 frame sdk 保持一致。
外部开发者使用方式:
下载下面的项目压缩包
我们提供的定制化页面开发项目使用到技术栈:vue3+pinia+jsx+vite
使用 yarn 安装系统,使用 npm run dev 调试代码
2 配置
2.1 资源名称
每个外部页面资源名称要保证唯一性,该资源名称会作为页面上的应用标识来加载对应的路由。
2.2 资源地址
联调资源地址: 编辑态、预览态使用的地址运行时资源地址: 运行态使用的地址联调资源地址如果是本地调试的环境,devServer 需要开启 cros 跨域模式,cros 的配置需要配置本地访问的 ip 地址,配置如下:
如果是生产环境地址,那么当前生产环境的静态资源访问也是要开启 cros 跨域支持,如果你使用了 nginx 反向代理,可以如下设置:
2.3 生命周期&编辑态预览
是否开启生命周期:开启生命周期 ,浏览器中会有外部页面加载卸载的提示 (暂时未开启钩子脚本功能)是否预览:开启的情况下,编辑态可以预览效果2.4 配置参数 (资源配置)
可以配置逻辑,页面,若未配置,则无权限调用。 (数据库、工作流处于实验阶段,未对外支持)
可以针对现有可以访问到的资源可以进行增删改,禁用等操作
2.5 传参配置
传参配置:用户可以设置给外部页面设置传参,传参获取方式可在下面介绍。定制化获取页面传参方式:
onMounted(() => {
const result = window?.$wujie.props?.getPropsData();
console.log('props.data', result);
});
效果展示:
2.6 可见性设置
具体描述请点击详情 查看通用的描述(详情)
2.7 设计
具体描述请点击详情 查看通用的描述(详情)
3、定制化页面接入规则
定制化页面接入规则:
// 指令名称
export const COMMAND_NAME = {
OPEN_PAGE: 'OPEN_PAGE', // 打开页面
CLOSE_PAGE: 'CLOSE_PAGE', // 关闭页面
RESET_FORM: 'RESET_FORM', // 重置表单
COMMON_ACTION: 'COMMON_ACTION', // commonAction
CALL_MICROFLOW: 'CALL_MICROFLOW', // 微流调用
CONTEXT_DATA: 'CONTEXT_DATA', // 上下文数据
SHOW_MESSAGE: 'SHOW_MESSAGE', // 提示信息
REFRESH_PAGE: 'REFRESH_PAGE', // 刷新页面
GET_PAGEPKID: 'GET_PAGEPKID', // 获取页面主键id
UPLOAD_FILE: 'UPLOAD_FILE' // 文件上传
};
定制化接入
调用共工提示消息
window?.$wujie.props?.callCmd(COMMAND_NAME.SHOW_MESSAGE, { content: '保存成功', type: 'success' });
调用共工打开页面
/** 打开弹窗 */
window?.$wujie.props?.callCmd(COMMAND_NAME.OPEN_PAGE, {
resourceName: '请假用户页',
params: {
test: 1
},
});
调用共工 API 调用逻辑调用共工 API 调用逻辑
案例逻辑配置:
配置完资源之后调用:
Tips:
调用逻辑场景:
1、对接人只知道逻辑的入参类型,入参顺序,入参个数的场景可以使用 params 内部传递 paramList 参数,参数使用数组形式呈现。
2、对接人只知道逻辑的入参名,可以使用 params 内部传递 paramKeyMap 参数,paramKeyMap 参数内部传递的 key|map 方式的对象数据
3、对接人如果要调用逻辑上传文件,可以直接在 params 字段直接传递 file 信息,最终会通过 form-data 形式接口调用逻辑
/** 逻辑无参数调用 */
window?.$wujie.props?.callCmd(COMMAND_NAME.CALL_MICROFLOW, { resourceName: '新增请假' }, (res) => {
console.log('getMiji', res);
});
// type类型: object 实体对象、list: 集合数据, long: 长整型, string: 字符串类型
// 逻辑调用 - 传递对象数据,只知道几个传参和传参类型、传参顺序的情况下使用
window?.$wujie.props?.callCmd(
COMMAND_NAME.CALL_MICROFLOW,
{
resourceName: 'saveTeacher',
params: {
paramList: [
{
type: 'object',
json: JSON.stringify(initData.value.current),
},
],
},
},
(res) => {
console.log('saveTeacher', res);
}
);
// 逻辑调用 - 传递对象数据
// type类型: object 实体对象、list: 集合数据, long: 长整型, string: 字符串类型, 知道逻辑的入参名
window?.$wujie?.props?.callCmd(
COMMAND_NAME.CALL_MICROFLOW,
{
resourceName: 'saveTeacher',
params: {
paramKeyMap: {
teacher: initData.value.current,
},
},
},
(res) => {
console.log('saveTeacher', res);
}
);
// 逻辑调用 - 传递普通数据,只知道几个传参和传参类型、传参顺序的情况下使用
window?.$wujie.props?.callCmd(
COMMAND_NAME.CALL_MICROFLOW,
{
resourceName: 'commonCall',
params: {
paramList: [
{
type: 'long',
simpleValue: initData.value.current['gonggong_id'],
},
],
},
},
(res) => {
console.log('commonCall', res);
}
);
// 逻辑调用 - 传递普通数据,知道逻辑入参名,直接通过key|map的形式传值
window?.$wujie?.props?.callCmd(
COMMAND_NAME.CALL_MICROFLOW,
{
resourceName: 'commonCall',
params: {
paramKeyMap: {
id: initData.value.current['gonggong_id'],
},
},
},
(res) => {
console.log('commonCall', res);
}
);
逻辑上传文件
还有一种场景,逻辑要调用文件上传能力,那么我们的逻辑配置如下:
function uploadFile() {
// 调用上传文件
window?.$wujie.props?.callCmd(
COMMAND_NAME.CALL_MICROFLOW,
{
resourceName: 'uploadFile',
params: {
name: formState.name,
file: formState.file
},
},
(res) => {
console.log('uploadFile', res);
}
);
}
调用共工业务接口
gg-sdk.ts 追加类型
/** 共工通用API接口调用 */
HTTP_REQUEST: 'HTTP_REQUEST'
无传参接口调用:
// 调用共工通用接口
const callRequest = () => {
// 无传参
window?.$wujie.props?.callCmd(
COMMAND_NAME.HTTP_REQUEST,
{
config: {
url: 'sysmanager/moduleGroup/tree',
method: 'get',
params: {},
},
},
([err, result]) => {
console.log('callRequest-sysmanager/moduleGroup/tree', err, result);
}
);
};
有传参接口调用:
// 调用共工通用接口
const callRequest = () => {
// 有传参
window?.$wujie.props?.callCmd(
COMMAND_NAME.HTTP_REQUEST,
{
config: {
url: 'object/entity/attr/list',
method: 'get',
params: {
id: '871407298094336',
},
},
},
([err, result]) => {
console.log('callRequest-object/entity/attr/list', err, result);
}
);
};
效果:
动态监听上层数据变化传参
Event 监听采用了去中心的思想,共工平台会监听上层数据变化,动态更新数据
定制化模块需要采用 bus.$on 监听数据的变化,能够动态更新数据
bus.on 监听的事件名规则:应用名称 + ': updateProps'
function updateProps(data: Record<string, any>) {
console.log('===数据更新===', data);
initData.value = data;
}
onMounted(() => {
const applicationName = window?.$wujie?.props?.name || 'test';
// 先销毁监听
window.$wujie?.bus.$off(`${applicationName}:updateProps`, updateProps);
// 初始化获取props传参数据
const result = window?.$wujie.props?.getPropsData();
initData.value = result;
console.log('props.data', result);
// 监测上层数据变化
window.$wujie?.bus.$on(`${applicationName}:updateProps`, updateProps);
});
onUnmounted(() => {
const applicationName = window?.$wujie?.props?.name || 'test';
// 销毁时候取消监听
window.$wujie?.bus.$off(`${applicationName}:updateProps`, updateProps);
});
效果可见如下:
定制化部分表单能力
针对于业务场景中的一个大表单,梳理出一部分是可以通过共工搭建支持的,还有一部分是需要定制化开发集成的,那共工平台如何能够将定制化表单能够实时同步到平台呢?
针对于上面的问题,共工平台提供了 COMMAND_NAME.UPDATE_FORM_DATA 命令,能够支持将表单合并到平台。
具体 demo 例子可以参考 gitlab.xuelangyun.com:cli-template/frame-project-template.git
文件地址:frame-project-template/src/views/test/Test.vue
具体实现大概如下:
/**
* 表单配置支持 - schema配置信息
*/
const generatorFormSchema = (formModel): FormSchema[] => {
const schemas = [
{
field: 'name',
component: 'Input',
label: '姓名',
componentProps: {
placeholder: '请输入姓名',
onChange: (e) => {
// 这里可以根据实际场景使用onChange或者onBlur,用于监听当前表单元素的同步
formModel.name = e.target.value;
mergeFormData(formModel);
},
},
},
] as FormSchema[];
return schemas;
};
// 合并可编辑表格数据
const mergeEditTableData = (data) => {
let tableData = [];
// 与原有的可编辑数据做融合处理
if (dataSource.value?.length) {
tableData = data.map((item, index) => {
const editValueRefs = dataSource.value[index]?.editValueRefs;
Object.keys(item)?.forEach((current) => {
editValueRefs[current] = item[current];
});
return {
...dataSource.value[index],
editValueRefs,
};
});
} else {
// 之前没有的对象,直接做融合处理
tableData = data.map((item, index) => {
return {
...item,
rowKey: `rowid_${buildShortUUID()}`,
needUpdate: true,
isAdd: true,
editable: true,
};
});
}
return tableData;
};
// 监听当前数据变化,动态更新组件的状态值
function updateProps(data: Record<string, any>) {
console.log('===数据更新===', data);
initData.value = data;
formModel.name = data?.current?.name;
dataSource.value = mergeEditTableData(data?.current?.['teacher_classes'] || []);
setFieldsValue({
name: data?.current?.name,
});
}
onMounted(() => {
// 先销毁监听
window.$wujie?.bus.$off('test:updateProps', updateProps);
// 初始化获取props传参数据
const result = window?.$wujie.props?.getPropsData();
initData.value = result;
// 初始化设置表单数据
formModel.name = result?.current?.name;
// 初始化设置可编辑表格数据
dataSource.value = mergeEditTableData(result?.current?.['teacher_classes'] || []);
console.log('props.data', result);
// 监测上层数据变化
window.$wujie?.bus.$on('test:updateProps', updateProps);
});
onUnmounted(() => {
// 销毁时候取消监听
window.$wujie?.bus.$off('test:updateProps', updateProps);
});
// 合并表单数据
function mergeFormData(data) {
window?.$wujie.props?.callCmd(COMMAND_NAME.UPDATE_FORM_DATA, {
data,
});
}
// 可编辑表格数据变化的时候及时同步表单数据
function onEditChange({ column, value, record }) {
// 本例
if (column.dataIndex === 'id') {
record.editValueRefs.name4.value = `${value}`;
}
console.log(column, value, record, generatorTableData(record));
// 和上层表单内容同步数据
window?.$wujie.props?.callCmd(COMMAND_NAME.UPDATE_FORM_DATA, {
data: {
teacher_classes: generatorTableData(record),
},
});
}
大致效果如下:
可以参考
定制化页面刷新事件处理
场景:用户使用共工搭建页面,页面中包含了搭建的内容和定制化页面内容,用户调用逻辑刷新页面,希望在定制化页面能够感知到动作,并处理事件的能力。
当共工页面感知到逻辑刷新命令的时候,会触发所有数据容器对应的 reload 函数,对于外部页面,我们也会去触发 reload 事件。
定制化代码如何编写: 注册 reload 事件
bus.on 监听的事件名规则:应用名称 + ': updateProps'
此处 test 是右侧面板配置的应用名称
// 刷新命令处理函数
const reload = (data: Record<string, any>) => {
const applicationName = window?.$wujie?.props?.name || 'test';
console.log(`===${applicationName}页面刷新reload===`, data);
};
onMounted(() => {
const applicationName = window?.$wujie?.props?.name || 'test';
// 先销毁监听
window.$wujie?.bus.$off(`${applicationName}:reload`, reload);
window.$wujie?.bus.$on(`${applicationName}:reload`, reload);
});
onUnmounted(() => {
const applicationName = window?.$wujie?.props?.name || 'test';
// 销毁时候取消监听
window.$wujie?.bus.$off(`${applicationName}:reload`, reload);
});
如果当前定制化页面要感知到刷新操作,可以实现 reload 函数,reload 函数可以处理刷新过程中需要更新的数据。
效果展示:
定制化页面切入切出事件处理
场景:用户使用共工搭建页面,页面中包含了搭建的内容和定制化页面内容,当前导航页面切换的时候,当前定制化页面要感知到当前页面是否展示还是不展示,如果不展示,有一些定时任务需要清除,那么我们就需要使用到 deactivated 事件;那么下次页面再切入进来的时候还需要重新设置定时任务,那么就需要使用到 actived 事件。
当共工页面路由变化的时候,会触发当前不展示的页面内部外部页面 deactivated 事件;当之前不显示的页面重新访问了,会触发当前页面中的外部页面 actived 事件,和 vue 的生命周期一样,第一次渲染是不会触发这 2 个事件。
定制化代码如何编写: 注册 actived 事件, deactivated 事件
bus.on 监听的事件名规则:应用名称 + ': actived', 应用名称 + ': deactivated'
此处 test 、test1 是 2 个外部页面 右侧面板配置的应用名称
代码参考:
// actived 事件
const activateApp = () => {
const applicationName = window?.$wujie?.props?.name || 'test';
console.log(`===${applicationName}页面actived===`);
};
// deactived 事件
const deactivatedApp = () => {
const applicationName = window?.$wujie?.props?.name || 'test';
console.log(`===${applicationName}页面deactived===`);
};
onMounted(() => {
console.log('===子系统onMounted事件===');
const applicationName = window?.$wujie?.props?.name || 'test';
// 先销毁监听
window.$wujie?.bus.$off(`${applicationName}:reload`, reload);
window.$wujie?.bus.$off(`${applicationName}:activated`, activateApp);
window.$wujie?.bus.$off(`${applicationName}:deactivated`, deactivatedApp);
// 监测事件
window.$wujie?.bus.$on(`${applicationName}:reload`, reload);
window.$wujie?.bus.$on(`${applicationName}:activated`, activateApp);
window.$wujie?.bus.$on(`${applicationName}:deactivated`, deactivatedApp);
});
定制化初始化项目接入:
1、脚手架方案(内部对接)
2、手动下载(外部对接)
手动下载文件,定制化代码接入
Q&A
1、使用了外部页面,定制化页面使用下拉框点击选不中问题
由于外部页面使用到无界微前端方案,会使用到 shandow-dom,外部页面的内容都会挂载到 shadow-dom 上,ant-design-vue 部分版本上还未考虑到这种兼容问题,我们拿定制化模版使用到的版本 2.1.6 来看
再看下 2.*版本下的最后一个版本 2.2.8
已经做了兼容,只需要把组件库对应的版本做下升级就可以
2、希望调用函数有提示信息
types/global.d.ts 内部添加类型定义
Declare global 命名空间内追加 window 的定义
interface WUJIE_PROPS {
// sdk调用函数
callCmd: (
cmd: typeof COMMAND_NAME,
data: Record<string, any>,
cb?: (params?: unknown) => unknown
) => any;
// 组件传参值
getPropsData: () => Record<string, any>;
}
declare interface Window {
// Global vue app instance
$wujie: {
props: WUJIE_PROPS;
bus: {
$on(method: string, fn: (...args: Record<string, any>[]) => void);
$emit(method: string, ...args: Record<string, any>[]);
$off(method: string, fn: (...args: Record<string, any>[]) => void);
};
};
}
3、使用 LuckySheet 使用 xlsx 文件的导入导出(不生效问题)
LuckySheet 导入文件使用到的 jszip 包去加载解析文件,解析的内容涉及到 message 事件监听和 postMessage 发送消息。无界本身通过代理 iframe 的方法,所以直接监听 message 事件和 postMessage 发送消息会存在问题,无界已经将 iframe 内容转换成 webComponent 渲染,所以要保证监听 message 事件和 postMessage 发送消息的是同一方才能保证消息是可监听的。
改造如下:
平台端改造如下,添加插件事件绑定和解除回调函数,同步能让当前主页面的 window 监听到
定制化端:修改源码补丁
效果: