[译]一种基于模块联邦的插件前端

简介: [译]一种基于模块联邦的插件前端

原文:malcolmkee.com/blog/a-plug…

在谈及模块联邦及其独立构建和部署的特性(通常称为微前端)时,一个常见的问题是,“为什么这比使用iframe更好?”虽然这的确是一个问题,特别是当只使用模块联邦拼接多个UI时,其好处可能不会立即显现的时候;答案就在于它无缝集成多个前端应用程序,并允许组件和函数调用一起工作的能力。这就是为什么模块联邦是目前构建微前端应用程序的最佳技术。

在本文中,我将为前端应用程序提供一个利用模块联邦的插件架构。该架构允许开发人员在既有应用程序中添加、删除或更新功能,而无需对应用程序进行任何更改。得益于模块联邦实现的无缝集成,该插件架构才成为可能。

插件架构是什么?

插件架构(plugin architecture)是一种软件架构,它允许 第三方开发者 通过编写插件来扩展现有软件的功能。

在插件系统中,“core”软件提供了 一组定义好的接口、API或钩子,以使开发人员在不修改核心软件的前提下添加新特性或修改应用程序的行为。这种方法促进了模块化,因为插件可以独立于核心软件开发,并且可以被轻松添加或删除以自定义应用程序。

插件系统通常用于需要大量定制的系统。例如,流行的软件,如浏览器,文本编辑器,构建工具和内容管理系统(CMS)都使用插件系统,使开发人员能够向软件添加新功能。VS Code 是一个流行的代码编辑器,它的扩展市场就是一个插件系统的例子。类似地,流行的 CMS WordPress 使用插件系统,使用户能够向其网站添加新功能。

以模块联邦实现的插件系统

模块联邦的一种典型模式包括一个单体应用程序(host),它从多个较小的应用程序(remote)中导入代码。host和remote都可以独立构建和部署,并且可以使用模块联邦在运行时将它们缝合在一起。

image.png

将插件系统应用于模块联邦,可以使host应用程序或者说"core",在添加、更新或移除充当插件的remotes 时保持不变。唯一的约束是所有remote必须遵循一组定义好的接口或钩子

举个例子,假设所有remote应用都必须按照以下约定导出单个远程模块/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-pluginregister函数是一个身份函数,用于强制类型安全:


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中无需改变单独的代码,只消在下次加载时便会自动出现了。


// 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

在模块联邦中的插件架构有了基本了解之后,你就可以通过创建更多的API或钩子来提高host的可扩展性了。下面是一些支持常见用例的插件API。请记住,它们不是详尽的,也不是必需的。可以根据你的用例来决定其取舍,或者也可以创建自己的API。

registerroutes 选项

这个选项在前面的部分中讨论过,是一个路由定义数组,通常可以从你使用的路由器库中扩展(在我的例子中,我重用了react-router-dom中的RouteObject接口)。它还可以包括子导航,比如在你的应用中要用tabs之类的时候。host将在构造其路由之前合并来自所有remote的路由定义。

从理论上讲,多个remote的路由可能会相互冲突,例如使用'*'这类过度贪婪的路径,当检测到这种情况时,你应该通过 linting 或控制台错误消息来缓解。

registernavItems 选项

也就是一个导航项目列表;你的host应用可能带有导航,此属性允许remote向其中添加/删除项目。该属性的可能定义为:


interface NavItem {
  path: string;
  label: string;
  /** 用去嵌套导航的章节或者说组 */
  section: string;
  /** 排序用 */
  order: number;
  /** 图标 */
  icon: React.ReactNode;
  /** 假设又区分了多种导航 */
  location: 'header' | 'footer' | 'sidebar';
}

结合了 <Slot /> 组件的 registerfills 选项

如果需要将组件从一个remote嵌入到另一个remote,这两个API可以帮上忙。

想象一个客户票证界面,它显示多个部分,如客户个人信息和过往订单等。客户票据界面由一个团队维护,而客户个人信息和订单由另一个团队开发,每个团队都维护着自己的remote应用。

image.png

要将客户个人信息和过往订单嵌入到客户票证界面中,我们可以使用以下元素:

  1. 在客户票证界面(在 customer-support-app 那个 remote 应用里编写)中,渲染一个<Slot id="customerTicketScreen" />组件。就其本身而言,它什么也没有显示。
  2. 在客户个人数据和订单两个 remote 应用中,为 register 提供 fills选项


// src/register.tsx
        
export default register({
  fills: [
    {
      slotId: 'customerTicketScreen', // 匹配在 support 中由 Slot 提供的 id
      component: PersonalInfoSection,
    },
  ],
});
  1. 在 host 中,使用 React context 注入所有按 slotId 分组的 fills。在Slot组件中,读取 context 的值,并按照slotIdid匹配,渲染所有 fills。

