17-微前端实现方式对比

什么是微前端

微前端是一种多个团队通过独立发布功能的方式来共同构建现代化 web 应用的技术手段及方法策略。

微前端架构是一种类似于微服务的架构,它将微服务的理念应用于浏览器端,即将 Web 应用由单一的单体应用转变为 多个小型前端应用聚合为一的应用

注意 :这里的前端应用指的是前后端分离的单应用页面,在这基础才谈论微前端才有意义。

由此带来的变化是,这些前端应用可以 独立运行独立开发独立部署

// todo

微前端架构具备以下几个核心价值:

  • 技术栈无关 主框架不限制接入应用的技术栈,微应用具备完全自主权
  • 独立开发、独立部署 微应用仓库独立,前后端可独立开发,部署完成后主框架自动完成同步更新
  • 增量升级 在面对各种复杂场景时,我们通常很难对一个已经存在的系统做全量的技术栈升级或重构,而微前端是一种非常好的实施渐进式重构的手段和策略
  • 独立运行时 每个微应用之间状态隔离,运行时状态不共享

微前端架构旨在解决单体应用在一个相对长的时间跨度下,由于参与的人员、团队的增多、变迁,从一个普通应用演变成一个巨石应用()后,随之而来的应用不可维护的问题。这类问题在企业级 Web 应用中尤其常见。

主流微前端框架

阿里 qiankun

京东 micro App

国外 bit

结合我最近半年在微前端open in new window方面的实践和研究来看,微前端架构一般可以由以下几种方式进行:

  1. 使用 HTTP 服务器的路由来重定向多个应用
  2. 在不同的框架之上设计通讯、加载机制
  3. 通过组合多个独立应用、组件来构建一个单体应用
  4. iFrame。使用 iFrame 及自定义消息传递机制
  5. 使用纯 Web Components 构建应用
  6. 结合 Web Components 构建

路由分发微前端

路由分发式微前端 ,即通过路由将不同的业务分发到不同的、独立前端应用上。其通常可以通过 HTTP 服务器的反向代理来实现,又或者是应用框架自带的路由来解决。

就当前而言,通过路由分发式的微前端架构应该是采用最多、最易采用的 “微前端” 方案。但是这种方式看上去更像是 多个前端应用的聚合 ,即我们只是将这些不同的前端应用拼凑到一起,使他们看起来像是一个完整的整体。但是它们并不是,每次用户从 A 应用到 B 应用的时候,往往需要刷新一下页面。

在几年前的一个项目里,我们当时正在进行 遗留系统重写 。我们制定了一个迁移计划:

  1. 首先,使用静态网站生成动态生成首页
  2. 其次,使用 React 技术栈重构详情页
  3. 最后,替换搜索结果页

整个系统并不是一次性迁移过去,而是一步步往下进行。因此在完成不同的步骤时,我们就需要上线这个功能,于是就需要使用 Nginx 来进行路由分发。

随后,我们在别的项目上也使用了类似的方式,其主要原因是: 跨团队的协作 。当团队达到一定规模的时候,我们不得不面对这个问题。除此,还有 Angluar 跳崖式升级的问题。于是,在这种情况下,用户前台使用 Angular 重写,后台继续使用 Angular.js 等保持再有的技术栈。在不同的场景下,都有一些相似的技术决策。

因此在这种情况下,它适用于以下场景:

  • 不同技术栈之间差异比较大,难以兼容、迁移、改造
  • 项目不想花费大量的时间在这个系统的改造上
  • 现有的系统在未来将会被取代
  • 系统功能已经很完善,基本不会有新需求

而在满足上面场景的情况下,如果为了更好的用户体验,还可以采用 iframe 的方式来解决。

使用 iFrame 创建容器

Frame 作为一个非常古老的,人人都觉得普通的技术,却一直很管用。

HTML 内联框架元素 <iframe> 表示嵌套的正在浏览的上下文,能有效地将另一个 HTML 页面嵌入到当前页面中。

iframe 可以创建一个全新的独立的宿主环境,这意味着我们的前端应用之间可以相互独立运行。采用 iframe 有几个重要的前提:

  • 网站不需要 SEO 支持
  • 拥有相应的 应用管理机制

如果我们做的是一个应用平台,会在我们的系统中集成第三方系统,或者多个不同部门团队下的系统,显然这是一个不错的方案。一些典型的场景,如传统的桌面应用迁移到 Web 应用:

// todo

如果这一类应用过于复杂,那么它必然是要进行微服务化的拆分。因此,在采用 iframe 的时候,我们需要做这么两件事:

  • 设计管理应用机制
  • 设计应用通讯机制

加载机制 。在什么情况下,我们会去加载、卸载这些应用;在这个过程中,采用怎样的动画过渡,让用户看起来更加自然。

