一文读懂微前端架构
转载本文请注明出处:微信公众号 EAWorld
前端开发在程序猿中无疑是一个比较苦逼的存在,作为一个前端开发,你必须要掌握 Javascript,HTML,CSS 这三大基础。Javascript 作为网络时代最为重要的开发语言,由于其设计上的限制,一直在演进,经历了 ES3,ES5,ES6(ECMAScript 2015)… … 而简单的 CSS 也无法完成你复杂的需求,你需要 Less/Sass/Sytlus 来增强你的 CSS 的功能。这些还远远只是一小部分,你还需要了解:
- 你需要有网络的基本知识,和常见的 API 接口例如 HTTP/REST/GraphQL
- 你需要知道浏览器的兼容性,什么 IE,Chrome,Safari,Firefox,等等
- 你需要知道应用如何打包,了解 Webpack ,还有 gulp, Babel, Parcel, Browserify, and Grunt 等是怎么工作的
- 你需要熟悉 HTML 的 DOM 操作,和相关的工具库例如 jquery 和 d3js
- 需要了解不同的框架和他们的优缺点 例如 React,AngularJS 和 VueJS,这三大框架都不兼容,而且各自都有自己庞大的生态
- 虽然 NodeJS 主要用于后端开发,但是很多前端的工具链和这个相关,例如构建工具 npm,yarn …
- 如果你的项目足够复杂,你需要引入 Monorepo,使用例如 lerna,nx 等工具来管理多个项目的包和依赖
- 你需要掌握基本的前端测试工具和框架,例如:Jasmin,Jest,Selenium,Mocha 等等
最可怕的是,这些东西都在飞快地发展和变化中,当你正忙于学习 ES8,ES9,ES10 的新特性的时候,今天我要和大家分享的希望不是压死前端开发小骆驼的最后一根稻草–微前端。
目录:
1.什么是微前端
2.为什么需要微前端,它有什么优势
3.如何实现微前端架构
4.运行时微前端的具体实现方式
5.微前端的问题和缺点
一、什么是微前端
而提到微前端就离不开微服务,大家对微服务都比较熟悉了,微服务允许后端体系结构通过松散耦合的代码库进行扩展,每个代码库负责自己的业务逻辑,并公开一个 API,每个 API 均可独立部署,并且各自由不同的团队拥有和维护。
前端架构经历了从单体,到前后端分离,再到微服务,最终发展到现在的微前端的过程如下图所示:
微前端的思路是把微服务的架构引入到前端,其核心都是要能够以业务为单元构建端到端的垂直架构,使得单个的团队能够独立自主的进行相关的开发,同时又具备相当的灵活性,按需求来组成交付应用。
“微前端”一词最早于 2016 年底在 ThoughtWorks 技术雷达中提出的。它将微服务的概念扩展到了前端世界。当前的趋势是构建一个功能强大且功能强大的浏览器应用程序(也称为单页应用程序),该应用程序位于微服务架构之上。随着时间的流逝,通常由一个单独的团队开发的前端层会不断增长,并且变得更加难以维护。
微前端背后的想法是将网站或 Web 应用程序视为由独立团队拥有的功能的组合。每个团队都有自己关心和专长的不同业务或任务领域。一个团队是跨职能的,并且从数据库到用户界面,端到端地开发其功能。
但是,这个想法并不新鲜。它与“单体系统”概念有很多共同点。在过去,类似的方法被称为“垂直系统的前端集成”。但是微前端显然是一个更友好,更轻巧的术语。
在微服务的架构中,后台的服务已经按照业务进行了分离,而前端仍然是一个单体构建,通过网关来调用不同的后台服务。这个微服务的思路是相违背的,这也就造成了你的后端团队是按照业务分割的,但是前端团队仍然是一个整体。微前端可以有效地改进这一点。
微前端的核心思路其实是远程应用程序,包含组件/模块/包的运行时加载。
如上图,对于用户而言,访问的是一个微前端的容器(container),容器加载运行在远程服务上的应用,把这些远程应用作为组件/模块/包在本地浏览器中加载。
- 组件是底层 UI 库的构建单元
- 模块是相应运行时的构建单元
- 包是依赖性解析器的构建单元
- 微前端是所提出的应用程序的构建块
二、为什么需要微前端?
它有什么优势?
在前面我们看到的微前端之前的架构,所有的前端还是一个单体,前端团队会依赖所有的服务或者后台的 API,前端开发会成为整个系统的瓶颈。使用微前端,就是要让前端业务从水平分层变为垂直应用的一部分,进入业务团队,剥离耦合。
那么微前端有什么好处,为什么要采用微前端架构呢?
- 各个团队独立开发,相互不影响,独立开发、独立部署,微应用仓库独立,前后端可独立开发,部署完成后主框架自动完成同步更新
- 增量升级,在面对各种复杂场景时,通常很难对一个已经存在的系统做全量的技术栈升级或重构,而微前端是一种非常好的实施渐进式重构的手段和策略。因为是运行时加载,可以在没有重建的情况下添加,删除或替换前端的各个部分。
- 不受技术影响,每个团队都应该能够选择和升级其技术栈,而无需与其他团队进行协调。也就是说 A 应用可以用 React,而 B 应用使用 Vue,大家可以通过同一个微前端来加载
- 独立运行时,每个微应用之间状态隔离,运行时状态不共享。隔离团队代码,即使所有团队都使用相同的框架,也不要共享运行时。构建自包含的独立应用程序。不要依赖共享状态或全局变量。
- 建立团队命名空间,对于 CSS,事件,本地存储和 Cookies,可以避免冲突并阐明所有权。
因此,微前端和微服务的本质都是关于去耦合。而只有当应用程序达到一定规模时,这才开始变得更有意义。
三、如何实现微前端架构
微前端不是一个库,是一种前端架构的设计思路,要实现微前端,本质上就是在运行时远程加载应用。
实现微前端,有几个思路,从构建的角度来看有两种,编译时构建微前端和运行时构建微前端:
- 编译时微前端,通常将第三方库中的组件作为包,在构建时引入依赖。这种实现引入新的微前端需要重新编译,不够灵活。编译时的微前端可以通过 Web Components,Monorepo 等方式来实现。其中 Monorepo 非常流行,常见的工具有 nx,rush,lerna 等。
- 运行时微前端,是一次加载或通过延迟加载按需动态将微型前端注入到容器应用程序中时。当引入新的微前端的时候,不需要构建,可以动态在代码中定义加载。我眼中的微前端更多是指这种运行时加载的微前端,因为独立构建,部署和测试是我们对于“微”的定义。
从前后端责任分层来看,可以从前端或者后端来实现。
通过客户端框架来实现
微前端通常由客户端工具来支持实现(听上去好有道理),有许多支持客户端开发微前端的实现工具,包括:Piral,Open Components,qiankun,Luigi,Frint.js 等。其中 qiankun 是蚂蚁金服开发的。
在客户端还可以通过辅助库的方式来实现,辅助库可以为共享依赖项,路由事件或不同的微前端及其生命周期来提供一些基础架构。
下面的一个示例是通过诸如导入映射或打包特定块等机制处理共享依赖关系。
相关的工具有 Webpack5 Module Federation,Siteless,Single SPA,Postal.js 等
通过服务器端实现
微前端并非只能在客户端来实现,类似于服务端渲染,同样可以通过服务端来实现。
服务端微前端的支持工具有:Mosaic,PuzzleJs,Podium,Micromono 等。
好了,说了这么多我相信你是一脸懵逼的,到底怎么实现的?我们抛开架构不说,来看看到底如何实现吧。
四、运行时微前端的具体实现方式
Iframe
iframes 是可以在 html 中嵌入另一个 HTML。下面就是用 iframe 实现微前端的一个例子:
<!DOCTYPE html><html><body><iframe src="https://localhost:3006" width="600" height="900"> <p>Your browser does not support iframes.</p></iframe><iframe src="https://localhost:3007" width="600" height="900"> <p>Your browser does not support iframes.</p></iframe></body></html>
如果不考虑体验问题,iframe 几乎是最完美的微前端解决方案了。iframe 提供了浏览器原生的隔离方案,不论是样式隔离、js 隔离这类问题统统都能被完美解决。但它的最大问题也在于他的隔离性无法被突破,导致应用间上下文无法被共享,随之带来的开发体验、产品体验的问题。这里的主要问题包括:
- url 不同步。浏览器刷新 iframe url 状态丢失、后退前进按钮无法使用。
- UI 不同步,DOM 结构不共享。
- 全局上下文完全隔离,内存变量不共享。
- 慢。每次子应用进入都需要次浏览器上下文的重建、资源重新加载。
所以虽然使用 iframe 可以实现远程加载的效果,但是因为这些限制,很少会有应用会使用。
Nginx 路由
利用 Ngix 路由,我们可以把不同的请求路由到不同的微前端的应用。
例如 Nginx 的路由能力,在前端可以动态请求不同的后端应用,而每一个后端应用独立运行,前端可以把这些不同的后端应用加载,编排在一起。下面的代码是一个 Nginx 的配置,customers/users/admins 分别表示了三个不同的应用,前端通过路由来加载位于不同服务的后端应用。
worker_processes 4;events { worker_connections 1024; }http { server { listen 80; root /usr/share/nginx/html; include /etc/nginx/mime.types; location /app1 { try_files $uri app1/index.html; } location /app2 { try_files $uri app2/index.html; } location /app3 { try_files $uri app3/index.html; } }}
无论你采用哪一种的微前端架构,Nginx 方向代理或者其它的 API 网关的解决方案都能够提供方便灵活的后端路由功能。但是通过这种方式,需要定义一个通用可扩展的路由规则,否则当引入新的应用的时候,还需要修改 Nginx 的路由配置,那就很不方便了。
Webpack 5 Module Federation
Webpack5 的 Module Federation 是一个令人兴奋的革新,它能够很方便的支持微前端的构建。模块联合允许 JavaScript 应用程序从另一个应用程序动态加载代码,并在此过程中能共享依赖关系。如果使用 Module Federation 的应用程序不具有联合代码所需的依赖关系,则 Webpack 将从该联合构建源中下载缺少的依赖关系。
在 Module Federation 的上下文中,启动代码是一种将运行时代码附加到远程容器启动序列的实施策略。这真的很有用,因为通过 Hook 无法访问 ModuleFederation 及其运行时,无法对其进行扩展或添加一行代码,这些代码可以像动态设置远程容器的公共路径那样进行操作。这在普通的 webpack 应用程序中是微不足道的,但是在一个无法访问的自定义运行时容器中却很难做到,该容器为模块联合远程编排提供了动力。简单来说,Module Federation 注入一段运行时的代码来负责加载和编排远程的应用代码,并能够管理和加载远程应用的依赖。
下面是一个对应的例子:
module.exports = { mode: 'development', devServer: { port: 8080, }, plugins: [ new ModuleFederationPlugin({ name: 'container', remotes: { microFrontEnd1: 'microFrontEnd1@https://localhost:8081/remoteEntry.js', microFrontEnd2: 'microFrontEnd2@https://localhost:8082/remoteEntry.js', }, }) ]};
上面的代码是微前端的容器端的配置,容器负责加载其它远程应用的代码。这个例子里,它加载了两个远程应用。
module.exports = { mode: 'development', devServer: { port: 8081, }, plugins: [ new ModuleFederationPlugin({ name: 'microFrontEnd1', filename: 'remoteEntry.js', exposes: { './MicroFrontEnd1Index': './src/index', }, }), ]};
每一个微前端的 Webpack 配置如上。
利用 ModuleFederationPlugin,remote 可以用来加载远端的应用,而 Expose 可以把自己的组件暴露为远端组件。
在 container 中,只需要调用以下的代码来加载远端组件。
import 'microFrontEnd1/MicroFrontEnd1Index';import 'microFrontEnd2/MicroFrontEnd2Index';
Module Federation 的加载过程如上图所示:
- localhost 加载 index.html
- main.js 是 Module Federation 的核心的编排代码,负责加载远程组件。
- remoteEntry.js 是 Module Federation 暴露的远程组件的代码
- src_ 是打包后的代码,其中 bootstrap_js 是容器侧的代码,index_js 是微前端侧的代码。
Module Federation 实现了类似动态链接库的能力,可以在运行时加载远程代码,远程代码本质上是一个加载在 window 上的全局变量,Module Federation 可以帮助解决依赖的问题。Javascrip 作为上古语言,没有提供依赖管理,导致留给各路大神各种发挥的空间。
Module Federation 的缺点就是依赖 Webpack 5,包直接挂载为全局变量。
EMP 微前端是基于 Module Federation 的微前端解决方案。
Single SPA
单页面应用是当今为 Web 应用的主流,区别于传统的多页面应用,整个 SPA 只有一个页面,其内容都是通过 Javascript 的功能来加载。
SPA 是一个 Web 应用程序,仅包含一个 HTML 页面。提供动态更新,它允许在不刷新页面的情况下与页面进行交互。利用单页应用程序,可以显着降低服务器负载并提高加载速度,从而获得更好的用户体验,因为 SPA 仅在先前加载整个页面时才按需导入数据。
除了开发复杂,对于 SEO 不友好,但页面应用的最大技术缺陷是 URL 不适合共享,因为 SPA 只有一个地址。
single-spa 是一个框架,用于将前端应用程序中的多个 JavaScript 微前端组合在一起。
使用 single-spa 构建前端可以带来很多好处,例如:
- 在同一页面上使用多个框架而无需刷新页面(React,AngularJS,Angular,Embe)
- 独立部署微前端
- 使用新框架编写代码,而无需重写现有应用程序
- 延迟加载代码可缩短初始加载时间
single-spa 应用程序包含以下内容:
- single-spa 根配置,用于呈现 HTML 页面和注册应用程序的 JavaScript。每个应用程序都注册了以下三项内容:name,加载应用程序代码的函数,确定应用程序何时处于活动状态/非活动状态的函数,
- 打包成模块的单页应用程序的应用程序。每个应用程序必须知道如何从 DOM 引导,安装和卸载自身。传统 SPA 和 Single-SPA 应用程序之间的主要区别在于,它们必须能够与其他应用程序共存,因为它们各自没有自己的 HTML 页面。例如,React 或 Angular SPA 应用程序。处于活动状态时,他们可以侦听 url 路由事件并将内容放在 DOM 上。处于不活动状态时,它们不侦听 url 路由事件,并且已从 DOM 中完全删除。
Single-SPA 注册的应用程序拥有普通 SPA 所具有的所有功能,只是它没有 HTML 页面。SPA 包含许多已注册的应用程序,每个应用程序都有其自己的框架。已注册的应用程序具有其自己的客户端路由和它们自己的框架/库。它们呈现自己的 HTML,并且在安装时有完全的自由去做他们想做的任何事情。挂载的概念是指已注册的应用程序是否正在将内容放在 DOM 上。决定是否挂载已注册应用程序的是其活动功能。每当未挂载已注册的应用程序时,它都应保持完全休眠状态直到挂载。
Single SPA 的样例代码如下:
1. 微前端代码:
import React from "react";import ReactDOM from "react-dom";import singleSpaReact from "single-spa-react";import Root from "./root.component";const lifecycles = singleSpaReact({ React, ReactDOM, rootComponent: Root, errorBoundary(err, info, props) { // Customize the root error boundary for your microfrontend here. return null; },});export const { bootstrap, mount, unmount } = lifecycles;
Single SPA 的微前端是纯的 JS 组件,不包含 HTML,需要通过容器来加载。
2.容器的 Root Config
在容器侧,需要通过 Import Map 或者 Webpack 来定义远程组件,并注册远程应用。
{ "imports": { "@naughty/root-config": "//localhost:9000/naughty-root-config.js", "@naughty/app": "//localhost:8500/naughty-app.js", "react": "https://cdn.jsdelivr.net/npm/react@16.13.1/umd/react.production.min.js", "react-dom": "https://cdn.jsdelivr.net/npm/react-dom@16.13.1/umd/react-dom.production.min.js" }}
容器侧的 HTML 文件使用 import map 来定义远程依赖,其中 root-config 是编排代码,负责远程应用的注册和加载。同时需要定义所有共享的依赖,这里例子中是 react 和 react-dom
import { registerApplication, start } from "single-spa";registerApplication({ name: "@single-spa/welcome", app: () => System.import( "https://unpkg.com/single-spa-welcome/dist/single-spa-welcome.js" ), activeWhen: ["/welcome"],});registerApplication( '@naughty/app', () => System.import('@naughty/app'), location => location.pathname.startsWith('/app'));start({ urlRerouteOnly: true,});
在 root-config 中,我们注册了两个远程应用,使用不同的 url 来加载。/welcome 会加载 welcome 应用,而/app 会加载我们的 app 应用。
Single SPA 的核心是利用不同的 URL 路由来加载远程组件,它可以和 Webpack(打包时构建依赖)或者 Import Map(运行时使用浏览器导入依赖)一起工作。注意,不要在你的微前端中混用两种依赖机制。
Single SPA 还提供一个 layout 引擎,可以帮助你快速的构建微前端。
相比 Module Federation,Single SPA 的代码和生命周期的管理更清楚,提供清晰的接口,缺点是共享的依赖需要手工通过 import map 来管理。
要做一个好的微前端因为受限于浏览器和 JS 的一些特性,并不容易。除了我们今天分享的内容,还面临着诸多的挑战:如何解决 css/js 的冲突,使得组件和应用完全隔离;如何解决不同应用间的通信;如何处理路由;如何保证 UI 风格的统一等等。
五、微前端的问题和缺点
讲了这么多的优点和实现,那么微前端是不是解决前端开发问题的银弹呢?当然不是。所有的架构都是取舍和权衡,这个世界上并不存在银弹,微前端架构和微服务一样也存在他的弊端,单体架构未必就差。
1. 微前端的构建通常比较复杂,从工具,打包,到部署,微前端都是更为复杂的存在,天下没有免费的午餐,对于小型项目,它的成本太高。
2. 每个团队可以使用不同的框架,这个听上去很美,但是实际操作起来,除了要支持历史遗留的应用,它的意义不大。同时也为带来体验上的问题。可以远程加载不同的框架代码是一回事,把它们都用好是另一回事。
3. 性能上来看,如果优化得不好微前端的性能可能会存在问题,至少微前端框架是额外的一层加载。如果不同的微前端使用了不同的框架,那么每一个框架都需要额外的加载。
微前端架构还在发展之中,本文提到的 iframe/nginx/module federation/single-spa 只是诸多解决方案中的一小部分,前端的发展变化和生态系统实在是丰富,其他的方案诸如 umd/乾坤,Piral,open comonent 等等。当使用你也可以选择标准的 Web Component 或者 ES Modules 来构建微前端,但是这些标准的浏览器支持不是特别好,这个是前端开发永远的痛。(诅咒 IE)
大家对于微前端有什么想法或者问题,欢迎一起讨论。
关于作者:陶刚,Splunk 资深软件工程师,架构师,毕业于北京邮电大学,现在在温哥华负责 Splunk 机器学习云平台的开发,曾经就职于 SAP,EMC,Lucent 等企业,拥有丰富的企业应用软件开发经验,熟悉软件开发的各种技术,平台和开发过程,在商务智能,机器学习,数据可视化,数据采集,网络管理等领域都有涉及。
你也许感兴趣的:
- 【外评】电脑从哪里获取时间?
- 【外评】为什么 Stack Overflow 正在消失?
- Android 全力押注 Rust,Linux 却在原地踏步?谷歌:用 Rust 重写固件太简单了!
- 【外评】哪些开源项目被广泛使用,但仅由少数人维护?
- 【外评】好的重构与不好的重构
- C 语言老将从中作梗,Rust for Linux 项目内讧升级!核心维护者愤然离职:不受尊重、热情被消耗光
- 【外评】代码审查反模式
- 我受够了维护 AI 生成的代码
- 【外评】Linux 桌面市场份额升至 4.45
- 【外评】作为全栈开发人员如何跟上 AI/ML 的发展?
你对本文的反应是: