抖音技术能力大揭密!钜惠大礼、深度体验,尽在火山引擎增长沙龙,就等你来! 立即报名>> 了解详情
写点什么

如何开发跨框架的组件

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

评论 1 条评论

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

新手村:MySQL 学习经验、资源的分享

多选参数

MySQL MySQL 高可用 MySQL优化

第7周-作业1

seng man

MinIO 参数解析与限制

耳东

配置 Minio

第七周学习总结

王锟

面试官拿System.out.println()考了我半个小时?我傻了

Java小咖秀

Java 面试

手写实现Promise

GKNick

WebRTC框架下的实时视频关键路径

fumingwang

BIGO | Likee深度推荐模型的特征工程优化

InfoQ_3597a20b53cc

人工智能

http请求压测工具

潜默闻雨

【一起学系列】之命令模式:封装一个简单Jedis

Kerwin

设计模式 命令模式 Jedis

性能测试和并发压力的关系

王锟

英特尔唐炯:竞争推动PC行业良性发展,促使英特尔前行

最新动态

【小白学YOLO】YOLOv3网络结构细致解析

华为云开发者社区

人工智能 网络 物体检测 华为云 网络层

MinIO 分布式集群搭建

耳东

Minio cluster Minio 集群

企业架构框架之FEA

Winfield

企业架构

JVM系列之:String,数组和集合类的内存占用大小

程序那些事

Java JVM GC JOL

架构训练营第七周作业

张锐

如何写出完美的接口:接口规范定义、接口管理工具推荐

xcbeyond

接口规范 接口管理工具 swagger Easy Mock

架构师第七周作业

傻傻的帅

秒懂云通信:选云通信到底哪家强?

阿里云Edge Plus

云通信 通信云

来了,来了,你们要的Nginx教程来了

Java旅途

nginx

数据湖应用解析:Spark on Elasticsearch一致性问题

华为云开发者社区

大数据 spark elasticsearch 数据湖 华为云

百度人脸算法“飞速迭代”,多模态活体检测V3.1获银行卡检测中心增强级认证

百度大脑

人工智能 人脸识别 百度大脑

ARTS-WEEK7

一周思进

ARTS 打卡计划

英特尔唐炯:把最好、最合适的产品带给最需要的消费者

最新动态

性能测试与优化 和 操作系统与文件系统

满山李子

极客大学架构师训练营

计算机网络基础(八)---网络层-路由概述

书旅

计算机网络 网络协议 计算机基础 AS

直播中那几秒延时到底来自哪?

阿里云Edge Plus

CDN 云直播 直播 直播带货

我向面试官讲解了单例模式,他对我竖起了大拇指

cxuan

设计模式 单例模式

手把手教你写数独计算器(1)

一直AC一直爽

c++ 算法 数独

47 张图带你 MySQL 进阶!!!

cxuan

MySQL 后端

Study Go: From Zero to Hero

Study Go: From Zero to Hero

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