工作台开发
一、 前言
工作台分为个人工作台和系统工作台(系统场景),个人工作台顾名思义就是用户自己定义的工作台,系统工作台就是由运维人员事先定义好的工作台,其它人可以直接使用(授权后可使用)。
工作台相当于一个容器,容器包含很多面板,每一个面板可以承载一个或多个组件,面板可以随意缩放大小,面板与面板之前可以相互联动。工作台或者面板只是提供一个壳,这个是框架已经支持了,但是里面的组件是需要开发。
二、 组件开发规范
工作台中的组件开发跟开发一个普通页面是没什么区别的,任何一个页面都可以放入工作台,但还是有些规范需要遵守:
第一,组件设计阶段,需要知道组件的最佳展示效果,假设组件最终要放在一个四等分的工作台中(如下图),那么设计组件的时候,就需要按照这个尺寸来设计,这样开发出来的组件可以更好的在工作台进行展示。
第二,组件开发阶段,按照新增页面就可以快速开发一个组件,但需要注意的是,开发的时候需要按照设计师给的尺寸进行开发,可以通过缩小浏览器窗口来模拟组件的尺寸。
第三,组件开发完成之后,可以看一下在工作台的展示效果。
三、 事件总线
工作台内部自定义了八个通信通道,默认是白色通道。
组件实现通信有两种情况:
第一种是,ABCD 是不相同组件,可以使用自定义事件或者是使用系统内置通信通道。
- 工作台 1 中的组件 A 发生变化,将消息通知出去,同一个工作台中的组件 B 接收到事件,响应变化
- 事件仅局限于当前工作台,也即工作台 2 中同样的组件 B 是收不到事件的。
第二种是,BCD 是同一个组件,但是只有 B 组件可以接收 A 组件的消息变化,只能使用系统内置通信通道。
- 工作台 1 中的组件 A 发生变化,将消息通知出去,同一个工作台中的组件 B 接收到事件,但 CD 组件不会收到信息,只要将通道换成不一致即可,如:AB 组件使用白色通道,CD 组件使用其他通道
1. 发送事件
API
工作台默认会传递 props 给插入的组件,props 结构如下:
{
workbenchId: '', // 工作台id
layoutItemId: '', // 面板id
linkageValue: '', // 通信通道
}
/*
* event 事件名称 / 通信通道
* data 携带的数据
* this 组件的实例
*/
window.globalStore.emitWorkbenchEvent(event, data, this);
代码参考(Class写法)
// 代码来源于示例代码app/pages/sample/quickStart/TodoList
export default class TodoList extends Component<any, IState> {
handleSubmit = (value: string) => {
const todos = this.state.todos;
const item = {
id: uniqueId(),
name: value,
};
todos.push(item);
this.setState({ todos });
// 向另外一个窗口发出todo-list变动事件
this.emitTodoListChanged({ type: 'add', data: item });
};
// 测试工作台面板之间互联互通专用代码
emitTodoListChanged = (event: any) => {
// 1.自定义事件方式
window.globalStore.emitWorkbenchEvent('todo-list-changed', event, this);
// 2.使用系统内置通信通道
window.globalStore.emitWorkbenchEvent(this.props.linkageValue, event, this);
};
render() {
const { todos } = this.state;
return (
<div>
<Header onSubmit={this.handleSubmit} />
<Content todos={todos} onDelete={this.hanldeDelete} />
<Footer todos={todos} />
</div>
);
}
}
代码参考(Hooks写法)
const UserComponent = (props: any, ref: any) => {
// 测试工作台面板之间互联互通专用代码
const emitUserChanged = (type: string) => {
// 1.自定义事件方式
window.globalStore.emitWorkbenchEvent('todo-list-changed', { type }, ref.current);
// 2.使用系统内置通信通道
window.globalStore.emitWorkbenchEvent(props.linkageValue, { type }, ref.current);
};
const onClose = () => {
modal.close(); // 关闭模态框
tableStore.reload(true); // 刷新表格数据
emitUserChanged('addOrUpdate');
};
return (
<Fragment>
<div className="qx-main">
<AgGrid.SearchFormTable tableConfig={tableConfig} formConfig={formConfig} searchApi={searchApi}>
<Button size="small" onClick={showAddForm} type="primary">
新增
</Button>
</AgGrid.SearchFormTable>
</div>
<Modal store={modal}></Modal>
</Fragment>
);
};
// 这里为了能够在组件内部拿到ref属性,必须用forwardRef来封装
export default observer(React.forwardRef(UserComponent));
2. 接收事件
API
// 组件实例实现onWorkbenchEvent方法,返回需要接受的事件以及对应的处理函数
onWorkbenchEvent = () => {
return {
[eventNawme]: (event: any) => {},
};
};
代码参考(Class写法)
export default class TodoList extends Component<any, IState> {
// 监听来自另外一个窗口的todo-list-changed消息,同步更新数据
// 主要:只有在同一个工作台的其它窗口发送的消息才有效
// 这个只是为了测试工作台中面板之间的连通性
onWorkbenchEvent = () => {
return {
// 监听自定义事件
'todo-list-changed': (event: any) => {
const { type, data } = event;
if (type === 'add') {
const todos = this.state.todos;
todos.push(data);
this.setState({ todos: [...todos] });
} else if (type === 'remove') {
let todos = this.state.todos;
todos = todos.filter((item) => item.id !== data.id);
this.setState({ todos: [...todos] });
}
},
// 监听系统内置通信通道
[`${this.props.linkageValue}`]: (data: any) => {
console.log(data);
},
};
};
render() {
const { todos } = this.state;
return (
<div>
<Header onSubmit={this.handleSubmit} />
<Content todos={todos} onDelete={this.hanldeDelete} />
<Footer todos={todos} />
</div>
);
}
}
代码参考(Hooks写法)
const UserComponent = (props: any, ref: any) => {
// 要使父组件能够通过useRef或者createRef拿到组件实例并调用组件方法,
// 必须使用useImperativeHandle配合forwardRef函数
React.useImperativeHandle(ref, () => {
return {
props,
onWorkbenchEvent: () => {
return {
'workbench-user-changed': (data: any) => {
tableStore.reload();
},
// 监听系统内置通信通道
[`${props.linkageValue}`]: (data: any) => {
console.log(data);
},
};
},
};
});
return (
<Fragment>
<div className="qx-main">
<AgGrid.SearchFormTable
tableConfig={tableConfig}
formConfig={formConfig}
searchApi={searchApi}
></AgGrid.SearchFormTable>
</div>
<Modal store={modal}></Modal>
</Fragment>
);
};
// 这里为了能够在组件内部拿到ref属性,必须用forwardRef来封装
export default observer(React.forwardRef(UserComponent));
四、 iframe 事件总线
- 工作台 1 中的子系统 A 发生变化,将消息通知出去,同一个工作台中的子系统 B 接收到事件,响应变化
- 事件既可以仅局限于当前工作台,即在其它工作台中同样的子系统 B 是收不到事件的。
- 事件也可以不局限于当前工作台,即在其它工作台中同样的子系统 B 也能收到事件。
1. 发送事件
示例:子系统 A 发送【用户变化】事件
方式一:使用 postMessage
函数发送事件
sendWorkbenchEvent = (props) => {
window.isInWorkbench &&
window.parent.postMessage(
{
// 类型,标识为工作台事件,写死
type: 'workbenchEvent', // 类型,标识为工作台事件
// 目标应用Id,向那个应用发送事件,如果不指定,则向所有iframe发送事件
appId: 'godzillaPro2',
// 工作台Id,这个参数会在加载iframe的时候,通过url传给子系统
// 指定事件只会向所在的工作台中的其它iframe发送,如果不指定,则向所有iframe发送
// window._workbenchId,这个变量就是从url获取到,然后存储在window对象
workbenchId: window._workbenchId,
// 面板Id,,这个参数会在加载iframe的时候,通过url传给子系统
// 指定事件不会向自身应用发送,也即自身即使监听了该事件,也不会收到事件
// window._layoutItemId,这个变量就是从url获取到,然后存储在window对象
layoutItemId: window._layoutItemId,
// 事件名称,可以随意指定,但要确保唯一,事件要有含义
eventName: 'user-data-changed',
// 携带的业务数据,对象存储
props: {
type: 'addUser',
data: {}, // user数据
}, // 携带的业务数据
},
'*',
);
};
方式二:使用 GlobalStore 中封装工作台子应用通信函数 emitSubAppWorkbenchEvent
发送事件
// =============== 函数定义 ==================
/**
* Godzilal 子应用发送工作台 iframe 事件总线事件
* @param event 事件名称
* @param payload 传输数据
* @param config 额外配置
* @param targetOrigin postMessage 的 targetOrigin 参数
*/
emitSubAppWorkbenchEvent = (
event: string,
payload: any,
config: Omit<ISubAppWorkbenchEventConfig, 'eventName' | 'type'> = {},
targetOrigin = '*',
) => {
// 封装了方式一中的 postMessage 逻辑
// ...
};
// ===== 1、自定义通信事件使用示例 =====
window.globalStore.emitSubAppWorkbenchEvent('user-data-changed', {
type: 'addUser',
data: {},
});
// =================================================
// ==== 2、使用 emitSubAppWorkbenchEvent 函数进行通信通道示例 ====
// event 为通道名称 linkageValue
// payload 传递的参数数据
// config 需要传入 workbenchId、layoutItemId、linkageValue、eventName 来限制只能当前工作台相同信道通信
window.globalStore.emitSubAppWorkbenchEvent(
this.props.linkageValue,
{
type: 'addUser',
data: {
code: this.formIns.getFieldValue('code'),
},
},
{
workbenchId: this.props.workbenchId,
layoutItemId: this.props.layoutItemId,
linkageValue: this.props.linkageValue,
eventName: this.props.linkageValue,
// ... 其他配置参数
},
);
方式三:使用 emitSubAppWorkbenchLinkageEvent 通信通道函数发送通道事件(推荐)
// =============== 函数定义 ==================
/**
* 发送 Godzilal 子应用工作台 iframe 事件通信通道总线事件
* @param event 事件名称
* @param payload 传输数据
* @param config 额外配置
* @param targetOrigin postMessage 的 targetOrigin 参数
*/
emitSubAppWorkbenchLinkageEvent = (
event: string,
payload: any,
config: Omit<ISubAppWorkbenchEventConfig, 'eventName' | 'type' | 'linkageValue'> = {},
targetOrigin?: string,
) => {
// 封装了 方式二 中 emitSubAppWorkbenchEvent 的逻辑可以不需要传 config
// ...
};
// ====== 使用 emitSubAppWorkbenchLinkageEvent 进行通信通道示例 ======
// emitSubAppWorkbenchLinkageEvent 函数在通用的 emitSubAppWorkbenchEvent 函数基础上封装了通道通信内容
// event 为通道名称 linkageValue
// payload 传递的参数数据
// config 如果不传函数会自己获取
window.globalStore.emitSubAppWorkbenchLinkageEvent(
this.props.linkageValue,
{
type: 'addUser',
data: {},
},
{
workbenchId: this.props.workbenchId, // 也可以省略
layoutItemId: this.props.layoutItemId, // 也可以省略
},
);
// ===== 省略 config 配置 =====
// 注意⚠️:省略 config 时限制只在当前工作台通信
window.globalStore.emitSubAppWorkbenchLinkageEvent(this.props.linkageValue, {
type: 'addUser',
data: {},
});
2. 接收事件
示例:子系统 B 响应【用户变化】事件
方式一:通过 addEventListener 监听事件
window.addEventListener(
'message',
(event) => {
const { type, eventName, props } = event.data;
if (type === 'workbenchEvent') {
if (eventName === 'user-data-changed') {
// 响应事件逻辑
console.log(data);
}
}
},
true,
);
方式二:通过 onSubAppWorkbenchEvent 监听事件(推荐)
// ============== 函数定义 ================
/**
* 响应 Godzilal 子应用工作台 iframe 事件通信通道总线事件
* @params name 事件名称
* @params handler 响应事件的处理函数
* @parmas capture 冒泡/捕获阶段设置处理程序
* @return 返回移除事件监听的函数
*/
onSubAppWorkbenchEvent = (
name: string,
handler: (payload: any, event: MessageEvent<ISubAppWorkbenchEventConfig>) => void,
capture = true,
) => {
// 处理逻辑
const listener = () => {
// ...
handler();
};
window.addEventListener('message', listener, capture);
return () => window.removeEventListener('message', listener, capture);
};
// ============== 使用示例 ================
useEffect(() => {
// name 为自定义的事件名称,或者内置的通信通道名称(linkageValue)
// handler 为自定义的事件处理函数
const unsubscribe = window.globalStore.onSubAppWorkbenchEvent(props.linkageValue, (payload) => {
// 接收到数据进行逻辑处理
formRef.setFieldsValue(payload.data);
});
return () => unsubscribe();
}, []);
五、 工作台组件的参数存储
v2.1.14 以上版本可用
实现原理图
- 实现在工作台上保存组件的参数,其中工作台组件包含,portal 自带组件、 gza 微应用组件和 gza 子应用组件。
- 工作台提供了两种传递组件参数的方式,提供了一个保存组件参数的 API。
- 工作台还实现了自动保存组件参数的功能,需要在新建或编辑工作台时设置。
- 组件参数只能在组件首次渲染时取到。
- 工作台使用点击“关闭组件”按钮后组件参数会跟随组件一起删除。
1. props
使用 props 可以在组件中直接引用,这种方式对于 portal 自带组件和 gza 微应用组件没有什么限制,但是对于 gza 子应用组件有长度限制,当 params 长度大于 2k 字节时,不建议使用。
props 还有一个优点,可以在组件加载完成之前获取到 params,例如使用 props 可以在表格自动加载数据前设置参数。
props = {
...
params: Object, // 组件参数
layoutItemId: String, // 工作台面板Id
workbenchId: String // 工作台Id
...
}
2. onWorkbenchEvent
onWorkbenchEvent 是另一种传递方式的载体,使用事件回调的方式传递组件数据,想要接收到参数必须按照下面的用法配置。
onWorkbenchEvent 可以传递 params 长度大于 2k 字节,但是有一个缺点,只能在组件完全加载之后才能获取到 params。
export default class TodoList extends Component {
// componentParams 是系统内部定义的默认事件,业务开发人员无需关心。
onWorkbenchEvent = () => {
return {
// 监听 componentParams 事件
componentParams: (data) => {
console.log('组件参数', data);
},
};
};
}
const UserComponent = (props, ref) => {
// componentParams 是系统内部定义的默认事件,业务开发人员无需关心。
React.useImperativeHandle(ref, () => {
return {
props,
onWorkbenchEvent: () => {
return {
// 监听 componentParams 事件
componentParams: (data) => {
console.log('组件参数', data);
},
};
},
};
});
};
// 这里为了能够在组件内部拿到ref属性,必须用forwardRef来封装
export default React.forwardRef(UserComponent);
3. addWorkbenchComParams
addWorkbenchComParams 是 globalStore 的方法,通过调用此方法向 portal 传递组件参数。工作台组件的所有组件参数均在 portal 临时存储,用户可以根据需求设置工作台组件参数的存储方式。
type Config = { layoutItemId: string; workbenchId: string };
type addWorkbenchComParams = (config: Config, params: Object) => void;
class Command extends Component {
componentDidMount() {
console.log('组件参数', this.props.layoutItemId, this.props.params);
}
handleSubmit = async (e) => {
e.preventDefault();
this.props.form.validateFields((err, values) => {
if (!err) {
// 注意!!!isInWorkbench 判断当前组件是否在工作台打开,没有这层判断,当组件在 tab 打开时会报错,因为 globalStore 没有注册
if (window.isInWorkbench) {
window.globalStore.addWorkbenchComParams(
{
layoutItemId: this.props.layoutItemId,
workbenchId: this.props.workbenchId,
},
{
...values,
},
);
}
}
});
};
render() {
return (
<Form onSubmit={this.handleSubmit}>
<Form.Item label="指令编码">
{getFieldDecorator('instCode', {
rules: [{ required: true, message: '请输入指令编码' }],
})(<Input />)}
</Form.Item>
<Form.Item style={{ textAlign: 'right' }}>
<Button type="primary" htmlType="submit">
发送
</Button>
</Form.Item>
</Form>
);
}
}
const Com = Form.create({})(Command);
export default React.forwardRef((props, ref) => {
return <Com wrappedComponentRef={ref} {...props} />;
});
addWorkbenchComParams 后有两种情况。
情况一:在新增或编辑工作台的时候自动保存组件参数没有勾选,调用 addWorkbenchComParams 后需要操作人员手动点击保存,组件参数才会保存生效。
情况二:在新增或编辑工作台的时候自动保存组件参数已经勾选,调用 addWorkbenchComParams 后无需操作人员手动点击保存,系统内部会自动保存组件参数。