对中国开发者最具吸引力的科技企业有哪些?快来为你 pick 的企业投票! 了解详情
写点什么

如何开发跨框架的组件

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:051

评论

发布
暂无评论
发现更多内容

区块链数字货币交易所开发,交易系统搭建方案

WX13823153201

一文快速入门分库分表中间件 Sharding-JDBC (必修课)

程序员内点事

Java 分库分表

配置企业应用业务流程别头大,有工作流引擎就不怕

Marilyn

敏捷开发

阿里云盘线下交流会

兔2🐰🍃

阿里云网盘 Teambition 线下体验

【架构师训练营 1 期】第五周学习总结

诺乐

Go发起HTTP2.0请求流程分析(后篇)——标头压缩

Gopher指北

golang 后端开发 HTTP2.0

这个应用魔方厉害了,让软件开发者效率提升10倍

华为云开发者社区

软件开发 代码

前端科普系列(5):ESLint - 守住优雅的护城河

vivo互联网技术

Java 前端 代码仓库

【架构师训练营 1 期】第五周作业

诺乐

Docker内部组件

混沌畅想

Docker 容器 运维

架构师训练第五周 -编程语言实现一致性 hash 算法

郎哲158

LAXCUS 大数据集群操作系统:一个分布式分时共享 E 级系统软件(五)

陈泽云

人工智能 数据库 大数据 操作系统

低代码开发平台,真的是为了“干掉“程序员嘛?

力软.net/java开发平台

软件开发 低代码

独家揭秘 | 京东物流Elasticsearch大规模“迁移上云”实践

京东科技开发者

云计算

JVM系列笔记 - 虚拟机栈

朱华

JVM

算法图解:如何用两个栈实现一个队列?

王磊

Java 数据结构 算法和数据结构

架构师训练营 - 第 5 周课后作业(1 期)

Pudding

超越视觉支持语音新版OpenVINO发布,为更多智能边缘开发者赋能

intel001

1024丨奈学教育致敬程序员:‘3+2’战略发布会圆满落幕

奈学教育

奈学教育 程序员节

5. Bean Validation声明式验证四大级别:字段、属性、容器元素、类

YourBatman

Hibernate-Validator Bean Validation 数据校验

使用Hugo和GitHub搭建博客

Félix

GitHub GitHub Pages Blog Hugo

环信入选2020在线教育视频云创新排行TOP10

DT极客

iOS touch事件点的获取

teoking

ios

架构训练营 - 第5周课后作业 - 学习总结

Pudding

万字长文深入理解java中的集合-附PDF下载

程序那些事

java编程 JAVA集合 java集合总结 java集合使用 java秘籍

非阻塞的无界线程安全队列 —— ConcurrentLinkedQueue

程序员小航

Java 源码 并发 源码阅读 JUC

1024丨奈学教育致敬程序员:‘3+2’战略发布会圆满落幕

古月木易

奈学教育

mongodb源码实现系列-网络传输层模块实现二

杨亚洲(专注mongodb及高性能中间件)

MySQL 数据库 mongodb 高性能 分布式数据库mongodb

Consistent Hashing算法实现 - JavaScript

10月24日,“网安小酒馆”线上活动开启,有红包,更有名酒相送

Cloudaemon

JMM 应用实例:单例模式

朱华

单例模式

混合云之争的开端与终途

混合云之争的开端与终途

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