第 61 篇原创好文~
本文首发于政采云前端团队博客:如何开发跨框架的组件
背景
题主所在的业务中台团队,须要提供业务组件给不同的下层业务方应用,但因为一些历史遗留问题,不同业务线应用的框架不对立,包含 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 能够跨文档通信, 在 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 地址来实现版本控制
总结
本计划解决了多框架背景下的组件反复开发问题,但本源还是多框架的历史债权问题。更好的形式则是推动技术栈的对立,从本源上避免出现此种状况。
招贤纳士
政采云前端团队(ZooTeam),一个年老富裕激情和创造力的前端团队,隶属于政采云产品研发部,Base 在风景如画的杭州。团队现有 50 余个前端小伙伴,平均年龄 27 岁,近 3 成是全栈工程师,妥妥的青年风暴团。成员形成既有来自于阿里、网易的“老”兵,也有浙大、中科大、杭电等校的应届新人。团队在日常的业务对接之外,还在物料体系、工程平台、搭建平台、性能体验、云端利用、数据分析及可视化等方向进行技术摸索和实战,推动并落地了一系列的外部技术产品,继续摸索前端技术体系的新边界。
如果你想扭转始终被事折腾,心愿开始能折腾事;如果你想扭转始终被告诫须要多些想法,却无从破局;如果你想扭转你有能力去做成那个后果,却不须要你;如果你想扭转你想做成的事须要一个团队去撑持,但没你带人的地位;如果你想扭转既定的节奏,将会是“5 年工作工夫 3 年工作教训”;如果你想扭转原本悟性不错,但总是有那一层窗户纸的含糊… 如果你置信置信的力量,置信平凡人能成就不凡事,置信能遇到更好的本人。如果你心愿参加到随着业务腾飞的过程,亲手推动一个有着深刻的业务了解、欠缺的技术体系、技术发明价值、影响力外溢的前端团队的成长历程,我感觉咱们该聊聊。任何工夫,等着你写点什么,发给 ZooTeam@cai-inc.com