背景:對于大型web應(yīng)用而言,功能極其豐富復(fù)雜,為了具備擴展性,部分項目選擇插件化架構(gòu)方式,開放一部分系統(tǒng)Hook給具備開發(fā)能力的用戶,不但提升用戶的體驗感,還同時豐富平臺功能,一舉兩得。如何構(gòu)建具備插件化能力的平臺?本文嘗試通過分析jenkins jar包插件實現(xiàn)方式及Figma前端插件實現(xiàn)方式探究插件化架構(gòu)方案。
我們常見的支持插件化的應(yīng)用是一些桌面端編輯器,如VSCode,Eclipse,Idea,Sublime等,也有支持動態(tài)擴展的web應(yīng)用,如構(gòu)建工具Jenkins,支持前端插件擴展的web應(yīng)用,如設(shè)計工具Figma。不管哪種應(yīng)用方式,基本的設(shè)計邏輯大致如下。

我們當前主要研究的是web系統(tǒng)的插件化構(gòu)架方案,本地插件化軟件先不討論。主要以jenkins和figma的兩種實現(xiàn)方式進行探討。
Jenkins插件化系統(tǒng)

Jenkins可以支持git, svn, maven等很多功能,這些都是Jenkins的插件,Jenkins通過擴展點及前端視圖模板來提供插件擴展能力,以jar包的方式上傳到指定目錄,創(chuàng)建類加載器 class-loader,使用插件策略PluginStrategy加載可以激活的插件。
(一)擴展點
Jenkins有很多的擴展點(ExtensitonPoint),它是Jenkins系統(tǒng)的某個方面的接口或抽象類。這些接口定義了需要實現(xiàn)的方法,而Jenkins插件需要實現(xiàn)這些方法,也可以叫做在此擴展點之上進行擴展Jenkins。有關(guān)擴展點的詳細信息,請參閱Jenkins 官方ExtentionPoints文檔。通過這些擴展點我們可以寫插件來實現(xiàn)自己的需求。
下面是一些常用的擴展點:
- Scm :代表源碼管理的一個步驟,如下面的Git,Subversion就是擴展的Scm

-
Builder : 代表構(gòu)建的一個步驟,如下圖中在構(gòu)建過程中,我們可以增加一個構(gòu)建步驟,而每一個選項都是對應(yīng)一個Builder,在每一個Builder中都有自己不同的功能。如Execute shell,這就是一個ShellBuilder,意味著在構(gòu)建過程中會執(zhí)行一個shell命令
12.png Trigger:代表一個構(gòu)建的觸發(fā),當滿足一個什么樣的條件時觸發(fā)這個項目開始構(gòu)建。比較常用的觸發(fā)就是當代碼變更時觸發(fā),如果我們需要實現(xiàn)一些比較復(fù)雜的觸發(fā)邏輯,就需要擴展Trigger這個擴展點

- Publisher:Publisher代表一個項目構(gòu)建完成后需要執(zhí)行的步驟,如選項中的E-Mail Notifaction就是一個Publisher插件,選擇這個選項后,當項目構(gòu)建完成,就會使用email來通知用戶,假如想要在項目構(gòu)建完成后將構(gòu)建目標產(chǎn)物發(fā)送到服務(wù)器上,則可以擴展此擴展點。

(二)Jenkins中的視圖
Jenkins 使用jelly來編寫視圖,Jelly 是一種基于 Java 技術(shù)和 XML 的腳本編制和處理引擎。Jelly 的特點是有許多基于 JSTL (JSP 標準標記庫,JSP Standard Tag Library)、Ant、Velocity 及其它眾多工具的可執(zhí)行標記。Jelly 還支持 Jexl(Java 表達式語言,Java Expression Language),Jexl 是 JSTL 表達式語言的擴展版本。Jenkins的界面繪制就是通過Jelly實現(xiàn)的。
另外一個開源的插件化后臺管理系統(tǒng):grape: 前后端可插件開發(fā)的后臺管理系統(tǒng) (gitee.com)
Figma前端插件系統(tǒng)
Figma 是一個在線協(xié)作式 UI 設(shè)計工具,具有插件擴展功能,只要有前端開發(fā)能力的用戶均可開發(fā)自己的插件來擴展設(shè)計體驗。

Figma的插件是純前端的插件方式,沒有后端代碼,它的插件系統(tǒng)是如何工作的?
這是一個基于 TypeScript + React 技術(shù)棧,使用 Webpack 構(gòu)建的 Figma 插件目錄結(jié)構(gòu)如下:
├── 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地址內(nèi)容:
figma.showUI(__html__);
可以看出 Figma 將插件入口分為了 main 與 ui 兩部分, main 中包含了插件實際運行時的邏輯,而 ui 則是一個插件的 HTML 片段。即 UI 與邏輯分離。 main 中的 js 文件被包裹在一個 iframe 里加載到頁面上。而 ui 中的 HTML 最終也被包裹在一個 iframe 里渲染出來。
為什么這么要用iframe包裹?
- 首先是安全性考慮
iframe,一個瀏覽器自帶的沙箱環(huán)境。將插件代碼由 iframe 包裹起來,由于 iframe 天然的限制,這將確保插件代碼無法操作 Figma 主界面上下文,同時也可以只開放一份白名單 API 供插件調(diào)用。
iframe參考 - 其次是避免樣式污染
這將有效的避免插件 UI 層 CSS 代碼導(dǎo)致全局樣式污染,使主程序與插件樣式相互獨立。
插件如何與主程序通信?
在上一層使用 window.addEventListener進行監(jiān)控,事件通信使用 parent.postMessage,發(fā)送事件及數(shù)據(jù)。
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
整體架構(gòu)圖描述,大致如下:

開發(fā)的插件可在本地app中進行調(diào)試,最終發(fā)布到服務(wù)器。

總結(jié)
插件系統(tǒng)在設(shè)計時要考慮的基本內(nèi)容,如何開放數(shù)據(jù)接口,如何加載插件,何時何地啟動插件,插件如何與主程序通信問題,如何保證插件安全性?前端插件化使用iframe sandbox是一個通用可行的辦法,但依然會有很多問題。
