Skip to main content
Version: 2.x

子应用开发或迁移

如果采用 Godzilla 框架,则参考开始使用可以很方便的创建一个应用,如果不是,那么也没关系,这里也提供了接入指南

一、 子应用接入

非 Godzilla 框架的系统我们称之为异构系统,针对异构系统,目前仅支持以 iframe 的方式嵌入,接下来我们分别接入几个不同类型的子应用

  1. 嵌入单一页面子应用,比如嵌入百度首页,这种类型,一个子应用对应一个菜单
  2. 嵌入多页面子应用,这种一般指遗留系统,这种类型,一个子应用拥有多个菜单
  3. 嵌入微前端子应用,微前端目前仅支持 Godzilla 开发框架开发的应用,这种也是一个子应用拥有多个菜单

1. 嵌入单页子应用

首先,我们要新增一个应用,打开[应用管理]菜单,按照如下信息进行新增

  • 应用 ID,这个必须唯一,每一个接入的子应用都需要提供应用 ID,这个应该 ID 非常重要,在很多地方都会使用,并且一旦定下来,不可随意修改,这里可填入baidu
  • 应用名称,子应用的名称,这里可填入百度
  • 应用地址,子应用地址,这里填入https://www.baidu.com
  • 应用类型,选择普通子应用
  • 应用状态,选择启用

PNG

其次,我们需要配置一个菜单,打开 layouts 并新增一个菜单(目前都是静态菜单,将来改为动态菜单时可通过界面配置),参考路由和菜单,菜单信息如下

// src/app/layouts/index.ts
{
"id": 30, // 新增一级菜单(子应用)
"pId": 30,
"icon": "chanpinguanli", // 菜单图标
"appId": "portal", // 统一为 portal
"name": "子应用"
},
{
"id": 31, // 菜单ID,需要唯一
"pId": 30, // 父级菜单ID
"name": "百度", // 菜单名称
"appId": "baidu", // 应用
"url": "" // 由于这个子应用只有一个菜单,所以不需要配置菜单URL,菜单URL已经在应用接入那里配置了
},

最后,并刷新页面,可以看到新增了一个一级菜单[子应用]和对应的二级菜单[百度] PNG

注意,需要通过 127.0.0.1 访问,而不能通过 localhost 访问,否则可能出现页面访问不了的情况

2. 嵌入异构系统子应用

异构系统,一般指遗留系统,既可以是 React/Vue/Angular 开发的 SPA 应用,也可以是以前的 JSP、ASP 应用,只要系统稍加改造,就能够嵌入到这个平台上来,具体可参考子应用以 Iframe 方式接入,在这里,为了演示方便,Godzilla 框架是内置支持以 Iframe 的形式作为子应用,下面详解操作步骤

第一步,我们复制 godzilla-portal 的代码并重命名为 godzilla-pro,这个将作为一个子应用

第二步,修改 config/config.js 配置文件,将 APP_ID 由portal改为godzillaPro,将 isPortal 设置为false(或者删除这个配置项),将 devServerPort 设置为7888(或者其它端口)

第三步,增加子应用的测试菜单,同样在 layouts 增加静态菜单,这里需要注意 appId 必须为 godzillaPro(也即子应用的 ID),以下为示例菜单,按照这种数据结构可增加其它菜单

({
"id": 38,
"pId": 30,
"name": "godzillaPro-用户管理",
"appId": "godzillaPro",
"url": "sample/table/User"
},
{
"id": 39,
"pId": 30,
"name": "godzillaPro-应用接入",
"appId": "godzillaPro",
"url": "system/Application"
})

最后,启动 godzilla-pro,访问http://127.0.0.1:7888, 这个子应用就跑起来了,这个子应用可独立开发和部署

PNG

接下来,我们将这个子应用接入 portal 平台,首先新增一个子应用

  • 应用 ID,godzillaPro
  • 应用名称,Godzilla Pro
  • 应用地址,http://127.0.0.1:7888
  • 应用类型,选择Godzilla子应用
  • 应用状态,选择启用

其次,改变子应用的访问方式,原来是通过浏览器直接访问http://127.0.0.1:7888, 现在通过portal菜单可以访问http://127.0.0.1:8889/godzillaPro,其中 godzillaPro 就是子应用的 ID,如果可以访问,则说明程序已经没问题 PNG

最后,同样改变 portal 的访问方式,访问http://127.0.0.1:8889 (注意不是http://127.0.0.1:8888啦), 进入系统,打开子应用的菜单,如果能够打开,就说明集成成功啦 PNG

3. 嵌入 Ant Design Pro 子应用

上面接入的子应用是用 Godzilla 框架开发的,可能还不够有说服力,下面我们尝试将Ant Design Pro嵌入到主应用 代码如何改造,请参考子应用以 Iframe 方式接入,目前我们已经对 Ant Design Pro 进行改造,并提交到 github 上,修改的内容并不多,具体请参考commit

