一文读懂微前端架构

EAWorld
关注

三、如何实现微前端架构

微前端不是一个库,是一种前端架构的设计思路,要实现微前端,本质上就是在运行时远程加载应用。

实现微前端,有几个思路,从构建的角度来看有两种,编译时构建微前端和运行时构建微前端:

编译时微前端,通常将第三方库中的组件作为包,在构建时引入依赖。这种实现引入新的微前端需要重新编译,不够灵活。编译时的微前端可以通过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="http://localhost:3006" width="600" height="900">  <p>Your browser does not support iframes.</p></iframe><iframe src="http://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@http://localhost:8081/remoteEntry.js',                microFrontEnd2: 'microFrontEnd2@http://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等企业,拥有丰富的企业应用软件开发经验,熟悉软件开发的各种技术,平台和开发过程,在商务智能,机器学习,数据可视化,数据采集,网络管理等领域都有涉及。

声明: 本文由入驻OFweek维科号的作者撰写,观点仅代表作者本人,不代表OFweek立场。如有侵权或其他问题,请联系举报。
侵权投诉

下载OFweek,一手掌握高科技全行业资讯

还不是OFweek会员,马上注册
打开app,查看更多精彩资讯 >
  • 长按识别二维码
  • 进入OFweek阅读全文
长按图片进行保存