動態表單之表單組件的插件式加載方案
前言
關於動態化表單方案前面我們已經有過一次分享,沒看過的同學可以看下之前的文章 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 項目也有一個入口,這會導致出現兩個入口。兩者用法並不能很好的並存。
需求拆解
那麼現在來分析一下實現組件插件式加載的關鍵問題:
一、加載資源
- 因爲插件單獨發佈之後要放在 CDN 上,所以加載靜態資源的方案需要滿足沒有跨域限制的條件。
二、插件模塊打包
-
插件模塊最好能使用現有模塊標準例如 CMD、AMD 模塊標準,這樣我們就可以使用更多的社區開源方案,降低方案的風險性。同時降低團隊成員學習使用成本。
-
插件需要能夠被注入依賴,例如項目中已經包含有 Lodash 或者 AntD 組件庫的包,這時候插件模塊中使用 Lodash 或者 AntD 組件庫的話我們當然希望能夠直接引用項目中已有的,而不是插件模塊中重新引入一個。
需求分析
一、靜態資源加載
對於運行中加載靜態資源,現有解決方案中不論是哪一種,都是利用動態插入 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 了。接下來想在哪裏調用就在哪裏調用。想注入什麼變量就注入什麼變量了。
-
備註:
-
這裏是對 AMD 進行了粗略的原理解釋,具體實現還有很多細節,想要了解的話,可以在網上找到很多源碼解析,這裏就不再細講。
-
Webpack 打包之後的代碼的模塊管理方式是 Webpack 自己實現的一套類似 CommonJS 規範的東西。去看看打包生成的代碼就可以發現裏面都是一些 webpack_modules__,webpack_require,webpack_exports 這樣的關鍵詞,和 CommonJS 規範的 modules,require,exports 相對應。
三、模塊打包標準
由於我們團隊使用的是 Webpack 的打包體系,因此想要保持技術棧統一,則要先從 Webpack 的打包入手。讓我們將 Webpack 的模塊化打包都試一下看看能得出什麼。
Webpack library 打包方式有 5 種。
-
變量:作爲一個全局變量,通過
script
標籤來訪問(libraryTarget:'var'
)。 -
this:通過
this
對象訪問(libraryTarget:'this'
)。 -
window:通過
window
對象訪問,在瀏覽器中(libraryTarget:'window'
)。 -
UMD:在 AMD 或 CommonJS 的
require
之後可訪問(libraryTarget:'umd'
)。 -
AMD:基於 AMD 規範的打包方式(
libraryTarget:'amd'
)。
可以排除前三個,我們並不想將模塊掛到 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 規範。
方案選取
一、加載資源的方案
- 採用動態插入 Script 方式實現 JS 資源加載。
二、模塊打包方案
- 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