工作台融合方案
前言
Godzilla可以作为一个独立的门户应用,接入其它子系统,也可以作为一个子应用,被嵌入到其它门户应用,并利用工作台来接入其它子系统,提供灵活、个性化工作台配置的能力。下面详细介绍GZA如何以子系统的方式被嵌入到另外一个门户中。
门户接入GZA应用
总体部署图
iframe src格式
假设GZA的环境信息如下
- 门户地址 http://portal.com
- UI地址 http://gza.ui.com
- BFF地址 http://gza.bff.com
- 应用ID gza
假设门户的信息如下
- nginx地址 http://portal.com , 所有的静态资源和动态资源都在这里进行转发,包括门户应用和子应用
- 统一API网关 http://portal.com/api-gateway/xxx 所有的请求的url前缀都是api-gateway,后面+服务名称+path
- 静态资源转发 http://portal.com/xxx,所有的系统静态资源都通过nginx来转发,例如会将 http://portal.com/gza 转发到 http://gza.ui.com,同样门户也在这里转发
两种认证方式
token认证
嵌入GZA时,需按照如下格式以iframe的形式加载GZA
各参数含义如下
appId,应用Id,必须提供
authToken,认证的token,需提供私钥用来验证和解析token,从而拿到用户信息。
page,需要打开的页面组件URL,可以是某个具体菜单的URL,也可以是工作台的URL
isInIframe, 这里为true
这里需要注意,需要提供子系统接入的认证token(和相应的秘钥),如果不提供,也可以直接明文传输用户信息,但通常不建议这样做。
BFF拿到authToken,调用门户提供的获取用户信息的接口(例如sys/loginController/userInfo),第一可以校验用户,第二可以拿到用户信息,验证通过后重定向到http://portal.com/gza?userCode=xxx&token=xxx&appId=gza&page=system/Application&isInIframe=true 其中userCode为用户编码,token为jwt包含用户信息(这两个参数为GZA专用,其它子系统不会用到),其它参数是透传的
cookies认证
嵌入GZA时,需按照如下格式以iframe的形式加载GZA
http://portal.com/api-gateway/gza/login?appId=gza&page=system/Application&isInIframe=true
各参数含义如下
- appId,应用Id,必须提供
- page,需要打开的页面组件URL,可以是某个具体菜单的URL,也可以是工作台的URL
- isInIframe, 这里为true
BFF通过cookies拿到token,接下来的流程同上
两种嵌入方案
方案一
多个子页面(属同一个子应用)共享同一个iframe实例
GZA初始化之后,会向门户发送一个iframeReady的消息,门户收到iframeReady的消息后,需要再向GZA发送一个openPage的消息并指定要打开的页面。
为什么不可以在嵌入iframe后就向该iframe发送消息,这是因为iframe内部可能还没加载完,如果在没加载完之前就向iframe发送消息,iframe可能无法收到消息,所以需要等待iframe加载完之后才才可以发送消息,iframeReady就是这样的一个事件,只有收到这个事件了,才可以发消息
// 1. GZA子系统向门户发送iframeReady消息
window.parent.postMessage({
type: 'iframeReady', // 消息类型
appId: 'gza'
}, '*');
// 2. 门户收到iframeReady消息,然后向刚加载的iframe发送消息
window.addEventListener('message', (event) => {
const {type, appId} = event.data;
if(type === 'iframeReady') {
// 门户最好维护一份iframes列表,key为应用id,value为iframe实例
// 然后这里就可以通过应用id直接拿到iframe实例并向其发送消息
gzaIframe.postMessage({
type: 'openPage',
page: 'system/Application'
});
}
}, true);
// 3. GZA子系统收到openPage事件,打开相应的页面
方案二
多个子页面(属同一个子应用)独享iframe,也即会有多个iframe实例
只需要在url上再增加一个参数isInWorkbench=true即可,例如:http://127.0.0.1:8889/login?appId=gza&authToken=xxx&page=system/Application&isInIframe=true&isInWorkbench=true
GZA工作台嵌入子应用
子应用管理
需要将接入工作台的子应用信息管理起来,并提供管理页面,GZA已经提供界面录入,只需要修改BFF,调用服务端接口将数据保存到后端数据库即可。
管理界面
对应的page为page=system/Application
,需要将该菜单挂在到门户菜单上,然后可以通过门户打开该界面进行维护
表结构(建议)
需要一张表来存储应用信息(sys_application),以下表结构为Godzilla推荐字典,当然您也可以按照自己的规范来定义,只要信息能够对应上即可
字段 | 描述 | 类型 | 非空 | 备注 |
---|---|---|---|---|
app_id | 应用编码 | varchar(32) | 是 | 唯一 |
app_name | 应用名称 | varchar(64) | 是 | |
app_url | 应用地址 | varchar(512) | 是 | |
app_type | 应用类型 | varchar(16) | 是 | 仅支持iframe_src |
status | 应用状态 | int(1) | 是 | 1=启用,2=禁用 |
修改接口
修改BFF中src/lib/dao/application.ts代码,修改所有的实现,指向后端服务(目前是存储在本地的,上线后数据会丢失)
调用门户提供的接口时,按照门户的接口规范提供即可,例如需要token、netNo、language,则按照这种规范提供
import { BaseDao } from './base_dao';
import { provide, inject } from 'midway';
// const tableName = 'applications';
@provide()
export class ApplicationDao extends BaseDao {
// db: any;
constructor(@inject() ctx) {
super(ctx);
// this.db = this.app.lowdb.get(tableName);
}
/**
* 根据应用ID获取应用信息
* @param appId 应用ID
*/
getByAppId(appId) {
// TODO 修改为自己的实现,其它接口类似
// const data = this.db.find({ appId }).value();
// return Promise.resolve(data);
}
// ... 省略其它接口
}
接口定义(建议)
以下接口只是推荐,您可以按照自己的规范提供,功能对应上即可。
获取所有应用信息列表
url: api/v1/sys_applications/list
method: GET
response
{
code: 200,
data: {
list: [{
"appName": "百度",
"appId": "baidu",
"appUrl": "https://www.baidu.com",
"status": "1",
"appType": "iframe_src"
}, {
// ...
}]
},
msg: '操作成功'
}
获取单个应用信息
url: api/v1/sys_applications/:appId
method: GET
response
{
code: 200,
data: {
"appName": "百度",
"appId": "baidu",
"appUrl": "https://www.baidu.com",
"status": "1",
"appType": "iframe_src"
},
msg: '操作成功'
}
保存应用信息
url: api/v1/sys_applications
method: POST
request
{
"appName": "百度",
"appId": "baidu",
"appUrl": "https://www.baidu.com",
"status": "1",
"appType": "iframe_src"
}response
{
code: 200,
msg: '操作成功'
}
修改应用信息
url: api/v1/sys_applications/:appId
method: PUT
request
{
"appName": "百度",
"appUrl": "https://www.baidu.com",
"status": "1",
"appType": "iframe_src"
}response
{
code: 200,
msg: '操作成功'
}
修改应用信息
url: api/v1/sys_applications/:appId
method: DELETE
response
{
code: 200,
msg: '操作成功'
}
门户配置应用菜单
应用信息是在GZA维护的,但是菜单是需要在门户统一维护的,在维护菜单的时候,需要把应用id携带上,工作台在加载子应用的时候,是根据应用id去找到对应的应用地址的,而不是在维护菜单的时候就把应用地址维护上了,配置完菜单后,就可以给用户授予菜单权限。
GZA需要的菜单信息如下
id, 菜单id
pId,菜单父级id,如果是一级菜单,则和id一样。
appId, 应用Id,例如这里填gza
name, 菜单名称(或者工作台名称)
url, 菜单url(或者工作台url),例如
system/Application
或者FlexWorkbench/layout-1584927132939
工作台显示组件列表
这里显示的组件列表需要通过一个接口拿到,通常是门户提供的获取用户菜单的接口,接口需要包含如下要素
- id, 菜单id
- pId,菜单父级id,如果是一级菜单,则和id一样。
- appId, 应用Id,例如这里填gza
- name, 菜单名称(或者工作台名称)
- url, 菜单url(或者工作台url),例如
system/Application
或者FlexWorkbench/layout-1584927132939
需要注意的是所返回的菜单,如果对应的应用信息没有在GZA中维护,那么是不能够被加载的,需要维护进去或者将这一部分的菜单过滤掉。
修改BFF代码src/lib/dao/menu.ts中的getUserMenu方法,调用服务端提供的方法
子应用接入认证
工作台中的子应用如果也需要认证,那么最好提供统一认证方式,比如JWT,然后在加载子应用的时候将token通过url传递给子应用,这个token既可以在GZA中来生成和维护,也可以门户统一提供,工作台在加载子应用的时候将token透传给子应用。
注意,这里的token只是一种认证方式,也可以换成cookies认证
工作台配置管理
表结构
服务端提供一张表用于存储工作台配置(sys_workbench),表结构如下,并提供增删改查的接口
注意,您可以按照自己的规范定义表结构,以下只是推荐,信息对应上即可。
字段 | 描述 | 类型 | 非空 | 备注 |
---|---|---|---|---|
id | 工作台id | varchar(64) | 是 | 非自增,由客户端生成 |
user_code | 用户编码 | varchar(32) | 是 | |
type | 工作台类型 | varchar(16) | 是 | system=系统场景&user=个人工作台 |
config | 工作台配置 | blob | 是 | json结构 |
获取用户工作台列表
包含系统场景
url: api/v1/sys_workbenchs/list
method: GET
response
{
code: 200,
data: {
list: [{
"id": "layout-xxx",
"userCode": "admin",
"config": {
// json结构
},
}, {
// ...
}]
},
msg: '操作成功'
}
新增工作台
url: api/v1/sys_workbenchs
method: POST
request
{
id: '',
userCode: '',
type: '',
config: {}
}response
{
code: 200,
msg: '操作成功'
}
修改工作台
url: api/v1/sys_workbenchs/:id
method: PUT
request
{
userCode: '',
type: '',
config: {}
}response
{
code: 200,
msg: '操作成功'
}
删除工作台
url: api/v1/sys_workbenchs/:id
method: DELETE
response
{
code: 200,
msg: '操作成功'
}
工作台使用
打开工作台管理
对应的page为page=WorkbenchManager ,需要将该菜单挂载到门户菜单上,然后可以通过门户打开该界面进行工作台维护
新增工作台
点击【新增个人工作台】或者【新增系统场景】按钮,选择布局后可制作工作台
点击【确定】按钮后,会打开一个新的tab,并在新的tab上选择组件制作工作台,由于GZA是作为一个子系统被嵌入的,而打开一个新的tab是门户的行为,所以GZA会选择向门户发送postMessage消息,并要求打开一个新的tab,注意该tab并不存在于当前系统菜单中。
// GZA发送postMessage
window.parent.postMessage({
type: 'openPage', // 消息类型
name: '我的工作台', // 工作台名称
page: 'FlexWorkbench',
appId: 'gza', // 应用ID
props: {
key: 'layout-1584754227011', // layout-1584754227011为工作台Id
createConfig, // 工作台配置
}
});
// 门户收到openPage消息,打开一个新的tab,并向gza的iframe发送openPage消息
gzaIframe.postMessage({
type: 'openPage',
name: '我的工作台',
page: 'FlexWorkbench',
props: {
key: 'layout-1584754227011', // layout-1584754227011为工作台Id
createConfig, // 工作台配置
}
});
// GZA收到openPage事件,打开相应的工作台页面
以上是假设gza是只有一个实例的情况,如果是以多实例(每个工作台独享iframe)存在的,则稍微简单些,门户收到openPage消息后,打开一个新tab,加载新的iframe即可,其中page=FlexWorkbench/layout-1584754227011
将工作台url维护到门户
新增工作台之后,工作台列表就多了一个工作台,如果新增的工作台类似是系统场景,此时可以将工作台的url维护到门户菜单上,然后通过菜单授权,用户就可以直接通过菜单打开工作台。
设置默认工作台
默认工作台是用户个性化的行为,也即每一个用户设置的默认工作台可能会不一样,门户在初始化的时候先加载工作台列表(含个人和系统工作台),并筛选出默认工作台,然后打开相应的工作台,如果默认工作台有多个,则默认打开多个tab,并且显示最后一个tab的内容。
如何判读工作台:config配置中isDefaultOpen=true为默认工作台
修改工作台名称
修改工作台名称的时候,如果被修改的工作台已经打开,则会强制关闭原先的工作台,否则会造成工作台不一致的情况, GZA会向门户发送closePage的消息通知关闭tab
window.parent.postMessage({
type: 'closePage',
appId: 'gza',
page: 'FlexWorkbench/layout-1584754227011'
});
删除工作台
删除工作台的时候,如果被删除的工作台已经打开,则会强制关闭原先的工作台,否则会造成工作台不一致的情况, GZA会向门户发送closePage的消息通知关闭tab(同上)
工作台组件间通信
事件总线
发送事件
示例:子系统 A 发送【用户变化】事件
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数据
} // 携带的业务数据
},
'*'
);
};
接收事件
示例:子系统 B 响应【用户变化】事件
window.addEventListener(
'message',
event => {
const { type, eventName, props } = event.data;
if (type === 'workbenchEvent') {
if (eventName === 'user-data-changed') {
// 响应事件逻辑
console.log(data);
}
}
},
true
);
如果通过子系统打开新的标签页
方案一,通过postMessage向门户发送消息,打开新标签页
方案二,方案一有个缺点,只能通过api触发,但是有很多是通过a标签触发的,这个时候要改造成api调用,是比较麻烦的,风险也比较大,但也不是没办法,可以通过捕获a标签的点击事件,如下为代码参考。
/* eslint-disable wrap-iife */
(function m() {
// eslint-disable-next-line space-before-function-paren
window.__link_proxy = function(options = {}) {
if (window === window.top) {
// return;
}
if (!options.appMapping) {
return;
}
document.body.addEventListener(
'click',
e => {
// 1. A标签
// 2. href是一个有效的url
// 3. target是_blank,也即原本是新窗口打开
// 符合以上条件,则会通知门户打开新的tab并加载其内容
if (e.target && e.target.tagName === 'A') {
const { target, innerText, href, host, pathname } = e.target;
if (target === '_blank' && href && href.startsWith('http')) {
console.log(e);
e.preventDefault();
let appId = options.appMapping[host];
if (!appId && window.location.host === host) {
appId = options.appId;
}
if (appId) {
window.top.postMessage({
type: 'openPage',
page: pathname.substr(1),
appId,
name: innerText || '详情',
});
return false;
}
}
}
return true;
},
true
);
};
})();
其它(建议)
工作台如何运维,如果将工作台url挂在到门户菜单上,则用户自定的工作台要挂在上去可能没那么容易。要么就提供单独的管理功能,比如右上角显示工作台的列表,用户可通过这个地方来打开工作台,且仅提供打开工作台的功能,如需新增、修改或删除工作台,可能最好还是打开【工作台管理】菜单进行操作。