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

本文探讨了使用iframe作为微前端技术的优势与挑战,包括其强大的隔离性和通信机制,同时也讨论了性能开销、无障碍性等问题,并提供了一个具体的商品选择器案例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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

iframe难题

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

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

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

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

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

在微店商家营销活动设置中,有一个商品选择功能。他会弹出来一个选择框。这时候我们注意到:只有一个商品的浮层其实用不了十个商品那么高的高度。这时候我们需要“响应式”height
在我司“天生支持”微前端的脚手架的架构下,商品选择是作为“通用业务组件”方式实现的,你可以理解为远程组件。然后在当前项目中以iframe形式引入
iframe使用

为了避免常见组件封装的一些缺点。比如:回调函数需要以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);
      });
    },

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

n个商品

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, data) => {
  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 '通知事件文件路径';
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

恪愚

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值