最近在研究微前端。我觉得从理论上来说,iframe是微前端最理想的组合技术。使用iframe能够将一个页面内嵌到另一个页面中,并且和链接集成一样具有松散的耦合和高鲁棒性。iframe具有极强的隔离性,其中发生的一切只会影响到自身 —— 实际上目前大多数在线编辑平台都是iframe技术实现的。

iframe难题

可是慢慢地,你会发现iframe的负面效果极其糟糕,以至于足以让人忽略其高隔离和易于实现的优势:

  • 性能开销:从浏览器角度来说,向页面添加一个iframe是一项开销巨大的操作。每个iframe都会创建一个新的浏览器上下文,这会导致额外的内存和CPU消耗;
  • 破坏无障碍可访问性标准:iframe破坏了页面的语义话,因为它属于另一个页面。我们可以设置iframe样式使其和页面其它部分“无缝衔接”,但是屏幕阅读器并不会被我们“欺骗”;
  • SEO不友好:爬虫会将使用了iframe的页面当作两个不同的页面进行索引。依然从浏览器角度来说,iframe内外的内容看起来在同一个浏览器窗口中,实际它们不在同一个文档中;

如果你打算在项目中使用不止一个的iframe,请测试足够的用例以保证它们对性能的影响。

除了上面说的,iframe还有一个致命缺陷:缺乏可靠的iframe自动高度的解决方案。

但是笔者觉得这在某些情况下是可以尝试的。现在举一个场景实例:

让iframe为项目增加更多可能性_javascript

在微店商家营销活动设置中,有一个商品选择功能。他会弹出来一个选择框。这时候我们注意到:只有一个商品的浮层其实用不了十个商品那么高的高度。这时候我们需要“响应式”​​height​​。

在我司“天生支持”微前端的脚手架的架构下,商品选择是作为“通用业务组件”方式实现的,你可以理解为远程组件。然后在当前项目中以iframe形式引入

让iframe为项目增加更多可能性_iframe_02

为了避免常见组件封装的一些缺点。比如:回调函数需要以​​v-bind​​形式单独再处理、暴露方法名改动文档同步不及时等等。我们采用了之前文章中提过的“大组件调用”方式。(实际上,“通用业务组件”的概念就和笔者提的“大组件”不谋而合)

//通用组件,无敏感代码。使用时保留下面一行即可。
// from 营销team@weidian
import ModalPC from './index.js';

let ins = null;

// 初始化选择器
export function initModal(cfg) {
if (ins) {
// 已有实例
console.warn("已有实例化的商品选择器。"); // ignore-console
} else {
/**
* @description
* 对回调函数进行了包装
*/
ins = new ModalPC({
url: cfg.url,
callback: (msg) => {
let data = msg.data;
// 页面加载以后做数据传输用途
if (data.type === 'mkt-load') {
ins && ins.sendMsg({
//...
});
} else {
cfg.callback && cfg.callback(msg);
}
}
});
}
}

export function closeModal() {
if (ins) {
ins.close();
ins = null;
}
}

export function getInstance() {
return ins
}

在index.js中:

//通用组件,无敏感代码。使用时保留下面一行即可。
// from 营销team@weidian
/**
* @description
* PC模态窗组件 -
*
*/
export default class ModalPC {
constructor(param) {
// 基础信息
//...

this.domWrapper = null;
this.domWindow = null;
this.domIframe = null;

this.init(param);
}

// 初始化方法
init(param) {
// 缓存配置项参数
Object.assign(this.cfg, param);
let status = this.initOption(param);
if (status) {
this.initDom();
this.bindEvent(); // 页面事件绑定
this.render(); // 组件渲染
}
}

// 配置项初始化
initOption(opt) {
}

// dom结构初始化
initDom() {
// 容器初始化
let domWrapper = this.doc.createElement('div'); // 模态窗整体容器
let domWindow = this.doc.createElement('div'); // 窗口容器
let domIframe = this.doc.createElement('iframe');

domWrapper.setAttribute('style', `
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 99999;
background: rgba(0,0,0,.5);
`);

domIframe.setAttribute('width', '100%');
domIframe.setAttribute('height', '100%');
domIframe.setAttribute('frameborder', 0);
domIframe.src = this.url;

domWrapper.append(domIframe);
// 容器缓存
this.domWrapper = domWrapper;
this.domWindow = domWindow;
this.domIframe = domIframe;

}
close() {
// 组件注销
this.win.removeEventListener('message', this.transportMsgFn)
this.doc.body.removeChild(this.domWrapper);
}

// 父作用域向iframe中传值
sendMsg(msg) { this.domIframe.contentWindow.postMessage(msg, "*") }
//...
}

可以看到,我们是用一个div包裹了iframe,在iframe中又是一个div包裹整个元素。那我们是不是可以通过控制这两个“外层元素”里任意一个去控制里面的iframe呢?

我认为,最外层的元素(iframe也好、div也好)应该具备一个最大值​​max-height​​,然后有一个动态style去根据内容展示适当的高度:

// 设置列表的高度
_setListHeight() {
if (this.listHeight) return;
this.$nextTick(() => {
let _dom = this.$refs.itemListWrapper; //需要动态高度的元素
this.listHeight = Math.floor(_dom.clientHeight);
});
},

这段代码在获取商品列表数据后调用。

让iframe为项目增加更多可能性_iframe_03

让iframe为项目增加更多可能性_微前端_04

iframe通信

如果你用iframe构建微前端应用。那必然首要考虑iframe和页面的通信(数据传递)。

监听事件:

import MessageType from "./message-type";

/**
* 主应用,
*/
class MainApp {
constructor() {
this.registerEvents();
}

// 注册事件
registerEvents() {
window.addEventListener("message", (e) => {
try {
const { type, data } = e.data;
const arg = { data, originEvent: e };
if (type === MessageType.CHECK_COOKIE) {
app.checkCookie(arg);
}
} catch (err) {
console.error("主应用接收到消息失败", err);
}
});
}
}

let app = null;
const start = ({ onCheckCookie }) => {
app = new MainApp();
app.checkCookie = onCheckCookie;
};

export default {
start,
};
// message-type.js
const MESSAGE_TYPE = {
CHECK_COOKIE: "CHECK_COOKIE", // 验证 cookie
};
export default MESSAGE_TYPE;

通知事件:

import MessageType from "./message-type";

let _targetOrigin = "*";

const setup = ({ targetOrigin }) => {
_targetOrigin = targetOrigin;
};

// 通知事件
const notify = (type,) => {
top.postMessage(
{
type,
data,
info: { //单独拿出来
data.version,
data.name,
},
},
_targetOrigin
);
};

// 验证 cookie 是否过期
const checkCookie = (data) => {
notify(MessageType.CHECK_COOKIE, data);
};

// 是否 iframe
const isIframe = () => {
return window.top !== window;
};

export default {
setup,
notify,
checkCookie,
isIframe,
};
// index.js
export { default as MainApp } from '监听事件文件路径';
export { default as MicroApp } from '通知事件文件路径';