微前端初步

本文最后更新于:1 年前

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

由此带来的变化是,这些前端应用可以独立运行、独立开发、独立部署。以及,它们应该可以在共享组件的同时进行并行开发——这些组件可以通过 NPM 或者 Git Tag、Git Submodule 来管理。
注意:这里的前端应用指的是前后端分离的单应用页面,在这基础才谈论微前端才有意义。
微前端架构一般可以由以下几种方式进行:

  1. 使用 HTTP 服务器的路由来重定向多个应用
  2. 在不同的框架之上设计通讯、加载机制,诸如 Mooa 和 Single-SPA
  3. 通过组合多个独立应用、组件来构建一个单体应用
  4. iFrame。使用 iFrame 及自定义消息传递机制
  5. 使用纯 Web Components 构建应用,Web Components详见:http://www.ruanyifeng.com/blog/2019/08/web_components.html
  6. 结合 Web Components 构建

总的来说就是,拆分前端应用,各个应用独立开发,独立部署,不同模块可以使用不同技术栈。


摘自vickylinj

什么是微前端

官网上是这么描述的:

Techniques, strategies and recipes for building a modern web app with multiple teams that can ship features independently.

从官网我们可以了解到:微前端最早是出现在2016年的ThoughtWorks Technology Radar。它将微服务的概念推广到了前端。当前的趋势是构建一个功能丰富且强大的单页面应用程序,它位于微服务架构之上。但是随着我们不断的迭代产品,前端层的业务变得繁重复杂且越来越难维护。这就是我们说的前端单体( Frontend Monolith )。

微前端背后的想法是将整个的网站或者Web应用程序看作是独立团队所拥有的功能组合,每个团队负责各自的功能模块,每个团队是跨职能的,从数据库到用户界面,端到端的开发其功能。

通过下面这个图,我们可以更好的了解到:底部垂直排列的团队是这个架构的核心。它们各自以页面或片段的形式产生特征。您可以使用 SSI 或 Web Components 之类的技术将它们集成到到达客户的组合页面中。

前端集成描述了一组用于将团队的用户界面(页面和片段)组装到集成应用程序中的技术。您可以将这些技术分为三类:路由、组合和通信。根据您的架构选择,您有不同的选择来解决这些类别。

微前端解决什么问题

公司选择使用微前端的第一个路线是提高开发效率。在分层架构中,多个团队参与构建新的功能。减少团队之间的等待时间是微前端的主要目标
现在的架构都没有扩展到前端开发的概念,依然是单体、前端/后端拆分和微服务。他们都带有一个整体式的前端。

优点

  • 可独立部署
  • 将故障风险隔离到更小的区域
  • 范围更窄,因此更容易理解
  • 具有较小的代码库,可以在您想要重构或替换它时提供帮助;并且可以减少意外耦合的情况出现
  • 更可预测,因为它不与其他系统共享状态

微前端解决了的问题

  1. 拆分和细化(低耦合)
    当下前端领域中,单页面应用(SPA)是非常流行的前端趋势。但是随着项目的迭代,功能和代码会越来越繁杂。耦合度也会越来也高。微前端的意义之一就在于将这些繁杂的代码和功能模块进行拆分和细化

  2. 整合历史系统(新旧系统关系)
    在不少的业务中,老系统和新系统是共存的,作为开发人员没办法浪费时间和经历将老系统进行改写。微前端可以将新旧系统进行整合。在一套系统中同时兼顾两个子系统

微前端的好处

  • 增量升级
  • 简单、解耦的代码库
  • 独立部署
  • 自治团队

目前国内的微前端的种类

  • 基座模式
    通过搭建基座、配置中心来管理子应用。如目前大部分的单页面应用基本都会选择qiankun框架,也存在基于业务的自制方案
  • 自组织模式
    通过约定进行互相调用
  • 去中心模式
    脱离基座模式,每个应用之间可以彼此分享资源。如基于 webpack 5 moudle Federation 实现的 EMP微前端方案,可以实现多个应用彼此共享资源的分享

