微前端在企业级应用中的实践(上)丨【WEB前端大作战】
DevUI是一支兼具设计视角和工程视角的团队,服务于华为云DevCloud平台和华为内部数个中后台系统,服务于设计师和前端工程师
官方网站:devui.design
Ng组件库:ng-devui(欢迎Star)
引言
还记得19年redux的作者Dan Abramov那篇关于微前端的Twitter吗,当时引起了前端届广泛的争论,很多人说微前端是伪命题,但是进入到2020年之后,各种有关微前端的文章和框架层出不穷,又将这个话题推到了风口浪尖,事实证明微前端已经继模块化,组件化之后作为另一种前端架构模式逐渐被业内所接受,在大型的ToB的中后台企业级应用开发场景下,会扮演越来越重要的角色,所以,现在是时候聊一聊微前端了。本篇文章分为上下两个部分,在上部主要探讨微前端的起源,应用场景,DevUI探索微前端的演进以及对single-spa的详细研究,下部分会以DevUI微前端改造过程为例,来详细探讨如何自研一个企业级微前端解决方案,希望本篇文章可以作为微前端研究者入坑的重要参考。
起源
微前端的概念是随着后端微服务的兴起,导致业务团队被分割为不同的小的开发团队,每个团队中有前后端,测试等角色,后端服务间可以通过http或者rpc互相调用,也可以通过api gateway进行接口的集成聚合,随之而来的是希望前端团队也能够独立开发微应用,然后在前端某个阶段(build,runtime)将这些微应用聚合起来,形成一个完整的大型web应用。于是这个概念在2016年thoughtworks技术雷达中被提出来了。
对于微前端概念来讲,其本质还是web应用的复用与集成,尤其是当单页面应用出现后,每个团队不能按照以前那种服务端路由直出套模板的模式去开发页面了,整个路由都是被前端接管,所以最重要的两个问题就是web应用如何集成以及在哪个阶段集成,对应不同选择最终的实现方案也会有很大差异,这取决于你的业务场景,但是对于大多数团队,考虑微前端这种架构模式通常的诉求都是下面这样的:
- 独立开发,独立部署,增量更新:对应上图,团队A,B,C最好互相无感知,每个子应用完全按照自己的版本节奏去开发,部署,更新。
- 技术栈无关:团队A,B,C可以按照自己的需要选用任意框架开发,不需要强制保持一致
- 运行时隔离与共享:在运行时,应用A,B,C组成了一个完整应用,通过主应用入口访问,需要保证A, B,C对应的js以及css互相隔离不受影响,同时有通信机制保证A,B, C能够互相通信或数据共享。
- 单页面应用的良好体验:从一个子应用切换到另一个子应用的时候,路由变化不会reload整个页面,切换效果如同单页面应用的站内路由切换。
web集成方式
通常应用集成的阶段有两个,即构建时和运行时,不同阶段对应的实现方式也不同,大体来讲主要有下面几个:
- 构建时集成:通过git sub module或者npm package在构建阶段集成在主应用仓库中,团队A,B,C独立开发,优点是实施简单,依赖也可以共享,缺点是A,B,C无法独立更新,其中一个发生更新,都需要主应用进行构建部署来更新完整应用。如下:
这种方式更适合于小团队,因为当package越来越多的时候,会导致主应用频繁发布更新,此外还会让主应用的构建速度增长,代码维护成本越来越高,所以大多数选择微前端架构的,都是希望能够在运行时集成。
- 服务端模板集成:在主应用的首页中定义模板,通过类似于nginx的SSI这样的技术让服务端通过路由动态选择集成团队A,B,C哪个子应用,如下:
index.html
<html lang="en" dir="ltr">
<head>
<meta charset="utf-8">
<title>Feed me</title>
</head>
<body>
<h1>content here</h1>
<!--# include file="$CONTENT.html" -->
</body>
</html>
对应的nginx配置nginx.conf
server {
root html; #ssi配置开始
ssi on;
ssi_silent_errors on;
ssi_types text/shtml;
#ssi配置结束
index index.html index.htm; rewrite ^/$ http://localhost/appa redirect;
location /appa {
set $CONTENT 'appa';
}
location /appb {
set $CONTENT 'appb'; }
location /appc {
set $CONTENT 'appc' }
}
团队A, B,C最终产出的是位于服务器上的某个模板文件,类似于PHP,JSP服务端集成基本上都是这样的原理,在服务端通过路由选择不同模板,拼装好首页内容并返回。这种模式首先是违背前后端分离的大趋势,会造成耦合,同时为了又需要花一定量的工作去维护server端,不适用于大型单页面应用集成的场景。
- 运行时Iframe集成:可以说这种方式早期应该是最简单有效的,不同团队独立开发部署,只需要一个主应用通过iframe指向应用A, B,C对应的地址即可,如果需要主应用以及子应用之间通信,通过post message也能够很容易做到。,如下:
<html>
<head>
<title>index.html</title>
</head>
<body>
<iframe id="content"></iframe>
<script type="text/javascript">
const microFrontendsByRoute = {
'/appa': 'https://main.com/appa/index.html',
'/appb': 'https://main.com/appb/index.html',
'/appc': 'https://main.com/appc/index.html',
};
const iframe = document.getElementById('content');
iframe.src = microFrontendsByRoute[window.location.pathname];
window.addEventListener("message", receiveMessage, false);
function receiveMessage(event) {
var origin = event.origin
if (origin === "https://main.com") {
// do something
}
}
}
</script>
</body>
</html>
但是这种方式缺点也很明显,尤其是用户体验会很差,iframe带来的整个页面的reload以及在某些场景下(例如iframe中某些全局dialog或者modal展示,二级路由状态丢失,session共享,开发调试困难)等问题,还是决定了它无法作为一个微前端模式下首选的web集成方案。
- 运行时JS集成:这种集成方式一般情况下有两种模式,第一种就是将应用A, B,C打包成为不同的bundle,然后通过loader加载不同bundle,动态运行bundle的逻辑,渲染页面,如下:
这个时候应用A, B,C完全是互相无感知的,可以采用任何框架开发,路由切换导致的应用切换也不会造成页面reload,在运行时如果A,B,C想进行通信使用CustomEvent或者自定义EventBus都可以,A,B,C也可以通过不同框架本身的隔离机制或者通过一些沙箱机制实现应用隔离,这种方式看上去很不错。
运行时集成的第二种模式就是使用web components,应用A, B,C将自己的业务逻辑最终写成一个 web components并到打包成一个bundle,然后由主应用来加载,执行并渲染,如下:
<html>
<body>
<script src="https://main.com/appa/bundle.js"></script>
<script src="https://main.com/appb/bundle.js"></script>
<script src="https://main.com/appc/bundle.js"></script>
<div id="content"></div>
<script type="text/javascript">
const routeTypeTags = {
'/appa': 'app-a',
'/appb': 'appb',
'/appc': 'app-c',
};
const componentTag = routeTypeTags[window.location.pathname];
const content = document.getElementById('content');
const component = document.createElement(componentTag);
content.appendChild(component);
</script>
</body>
</html>
这种方式一般会有浏览器兼容性问题(需要引入polyfill),对三大框架的使用者来讲,并不是很友好(组件编写方式,改造成本,webcomponents版本组件库,开发效率,生态等),对于复杂应用来讲,整站选择这种开发模式会遇到很多坑,但是并不是不值得尝试,如果页面某些区域(一小块)需要独立开发部署的话,也可以采用这种集成方式(DevUI目前页面某些区域渲染就采用了web components)。目前有很多框架已经考虑到了上述的一些限制,最大限度的进行了优化,达到开箱即用的效果,这里推荐 stencil ,可以帮助你快速开发。
综上所述,从web应用集成方式来看,当前更适合微前端架构采用的应该是运行时通过JavaScript构造一个主从应用模型结构,然后通过不同路由来集成不同子应用,对应着上述不同方式,DevUI其实也经历了如下的几个阶段。
DevUI前端集成模式演进
如上图所示,devui前端显著地特点是:
1)有很多个服务,每个服务有自己的前端代码仓库,需要独立开发,测试,部署;
2)每个服务的前端都是由header和content内容区域组成的,都是一个基于Angular的单页面应用,服务间只有content内容区域不同,header都是一样的;
简单来讲,就是各业务团队独立的开发,通过路由分发到对应不同的服务,每个服务的前端都是一个完整的单页面应用。针对这样一种业务场景,各服务间集成与复用模式大致经历了以下几个阶段。
阶段一:公共组件化 + 服务间超链接
在这一阶段,我们将每一个服务都使用的header等区域独立出来做成了组件,以此来解决复用问题,服务间跳转仍然是使用最普通的超链接,如下所示:
这个阶段最大的遗留问题是:服务间跳转白屏明显,服务间session管理割裂,服务间跳转需要重新验证,用户体验极差。
阶段二:App Shell(Pre render) + Session共享
关于单页面应用渲染白屏的问题业内是有标准解决办法的,通常使用的是SSR(服务端渲染)以及预渲染(Prerender),两者的区别就是SSR会在服务端(通常是Node)执行一些逻辑,将当前路由对应的HTML 首先生成,然后再返回给浏览器,而Prerender通常是在build阶段根据一些规则已经生成了对应的HTML内容,用户访问的时候直接返回给浏览器,如下:
单纯从用户体验和效果上讲,SSR无疑是最优的,但是如果全站都SSR,成本是很大的(每个服务都需要增加一层Node渲染层,而且SSR对于代码质量要求很高,Angular本身的SSR也不够成熟),所以互相权衡之下,我们选择了Prerender,通过在build阶段生成一个App Shell来解决白屏问题,如下:
在这个阶段,我们将header等各个服务都有的一些逻辑分为了两部分,一部分是当页面刷新时可以直观看到的header左侧部分,这一部分连同一些全局状态以及一个内置的event bus(用于通信)全部做成了一个npm包,在构建的时候统一注入在业务的人index.html中,header右侧部分依然是一个Angular组件(下拉菜单等需要用户操作才能看到的区域,即使延迟渲染也不会影响体验),需要业务引入在自己的组件树中,在运行阶段,用户访问index.html,整个应用的shell部分先渲染出来,然后接着加载angular对应的静态资源,接着渲染右侧header下拉菜单以及业务内容,header通过event bus与业务通信,当header右侧下拉菜单等内容渲染成功之后,我们将这些内容append到整个header区域中。
同时,我们又通过通过子域名session共享的方式也解决了服务间跳转之间重新登录校验的问题,通过这种渐进式渲染 + 预渲染模型,提升了用户体验,虽然是多页面应用,但是服务间跳转经过优化给人的感觉还是站内跳转,同时又能够保证不同团队独立开发,部署。这个阶段最大的遗留问题是,header等公共组件仍然作为npm包的形式下发给到不同服务,一旦header上的公共逻辑更新,会导致每个业务都要被动发布版本,造成人力浪费,所以大家都希望能够把公共组件解耦出来。
阶段三:widget(微应用)
至此,类似于header这样的公共组件已经是一个代表devui大部分公共逻辑的很复杂的组件了,它除了自身的一些view需要渲染之外,还需要执行大部分公共逻辑,缓存接口请求数据等供业务消费,所以在这个阶段,我们希望header这样的组件能够由公共团队独立开发,部署,在运行时与每个业务集成,形成一个完整的应用,如下:
我们希望的是业务开发自己的逻辑,header开发公共逻辑,互不干扰,独立发布更新,然后在运行时,业务通过一个类似于header-loader的东西将header引用进来(注意这里是运行时引用),通过这样一种方式,就能够免去header公共逻辑更新带给业务的被动升级工作,业务对header无感知。所以这里核心的问题就是header如何被集成,按照上一章节,这里是有两种方式的,即使用iframe集成及使用javascript动态渲染。iframe显然不太适合这样的场景,无论是从实现效果还是与业务通信复杂度来讲,所以这里我们通过类似于web components这种方式来实现(实际过程中你可以选用任何框架,只要它能满足加载bundle,执行逻辑并渲染这样的模式即可)
在这一阶段,我们解决了公共逻辑更新导致业务被动更新的问题,大大减少了业务的工作并大大提升了公共逻辑更新回退的响应速度,业务与公共逻辑独立开发部署。基本上满足了在一个大的应用内部,各个子业务与公共团队友好共存。但是挑战永远是存在的,业务又提出了更高的目标。
阶段四:跨应用的编排与集成
设想这样一个场景,对于一个大型企业来讲,内部有很多个中后台应用,可以把它想象为一个应用池(应用市场),对于某些业务,我希望从应用市场中拿出来 C,D ,E把它们都整合起来,形成一个大型业务A,供用户使用,同时我又希望从应用市场拿出来D, E, F,把他们整合成一个大型业务B,提供统一入口供用户使用,其中,A, B,C,D,E,F这些应用都是由不同团队开发维护的。在这种情况下,就需要有一种机制去定义一个标准的子应用需要去遵循什么样的规则,主应用如何去集成(加载,渲染,执行逻辑,隔离,通信,响应路由,依赖共享,框架无关等等)
这样一种机制,就是微前端本身需要去探讨的内容,在这个阶段,其实如何实现取决于你的业务复杂度,它可以很简单,也可以很复杂,甚至可以做一个服务化的产品并提供一整套解决方案来帮大家实现这样的目标(参考微前端架构体系),目前整个DevUI也处于这样一个探索阶段,会在本文的下半部分讲述其中的一部分要点。目前按照这样的要求,我们首先需要研究这种主从应用模式下微前端如何实现。
关联阅读:
加入我们
我们是DevUI团队,欢迎来这里和我们一起打造优雅高效的人机设计/研发体系。招聘邮箱:muyang2@huawei.com。
文/DevUI myzhibie
往期文章推荐
《2021 年最值得推荐的 7 个 Angular 前端组件库 - DevUI》
- 点赞
- 收藏
- 关注作者
评论(0)