第一步,克隆 ant-design-pro 代码,并跑起来

git clone https://github.com/jackshen310/ant-design-pro.git
cd ant-design-pro
git checkout -b iframe origin/iframe # 切换到iframe分支
npm install # 安装依赖
npm start # 访问 http://127.0.0.1:8888

第二步,增加这个子应用,应用 ID 为antd-pro

  • 应用 ID,antd-pro
  • 应用名称,Ant Design Pro
  • 应用地址,http://127.0.0.1:8000
  • 应用类型,选择普通子应用
  • 应用状态,选择启用

第三步,增加测试菜单,这里面的菜单,可以随便增加,我们这边增加三个测试菜单

({
"id": 50,
"pId": 50,
"name": "Ant Design Pro",
"appId": "antd-pro",
"icon": "chanpinguanli"
},
{
"id": 51,
"pId": 50,
"name": "基础表单",
"appId": "antd-pro",
"url": "form/basic-form"
},
{
"id": 52,
"pId": 50,
"name": "工作台",
"appId": "antd-pro",
"url": "dashboard/workplace"
},
{
"id": 53,
"pId": 50,
"name": "流程编辑器",
"appId": "antd-pro",
"url": "editor/flow"
})

最后,刷新浏览器,这个时候,这个子应用就嵌入进来了 PNG

4. 嵌入 Godzilla 微应用

微前端,是最近比较流行的针对巨石应用的一种解决方案,微前端是什么?在这里不做阐述,请大家自行 google,我们采用的是 Single-SPA 这种解决方案,并在此基础上支持多实例的功能(多实例,也即多个子应用实例同时存在)以解决一个工作台内嵌不同子应用模块,下面详解操作步骤

第一步,我们复制 godzilla-pro 的代码并重命名为 godzilla-pro2,这个将作为另外一个子应用 第二步,修改 config/config.js 配置文件,将 APP_ID 由godzillaPro改为godzillaPro2,将 devServerPort 设置为6888(或者其它端口) 第三步,增加子应用的菜单,同样在 layouts 增加静态菜单,这里需要注意 appId 必须为 godzillaPro2(也即子应用的 ID),以下为示例菜单(按照这种数据结构可增加其它菜单)

  {
"id": 36,
"pId": 30,
"name": "godzillaPro2-表格性能测试",
"appId": "godzillaPro2",
"url": "sample/Performance"
},
{
"id": 37,
"pId": 30,
"name": "godzillaPro2-用户管理",
"appId": "godzillaPro2",
"url": "system/table/User"
},

最后,启动 godzilla-pro2 应用并访问 http://127.0.0.1:6888, 这个子应用就跑起来了,同样,这个子应用可独立开发和部署 PNG

接下来,我们将这个子应用接入 portal 平台,首先新增一个子应用

  • 应用 ID,godzillaPro2
  • 应用名称,Godzilla Pro2
  • 应用地址,http://127.0.0.1:6888
  • 应用类型,选择Godzilla子应用
  • 应用状态,选择启用

其次,改变子应用的启动方式,原来是npm start,现在需要改为spa:start,注意以这种方式启动后,就不能够单独访问这个这个子系统了,需要通过 portal 来访问

这里需要特别注意,微应用(single-spa)的部署方式和子应用(iframe)的部署方式是不一样的,所以他们的打包方式也不一样,注意区分

最后,访问 http://127.0.0.1:8889, 进入系统,打开子应用的菜单,如果能够打开,就说明集成成功啦 PNG

二、 子应用交互

将一个系统拆分成多个子系统后,不可避免的问题是他们之间的交互,原来大家都在一个系统,我打开另外一个模块的菜单都是非常容易,假设子系统 A 要打开子系统 B 的页面,这中间应有某种通信方式,如果是 iframe 嵌入的,那么一般会通过 window.postMessage 这种方式来通信,如果是 single-spa(微前端)嵌入的,那么应有某种类似消息总线的,来协同各个子应用,下面分别介绍几种场景以及他们的使用方式

1. 主应用打开子应用页面

也就是在 portal 通过 API 的形式打开子应用(iframe)的页面,框架要实现这样的功能,大概的思路是这样的

  1. 通过 appId 找到对应的子应用的 iframe
  2. 向子应用发送 openPage 消息,iframeWindow.postMessage({ type:'openPage', page:'sample/User', appId:'godzillaPro', name: '用户管理'})
  3. 子应用收到 openPage 消息,自行打开相应的页面,也即主应用无法直接打开子应用的页面,而是告诉子应用,我要打开某个页面,你收到消息后,帮我打开这个页面而已。所以,每一个子应用都需要实现这样的逻辑

Godzilla 框架以及 Godzilla 子应用,已经封装了这些底层操作,对外提供一致的 API 进行操作,假设我要在用户管理页面打开表格性能测试页面,打开 sample/User/index.tsx 文件,增加一个按钮打开Iframe子页面,并增加一个方法 openIframePage,然后刷新页面,点击按钮[打开 Iframe 子页面]就可以打开这个子应用页面啦

