背景:對于大型web應用而言,功能極其豐富復雜,為了具備擴展性,部分項目選擇插件化架構方式,開放一部分系統Hook給具備開發能力的用戶,不但提升用戶的體驗感,還同時豐富平臺功能,一舉兩得。如何構建具備插件化能力的平臺?本文嘗試通過分析jenkins jar包插件實現方式及Figma前端插件實現方式探究插件化架構方案。
我們常見的支持插件化的應用是一些桌面端編輯器,如VSCode,Eclipse,Idea,Sublime等,也有支持動態擴展的web應用,如構建工具Jenkins,支持前端插件擴展的web應用,如設計工具Figma。不管哪種應用方式,基本的設計邏輯大致如下。
我們當前主要研究的是web系統的插件化構架方案,本地插件化軟件先不討論。主要以jenkins和figma的兩種實現方式進行探討。
Jenkins插件化系統
Jenkins可以支持git, svn, maven等很多功能,這些都是Jenkins的插件,Jenkins通過擴展點及前端視圖模板來提供插件擴展能力,以jar包的方式上傳到指定目錄,創建類加載器 class-loader,使用插件策略PluginStrategy加載可以激活的插件。
(一)擴展點
Jenkins有很多的擴展點(ExtensitonPoint),它是Jenkins系統的某個方面的接口或抽象類。這些接口定義了需要實現的方法,而Jenkins插件需要實現這些方法,也可以叫做在此擴展點之上進行擴展Jenkins。有關擴展點的詳細信息,請參閱Jenkins 官方ExtentionPoints文檔。通過這些擴展點我們可以寫插件來實現自己的需求。
下面是一些常用的擴展點:
- Scm :代表源碼管理的一個步驟,如下面的Git,Subversion就是擴展的Scm
-
Builder : 代表構建的一個步驟,如下圖中在構建過程中,我們可以增加一個構建步驟,而每一個選項都是對應一個Builder,在每一個Builder中都有自己不同的功能。如Execute shell,這就是一個ShellBuilder,意味著在構建過程中會執行一個shell命令
12.png Trigger:代表一個構建的觸發,當滿足一個什么樣的條件時觸發這個項目開始構建。比較常用的觸發就是當代碼變更時觸發,如果我們需要實現一些比較復雜的觸發邏輯,就需要擴展Trigger這個擴展點
- Publisher:Publisher代表一個項目構建完成后需要執行的步驟,如選項中的E-Mail Notifaction就是一個Publisher插件,選擇這個選項后,當項目構建完成,就會使用email來通知用戶,假如想要在項目構建完成后將構建目標產物發送到服務器上,則可以擴展此擴展點。
(二)Jenkins中的視圖
Jenkins 使用jelly來編寫視圖,Jelly 是一種基于 Java
技術和 XML
的腳本編制和處理引擎。Jelly 的特點是有許多基于 JSTL (JSP 標準標記庫,JSP Standard Tag Library)、Ant、Velocity 及其它眾多工具的可執行標記。Jelly 還支持 Jexl(Java 表達式語言,Java Expression Language),Jexl 是 JSTL 表達式語言的擴展版本。Jenkins的界面繪制就是通過Jelly實現的。
另外一個開源的插件化后臺管理系統:grape: 前后端可插件開發的后臺管理系統 (gitee.com)
Figma前端插件系統
Figma 是一個在線協作式 UI 設計工具,具有插件擴展功能,只要有前端開發能力的用戶均可開發自己的插件來擴展設計體驗。
Figma的插件是純前端的插件方式,沒有后端代碼,它的插件系統是如何工作的?
這是一個基于 TypeScript + React 技術棧,使用 Webpack 構建的 Figma 插件目錄結構如下:
├── README.md
├── figma.d.ts
├── manifest.json
├── package-lock.json
├── package.json
├── src
│ ├── code.ts
│ ├── logo.svg
│ ├── ui.css
│ ├── ui.html
│ └── ui.tsx
├── tsconfig.json
└── webpack.config.js
在其 manifest.json
文件中包含了一些簡單的信息。
{
"name": "React Sample",
"id": "738168449509241862",
"api": "1.0.0",
"main": "dist/code.js",
"ui": "dist/ui.html"
}
ui展示是通過如下代碼加載,會彈出個DIV,展示manifest.josn中指定的ui地址內容:
figma.showUI(__html__);
可以看出 Figma 將插件入口分為了 main
與 ui
兩部分, main 中包含了插件實際運行時的邏輯,而 ui 則是一個插件的 HTML 片段。即 UI 與邏輯分離。 main
中的 js 文件被包裹在一個 iframe 里加載到頁面上。而 ui
中的 HTML 最終也被包裹在一個 iframe 里渲染出來。
為什么這么要用iframe包裹?
- 首先是安全性考慮
iframe,一個瀏覽器自帶的沙箱環境。將插件代碼由 iframe 包裹起來,由于 iframe 天然的限制,這將確保插件代碼無法操作 Figma 主界面上下文,同時也可以只開放一份白名單 API 供插件調用。
iframe參考 - 其次是避免樣式污染
這將有效的避免插件 UI 層 CSS 代碼導致全局樣式污染,使主程序與插件樣式相互獨立。
插件如何與主程序通信?
在上一層使用 window.addEventListener
進行監控,事件通信使用 parent.postMessage,發送事件及數據。
Inner Plugin Iframe
:
document.getElementById('create').onclick = () => {
const textbox = document.getElementById('count');
const count = parseInt(textbox.value, 10);
parent.postMessage({ pluginMessage: { type: 'create-rectangles', count } }, '*')
}
document.getElementById('cancel').onclick = () => {
parent.postMessage({ pluginMessage: { type: 'cancel' } }, '*')
}
Shim Plugin Iframe
:
var messageHandler = (event) => {
var pluginIframeElement = document.getElementById("plugin-iframe")
if (pluginIframeElement && event.source === pluginIframeElement.contentWindow) {
parent.postMessage({ origin: event.origin, data: event.data }, window.location.origin)
}
}
window.addEventListener("message", messageHandler)
window.__FIGMA_PLUGIN_SANDBOX_PAGE_LOADED = true
整體架構圖描述,大致如下:
開發的插件可在本地app中進行調試,最終發布到服務器。
總結
插件系統在設計時要考慮的基本內容,如何開放數據接口,如何加載插件,何時何地啟動插件,插件如何與主程序通信問題,如何保證插件安全性?前端插件化使用iframe sandbox是一個通用可行的辦法,但依然會有很多問題。