写点什么

如何开发跨框架的组件

2021 年 4 月 07 日

如何开发跨框架的组件

背景


笔者所在的业务中台团队,需要提供业务组件给不同的上层业务方使用,但因为一些历史遗留问题,不同业务线使用的框架不统一,包括  jQuery、React 、Vue。为了满足不同业务方的需求,往往需要根据业务方使用的框架,开发对应框架的组件。


这样做就会产生一些痛点


  • 每种选型都需要开发一次,费时劳力

  • 组件升级,需要业务方同步发版升级,沟通成本高、迭代效率低

理想中的组件


  • 跨框架:Write once, run everywhere

  • 少升级:组件升级,业务方少升级不升级(注意:组件升级后业务线回归还是必要的)


实现方案


如何设计一个符合上面提到的跨框架、少升级期望的通用方案呢?


很容易想到用原生 JS 来实现,避免跨框架的问题。


原生实现


用原生 JS 实现,包含页面里用到的 UI 组件,不依赖任何框架。


优点:

  • 跨框架:不依赖于框架实现

  • 轻量:可以不依赖其他 UI 组件,体积较小

缺点:

  • 投入产出比低:实现一套常用工具方法和 UI 组件,投入时间长

  • 踩坑:兼容性问题的坑要走一遍,风险大

  • 很难满足复杂业务场景的需求

适用场景

不需要复杂交互的场景,如前台吊顶、后台菜单侧边栏可采用这种方式。

但在实际的业务场景中,业务组件中有比较多复杂的交互场景, 上面的方案不太能满足要求,所以我们在上面的方案之上进行迭代:


原生容器组件 + iframe 加载业务逻辑组件


我们将业务组件拆分为两部分:


一、容器组件

用原生 JS 实现中间层容器组件,解决跨框架的加载问题,容器组件主要负责:

  • 收集组件需要的参数

  • 注册全局回调

  • 组件挂载

  • 加载 iframe

二、业务逻辑组件

根据 iframe 天然的沙箱特性,业务逻辑用 iframe 页面加载,就保证了业务组件的实现不受框架的限制,可以完美解决问题。业务逻辑组件主要负责:

  • 与容器组件通信

  • 运行环境隔离,可以使用任意框架实现业务逻辑

缺点:

  • 动态加载静态资源,iframe 加载略慢,实际体验在接受范围内

  • 跨域通信问题

此方案容器组件作为中间层,封装不变的逻辑,将多变的业务逻辑隔离出来,从而保证协作方尽量少升级或不升级。业务定制性可根据接口配置,返回不同的 iframe 地址,加载不同的业务逻辑组件,一次开发任意使用。


如何实现


下面是整个组件的逻辑图



使用方通过容器组件初始化参数、并注册相应的回调:


容器组件


  • 初始化

  • 设置 document.domain,让外部组件和 iframe 可以通信

// 获取主域名function getTopLevelDomain(host) {    let data = host || window.location.host;    return data.split('.').slice(-2).join('.');}// 设置主域名function setDomainToTopLevelDomain() {  try {    window.document.domain = getTopLevelDomain();  } catch (error) {    console.error("设置domain失败")  }}
复制代码


  • render:

  • 生成外部容器 div ,设置 loading 图,挂载组件

class Vanilla {  // 获取配置信息  constructor(config) {    const options = { ...defaultConfig, ...config };    this.options = options;     this.elCls = options.elCls;  } // 生成容器 div  render() {    const div = document.createElement('div');    this.el = div;        const { width, height } = this.options;    div.className = `${prefixCls}-wrap ${prefixCls}-wrap-loading ${this.elCls || ''}`;    const maskNode = getMaskNode(prefixCls);    const iframeNode = getIframeNode(prefixCls, width, height);    div.innerHTML = maskNode + iframeNode;    document.body.appendChild(div);    this.fn();  }  init() {    // 设置主域名    setDomainToTopLevelDomain();    // 初始化 div    this.render();    // 初始化全局回调函数    this.initCallbacks();  }  ...}
复制代码