const UserComponent = () => {
const showIframePage = () => {
window.openTab({
url: 'sample/Performance',
appId: 'godzillaPro',
name: '表格性能测试'
});
// 当然,你也可以通过如下方式来操作,只是不建议这么做
// window.postMessage(
// {
// type: 'openPage',
// appId: 'godzillaPro',
// page: 'sample/Performance',
// name: '表格性能测试',
// },
// '*'
// );
};
return (
<Fragment>
<div className="qx-main">
<AgGrid.SearchFormTable>
<Button size="small" onClick={showAddForm} type="primary">
新增
</Button>
<Button size="small" onClick={showIframePage} type="primary">
打开Iframe子页面
</Button>
</AgGrid.SearchFormTable>
</div>
<Modal store={modal}></Modal>
</Fragment>
);
};

另外,也支持传递参数,比如这个页面用户详情,假设这个详情是放在 godzillaPro 这个子应用的,打开 sample/User/index.tsx 文件,增加一个操作按钮详情(iframe),并增加一个方法 showDetailByIframe,然后刷新页面,点击按钮[详情(iframe)]就可以打开这个详情页啦,注意,这个详情页是放在子应用的

const UserComponent = () => {
const showDetailByIframe = (data: any) => {
window.openTab({
url: 'sample/UserDetail',
name: `用户详情(${data.code})`,
appId: 'godzillaPro',
props: {
key: data.code, // 去掉key则同一时刻只会有一个详情页面
code: data.code // 这里就是参数,在详细页面,可以通过this.props.code获取
}
});

// 同样,也可使用如下方式,但不建议
// window.postMessage(
// {
// type: 'openPage',
// appId: 'godzillaPro',
// page: 'sample/UserDetail',
// name: `用户详情(${data.code})`,
// props: {
// key: data.code, // 去掉key则同一时刻只会有一个详情页面
// code: data.code,
// },
// },
// '*'
// );
};

const tableConfig: TableConfig = {
props: {
columnDefs: [
{
headerName: '操作',
colId: 'operation',
cellRendererFramework: ({ data = {} }: any) => {
return (
<AButtonGroup>
<a onClick={() => showDetail(data)}>详情</a>
<a onClick={() => showUpdateForm(data)}>修改</a>
<Popconfirm
title="确定删除?"
onConfirm={() => {
deleteUser(data);
}}
>
<a>删除</a>
</Popconfirm>
</AButtonGroup>
);
}
}
]
}
};
};

2. 子应用打开子应用页面

这个时候,我们切换到子应用 godzillaPro,假设想在用户管理页面打开子应用 godzillaPro2 的表格性能测试页面,这个时候我们该怎么做呢,其实也是很简单,操作的 API 基本是一样的

const UserComponent = () => {
const showIframePage = () => {
window.openTab({
url: 'sample/Performance',
appId: 'godzillaPro2',
name: '表格性能测试'
});
// 当然,你也可以通过如下方式来操作,只是不建议这么做
// window.parent.postMessage(
// {
// type: 'openPage',
// appId: 'godzillaPro2',
// page: 'sample/Performance',
// name: '表格性能测试',
// },
// '*'
// );
};
return (
<Fragment>
<div className="qx-main">
<AgGrid.SearchFormTable>
<Button size="small" onClick={showAddForm} type="primary">
新增
</Button>
<Button size="small" onClick={showIframePage} type="primary">
打开子页面
</Button>
</AgGrid.SearchFormTable>
</div>
<Modal store={modal}></Modal>
</Fragment>
);
};

三、 子应用互相嵌套

在微前端模式下,有一种特性是用 iframe 模式很难解决的,那就是在一个页面中去加载并显示另外一个应用的页面资源,传统的做法是通过 iframe 的形式,但这种形式资源消耗很大而且用户体验不是很好,Godzilla 框架,在微前端的模式下,所有子应用的页面都是统一管理的,很容易自由组合这些模块,互相嵌套甚至多层嵌套那都是没问题的

我们在用户管理操作栏增加详情(modal),意思是以模态框的方式加载这个模块,并且这个详情模块是在子应用 godzilla2 中的,我们可以利用 window.importMicroPage 动态加载子应用的页面并进行展示

const UserComponent = () => {
const showDetailModalBySpa = (data: any) => {
type UserDetailProps = { code: string };
const MyUserDetail = window.importMicroPage<UserDetailProps>(
'godzillaPro2',
'sample/UserDetail'
);
modal.open('用户详情', <MyUserDetail code={data.code} />);
};

const tableConfig: TableConfig = {
props: {
columnDefs: [
{
headerName: '操作',
colId: 'operation',
cellRendererFramework: ({ data = {} }: any) => {
return (
<AButtonGroup>
<a onClick={() => showDetailModalBySpa(data)}>详情(modal)</a>
</Popconfirm>
</AButtonGroup>
);
}
}
]
}
};
};