[譯]一種基于模塊聯(lián)邦的插件前端
原文:https://malcolmkee.com/blog/a-plugin-based-frontend-with-module-federation/
在談及模塊聯(lián)邦及其獨立構(gòu)建和部署的特性(通常稱為微前端)時,一個常見的問題是,“為什么這比使用iframe更好?”雖然這的確是一個問題,特別是當只使用模塊聯(lián)邦拼接多個UI時,其好處可能不會立即顯現(xiàn)的時候;答案就在于它無縫集成多個前端應(yīng)用程序,并允許組件和函數(shù)調(diào)用一起工作的能力。這就是為什么模塊聯(lián)邦是目前構(gòu)建微前端應(yīng)用程序的最佳技術(shù)。
在本文中,我將為前端應(yīng)用程序提供一個利用模塊聯(lián)邦的插件架構(gòu)。該架構(gòu)允許開發(fā)人員在既有應(yīng)用程序中添加、刪除或更新功能,而無需對應(yīng)用程序進行任何更改。得益于模塊聯(lián)邦實現(xiàn)的無縫集成,該插件架構(gòu)才成為可能。
插件架構(gòu)是什么?
插件架構(gòu)(plugin architecture)是一種軟件架構(gòu),它允許 第三方開發(fā)者 通過編寫插件來擴展現(xiàn)有軟件的功能。
在插件系統(tǒng)中,“core”軟件提供了 一組定義好的接口、API或鉤子,以使開發(fā)人員在不修改核心軟件的前提下添加新特性或修改應(yīng)用程序的行為。這種方法促進了模塊化,因為插件可以獨立于核心軟件開發(fā),并且可以被輕松添加或刪除以自定義應(yīng)用程序。
插件系統(tǒng)通常用于需要大量定制的系統(tǒng)。例如,流行的軟件,如瀏覽器,文本編輯器,構(gòu)建工具和內(nèi)容管理系統(tǒng)(CMS)都使用插件系統(tǒng),使開發(fā)人員能夠向軟件添加新功能。VS Code 是一個流行的代碼編輯器,它的擴展市場就是一個插件系統(tǒng)的例子。類似地,流行的 CMS WordPress 使用插件系統(tǒng),使用戶能夠向其網(wǎng)站添加新功能。
以模塊聯(lián)邦實現(xiàn)的插件系統(tǒng)
模塊聯(lián)邦的一種典型模式包括一個單體應(yīng)用程序(host),它從多個較小的應(yīng)用程序(remote)中導入代碼。host和remote都可以獨立構(gòu)建和部署,并且可以使用模塊聯(lián)邦在運行時將它們縫合在一起。
將插件系統(tǒng)應(yīng)用于模塊聯(lián)邦,可以使host應(yīng)用程序或者說"core",在添加、更新或移除充當插件的remotes 時保持不變。唯一的約束是所有remote必須遵循一組定義好的接口或鉤子。
舉個例子,假設(shè)所有remote應(yīng)用都必須按照以下約定導出單個遠程模塊/register:
// src/register.tsx
import { register } from '@company/core-plugin';
import * as React from 'react';
const OrdersPage = () => <h1>Orders</h1>;
export default register({
routes: [
{
path: 'orders',
element: <OrdersPage />,
},
],
});
來自包@company/core-plugin的register函數(shù)是一個身份函數(shù),用于強制類型安全:
import { RouteObject } from 'react-router-dom';
export interface Plugin {
routes: Array<RouteObject>;
}
export const register = (plugin: Plugin) => plugin;
通過所有remote都使用該接口暴露的register模塊,host就可以渲染已在所有remote上注冊的全部路由:
//app.tsx in host
import { Plugin } from '@company/core-plugin';
import * as React from 'react';
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
import { createRoot } from 'react-dom/client';
const getAllRemotes = () =>
Promise.all([
import('microfrontend1/register'),
import('microfrontend2/register'),
import('microfrontend3/register'),
]) as Promise<Array<{ default: Plugin }>>;
getAllRemotes()
.then((mods) => mods.map((mod) => mod.default))
.then((remotes) => {
const router = createBrowserRouter(remotes.map((remote) => remote.routes).flat());
createRoot(document.getElementById('app')!).render(<RouterProvider router={router} />);
});
如下例所示,每當在remote中增添新的路由,則host中無需改變單獨的代碼,只消在下次加載時便會自動出現(xiàn)了。
// src/register.tsx
import { register } from '@company/core-plugin';
import * as React from 'react';
const OrdersPage = () => <h1>Orders</h1>;
const OrdersDetailsPage = () => <h1>Orders Details</h1>;
export default register({
routes: [
{
path: 'orders',
element: <OrdersPage />,
},
{
path: 'orders/:orderId',
element: <OrdersDetailsPage />,
},
],
});
可能的插件API
在模塊聯(lián)邦中的插件架構(gòu)有了基本了解之后,你就可以通過創(chuàng)建更多的API或鉤子來提高host的可擴展性了。下面是一些支持常見用例的插件API。請記住,它們不是詳盡的,也不是必需的??梢愿鶕?jù)你的用例來決定其取舍,或者也可以創(chuàng)建自己的API。
register 的 routes 選項
這個選項在前面的部分中討論過,是一個路由定義數(shù)組,通??梢詮哪闶褂玫穆酚善鲙熘袛U展(在我的例子中,我重用了react-router-dom中的RouteObject接口)。它還可以包括子導航,比如在你的應(yīng)用中要用tabs之類的時候。host將在構(gòu)造其路由之前合并來自所有remote的路由定義。
從理論上講,多個remote的路由可能會相互沖突,例如使用'*'這類過度貪婪的路徑,當檢測到這種情況時,你應(yīng)該通過 linting 或控制臺錯誤消息來緩解。
register 的 navItems 選項
也就是一個導航項目列表;你的host應(yīng)用可能帶有導航,此屬性允許remote向其中添加/刪除項目。該屬性的可能定義為:
interface NavItem {
path: string;
label: string;
/** 用去嵌套導航的章節(jié)或者說組 */
section: string;
/** 排序用 */
order: number;
/** 圖標 */
icon: React.ReactNode;
/** 假設(shè)又區(qū)分了多種導航 */
location: 'header' | 'footer' | 'sidebar';
}
結(jié)合了 <Slot /> 組件的 register 之 fills 選項
如果需要將組件從一個remote嵌入到另一個remote,這兩個API可以幫上忙。
想象一個客戶票證界面,它顯示多個部分,如客戶個人信息和過往訂單等。客戶票據(jù)界面由一個團隊維護,而客戶個人信息和訂單由另一個團隊開發(fā),每個團隊都維護著自己的remote應(yīng)用。
要將客戶個人信息和過往訂單嵌入到客戶票證界面中,我們可以使用以下元素:
-
在客戶票證界面(在 customer-support-app 那個 remote 應(yīng)用里編寫)中,渲染一個
<Slot id="customerTicketScreen" />組件。就其本身而言,它什么也沒有顯示。 -
在客戶個人數(shù)據(jù)和訂單兩個 remote 應(yīng)用中,為
register提供fills選項
// src/register.tsx
export default register({
fills: [
{
slotId: 'customerTicketScreen', // 匹配在 support 中由 Slot 提供的 id
component: PersonalInfoSection,
},
],
});
- 在 host 中,使用 React context 注入所有按
slotId分組的 fills。在Slot組件中,讀取 context 的值,并按照slotId與id匹配,渲染所有 fills。
usePluginEventEmitter 和 usePluginEventListener
讓來自多個 remote 的組件在同一個界面上共存,那么它們不可避免地要相互通信。usePluginEventEmitter 和 usePluginEventListener 就是用于讓組件發(fā)出/監(jiān)聽事件的自定義鉤子。
從原理上來講,這類鉤子可以使用 mitt 事件總線或 window.dispatch(CustomEvent) 這樣的自定義事件來實現(xiàn)。
總結(jié)
一個使用模塊聯(lián)邦的基于插件的前端架構(gòu),是創(chuàng)建復雜應(yīng)用程序的強大方法,這樣的應(yīng)用允許來自多個項目的UI組件無縫集成。通過使用插件系統(tǒng),開發(fā)人員可以在不修改host應(yīng)用的前提下擴展其功能。
同時,雖然這種方法帶來諸多便利,留意其潛在的挑戰(zhàn)和走好鋼絲也是很重要的。例如,如果要在多應(yīng)用間復用工具函數(shù)或類,插件系統(tǒng)可能并不適用,反而 npm 包是個更好的選擇。盡管有這些潛在限制,經(jīng)過細心計劃和實現(xiàn),基于插件的前端架構(gòu)還是可以為構(gòu)建復雜應(yīng)用提供一個靈活和可擴展的平臺。