如果你感兴趣,拥有英文基础,可以看看原文,这里给出原文地址,用来考古


摘自前端李墩墩

基础铺垫

应用分发路由 -> 路由分发应用

在一个单体前端、单体后端应用中,有一个典型的特征,即路由是由框架来分发的,框架将路由指定到对应的组件或者内部服务中。微服务在这个过程中做的事情是,将调用由函数调用变成了远程调用,诸如远程 HTTP 调用。而微前端呢,也是类似的,它是将应用内的组件调用变成了更细粒度的应用间组件调用,即原先我们只是将路由分发到应用的组件执行,现在则需要根据路由来找到对应的应用,再由应用分发到对应的组件上。

也就是说,微前端的思想实际上是启发与微服务的思想,微前端的应用分发路由是由应用自己来定义的,而不是由框架来定义的。分发的路由也从应用内组件调用变成了应用间组件调用。

后端微服务【函数调用 -> 远程调用】

在大多数CRUD应用中,都是做的一些类似的事情——首页,列表,详情
在spring中,你可能是这么写的

1
2
3
4
5
@RequestMapping(value="/detail/{detailId}")
public ModelAndView detail(HttpServletRequest request, ModelMap model){
....
return new ModelAndView("/WEB-INF/jsp/detail.jsp", "detail", detail);
}

那么在微服务的情况下,他会这么写

1
2
3
4
5
@RequestMapping("/name")
public String name(){
String name = restTemplate.getForObject("http://account/name", String.class);
return Name" + name;
}

此外,后端在微服务化的过程中,还多了个发现服务的服务,就比如Euraka

前端微前端【组件调用 -> 应用调用】

在形式上来说,单体应用的前端和单体后端应用是没什么大的区别的,都是根据不同的路由返回不同的模板。

1
2
3
4
const appRoutes: Routes = [
{ path: 'index', component: IndexComponent },
{ path: 'detail/:id', component: DetailComponent },
];

但是在微前端化后,我们将会拆解成两个应用的路由,即应用A的路由和应用B的路由
应用A的路由

1
2
3
const appRoutes: Routes = [
{ path: 'index', component: IndexComponent },
];

应用B的路由

1
2
3
const appRoutes: Routes = [
{ path: 'detail/:id', component: DetailComponent },
];

问题的关键就在于:怎么将路由分发到这些不同的应用中去。与此同时,还要负责管理不同的前端应用。

微前端的六种方式

路由分发式微前端

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

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

适用场景

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

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

使用 iFrame 创建容器

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

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

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

  • 网站不需要 SEO 支持
  • 拥有相应的应用管理机制。
    如果我们做的是一个应用平台,会在我们的系统中集成第三方系统,或者多个不同部门团队下的系统,显然这是一个不错的方案。一些典型的场景,如传统的 Desktop 应用迁移到 Web 应用

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

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

加载机制:在什么情况下,我们会去加载、卸载这些应用;在这个过程中,采用怎样的动画过渡,让用户看起来更加自然。
通讯机制:直接在每个应用中创建 postMessage 事件并监听,并不是一个友好的事情。其本身对于应用的侵入性太强,因此通过 iframeEl.contentWindow 去获取 iFrame 元素的 Window 对象是一个更简化的做法。随后,就需要定义一套通讯规范:事件名采用什么格式、什么时候开始监听事件等等。
有兴趣的读者,可以看看笔者之前写的微前端框架:Mooa
不管怎样,iframe 对于我们今年的 KPI 怕是带不来一丝的好处,那么我们就去造个轮子吧。

自制框架兼容应用

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

那么,我们只需要:

  1. 在页面合适的地方引入或者创建 DOM
  2. 用户操作时,加载对应的应用(触发应用的启动),并能卸载应用。
    第一个问题,创建 DOM 是一个容易解决的问题。而第二个问题,则一点儿不容易,特别是移除 DOM 和相应应用的监听。当我们拥有一个不同的技术栈时,我们就需要有针对性设计出一套这样的逻辑。

