動態表單之表單組件的插件式加載方案

前言

關於動態化表單方案前面我們已經有過一次分享,沒看過的同學可以看下之前的文章 ZooTeam 拍了拍你,來看看如何設計動態化表單。文章中提到隨着業務差異化增多,我們採用了動態表單解決重複開發及邏輯堆疊的問題。隨着動態化表單系統運行過程中業務方接入的越來越多,自定義組件插件式加載的需求開始出現並慢慢變得強烈。

我們希望添加新的自定義組件之後可以不需要重新發布項目,只需要單獨發佈自定義組件,然後在系統中註冊該自定義組件,就能在配置表單頁面的時候直接使用了。那麼這就引出一個需求,表單組件的插件式加載並應用的能力。

組件插件式加載方案的現狀

關於異步加載,各平臺上一搜索,大多數出來的都是一些 Webpack 代碼分拆相關的內容。而關於組件插件式加載的內容寥寥無幾。讓我們具體梳理一下。

一、Webpack 懶加載

Webpack 懶加載,也就是 Webpack 的拆包按需加載功能,其主要使用 import 方法進行靜態資源的異步加載,具體使用方法爲,代碼中採用如下方式引入需要被拆包的文件:

import('./moduleA').then((moduleA) ={
  moduleA.add(1,2); // 3
})

這麼 Webpack 在打包時會將 moduleA 單獨拆分出來作爲一個 JS 文件,項目在執行到這段代碼的時候才動態加載這部分 JS 資源。但是如果直接使用 import 方法加載遠程資源,Webpack 打包過程會直接報錯。不滿足需求。

import('http://static.cai-inc.com/moduleA.js').then((moduleA) ={
   // ERROR,打包過程會出現報錯
  moduleA.add(1,2);
})

報錯信息:

二、現有瀏覽器支持的 Dynamic Import

對於這種方法,其瀏覽器兼容性問題難以滿足要求,IE 瀏覽器完全不支持並且有同域名的限制。使用方法同 Webpack 懶加載一樣:

import('http://static.cai-inc.com/moduleA.js').then((moduleA) ={
  moduleA.add(1,2); // 3
})

三、require.js AMD 規範

使用 require.js 去加載一個符合 AMD 規範的 JS 文件。具體使用方法如下:

// 需要被動態加載的 moduleA.js
define('moduleA'[]function () {
  var add = function (x, y) {
    return x + y;
  };
  return {
    add: add
  };
});
// 加載和使用
require.config({
  paths: {
    "moduleA""lib/moduleA"
  }
});
require(['moduleA']function (moduleA){
    // 代碼
    moduleA.add(1,2); // 使用被動態引入的插件的方法
});

在這個方法中,moduleA 是動態插件,要使用動態插件則需要配置好插件的路徑,然後使用 require 進行引用。這需要我們引用 require.js 到現有項目中,在項目的 HTML 中定義一個 Script 標籤並設置 data-main="scripts/main" 作爲入口文件。但是我們的 React 項目也有一個入口,這會導致出現兩個入口。兩者用法並不能很好的並存。

需求拆解

那麼現在來分析一下實現組件插件式加載的關鍵問題

一、加載資源

二、插件模塊打包

需求分析

一、靜態資源加載

對於運行中加載靜態資源,現有解決方案中不論是哪一種,都是利用動態插入 Script 或者 Link 標籤來實現的。而且這種方案不會有域名限制問題。具體實現大體如下:

export default function (url) {
  return new Promise(function (resolve, reject) {
    const el = document.createElement('script'); // 創建 script 元素
    el.src = url; // url 賦值
    el.async = false; // 保持時序
    const loadCallback = function () { // 加載成功回調
      el.removeEventListener('load', loadCallback);
      resolve(result);
    };
    const errorCallback = function (evt) { // 加載失敗回調
      el.removeEventListener('error', errorCallback);
      var error = evt.error || new Error("Load javascript failed. src=" + url);
      reject(error);
    };
    el.addEventListener('load', loadCallback);
    el.addEventListener('error', errorCallback);
    document.body.appendChild(el); // 節點插入
  });
}

二、爲加載模塊注入依賴

關於這一點我們可以看下遵循 AMD 規範的 require.js 是怎麼做的。代碼:

// require.js
const modules = {};
const define = function(moduleName, depends, callback){
  modules[moduleName] = { // 將模塊存起來,等待後續調用
    depends,
    callback,
  };
}
// moduleA.js
define('moduleA'[]()=>{
  // code
})

