如何開發跨框架的組件

61 篇原創好文~
本文首發於政採雲前端團隊博客: 如何開發跨框架的組件

背景

題主所在的業務中臺團隊,須要提供業務組件給不一樣的上層業務方使用,但由於一些歷史遺留問題,不一樣業務線使用的框架不統一,包括 jQuery、React 、Vue 。爲了知足不一樣業務方的需求,每每須要根據業務方使用的框架,開發對應框架的組件。javascript

這樣作就會產生一些痛點css

  1. 每種選型都須要開發一次,費時勞力
  2. 組件升級,須要業務方同步發版升級,溝通成本高、迭代效率低

理想中的組件

  • 跨框架:Write once, run everywhere
  • 少升級:組件升級,業務方少升級不升級(注意:組件升級後業務線迴歸仍是必要的)

實現方案

如何設計一個符合上面跨框架、少升級指望的通用方案呢?html

很容易想到用原生 JS 來實現,避免跨框架的問題前端

原生實現

用原生 JS 實現,包含頁面裏用到的 UI 組件,不依賴任何框架。java

優勢:api

  • 跨框架:不依賴於框架實現
  • 輕量:能夠不依賴其餘 UI 組件,體積較小

缺點:跨域

  • 投入產出比低:實現一套經常使用工具方法和 UI 組件,投入時間長
  • 踩坑:兼容性問題的坑要走一遍,風險大
  • 很難知足複雜業務場景的需求

適用場景app

不須要複雜交互的場景,如前臺吊頂、後臺菜單側邊欄可採用這種方式。框架

但在實際的業務場景中,業務組件中有比較多複雜的交互場景, 上面的方案不太能知足要求,因此咱們在上面的方案之上進行迭代:dom

原生容器組件 + iframe 加載業務邏輯組件

咱們將業務組件拆分爲兩部分

1、容器組件

用原生 JS 實現中間層容器組件,解決跨框架的加載問題,容器組件主要負責:

  • 收集組件須要的參數
  • 註冊全局回調
  • 組件掛載
  • 加載 iframe

2、業務邏輯組件

根據 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;
 }

須要注意的點

  1. 注意處理非白色背景的圓角部分,容易出現毛邊。處理方法是 iframe 容器不設置背景色,由 iframe 裏面設置圓角
  2. 版本控制:小版本保證向前兼容,大版本可經過動態獲取 iframe 地址來實現版本控制

總結

本方案解決了多框架背景下的組件重複開發問題,但根源仍是多框架的歷史債務問題。更好的方式則是推進技術棧的統一,從根源上避免出現此種狀況。

招賢納士

政採雲前端團隊(ZooTeam),一個年輕富有激情和創造力的前端團隊,隸屬於政採雲產品研發部,Base 在風景如畫的杭州。團隊現有 50 餘個前端小夥伴,平均年齡 27 歲,近 3 成是全棧工程師,妥妥的青年風暴團。成員構成既有來自於阿里、網易的「老」兵,也有浙大、中科大、杭電等校的應屆新人。團隊在平常的業務對接以外,還在物料體系、工程平臺、搭建平臺、性能體驗、雲端應用、數據分析及可視化等方向進行技術探索和實戰,推進並落地了一系列的內部技術產品,持續探索前端技術體系的新邊界。

若是你想改變一直被事折騰,但願開始能折騰事;若是你想改變一直被告誡須要多些想法,卻無從破局;若是你想改變你有能力去作成那個結果,卻不須要你;若是你想改變你想作成的事須要一個團隊去支撐,但沒你帶人的位置;若是你想改變既定的節奏,將會是「5 年工做時間 3 年工做經驗」;若是你想改變原本悟性不錯,但老是有那一層窗戶紙的模糊… 若是你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的本身。若是你但願參與到隨着業務騰飛的過程,親手推進一個有着深刻的業務理解、完善的技術體系、技術創造價值、影響力外溢的前端團隊的成長曆程,我以爲咱們該聊聊。任什麼時候間,等着你寫點什麼,發給 ZooTeam@cai-inc.com

相關文章
相關標籤/搜索