wdp新功能嵌入及微前端实践
# 背景
目前web项目底座技术都是7-8年前的,开发维护比较麻烦;比如
# 改造方案
# 方案
- vue内嵌渐进式【优先方案】
- iframe内嵌【过渡备用方案】
- 微前端集成【终极方案】
# 比较
# vue渐进式【优先方案】
# 考虑点
- 完成vue组件混合到ember项目框架中;
- 完成纯vue页面内嵌到ember项目框架中;
- 常用的原子组件及业务组件封装;
- 缺点:还要熟悉原有老系统的相关功能及维护;
# iframe内嵌【过渡备用方案】
# 考虑点
- 多页面vue集成及维护;
- 新部署方式,及配套的路由处理;
- 不好维护路由及多个项目;路由跳转目前用的是相对路径跳转;
- 多页面内嵌iframe的话用户体验非常差;
# 缺点
- url 不同步。浏览器刷新 iframe url 状态丢失、后退前进按钮无法使用。
- UI 不同步,DOM 结构不共享。想象一下屏幕右下角 1/4 的 iframe 里来一个带遮罩层的弹框,同时我们要求这个弹框要浏览器居中显示,还要浏览器 resize 时自动居中..
- 全局上下文完全隔离,内存变量不共享。iframe 内外系统的通信、数据同步等需求,主应用的 cookie 要透传到根域名都不同的子应用中实现免登效果。
- 慢。每次子应用进入都是一次浏览器上下文重建、资源重新加载的过程。
其中有的问题比较好解决(问题1),有的问题我们可以睁一只眼闭一只眼(问题4),但有的问题我们则很难解决(问题3)甚至无法解决(问题2),而这些无法解决的问题恰恰又会给产品带来非常严重的体验问题, 最终导致我们舍弃了 iframe 方案。
# 微前端集成【终极方案】
# 考虑点
- 打算用这种方案替换原有老的基座功能;在通过微前端,集成公司已有的react技术栈;
- 无技术栈限制,vue, react任意组合,还要慢慢推翻之前上云那种前端构建方式;之前上云模式有react技术栈限制及antd不能升级到v4等相关性能问题修复;
- 要考虑模块间通信;全新的部署构建方式,及组件管理体系;要考虑到方案多及人力成本;
- 开发难度大,适合慢慢过渡到这个方案中;
# 前期使用技术栈
基座
- vue2.x + ant vue + compose api
- qiankun
- ts + jsx
模块
- vue2.x
- umi3.x
- umi2.x
- qiankun
- vue3.x
- ts + jsx
# 实践vue内嵌渐进式【优先方案】
# 集成
# 组件通信
# 实践iframe内嵌【过渡备用方案】
# 集成
添加统一的iframe容器pages
添加controllers;
require('controllers/main/pages_controller');
require('controllers/main/pages/details_controller');
添加详情views;
require('views/main/pages/details');
配置路由; 可以先不用单独配置;
pages: require('routes/pages')
;
访问方式及替换逻辑
在容器内部做相应的替换路径处理;
web中有两种方式访问方式;真实访问和pages模式访问 http://localhost:4444/#/main/admin/backupTaskList http://localhost:4444/#/main/pages/backupTaskList
http://xxx:9800/#/main/admin/backupTaskList http://xxx:9800/#/main/pages/backupTaskList
pages多页面模块;
本地路径:http://localhost:9800/admin/backupTaskList
线上路径:http://xxx:9800/pages/admin/backupTaskList.html
路径替换逻辑处理;
src: function () { var srcPath var path = App.isPro ? window.location.href : window.location.hash if (hash === '#/main/pages') { // pages首页调试模式 srcPath = App.pagePath } else { var newPath = path.replace("#/main", App.pagePath) srcPath = App.isPro ? newPath + '.html' : newPath } return srcPath; }.property('controller.content')
1
2
3
4
5
6
7
8
9
10
11
# 组件通信
# 核心方法
目前应用间的通讯,是通过storage,来存储language字段;
window.parent.postMessage(e.data, '*');
window.addEventListener('message', function (e) {});
postMessage的第一个参数为消息实体,它是一个结构化对象,即可以通过“JSON.stringify和JSON.parse”函数还原的对象;第二个参数为消息发送范围选择器,设置为“/”意味着只发送消息给同源的页面,设置为“*”则发送全部页面。
window.addEventListner('message',(e)=>{
let {data,source,origin} = e;
source.postMessage('message echo','/');
});
componentDidMount() {
window.addEventListener('message', this._handleOnMessage, false);
}
componentWillUnmount() {
window.removeEventListener('message', this._handleOnMessage, false);
params = {};
iframeQuery = {};
}
2
3
4
5
6
7
8
9
10
11
12
13
# 通信交互【核心】
HTML5 提出了一个新的用来跨域传值的方法,即postMessage,IE8也开始支持了。我们假设有两个网站,1.com 与 2.com,我在 1.com 的页面上通过 iframe 或 window.open 或超链接打开了一个 2.com 的网页,此时我要在 2.com 上做操作的时候,给 1.com 传值,让 1.com 有所变化。这个过程就是个跨域的过程。
比如,你的父页面有个函数叫 callback,然后你子页面本可以这样调用:window.opener.callback(),同域时能成功,跨域时就没有权限了。但是,此时你调用window.opener.postMessage(),却可以成功。
我们看一下如何具体使用:
// 1、父页面向子页面发送消息
let data = {type: 'answerResult', data: jsonData.data};
this.$refs.iframe.contentWindow.postMessage(data, '*');
// 2、子页面向父页面发送消息
let parentData = {type: 'passDataBack', data: passData};
window.parent.postMessage(parentData, '*');
// 3、接收消息方法【父子都可用】
window.addEventListener('message', function (e) {})
2
3
4
5
6
7
8
9
10
方法封装之后在页面中的具体使用:
// 父页面
mounted(){
window.addEventListener("message", this.handleMessage)
},
methods:{
// 向iframe传值的方法 @param {Object} data
sendMessage(data){
const iframe = this.$refs.iframePage.contentWindow;
iframe.postMessage(data, '*');
},
// 监听子页面传过来的值的方法 @param {Object} event
handleMessage (event) {
// dosomething
}
}
// 子页面
mounted(){
window.addEventListener("message", this.handleMessage)
},
methods:{
// 向父页面传值的方法 @param {Object} data
sendMessage(data){
window.parent.postMessage(data, '*');
}
// 监听父页面传过来的值的方法 @param {Object} event
handleMessage (event) {
// dosomething
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
这里需要注意一点的就是:postMessage语法 - window.postMessage(msg,targetOrigin),postMessage要通过 window 对象调用!因为这里的window不只是当前window,大部分使用postMessage的时候,都不是本页面的window,而是其他网页的window!如:
- iframe的contentWindow
- 通过window.open方法打开的新窗口的window
- window.opener
- 如果你使用postMessage时没有带window,那么当然,你就是用的本页面的window来调用了它。
安全问题
如果您不希望从其他网站接收message,请不要为message事件添加任何事件侦听器。 这是一个完全万无一失的方式来避免安全问题。
如果您确实希望从其他网站接收message,请始终使用origin和source属性验证发件人的身份。 任何窗口(包括例如http://evil.example.com)都可以向任何其他窗口发送消息,并且您不能保证未知发件人不会发送恶意消息。 但是,验证身份后,您仍然应该始终验证接收到的消息的语法。 否则,您信任只发送受信任邮件的网站中的安全漏洞可能会在您的网站中打开跨网站脚本漏洞。
当您使用postMessage将数据发送到其他窗口时,始终指定精确的目标origin,而不是*。 恶意网站可以在您不知情的情况下更改窗口的位置,因此它可以拦截使用postMessage发送的数据。
# 实践通讯
主应用接受消息后处理;
_handleOnMessage = e => {
const {
comAcctId,
userLevel,
menu: { allMenus },
reloadAppLocale,
} = this.props;
const { panes } = this.state;
const { data: { type = '', data = '', param = '', currentId, search = null } = {} } = e;
params[data] = param;
switch (type) {
case 'openSelfUrl':
if (data) {
const actMenu = this.findMenuByUrl(allMenus, data);
if (_.isEmpty(actMenu)) {
message.error(
`${formatMessage({
id: 'NO_Permission_Access_Page',
defaultMessage: '您没有访问该页面的权限!',
})}`
);
return false;
}
JumpToRouter(data, { query: search || {} }, reloadAppLocale);
/* pushRoute({
pathname: data,
query: search || {},
reloadAppLocale,
}); */
}
break;
// 关联主页上需要跳转的页面,
// 却不希望被配到通用系统门户管理上的页面,故添加此方法
case 'openNoLimitPermissionSelfUrl':
if (data) {
JumpToRouter(data, { query: search || {} }, reloadAppLocale);
/* pushRoute({
pathname: data,
query: search || {},
reloadAppLocale,
}); */
}
break;
case 'openUrl':
if (search && typeof search === 'object') {
iframeQuery[data] = search;
} else if (iframeQuery[data]) {
delete iframeQuery[data];
}
if (this.checkOpenedIframe(data)) {
setTimeout(() => {
this.handleRefreshIframePage(data);
}, 100);
}
router.push(`/iframeTab?url=${encodeURIComponent(data)}`);
break;
case 'closeUrl':
this.onEdit(data, 'remove');
break;
// 关闭当前页面 同时跳转新页面
case 'closeAndOpenUrl':
// 此处 currentId 为要关闭页面的 partyId, data为要跳转页面的地址
this.handleCloseTab(currentId).then(() => {
router.push(`/iframeTab?url=${encodeURIComponent(data)}`);
});
break;
case 'deleteUrl':
this.onDeleteUrl(data, 'remove');
break;
case 'reloadUrl':
if (this.iframeRefs[data]) {
this.handleRefreshIframePage(data);
}
break;
case 'closeAllAddOpenNewUrl':
this.closeAllKeepingHome(true);
break;
case 'logout':
this.logout({ isRedirect: true });
break;
default:
break;
}
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
renderIframe = pane => {
if (!pane) {
return null;
}
const name = this.getFrameName(pane);
let url = pane.iframeUrl;
if (iframeQuery[url]) {
const search = iframeQuery[url];
url += url.indexOf('?') === -1 ? '?' : '&';
const keys = Object.keys(search);
const searchArr = [];
keys.forEach(key => {
searchArr.push(`${key}=${search[key]}`);
});
url += searchArr.join('&');
}
const sessionId = window.encodeURIComponent(name);
if (url) {
url = url.replace(/\$\(window\.name\)/g, sessionId);
}
url = this.addIframeVersion(url);
const iframeProps = {
url,
width: '100%',
className: 'systemIframe',
display: 'block',
position: 'relative',
allowFullScreen: true,
name,
};
const isIframeTab = pane.menuType === 'M';
if (isIframeTab) {
return (
<Iframe
ref={ref => {
this.iframeRefs[pane.partyId] = ref;
}}
{...iframeProps}
/>
);
}
return null;
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
子系统
发送消息
rindClick = item => {
if (!_.isEmpty(item)) {
const { urlAppend, menuType } = item;
if (menuType === 'O') {
const token = saas.getSubSystemSignatureId();
window.open(urlAppend, token);
} else if (urlAppend.startsWith('http')) {
window.postMessage(
{
type: 'openUrl',
data: urlAppend,
},
'*'
);
} else {
window.postMessage(
{
type: 'openSelfUrl',
data: urlAppend,
},
'*'
);
}
}
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 多页优化
参考【vue.config.js中的实践优化】;
# 碰到的问题
目前在把wdp项目集成到上云门户中遇到跨域设置cookie问题;
- 目前后端不支持wdp添加前缀机制,导致不能用nginx代理跨域处理;
- 目前后端不支持scure设置为none,受jdk版本限制,故用https也有问题;
- 最后方案,通过token方式替换限制的cookie提交登陆机制;