因爲通過插入 Script 的方式引入 JS 資源,JS 會被立刻執行,所以在 require.js 中加載進來的 JS 模塊都是被 define 方法包裹着的,真正需要執行的代碼是在回調函數中等待後續調用。當 moduleA.js 被加載成功之後,立即調用 define 方法,這裏執行的內容則是把項目的模塊儲存起來等待調用。依賴的注入則是回調中將依賴作爲參數注入。其實不論是基於哪一種規範,動態加載靜態資源的策略都大致一樣。模塊中使用一個函數 A 將目標代碼包起來。將該函數 A 作爲一個函數 D 的參數。當模塊被加載時,瀏覽器中已經定義好的 D 函數中就可以獲取到含有目標代碼塊的函數 A 了。接下來想在哪裏調用就在哪裏調用。想注入什麼變量就注入什麼變量了。

三、模塊打包標準

由於我們團隊使用的是 Webpack 的打包體系,因此想要保持技術棧統一,則要先從 Webpack 的打包入手。讓我們將 Webpack 的模塊化打包都試一下看看能得出什麼。

Webpack library 打包方式有 5 種。

可以排除前三個,我們並不想將模塊掛到 window 或者全局變量下。所以我們需要嘗試的只有後面兩個。

需要被打包的代碼塊:

export default {
  test: ()=>{
    console.log('測試模塊打包!');
  }
};

AMD 規範打包後:

define(["lodash"](__WEBPACK_EXTERNAL_MODULE__92__) =(() ={
  // code ...
  // return funciton
})());

UMD 規範打包後:

(function webpackUniversalModuleDefinition(root, factory) {
    if(typeof exports === 'object' && typeof module === 'object')
        module.exports = factory(require("lodash")); // cmd
    else if(typeof define === 'function' && define.amd)
        define(["lodash"], factory); // amd
    else { // 
        var a = typeof exports === 'object' ? factory(require("lodash")) : factory(root["_"]);
        for(var i in a) (typeof exports === 'object' ? exports : root)[i] = a[i];
    }
})(self, function(__WEBPACK_EXTERNAL_MODULE__92__) {
    // code
});

可以看出來,AMD 規範打包後,代碼執行了一個 define 方法。依賴注入是通過回調方法的參數進行注入的。那麼我們是不是可以在加載 JS 文件之前先在 window 下掛一個 define 方法,等文件加載完執行 define 方法的時候,我們就可以在 define 方法中做我們想做的事情了。同理 UMD 打包規範也可以通過類似的操作達到我們的目的。所以這兩種方案都可以。考慮到後期動態表單頁面轉本地代碼的需求,希望插件還能被 npm 安裝使用。這裏採用了 UMD 規範。

方案選取

一、加載資源的方案

二、模塊打包方案

最終實現代碼參考:

// importScript.js
export default function (url, _) {
  const defineTemp = window.define; // 將 window 下的 define 方法暫存起來。
  let result; // 結果
  window.define = (depends, func) ={ // 自定義 define 方法,
    result = func(_); // 包依賴注入 
  }
  window.define.amd = true; // 僞裝成 amd 的 define。
  return new Promise(function (resolve, reject) {
    const el = document.createElement('script'); // 創建 script 元素
    el.src = url;
    el.async = false; // 保持時序
    const loadCallback = function () { // 加載完成之後處理
      el.removeEventListener('load', loadCallback);
      window.define = defineTemp;
      resolve(result);
    };
    const errorCallback = function (evt) { // 加載失敗之後處理
      el.removeEventListener('error', errorCallback);
      window.define = defineTemp;
      var error = evt.error || new Error("Load javascript failed. src=" + url);
      reject(error);
    };
    el.addEventListener('load', loadCallback); // 綁定事件
    el.addEventListener('error', errorCallback); // 綁定事件
    document.body.appendChild(el); // 插入元素
  });
}

調用方式

import importScript from './importScript.js';
import _ from 'lodash';
importScript('http://static.cai-inc.com/app.bundle.js', _).then((mod)=>{
   // code mod.xxx
})

三、與自定義表單結合

組件插件式引入的方式解決了,但是又引入了一個新的問題,一個表單頁面如果有 10 個自定義組件的話,是不是就得動態加載 10 個靜態資源呢,如果每個組件都有一個 JS,一個 CSS。那就是 20 個。這是不具備可行性的。

所以就有了組件合併的需求。

在配置表單頁面的時候當用戶發佈該頁面的時候,服務端建一個臨時項目,將該頁面的所有涉及到的自定義組件安裝到該項目上,並 export 出去。編譯打包,生成符合 UMD 規範的文件模塊。然後再按照以上方式進行引入。這樣就解決了多文件合併的問題。

總結

最後方案其實很簡單,只是對 UMD 規範打包的一種靈活應用。基於 UMD 規範打包出一個組件代碼,通過動態插入 Script 標籤的方式引入該組件的 JS 代碼。在引入之前定義一個 window.define 方法。在該組件的 JS 代碼下載成功之後,就會調用到我們定義的 window.define 方法。這樣我們就能對插件模塊進行依賴注入並將它儲存起來備用了。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/ZMLf0znRMaeBMc5n-DgoXQ