通讯机制 。直接在每个应用中创建 postMessage 事件并监听,并不是一个友好的事情。其本身对于应用的侵入性太强,因此通过 iframeEl.contentWindow 去获取 iFrame 元素的 Window 对象是一个更简化的做法。随后,就需要 定义一套通讯规范 :事件名采用什么格式、什么时候开始监听事件等等。

自制框架兼容应用

不论是基于 Web Components 的 Angular,或者是 VirtualDOM 的 React 等,现有的前端框架都离不开基本的 HTML 元素 DOM。

那么,我们只需要:

  1. 在页面合适的地方引入或者创建 DOM
  2. 用户操作时,加载对应的应用(触发应用的启动),并能卸载应用。

第一个问题,创建 DOM 是一个容易解决的问题。而第二个问题,则一点儿不容易,特别是移除 DOM 和相应应用的监听。当我们拥有一个不同的技术栈时,我们就需要有针对性设计出一套这样的逻辑。

虽然,这种方式的上手难度相对比较高,但是后期订制及可维护性比较方便。在不考虑每次加载应用带来的用户体验问题,其唯一存在的风险可能是: 第三方库不兼容

但是,不论怎样,与 iFrame 相比,其在技术上更具有 操刀性 ,更有看点。同样的,与 iframe 类似,我们仍然面对着一系列的不大不小的问题:

  • 需要设计一套管理应用的机制。
  • 对于流量大的 toC 应用来说,会在首次加载的时候,会多出大量的请求

而我们即又要拆分应用,又想可维护/可定制/稳定,我们还能怎么做?

纯 Web Components 技术构建

Web Components 是一套不同的技术,允许您创建可重用的定制元素(它们的功能封装在您的代码之外)并且在您的 Web 应用中使用它们。

它主要由四项技术组件:

  • Custom elements,允许开发者创建自定义的元素 。
  • Shadow DOM,即影子 DOM,通常是将 Shadow DOM 附加到主文档 DOM 中,并可以控制其关联的功能。而这个 Shadow DOM 则是不能直接用其它主文档 DOM 来控制的。
  • HTML templates,即 <template><slot> 元素,用于编写不在页面中显示的标记模板。
  • HTML Imports,用于引入自定义组件。

每个组件由 link 标签引入:

<link rel="import" href="components/di-li.html">
<link rel="import" href="components/d-header.html">

随后,在各自的 HTML 文件里,创建相应的组件元素,编写相应的组件逻辑。一个典型的 Web Components 应用架构如下图所示:

https://microfrontends.cn/imgs/web-components-architecture.png***不正确的文件格式。

可以看到这边方式与我们上面使用 iframe 的方式很相似,组件拥有自己独立的 ScriptsStyles,以及对应的用于单独部署组件的域名。然而它并没有想象中的那么美好,要直接使用 Web Components 来构建前端应用的难度有:

  • 重写现有的前端应用。是的,现在我们需要完成使用 Web Components 来完成整个系统的功能。
  • 上下游生态系统不完善。缺乏相应的一些第三方控件支持,这也是为什么 jQuery 相当流行的原因。
  • 系统架构复杂。当应用被拆分为一个又一个的组件时,组件间的通讯就成了一个特别大的麻烦。

Web Components 中的 ShadowDOM 更像是新一代的前端 DOM 容器。而遗憾的是并不是所有的浏览器,都可以完全支持 Web Components。

结合 Web Components 构建

Web Components 离现在的我们太远,可是结合 Web Components 来构建前端应用,则更是一种面向未来演进的架构。或者说在未来的时候,我们可以开始采用这种方式来构建我们的应用。好在,已经有框架在打造这种可能性。

就当前而言,有两种方式可以结合 Web Components 来构建微前端应用:

  • 使用 Web Components 构建独立于框架的组件,随后在对应的框架中引入这些组件
  • 在 Web Components 中引入现有的框架,类似于 iframe 的形式

前者是一种组件式的方式,或者则像是在迁移未来的 “遗留系统” 到未来的架构上。

在 Web Components 中集成现有框架

现有的 Web 框架已经有一些可以支持 Web Components 的形式,诸如 Angular 支持的 createCustomElement,就可以实现一个 Web Components 形式的组件:

platformBrowser()
  .bootstrapModuleFactory(MyPopupModuleNgFactory)
    .then(({injector}) => {
      const MyPopupElement = createCustomElement(MyPopup, {injector});
      customElements.define(‘my-popup’, MyPopupElement);
});

在未来,将有更多的框架可以使用类似这样的形式,集成到 Web Components 应用中。

集成在现有框架中的 Web Components

另外一种方式,将组件直接构建成 Web Components 形式的组件,随后在对应的诸如,如 React 或者 Angular 中直接引用。

在这种情况之下,我们就可以构建出独立于框架的组件。

讲了这么多就希望大家能了解微前端不同实现方式下的优劣,在未来如果你们要接触这项技术也能依据不同的情况来选择合适的方案。