  • 注册回调函数

  • 通过注册全局回调函数,用于业务逻辑组件与容器组件进行通信


class Vanilla { ...   initCallbacks() {    const self = this;    const options = this.options;     // 初始化全局变量    window[paramsName] = options;        window.onSuccess = function onSuccess(data, res) {      options.onSuccess && options.onSuccess(data, res);      // 延迟1500ms删除用来显示成功提示      setTimeout(() => {        self.removeNode();      }, 1500);      self.resetCallbacks && self.resetCallbacks();    };    window.onCancel = function onCancel() {      options.onCancel && options.onCancel();      self.removeNode();      self.resetCallbacks && self.resetCallbacks();    };    window.onError = function onError(data) {      options.onError && options.onError(data);    };  }}
复制代码


  • 加载 iframe 页面:

  • 通过接口获取 iframe 地址,业务方可以根据配置动态,加载不同的业务组件


let timer = function timer() {};class Vanilla {  ...  $mount() {   ...   this.fn();  }  fn() {    const {      width,      height,      isAutoSize,    } = this.options;    const el = this.el;    const url = getContentUrl('你的iframe地址');    const iframeWidth = width;    const iframeHeight = height;    const iframeEle = el.querySelector('.J_CreditIframe');    const modalNode = el.querySelector(`.${prefixCls}`);
if (!isAutoSize && (iframeWidth !== width || iframeHeight !== height)) { this.setNodeSizeAndPostion(modalNode, iframeEle, iframeWidth, iframeHeight); } iframeEle.setAttribute('src', url); // 监听load后,隐藏loading addEvent(iframeEle, 'load', () => { el.className = `${prefixCls}-wrap ${this.elCls || ''}`; const maxTime = 3000; const duration = 1000; let timerCounter = 0; let w = defaultConfig.width; let h = defaultConfig.height; // 自适应宽高 if (isAutoSize) { timer = setInterval(() => { ... // this.setNodeSizeAndPostion(modalNode, iframeEle, scrollWidth, scrollHeight); } timerCounter += duration; if (timerCounter >= maxTime) { clearInterval(timer); } }, duration); } }); } // 设置iframe宽高 setNodeSizeAndPostion(container, iframe, width, height) { container.style.cssText = `width: ${width}px; height: ${height}px;margin-left: -${width / 2}px;margin-top: -${height / 2}px;`; iframe.style.cssText = `width: ${width}px; height: ${height}px;`; } // 删除DOM removeNode() { timer && clearInterval(timer); if (this.el) { document.body.removeChild(this.el); } } ...}
复制代码


上面我们完成了整个业务组件的加载过程,下面我们需要处理的就是业务逻辑组件如何与容器组件之间进行通信:


通常我们可以这样处理:


// 获取父页面属性const params = window.parent.paramsName;// 调用父页面方法window.parent.onSuccess && window.parent.onSuccess(data);
复制代码


但在实际的业务场景中,我们可能会面临的问题是业务方的域名与 iframe 加载的组件地址域名不一致,这个时候我们就必须要解决组件的跨域通信问题了.


跨域的通信问题


我们可以通过以下三种方式去解决:


postMessage

  • postMessage (https://developer.mozilla.org/zh-CN/docs/Web/API/Window/postMessage) 可以跨文档通信, 在 IE10 的支持性有问题,在 IE11 及以上可以完美解决跨域问题。笔者需要支持 IE9+,所以没有采用 postMessage


主域名修改

  • document.domain + iframe : 设置 document.domain 为主域名,业务方与 iframe 主域名相同,实现父子同域通信。这种实现的前提是两个域的主域名必须一致


Nginx 代理

  • Nginx 配置:iframe 页面的路径配置为通用路径,反向代理依赖接口,实现全域名可访问。将业务逻辑组件整合到一个或多个项目中使用,组件打包和发布逻辑可单独定制,适合大量跨框架组件

// 静态页面地址location ~ ^/your-project/ {  root /opt/front/your-project/;  try_files $uri $uri/ /index.html = 404;  access_log off;}// 反向代理location ~ ^/api/service/(.*)$ {   proxy_pass http://your-ip;   proxy_set_header        X-Real-IP $remote_addr;   proxy_set_header        X-Forwarded-For $proxy_add_x_forwarded_for;   proxy_set_header        Host $http_host;   proxy_set_header        requestId $request_id;   proxy_http_version      1.1;   proxy_set_header        Connection "";   expires 30d;   access_log off; }
复制代码


需要注意的点


  • 注意处理非白色背景的圆角部分,容易出现毛边。处理方法是 iframe 容器不设置背景色,由 iframe 里面设置圆角

  • 版本控制:小版本保证向前兼容,大版本可通过动态获取 iframe 地址来实现版本控制


总结


多框架背景下的组件重复开发问题,根源还是多框架的历史债务问题。更好的方式则是推动技术栈的统一,从根源上避免出现此种情况。此场景下更为完善的解决方案则是微前端,我们也在向这个方向探索,本文提供的是一种基础的解决多技术栈业务场景的思路,如果有更好的方案欢迎大家一起讨论~



头图:Unsplash

作者:明明

原文:https://mp.weixin.qq.com/s/BehjH5xVXFWohQXFl3u-kQ

原文:如何开发跨框架的组件

来源:政采云前端团队 - 微信公众号 [ID:Zoo-Team]

转载:著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

2021 年 4 月 07 日 15:051407

评论 1 条评论

发布
用户头像
首先要搞清楚的一个问题是:为什么要开发跨框架的组件?
为什么一个团队或者一个公司会有两套框架?为什么要增加这种学习成本?
2021 年 04 月 08 日 10:28
回复
没有更多了
发现更多内容

小谈 Java 单元测试

xcbeyond

Java 单元测试 28天写作

分享一个务实派CEO的理念和实践

boshi

经验分享 七日更 28天写作

第5周作业

KYoKO

0 期产品训练营第五周作业 - 学情周报活动图

skylar

第五六周心得

Trigger

极客时间 产品经理训练营

产品文档总结

mas

关于微服务的一点理解

风翱

微服务 开发

第5次作业

Geek_娴子

【管理笔记17】行销系统的建立

俊毅

设计模式【3.1】-- 浅谈代理模式之静态、动态、cglib代理

秦怀杂货店

动态代理 代理模式

算法刷题提醒——微信小程序 [持续优化ing]

Sicolas Flamel

深度思考 随笔杂谈 成长与思考

产品经理 - 第四章作业

Geek_971380

挑一个产品,想一个你想做的新功能,然后写个相对完整的文档

mas

作业

云随心

产品训练营作业

时间管理这件事儿「Day 11」

道伟

28天写作

3月份开始了

Nydia

(28DW-S8-Day11) 小数据与在线教育

mtfelix

28天写作 小数据 因材施教 用户分析

关于数码产品

ES_her0

28天写作 3月日更

产品经理训练营第四章作业(二)

新盛

抽奖助手小程序 发起抽奖流程图

Shine

产品

架构学习20210302日(001)

张小胖

第五周

Jove

产品经理训练营第四章作业(二)

猫。

极客时间购买课程的流程图

踏凌霄

「产品经理训练营」作业 05

🌟

产品 产品经理 产品经理训练营 产品训练营 产品训练营作业

抽奖助手--抽奖用例流程图

小蜜蜂

玩家永远是对的——认知失调

Justin

心理学 28天写作 游戏设计

靠速度说话!你还不够了解的“新基建闪电侠”

白洞计划

闲鱼UI快速变形利器--擎天柱

闲鱼技术

翻译:《实用的Python编程》03_03_Error_checking

codists

Python

第五周作业

z

2021年全国大学生计算机系统能力大赛操作系统设计赛 技术报告会

2021年全国大学生计算机系统能力大赛操作系统设计赛 技术报告会

如何开发跨框架的组件-InfoQ