尽管 Single-SPA 已经拥有了大部分框架(如 React、Angular、Vue 等框架)的启动和卸载处理,但是它仍然不是适合于生产用途。当我基于 Single-SPA 为 Angular 框架设计一个微前端架构的应用时,我最后选择重写一个自己的框架,即 Mooa。

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

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

需要设计一套管理应用的机制。
对于流量大的 toC 应用来说,会在首次加载的时候,会多出大量的请求
而我们即又要拆分应用,又想 blabla……,我们还能怎么做?

组合式集成:将应用微件化

组合式集成,即通过软件工程的方式在构建前、构建时、构建后等步骤中,对应用进行一步的拆分,并重新组合。

从这种定义上来看,它可能算不上并不是一种微前端——它可以满足了微前端的三个要素,即:独立运行、独立开发、独立部署。但是,配合上前端框架的组件 Lazyload 功能——即在需要的时候,才加载对应的业务组件或应用,它看上去就是一个微前端应用。

与此同时,由于所有的依赖、Pollyfill 已经尽可能地在首次加载了,CSS 样式也不需要重复加载。

常见的方式有:

独立构建组件和应用,生成 chunk 文件,构建后再归类生成的 chunk 文件。(这种方式更类似于微服务,但是成本更高)
开发时独立开发组件或应用,集成时合并组件和应用,最后生成单体的应用。
在运行时,加载应用的 Runtime,随后加载对应的应用代码和模板。
应用间的关系如下图所示(其忽略图中的 “前端微服务化”):

这种方式看上去相当的理想,即能满足多个团队并行开发,又能构建出适合的交付物。

但是,首先它有一个严重的限制:必须使用同一个框架。对于多数团队来说,这并不是问题。采用微服务的团队里,也不会因为微服务这一个前端,来使用不同的语言和技术来开发。当然了,如果要使用别的框架,也不是问题,我们只需要结合上一步中的自制框架兼容应用就可以满足我们的需求。

其次,采用这种方式还有一个限制,那就是:规范!规范!规范!。在采用这种方案时,我们需要:

  • 统一依赖。统一这些依赖的版本,引入新的依赖时都需要一一加入。
  • 规范应用的组件及路由。避免不同的应用之间,因为这些组件名称发生冲突。
  • 构建复杂。在有些方案里,我们需要修改构建系统,有些方案里则需要复杂的* 架构脚本。
  • 共享通用代码。这显然是一个要经常面对的问题。
  • 制定代码规范。
    因此,这种方式看起来更像是一个软件工程问题。

现在,我们已经有了四种方案,每个方案都有自己的利弊。显然,结合起来会是一种更理想的做法。

考虑到现有及常用的技术的局限性问题,让我们再次将目光放得长远一些。

纯 Web Components 技术构建

在学习 Web Components 开发微前端架构的过程中,我尝试去写了我自己的 Web Components 框架:oan。在添加了一些基本的 Web 前端框架的功能之后,我发现这项技术特别适合于作为微前端的基石。

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

它主要由四项技术组件:

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

每个组件由 link 标签引入:

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

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

可以看到这边方式与我们上面使用 iframe 的方式很相似,组件拥有自己独立的 Scripts 和 Styles,以及对应的用于单独部署组件的域名。然而它并没有想象中的那么美好,要直接使用纯 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 形式的组件:

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

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

集成在现有框架中的 Web Components

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

如下是一个在 React 中引用 Stencil 生成的 Web Components 的例子:

1
2
3
4
5
6
7
8
9
10
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import registerServiceWorker from './registerServiceWorker';

import 'test-components/testcomponents';

ReactDOM.render(<App />, document.getElementById('root'));
registerServiceWorker();

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

同样的 Stencil 仍然也只是支持最近的一些浏览器,比如:Chrome、Safari、Firefox、Edge 和 IE11

复合型

对上面几种方式挑选几个组合在一起