usePluginEventEmitterusePluginEventListener

让来自多个 remote 的组件在同一个界面上共存,那么它们不可避免地要相互通信。usePluginEventEmitterusePluginEventListener 就是用于让组件发出/监听事件的自定义钩子。

从原理上来讲,这类钩子可以使用 mitt 事件总线或 window.dispatch(CustomEvent) 这样的自定义事件来实现。

总结

一个使用模块联邦的基于插件的前端架构,是创建复杂应用程序的强大方法,这样的应用允许来自多个项目的UI组件无缝集成。通过使用插件系统,开发人员可以在不修改host应用的前提下扩展其功能。

同时,虽然这种方法带来诸多便利,留意其潜在的挑战和走好钢丝也是很重要的。例如,如果要在多应用间复用工具函数或类,插件系统可能并不适用,反而 npm 包是个更好的选择。尽管有这些潜在限制,经过细心计划和实现,基于插件的前端架构还是可以为构建复杂应用提供一个灵活和可扩展的平台。



相关文章
|
2月前
|
开发框架 前端开发 JavaScript
循序渐进VUE+Element 前端应用开发(15)--- 用户管理模块的处理
循序渐进VUE+Element 前端应用开发(15)--- 用户管理模块的处理
|
13天前
|
前端开发 开发者
在前端开发中,webpack 作为一个强大的模块打包工具,为我们提供了丰富的功能和扩展性
【9月更文挑战第1天】在前端开发中,Webpack 作为强大的模块打包工具,提供了丰富的功能和扩展性。本文重点介绍 DefinePlugin 插件,详细探讨其原理、功能及实际应用。DefinePlugin 可在编译过程中动态定义全局变量,适用于环境变量配置、动态加载资源、接口地址配置等场景,有助于提升代码质量和开发效率。通过具体配置示例和注意事项,帮助开发者更好地利用此插件优化项目。
43 13
|
2月前
|
前端开发 容器
前端框架与库 - Angular模块与依赖注入
【7月更文挑战第17天】探索Angular的模块化和依赖注入:模块用于组织组件、服务等,通过`@NgModule`声明。依赖注入简化类间依赖管理,但面临模块重复导入、服务作用域不当和依赖循环等问题。解决策略包括规划模块结构、正确设置服务作用域和使用工厂函数打破循环依赖。遵循最佳实践,构建高效、可维护的Angular应用。
53 17
|
1月前
|
开发框架 JSON 缓存
基于SqlSugar的开发框架循序渐进介绍(22)-- Vue3+TypeScript的前端工作流模块中实现统一的表单编辑和表单详情查看处理
基于SqlSugar的开发框架循序渐进介绍(22)-- Vue3+TypeScript的前端工作流模块中实现统一的表单编辑和表单详情查看处理
|
2月前
|
开发框架 前端开发 JavaScript
在微信框架模块中,基于Vue&Element前端的事件和内容的管理
在微信框架模块中,基于Vue&Element前端的事件和内容的管理
|
2月前
|
开发框架 移动开发 前端开发
在微信框架模块中,基于Vue&Element前端的后台管理功能介绍
在微信框架模块中,基于Vue&Element前端的后台管理功能介绍
|
30天前
|
前端开发 开发者
在前端开发中,webpack 作为模块打包工具,其 DefinePlugin 插件可在编译时动态定义全局变量,支持环境变量定义、配置参数动态化及条件编译等功能。
在前端开发中,webpack 作为模块打包工具,其 DefinePlugin 插件可在编译时动态定义全局变量,支持环境变量定义、配置参数动态化及条件编译等功能。本文阐述 DefinePlugin 的原理、用法及案例,包括安装配置、具体示例(如动态加载资源、配置接口地址)和注意事项,帮助开发者更好地利用此插件优化项目。
36 0
|
1月前
|
Web App开发 前端开发 JavaScript
React——前端开发中模块与组件【四】
React——前端开发中模块与组件【四】
25 0
|
2月前
|
开发框架 前端开发 JavaScript
在微信框架模块中,基于Vue&Element前端,通过动态构建投票选项,实现单选、复选的投票操作
在微信框架模块中,基于Vue&Element前端,通过动态构建投票选项,实现单选、复选的投票操作
|
2月前
|
开发框架 前端开发 JavaScript
循序渐进VUE+Element 前端应用开发(16)--- 组织机构和角色管理模块的处理
循序渐进VUE+Element 前端应用开发(16)--- 组织机构和角色管理模块的处理