微前端架构的六种实践方式。
微前端为何流行——Web 应用的聚合
实现微前端架构的六种方法
如何解构Mono前端应用——前端应用的微服务拆分
将大型 Angular 应用程序拆分为微前端的四种方法
微前端是一种类似微服务的架构,将微服务的理念应用到浏览器端。将单体应用改造成由多个小型前端应用组合而成的单体应用。每个前端应用也可以独立运行、独立开发、独立部署。
同时,它们也可以并行开发并共享组件——这些组件可以通过NPM或Git Tag、Git Submodule等进行管理。
注:这里的前端应用指的是前后端分离的单页应用,在此基础上讨论微前端才有意义。
目录:
微前端为何流行——Web 应用的聚合
采用新技术,更多不是因为先进,而是因为它能解决痛点。
过去我一直对人们是否真的需要微服务、是否真的需要微前端抱有疑虑。毕竟,没有银弹。当人们考虑是否采用一种新的架构时,除了考虑它带来的好处之外,还要考虑其中蕴含的大量风险和技术挑战。
迁移前端遗留系统
过去几年,我接到过一些关于如何实现微前端架构的咨询。在此期间,我发现一个很有意思的事情:解决遗留系统是人们使用微前端方案最重要的原因。
在这些咨询中,开发人员遇到的情况和我之前遇到的情况类似。我的场景是:设计一个新的前端架构。他们因为遗留系统而开始考虑使用微服务进行前端开发。
过去,使用 Backbone.js、Angular.js、Vue.js 1.0 等框架编写的单页应用程序一直稳定地在线运行,并且没有新增任何功能。对于这样的应用程序,没有理由浪费时间和精力去重写旧应用程序。这里提到的使用旧的、不再使用的技术栈的应用程序可以称为遗留系统。然而,这些应用程序需要被组合成新的应用程序。我遇到的比较多的情况是,旧应用程序是用 Angular.js 编写的,而新应用程序则从 Angular 2+ 开始。对于业务稳定的 IT 团队来说,这是一个非常常见的技术栈。
在不重写原有系统的前提下,可以抽取人力去开发新的业务,这不仅对业务人员来说非常有吸引力,对于技术人员来说,在不重写旧业务的情况下,做一些技术上的挑战,也是相当有挑战性的。
后端解耦,前端聚合
前端微服务的一个卖点也在于此,可以兼容不同类型的前端框架。这让我想起了微服务的好处,以及很多项目选择微服务的原因:
早期,后端微服务的一大卖点是可以使用不同的技术栈开发后台应用。但实际上,采用微服务架构的组织机构一般都是中大型规模的。相比中小型机构,它们对框架和语言的选择更加严格,例如内部限制框架,语言限制等。因此,能够充分利用不同技术栈来发挥微服务优势的组织机构几乎凤毛麟角。在这些大型组织中,采用微服务的主要原因是希望利用微服务架构来解耦服务间的依赖关系。
而在前端微服务中,情况恰恰相反,人们更想要的结果是聚合,尤其是那些To B(to Bussiness)的应用。
在过去的两三年里,移动应用呈现出一种趋势,用户不想安装那么多应用程序。大型商业公司通常会提供一系列应用程序。这些应用程序在某种程度上也反映了公司的组织架构。然而,在用户眼中,一家公司应该只有一个产品。同样,这种趋势也在桌面Web上出现。聚合已经成为一种技术趋势,而前端的聚合就是微服务架构。
与旧系统兼容
那么这个时候我们就需要用新的技术、新的架构来包容、兼容这些老的应用,前端微服务正好契合了人们想要的卖点。
实现微前端架构的六种方法
结合我这半年在微前端的实践和研究,微前端架构一般可以按照以下几种方式实现:
- 使用 HTTP 服务器路由重定向多个应用程序
- 设计不同框架上的通信和加载机制,例如Mooa和Single-SPA
- 通过组合多个独立的应用程序和组件来构建单个应用程序
- iFrame。使用 iFrame 和自定义消息传递机制
- 使用纯 Web 组件构建应用程序
- 使用 Web 组件进行构建
- 构建时单独应用(TBC)
- 本地构建时单独应用(TBC)
基础:应用分发路由 -> 路由分发应用
在单体前端、单体后端的应用中,有一个典型的特征就是路由由框架分发,框架会将路由分配到相应的组件或内部服务。微服务在这个过程中所做的,就是将函数调用转化为远程调用,比如远程 HTTP 调用。微前端与之类似,它把应用内的组件调用**变成了更细粒度的应用间组件调用,也就是说,我们只是将路由分发到应用组件执行。现在需要根据路由找到对应的应用,然后由应用分发到对应的组件。
后端:函数调用 -> 远程调用
在大多数CRUD类型的Web应用中,都有一些非常相似的模式,即:首页->列表->详情:
- 主页,用于向用户显示特定数据或页面。这些数据通常数量有限,且具有多种模型。
- 列表,数据模型的聚合,通常是某一类数据的集合,可以看到尽可能多的**数据汇总**(比如Google只返回100页),通常看到的是Google、淘宝/Ebay、Amazon的搜索结果页面。
- 详细信息,针对单条数据展示尽可能多的内容。
下面是一个Spring框架返回首页的例子:
@RequestMapping(value="/")
Public ModelAndView homePage(){
Return new ModelAndView("/WEB-INF/jsp/index.jsp");
}
对于详细信息页面,它可能看起来像这样:
@RequestMapping(value="/detail/{detailId}")
Public ModelAndView detail(HttpServletRequest request, ModelMap model){
....
Return new ModelAndView("/WEB-INF/jsp/detail.jsp", "detail", detail);
}
因此,对于微服务来说,它看起来像这样:
@RequestMapping("/name")
Public String name(){
String name = restTemplate.getForObject("http://account/name", String.class);
Return Name" + name;
}
在这个过程中,后端有一个服务发现服务来管理不同微服务之间的关系。
前端:组件调用->应用程序调用
形式上来说,单前端框架和单后端应用的路由没太大区别:根据不同的路由返回不同页面的模板。一个 Angular 的例子:
const appRoutes: Routes = [
{ path: 'index', component: IndexComponent },
{ path: 'detail/:id', component: DetailComponent },
];
而当我们对其进行微服务化之后,它就有可能变成应用A的路由:
const appRoutes: Routes = [
{ path: 'index', component: IndexComponent },
];
加上应用途径B:
const appRoutes: Routes = [
{ path: 'detail/:id', component: DetailComponent },
];
问题的关键在于:如何将路由调度到这些不同的应用程序。同时,它还要负责管理不同的前端应用程序。
路由调度微前端
路由分布式微前端,通过路由将不同的服务**分发到不同的、独立的前端应用上。通常可以通过 HTTP 服务器的反向代理来实现,也可以通过应用框架自带的路由来实现。
目前来看,通过路由分发的微前端架构应该是最流行、最易用的“微前端”解决方案。但这种方式看起来更像是多个前端应用的聚合,也就是说,我们只是把这些不同的前端应用拼凑在一起,让它们看起来像一个完整的整体。但事实并非如此,用户每次从 A 应用到 B 应用时,往往都需要刷新页面。
几年前的一个项目中,我们正在进行遗留系统的重写。我们有一个迁移计划:
- 首先,使用静态网站生成动态生成主页
- 二、使用 React 栈重构详情页
- 最后,替换搜索结果页面
整个系统并不是一次性迁移,而是一步一步逐步完成的。所以当需要完成不同的步骤时,需要上线,这就需要使用 Nginx 进行路由分发。
以下是一个基于路由分发的Nginx配置示例:
http {
server {
listen 80;
server_name www.phodal.com;
location /api/ {
proxy_pass http://http://172.31.25.15:8000/api;
}
location /web/admin {
proxy_pass http://172.31.25.29/web/admin;
}
location /web/notifications {
proxy_pass http://172.31.25.27/web/notifications;
}
location / {
proxy_pass /;
}
}
}}
}
在这个例子中,不同页面的请求被分发到不同的服务器。
后来,我们在其他项目上也采用了类似的做法,主要原因是:跨团队协作。当团队达到一定规模时,我们不得不面对这个问题。此外,还有Angular断崖式升级的问题。所以,在这种情况下,用户前台使用Angular rewrite,后台继续使用Angular.js等等,以保持技术栈的一致性。在不同的场景下,也有一些类似的技术决策。
因此在这种情况下它适用于以下场景:
- 不同技术栈之间差异比较大,兼容、迁移、修改比较困难。
- 项目不想花费大量时间在这个系统的改造上
- 现有系统未来将被取代
- 系统功能完善,无新增需求
在满足上述场景的情况下,如果为了更好的用户体验,也可以通过使用iframe来解决。
使用 iFrame 创建容器
iFrame 是一项非常古老的技术,大家都觉得很普通,但它一直都有效。
HTML 内联框架元素
<iframe>
代表正在浏览的嵌套上下文,可有效地将另一个 HTML 页面嵌入到当前页面中。
iframe 可以创建一个全新的独立托管环境,这意味着我们的前端应用程序可以彼此独立运行。使用 iframe 有几个重要的先决条件:
- 网站不需要SEO支持
- 有相应的应用管理机制。
如果我们在做应用平台,会把第三方系统集成到自己的系统里,或者集成到多个不同部门团队下的系统里。显然这是一个很好的解决方案。一些典型的场景,比如传统的桌面应用迁移到Web应用:
如果这类应用过于复杂,那么就必须拆分成微服务。因此,在使用 iframe 时,我们需要做两件事:
- 设计管理应用机制
- 设计应用程序通信机制
加载机制。在什么情况下,我们会加载和卸载这些应用程序;在这个过程中,使用什么样的动画过渡,才能让用户看起来更自然。
通信机制。postMessage
直接在每个应用中创建事件并监听并非易事。这本身就对应用有侵入性,因此获取 iFrame 元素的 Window 对象iframeEl.contentWindow
是一种更简单的方法。然后,您需要定义一组通信规范:事件名称使用什么格式,何时开始监听事件等等。
有兴趣的读者可以看看我之前写的微前端框架:Mooa。
不管怎样,iframe 恐怕都无法给我们今年的 KPI 带来收益,所以,我们还是造个轮子吧。:)
自制微前端框架
无论是基于Web Components的Angular,还是VirtualDOM的React,现有的前端框架都离不开基本的HTML元素DOM。
好吧,我们只需要:
- 在页面的适当位置引入或创建 DOM
- 当用户操作时,加载相应的应用程序(触发应用程序的启动)和卸载应用程序。
第一个问题,创建 DOM 是一个很容易解决的问题。第二个问题一点都不容易,尤其是要移除对 DOM 的监听以及相应的应用程序。当我们有不同的技术栈时,我们需要设计一套这样的逻辑。
虽然Single-SPA已经具备了针对大多数框架(例如 React、Angular、Vue 等)的启动和卸载处理,但它仍然不太适合用于生产环境。当我基于 Single-SPA 设计一个针对 Angular 框架的微前端架构应用时,我最终选择重写自己的框架Mooa。
这种方式虽然上手难度较高,但方便后期的排序和维护。先不论每次加载应用带来的用户体验问题,唯一可能的风险或许就是:第三方库不兼容。
不过,不管怎样,相比于 iFrame,它在技术上更加精明,也更加有趣。同样,与 iframe 类似,我们仍然面临一系列小问题:
- 需要设计一个机制来管理应用程序。
- 对于流量比较大的toC应用,首次加载时会产生大量的请求。
而且我们又要拆分应用程序,还想blabla...,我们还能做什么呢?
组合集成:Widging 应用
组合集成,利用软件工程的方法,按照构建前、构建时、构建后等步骤,对应用程序进行逐步的拆分和重新组合。
从这个定义来看,它可能算不上一个微前端——它能够满足微前端的三个要素,即:独立运行、独立开发、独立部署。但是,借助前端框架组件的Lazyload功能——即在需要的业务组件或应用时才加载,它看起来就像一个微前端应用。
同时,CSS样式不需要重新加载,因为所有的依赖项和pollyfill已经尽可能在第一次加载了。
常见的方式有:
- 独立构建组件和应用程序,生成 chunk 文件,然后构建并分类生成的 chunk 文件。(这种方式更类似于微服务,但成本更高)
- 开发时独立开发组件或应用程序,集成时将组件与应用程序进行合并,最终生成单体应用程序。
- 在运行时,加载应用程序的Runtime,然后加载相应的应用程序代码和模板。
应用程序之间的关系如下图所示
这种方式看上去比较理想,即满足多个团队的并行开发,又能构建出合适的可交付成果。
但首先,它有一个严重的限制:必须使用相同的框架。对于大多数团队来说,这不是问题。使用微服务的团队不会因为微服务的前端而使用不同的语言和技术来开发。当然,如果要使用其他框架,也不是问题,我们只需要在上一步中组合**自制框架兼容的应用程序即可满足我们的需求。
其次,这种方法有一个限制,那就是:规范! 规范! 规范!。在采用这种方法时,我们需要:
- 统一依赖关系。保留这些依赖版本并添加新的版本。
- 规范应用程序的组件和路由。避免由于这些组件名称冲突而导致不同应用程序之间发生冲突。
- 构建复杂。在某些情况下,我们需要修改构建系统,在某些情况下,我们需要复杂的模式脚本。
- 共享通用代码。这显然是我们必须经常面对的问题。
- 制定代码规范。
因此,这种方法看起来更像是一个软件工程问题。
现在我们有四个选项,每个选项都有各自的优缺点。显然,将它们结合起来是更理想的方法。
考虑到现有常用技术的局限性,让我们从长远角度重新审视。
纯Web Components技术构建
在学习 Web Components 开发微前端架构的过程中,我尝试编写了自己的 Web Components 框架:oan。在添加了一些基本的 Web 前端框架功能后,我发现这项技术特别适合作为微前端的基石。
Web 组件是一组不同的技术,允许您创建可重复使用的自定义元素(它们的功能打包在您的代码之外)并在您的 Web 应用程序中使用它们。
它主要由四个技术部分组成:
- 自定义元素,允许开发人员创建自定义元素,例如。
- Shadow DOM,即影子 DOM,通常将 Shadow DOM 附加到主文档 DOM 上,并控制其相关功能。此 Shadow DOM 不能被其他主文档 DOM 直接控制。
- HTML 模板、
<template>
和<slot>
元素用于编写未在页面上显示的标记模板。 - HTML 导入用于引入自定义组件。
每个组件都通过标签来介绍link
:
<link rel="import" href="components/di-li.html">
<link rel="import" href="components/d-header.html">
然后,在相应的 HTML 文件中,创建相应的组件元素并编写相应的组件逻辑。一个典型的 Web Components 应用程序架构如下所示:
您可以看到,这与上面我们使用 iframe 的方式类似。每个组件都有自己独立的scripts
和,以及各个部署组件对应的域名。然而,它并没有想象中那么好。直接使用Web Componentsstyles
构建前端应用程序比较困难:
- 重写现有的前端应用程序。是的,现在我们需要完全使用 Web Components 来完成整个系统的功能。
- 上下游生态还不够完善,对一些第三方控件的支持不够,这也是 jQuery 比较火爆的原因。
- 系统架构复杂,当一个应用被拆分成一个又一个的组件时,组件之间的通信就成为一个特别大的问题。
Web Components 中的 ShadowDOM 更像是新一代的前端 DOM 容器。遗憾的是,并非所有浏览器都能完全支持 Web Components。
使用 Web 组件进行构建
Web 组件离我们还太远,但结合 Web 组件来构建前端应用是未来发展的一种架构。或者说,未来我们可以开始用这种方式构建自己的应用。幸运的是,现在已经有一些框架可以实现这种可能性。
目前,使用 Web Components 构建微前端应用程序有两种方法:
- 使用 Web Components 构建独立于框架的组件,然后在相应的框架中引入它们
- 引入 Web Components 中现有的框架,类似 iframe 的形式
前者是一种基于组件的方法,或者就像将未来的“遗留系统”迁移到未来的架构中。
在 Web 组件中集成现有框架
现有的 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 组件
或者说,类似于Stencil的形式,将组件直接以 Web Components 的形式构建成组件,然后在相应的如 React 或者 Angular 中直接引用。
下面是一个 React 中引用 Stencil 生成的 Web Component 示例:
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 等较新的浏览器。
复合型微前端
复合型,就在上述类别中,选择几种组合在一起即可。
我没胡说:)
微前端快速选择指南
我还是直接给出结论:
相关要点解释如下:
框架限制。在后端微服务体系中,人们使用其他语言的库来开发新服务,例如用于人工智能的 Python。但在前端,几乎没有这种可能性。因此,当我们只有一个前端框架时,我们在使用微前端技术时拥有更广泛的选择。不幸的是,大多数组织需要兼容遗留系统。
IE 问题。无论是几年前,还是今年,我们在微前端落地的首要考虑就是对 IE 的支持。我接触过的项目,基本上都需要支持 IE,所以在技术选型上有一定的限制。而我们一些不需要支持 IE 的项目,可以使用 WebComponents 技术来构建微前端应用。
独立性。即各个微前端应用的依赖关系要么统一管理,要么在各个应用中自行管理。统一管理可以解决依赖重复加载的问题,独立管理则会带来额外的流量开销和延迟。
微前端解决方案比较:简要比较
如果你对以上几个方面还不太熟悉,请阅读《实现前端微服务的六七种方法》。
方法 | 开发成本 | 维护成本 | 可行性 | 相同的框架要求 | 实施困难 |
---|---|---|---|---|---|
路线分配 | 低的 | 低的 | 高的 | 不 | ★ |
iFrame | 低的 | 低的 | 高的 | 不 | ★ |
应用程序微服务 | 高的 | 低的 | 中等的 | 不 | ★★★★ |
微型小部件 | 高的 | 中等的 | 低的 | 是的 | ★★★★★ |
微应用 | 中等的 | 中等的 | 高的 | 是的 | ★★★ |
纯 Web 组件 | 高的 | 低的 | 高的 | 不 | ★★ |
Web 组件 | 高的 | 低的 | 高的 | 不 | ★★ |
同样,对一些复杂的概念解释如下:
应用微服务,即每个前端应用都是一个单独的服务化前端应用,并搭载统一的应用管理和启动机制,例如微前端框架Single-SPA或者mooa。
Micro-Widget,即通过对构建系统的 hack,使得不同的前端应用可以使用同一套依赖包。它从根本上改善了Microservices Apps中依赖文件重复加载的问题。
微应用,又称组合集成,即通过软件工程,在开发环境中拆分单体应用,在构建环境中将单体应用组合成一个应用。详细内容,可期待后续文章《单体前端应用的拆解与微服务化》。
微前端方案对比:复杂方式
看到一篇微服务相关的文章,介绍了不同微服务之间的区别,其中详细使用了一种比较有意思的对比方法,这里也用同样的方式展示:
架构目标 | 描述 |
---|---|
a. 独立开发 | 独立发展,不受影响 |
b. 独立部署 | 可以作为单一服务部署 |
c. 支持不同的框架 | 可以同时使用不同的框架,例如 Angular、Vue、React |
d.摇树优化 | 可以消除未使用的代码 |
e. 环境隔离 | 应用程序之间的上下文不受干扰 |
f. 多个应用程序同时运行 | 不同的应用程序可以同时运行 |
g. 共享依赖项 | 不同应用是否共享底层依赖库 |
h.依赖冲突 | 不同版本的依赖项是否会引起冲突 |
i. 集成编译 | 应用程序最终被编译成一个整体,而不是单独构建 |
然后,对于下面的表格,表中的a~j代表上面几种不同的架构考虑。
(PS:考虑到Web Components几个字的篇幅,暂时简称为WC~~)
方式 | 一个 | b | c | d | e | f | 克 | h | 我 |
---|---|---|---|---|---|---|---|---|---|
路线分配 | 哦 | 哦 | 哦 | 哦 | 哦 | 哦 | |||
iFrame | 哦 | 哦 | 哦 | 哦 | 哦 | 哦 | |||
应用程序微服务 | 哦 | 哦 | 哦 | 哦 | |||||
小部件 | 哦 | 哦 | - | - | 哦 | - | |||
微应用 | 哦 | 哦 | 哦 | - | - | 哦 | - | ||
纯净厕所 | 哦 | 哦 | 哦 | 哦 | 哦 | - | - | ||
联合厕所 | 哦 | 哦 | 哦 | 哦 | 哦 | 哦 |
图中的O表示支持,空白表示不支持,-表示没有影响。
结合之前的选型指南:
如何解构Mono前端应用——前端应用的微服务拆分
刷新页面?路由拆分?不行,动态加载组件。
本文分为以下四个部分:
- 前端微服务简介
- 微前端设计理念
- 实用微前端架构设计
- 基于Mooa的前端微服务
前端微服务
对于前端微服务,有一些选项:
- Web Component 显然拥有非常好的基础架构。然而,我们不太可能大量重写现有的应用程序。
- iFrame。你是认真的吗?
- 另一个微前端框架 Single-SPA 显然是一个更好的选择。然而,它还未达到生产环境的水平。
- 通过路由来拆分应用,这种跳转会影响用户体验。
- 等等。
因此,当我们考虑前端微服务时,我们希望:
- 独立部署
- 独立开发
- 独立技术
- 不影响用户体验
独立开发
过去几周,我花了很多时间学习 Single-SPA 的代码。然而,我发现它的开发和部署实在太繁琐,根本达不到独立部署的标准。根据 Single-SPA 的设计,我需要在入口文件中为我的应用命名,然后才能构建它:
declareChildApplication('inferno', () => import('src/inferno/inferno.app.js'), pathPrefix('/inferno'));
同时,在我的应用程序中,我仍然需要指定我的生命周期。这意味着当我开发一个新的应用程序时,我必须更新两部分代码:主项目和应用程序。这时我们也很有可能在同一个源中工作。
当有多个团队在同一个源中工作时,它显然变得相当不可靠——例如,另一个团队正在使用Tab,而我们正在使用2个空格,隔壁的Pharaoh使用了4个空格。
独立部署
单体前端应用最大的问题是构建的js、css文件比较大,微前端就是将文件独立拆分成多个文件,它们可以独立部署。
我们真的需要独立技术吗?
等等,我们真的需要独立开发技术吗?如果不需要技术,微前端的问题就好解决了。
其实,对大多数公司和团队来说,技术跟一篇无关紧要的演讲没什么关系。当一家公司几位创始人都在用 Java 的时候,很有可能以后的选型还会继续用 Java。除非有额外的业务需要用 Python 来实现人工智能。所以,大多数情况下,它仍然是唯一的技术栈。
对于前端项目来说尤其如此:一个部门基本上只选择一个框架。
所以我们选择了 Angular。
不影响用户体验
对于前端微服务来说,使用路由跳转是一种非常简单高效的拆分方式。然而,在路由跳转过程中,会有一个白屏的过程。在这个过程中,跳转前的应用和被跳转的应用都失去了对页面的控制。如果这个应用出现问题,那么用户就会不堪重负。
理想情况下,它应该是可控的。
微前端设计理念
设计理念1:集中式路由
互联网的本质是去中心化的吗?不,DNS决定了它不是。FLAG/TAB也决定了它不是。
本质上,微服务应该是去中心化的。然而,它不可能完全去中心化。对于微服务来说,它需要一个服务注册中心:
服务提供者要注册通知服务地址,服务的调用者要能够发现目标服务。
对于前端应用程序来说,这个东西就是路由。
从页面来看,只有我们在页面上添加了菜单链接,用户才能知道这个页面是可用的。
从代码角度来说,也就是我们需要有一个地方来管理我们的应用程序:找出有哪些应用程序存在,哪个应用程序使用了哪个路由。
管理我们的路线实际上就是管理我们的应用程序。
设计理念二:识别应用
在设计微前端框架的时候,每个项目的名称问题困扰了我很久——如何规范化这件事。直到,我再次想到了康威定律:
系统设计(产品结构相当于组织形式,每个设计系统的组织,其产生的设计相当于组织之间的沟通结构。
换句话说,同一个组织下不可能有两个同名的项目。
所以这个问题就简单的解决了。
设计理念3:生命周期
Single-SPA 设计了基本的生命周期(虽然没有统一管理),包含五种状态:
- 加载,决定加载哪个应用程序并绑定生命周期
- 引导程序,获取静态资源
- mount,安装应用程序,例如创建DOM节点
- 卸载,删除应用程序的生命周期
- unmount,卸载应用程序,例如删除DOM节点
所以,我在设计上基本遵循了这个生命周期。显然,像 load 这样的操作在我的设计中是多余的。
设计理念四:独立部署与配置自动化
从某种意义上来说,整个系统都是围绕着应用程序配置构建的,如果应用程序的配置能够自动化,那么整个系统就实现了自动化。
当我们只开发一个新组件时,我们只需要更新我们的组件,更新配置即可。而且这个配置本身也应该自动生成。
实用的微前端架构设计
基于上述前提,本系统的工作流程如下:
总体工程流程如下:
- 主项目运行时会去服务器获取最新的应用配置。
- 主项目获取到配置之后,会逐个创建应用,并将生命周期绑定到应用上。
- 当主项目检测到路由变化时,它会查找是否有与应用程序匹配的相应路由。
- 当匹配对与应用程序相对应时,则加载相应的应用程序。
因此其对应的结构如下图所示:
整体流程如下图所示:
独立部署和配置自动化
我们制定的部署策略是:我们的应用使用的配置文件叫做apps.json
,由主项目获取,每次部署时我们只需要指向apps.json
最新的配置文件即可,配置好的文件类如下:
- 96a7907e5488b6bb.json
- 6ff3bfaaa2cd39ea.json
- dcd074685c97ab9b.json
一个应用程序的配置如下:
{
"name": "help",
"selector": "help-root",
"baseScriptUrl": "/assets/help",
"styles": [
"styles.bundle.css"
],
"prefix": "help",
"scripts": [
"inline.bundle.js",
"polyfills.bundle.js",
"main.bundle.js"
]
}
这里selector
对应于应用程序所需的 DOM 节点,前缀用于 URL 路由。这些都是从index.html
文件 和 中自动生成的package.json
。
应用程序间路由 - 事件
因为当前应用已经变成了两个部分:主项目和子应用部分。这样就会出现一个问题:只有一个项目能够捕获路由变化。当主项目修改了应用的主路由时,无法有效地传达给子应用。此时只能通过事件的方式通知子应用,同时子应用也需要监听是否是当前应用的路由。
if (event.detail.app.name === appName) {
let urlPrefix = 'app'
if (urlPrefix) {
urlPrefix = `/${window.mooa.option.urlPrefix}/`
}
router.navigate([event.detail.url.replace(urlPrefix + appName, '')])
}
同样的,当我们需要从应用程序A跳转到应用程序B时,也需要这样的机制:
window.addEventListener('mooa.routing.navigate', function(event: CustomEvent) {
Const opts = event.detail
If (opts) {
navigateAppByName(opts)
}
});
其余动画(如加载)类似。
将大型 Angular 应用程序拆分为微前端的四种方法
2018 年,我们花了大量时间设计一个拆分大型 Angular 应用的方案,从 Angular 的 Lazyload 的使用,到前端微服务的实现,进行了一系列的讨论。最终,我们终于有了结果,采用了 Lazyload 的变种:构建时集成代码的方式。
作为一名“专业”的咨询师,我一直忙于为客户设计一套 Angular 拆分服务方案。主要为了实现以下设计目标:
- 构建插件式Web开发平台,满足业务快速变化及分布式多团队并行开发需求
- 构建服务化中间件,构建高可用、高复用的前端微服务平台
- 支持前端独立交付部署
简单来说就是支持应用程序插件化开发,以及多团队并行开发。
应用插件化开发,主要解决的问题是:臃肿的大型应用的拆分问题。大型前端应用在开发时面临大量**遗留代码,不同服务的代码耦合在一起。上传上线时,也面临加载缓慢,运行效率低下的问题。
最终落到路由懒加载及其变种、前端微服务两种方案上
1、前端微服务:路由懒加载及其变种
路由延迟加载,就是通过不同的路由将应用切分成不同的代码,当访问路由时才加载对应的组件。在 Angular、Vue 等框架中,可以通过路由 + Webpack 来实现。并且,不可避免的会遇到一些问题:
多个团队并行开发很困难。路由拆分意味着我们仍然在一个源码仓库中工作。您也可以尝试拆分成不同的项目,然后将它们一起编译。
每次发布都需要重新编译。没错,即使只是更新一个子模块的代码,我们也必须重新编译整个应用程序并重新发布。与其单独构建,不如直接发布。
统一 Vendor 版本,统一第三方依赖是好事。问题的关键在于,每当我们添加一个新的依赖,我们可能需要开会讨论。
然而,标准 Route Lazyload 最大的问题在于难以支持多个团队并行开发。之所以说“难”,是因为还是有办法解决这个问题的。在日常开发中,小团队总会在一个代码库中开发,而大团队则应该在不同的代码库中开发。
因此,我们对标准路由延迟加载进行了一些实验。
对于一个二三十人的团队来说,他们可能分属于业务上不同的部门,存在技术上不一致的规范,比如4空格、2空格或者Tab。尤其是当是不同的公司、不同的团队时,他们可能不得不放弃测试、代码静态检测、代码风格统一等等一系列的问题。
2、微服务解决方案:子应用模式
除了路由懒加载,我们还可以使用子应用模式,即各个应用相互独立。也就是说,我们有一个子项目,当用户点击相应路由时,我们会加载**独立**的 Angular 应用;如果是同一个应用下的路由,则无需重新加载。而且,这些都可以依靠浏览器缓存来实现。
除了路由懒加载,还可以使用类似 Mooa 的应用嵌入方案,以下是基于 Mooa 框架 + Angular 开发生成的 HTML 示例:
<app-root _nghost-c0="" ng-version="4.2.0">
...
<app-home _nghost-c2="">
<app-app1 _nghost-c0="" ng-version="5.2.8" style="display: none;"><nav _ngcontent-c0="" class="navbar"></app-app1>
<iframe frameborder="" width="100%" height="100%" src="http://localhost:4200/app/help/homeassets/iframe.html" id="help_206547"></iframe>
</app-home>
</app-root>
Mooa 提供两种模式,一种是基于 Single-SPA 实验,在同一个页面上加载和渲染两个 Angular 应用;一种是基于 iFrame 提供单独的应用容器。
解决了以下问题:
- 主页加载速度更快,因为只需要加载主页所需的功能,而不是所有依赖项。
- 多个团队并行开发,每个团队可以独立开发自己的项目。
- 模块化独立更新,现在我们只需要单独更新我们的应用程序,而不必更新整个完整的应用程序。
但它仍然存在以下问题:
- 依赖项的重复加载,即我们在 A 应用中使用的模块,也会在 B 应用中被复用。有些问题可以通过浏览器的缓存自动解决。
- 首次打开相应的应用程序需要时间,当然**预加载**可以解决部分问题。
- 在非iframe模式下运行,会遇到难以预料的第三方依赖冲突。
因此,在总结了一系列的讨论之后,我们形成了一系列的比较:
解决方案比较
在这个过程中,我们做了很多方案设计和比较,我想写一篇文章来比较一下之前的结果。先看图:
表格比较:
x | 标准延迟加载 | 构建时集成 | 构建后集成 | 独立于应用程序 |
---|---|---|---|---|
开发过程 | 多个团队在同一个代码库上进行开发 | 多个团队在不同的代码库上进行开发 | 多个团队在不同的代码库上进行开发 | 多个团队在不同的代码库上进行开发 |
构建并发布 | 构建只需要拿着这个代码就可以构建、部署 | 整合不同代码库的代码,然后构建应用程序 | 将直接编译到每个项目模块中,运行时将通过延迟加载进行合并 | 将被直接编译成几个不同的应用程序,运行时由主项目加载 |
适用场景 | 单一团队,依赖库少,单一业务 | 多个团队,较少依赖的库,单一业务 | 多个团队,较少依赖的库,单一业务 | 多团队、依赖多系统、业务复杂 |
性能模式 | 开发、建设、运营一体化 | 开发分离、建设一体化、运营一体化 | 开发分离、建设分离、运营一体化 | 开发、建设、运营分离 |
详细介绍如下:
标准延迟加载
开发流程:多个团队在同一个代码库中开发,只需要用这套代码进行部署即可。
行为:开发、建设和运营
适用场景:单个团队,依赖库较少,单一业务
LazyLoad 变体 1:构建时集成
开发流程:多个团队在不同的代码库进行开发,构建时将不同代码库的代码整合起来,构建应用程序。
适用场景:多团队,依赖库较少,单一业务
变体构建集成:开发分离,构建时集成,运行一个
LazyLoad 变体 2:构建后集成
开发流程:多个团队在不同的代码库进行开发,在构建时编译成不同的代码片段,通过延迟加载的方式合并在一起。
适用场景:多团队,依赖库较少,单一业务
变体-构建后集成:开发分离、构建分离、操作集成
前端微服务
开发流程:多个团队在不同的代码库进行开发,在构建时编译成不同的应用程序,在运行时由主项目加载。
适用场景:多团队、依赖Kudo、业务复杂度
前端微服务:开发、构建、运维分离
总对比度
总体对比如下表:
x | 标准延迟加载 | 构建时集成 | 构建后集成 | 独立于应用程序 |
---|---|---|---|---|
依赖管理 | 统一管理 | 统一管理 | 统一管理 | 应用程序的独立管理 |
部署方法 | 统一部署 | 统一部署 | 可以单独部署。更新依赖项时进行完整部署 | 完全独立部署 |
首屏加载 | 依赖于同一个文件,加载缓慢 | 依赖于同一个文件,加载缓慢 | 依赖于同一个文件,加载缓慢 | 靠自己管理,首页加载速度快 |
首次加载应用程序、模块 | 仅加载模块,速度快 | 仅加载模块,速度快 | 仅加载模块,速度快 | 单独加载,加载稍慢 |
预建成本 | 低的 | 设计建造过程 | 设计建造过程 | 设计通信机制及加载方法 |
维护成本 | 代码库管理不善 | 多个代码库并不统一 | 后期需要维护组件依赖关系 | 维护成本低 |
封装优化 | 摇树优化,AoT编译,删除无用代码 | 摇树优化,AoT编译,删除无用代码 | 无法确定应用程序依赖的组件,无法删除无用的代码 | 摇树优化,AoT编译,删除无用代码 |
GitHub上的原始帖子
文章来源:https://dev.to/phodal/micro-frontend-architecture-in-action-4n60