最近两年,火了很多的技术,当然也火了很多的专业领域名词,而前端领域来说,大家关注的点可能由最开始的原来的一些性能优化、交互展示等比较基础原始的前端相关问题,到现在来说,从基础服务、开发、维护、性能监控、容器化、海量埋点、devops等等各种,前端涉及领域越来越广,对前端的要求也是越来越高。
而微前端的概念,也是从后端微服务的理念一步步从后端蔓延到了整个开发体系。而微服务到底是什么,他主要解决的是什么问题,主要的技术壁垒是什么,目前市面中的各大解决方案中遇到的难点和痛点又是什么,本片文章主要会围绕这些进行展开。

1. 微前端简介

Techniques, strategies and recipes for building a modern web app with multiple teams that can ship features independently.
微前端是一种多个团队通过独立发布功能的方式来共同构建现代化 web 应用的技术手段及方法策略

1.1 核心理念

微前端架构具备以下几个核心理念:

  1. 技术栈无关
    主框架不限制接入应用的技术栈,子应用具备完全自主权,每个团队能够自由选择和升级他们的技术栈,而不必与其他团队进行协调。子应用完全独立,可以隐藏内部实现细节
  2. 独立开发、独立部署、独立运行时
    子应用仓库独立,前后端可独立开发,部署完成后主框架自动完成同步更新,即使所有的团队使用相同的框架,应用之间也会完全独立,不会共享一个运行环境,独立构建独立部署,不会依赖于共享状态或全局变量。
  3. 独立的app前缀
    在每个app不能实现完全隔离的情况下,需要统一的命名规范,namespace,事件,本地存储和cookie等应该使用统一规范,以避免冲突和澄清所有权。
  4. 增量升级
    每一个app都是整个应用的独立单元,app见完全解耦,更新和升级也完全独立,所以通过微前端也是一种非常好的渐进式重构的手段和策略

1.2 产生的原因

在传统模式开发中,维护一个大型的中后台并且快速迭代是一件很困难的事情,因为它们普遍都有下面几个问题。

  1. 技术栈过于陈旧,应用不可维护的问题,想象一下你公司最老的项目突然让你新增feature,用的是jQuery也还好,但用的是Angular1甚至Java Web,透着网线都能感觉到你的痛。
  2. 体积过于庞大,从一个普通应用演变成一个巨石应用( Frontend Monolith ),10W+行代码的祖传项目编译后即使抽离了dll,主包也起码要5M以上,编译慢且开发体验极差。
  3. 技术栈单一,无法满足业务需求。每个框架都有其优点,择其长处利用之岂不美哉?
  4. 重构代价大,无法步进式重构,即每次只重构一个模块,并且不影响现有版本的稳定性。只能一次性发布所有模块,风险大。

1.3 适用场景

  • 遗留系统迁移,解决遗留系统,才是人们采用微前端方案最重要的原因
  • 后台比较分散,体验差别大,因为要频繁跳转导致操作效率低,希望能统一收口的一个系统内
  • 单页面应用非常庞大,多人协作成本高,开发/构建时间长,依赖升级回归成本高
  • 系统有三方接入的需求

1.4 缺点

微前端不是银弹,它和微服务一样会带来大量的挑战,也会有一系列的缺点:

  1. 应用的拆分基础依赖于基础设施的构建,一旦大量应用依赖于同一基础设施,那么维护变成了一个挑战。
  2. 拆分的粒度越小,便意味着架构变得复杂、维护成本变高。
  3. 技术栈一旦多样化,便意味着技术栈混乱
  4. 对于小规模的团队来说,它带来的弊端可能会远远大于好处

2. 微前端架构实现方式

目前而言要设计出一个微前端应用不是一件容易的事——还没有最佳实践,虽然市面上出现了各种各样的框架,single-spa mooa,以及基于single-spa发开的世界上最好的前端框架qiankun等等,通过这些遍地开花框架就得以对微前端可见一斑。

2.1 名词解释

微前端中分为主应用和子app:

  • 主应用是用来控制子系统的调度中心,职责包括:
    1. 维护子系统的注册表。
    2. 管理各个子系统的生命周期。
    3. 传递路由信息。
    4. 加载子项目的入口资源。
  • 子app负责业务逻辑的实现

2.2 设计模式

微前端应用间的关系来看,分为两种:基座模式、自组织式。分别也对应了两者不同的架构模式:

  • 基座模式:通过一个主应用,来管理其它app应用,这种模式设计难度小,方便实践,但是通用度低。
  • 自组织模式:应用之间是平等的,不存在相互管理的模式。这种设计设计难度大,不方便实施,但是通用度高。

目前市面上的各种解决方案,都更偏向于基座模式,通过主应用来管理所有app应用,自组织模式在代码设计层面上会存在很多技术短板,这也是更多的基座模式的微前端框架诞生的原因。
而且基座模式实施起来比较方便,方案上便也是蛮多的。下图就是一个基座模式的微前端的概念图
image.png

2.3 设计思路

上述的模式中,不管使用何种方式,都需要一套子应用的注册、查找、激活、加载机制,和微服务架构相似,也都需要有一个应用注册表的服务,它可以是一个固定值的配置文件,又或者是一个可动态更新的配置,通过这个基础注册表实现整个微服务加载的基础前提条件,而这种思路,主要实现思想是:

    1. 定义一个注册表,可以动态,可以静态,定义好固定的schema,比方如下的结构
  1. [{
  2. "name": "user",
  3. "title": "user模块",
  4. "host": "http://localhost:8000/"
  5. }, {
  6. "name": "admin",
  7. "title": "admin模块",
  8. "host": "http://localhost:8001/"
  9. }]
  • 2.主应用app通过注册表发现其他子应用
  • 3.当浏览器访问主工程时,主工程会根据location与注册表中的app的激活规则进行匹配
  • 4.查找到需要激活的app,根据注册表中数据,加载该app的应用资源文件
  • 5.将该app挂载到dom中
  • 6.设计一套通讯访问机制,实现主应用与子app间、子app之间的通讯,主要包括访问权限

上述实现思路流程图展示如下:
image.png

2.4 生命周期

前端微架构与后端微架构的最大不同之处,也在于此——生命周期。微前端应用作为一个客户端应用,每个应用都拥有自己的生命周期,就如同rect、vue的每个组件一样,都拥有自己的生命周期,主要阶段为(参照single-spa):

  • Load,决定加载哪个应用,并为激活应用绑定生命周期(这里最好的是不侵入各个框架所搭建的应用的原始生命周期,最好使用原始框架的生命周期)
  • bootstrap,获取静态资源,即加载app构建后的静态资源
  • Mount,装载应用,即渲染该应用
  • Unload,删除应用的生命周期(这里主要是针对在各个应用框架以外自定义的生命周期函数)
  • Unmount,卸载应用,如删除 DOM 节点、取消事件绑定、路由监听等

image.png

上面短短几点虽然看似简单,确实一个微前端框架实现好快的决定性因素,也是微前端实现的难点所在,不同框架构建的app在渲染处理上也不尽相同

3. 实现方式

微前端架构一般可以由以下几种方式进行

  1. 使用 HTTP 服务器的路由来重定向多个应用
  2. 借助iFrame实现,使用 iFrame 以及自定义消息传递机制实现子应用之间数据传递
  3. 使用纯Web Components或者结合Web Components构建构建应用
  4. 微应用化,即以软件工程的方式,来完成前端应用的开发
  5. 前端微服务化,在不同的框架之上设计通讯、加载机制,例如single-spa

3.1 路由重定向实现

所谓路由的重定向,即记住类似nginx路由转发机制,或者webpack dev-server的rewrite机制。

  1. http {
  2. server {
  3. listen 80;
  4. server_name www.test.com;
  5. location /web/admin {
  6. proxy_pass http://172.31.25.29/web/admin;
  7. }
  8. location /web/user {
  9. proxy_pass http://172.31.25.27/web/user;
  10. }
  11. location / {
  12. proxy_pass /;
  13. }
  14. }
  15. }

路由重定向方式,将前端路由转发到不同的服务器中,这种方式应该是采用最多、最易采用的 “微前端” 方案。但是这种方式看上去更像是多个前端应用的聚合,即我们只是将这些不同的前端应用拼凑到一起,使他们看起来像是一个完整的整体。但是每次用户从 A 应用到 B 应用的时候,往往需要刷新一下页面。

3.1 iFrame方式

iframe是一个天然的沙箱,能够天然隔离js/css,且能够独立运行与任何浏览器环境下,而不对外部宿主环境产生影响。同时实现切换不同app使用更好的交互体验,即切换app可以不用刷新页面。

但是对于不同app之间存在一个很大的问题,就是app之间的通讯问题,iframe通讯需要借助postMessage api,而直接在每个应用中创建 postMessage 事件并监听,并不是一个友好的事情。其本身对于应用的侵入性太强,因此通过iframeEl.contentWindow去获取 iFrame 元素的 Window 对象是一个更简化的做法。随后,就需要定义一套通讯规范:事件名采用什么格式、什么时候开始监听事件等等

3.3 Web Components

3.3.1 Web Components简介

Web Components 是一套不同的技术,允许创建可重用的定制元素并在应用中使用,它主要由四大件组成,原有分享链接可见

  1. Custom elements,允许开发者创建自定义的元素,支持生命周期函数。
  2. Shadow DOM,即影子 DOM,它创建了一块私人空间,html dom与shadow dom的作用域是完全隔离的,而且html dom上的css无法影响到shadow dom中的元素。
  3. HTML templates,即 <template><slot> 元素,用于编写不在页面中显示的标记模板。
  4. HTML Imports,用于引入自定义组件(已废弃)

3.3.2 Web Components存在的问题

看上去,web components与iframe很相似,组件拥有自己独立的 Scripts 和 Styles,以及对应的用于单独部署组件的域名。然而它并没有想象中的那么美好,要直接使用纯 Web Components 来构建前端应用的难度有:

  1. 必须重写现有的前端应用。需要完成使用 Web Components 来完成整个系统的功能,虽然各大前端开发框架vue/react都可以借助一定手段来将项目打包成Web Components应用,但配置复杂,操作性差
  2. 生态系统不完善,缺乏相应的一些第三方控件支持
  3. 系统架构复杂。当应用被拆分为一个又一个的组件时,组件间的通讯就成了一个特别大的麻烦。
  4. 最大的一个问题,就是浏览器的兼容性,并不是所有的浏览器都完全支持Web Components

3.4 微应用化

微应用化,即在开发时,应用都是以单一、微小应用的形式存在,而在运行时,则通过构建系统合并这些应用,组合成一个新的应用
image.png

微应用化更多的是以软件工程的方式,来完成前端应用的开发,因此又可以称之为组合式集成。

3.5 前端微服务化

此种方式也就是本文说明的核心。前端微服务化,是微服务架构在前端的实施,每个前端应用都是完全独立自主运行的,最后通过模块化的方式组合出完整的前端应用。
采用这种方式意味着,一个页面上同时存在二个及以上的前端应用在运行。而路由分发式方案,则是一个页面只有唯一一个应用。
然而这种实现方式最为复杂,同时也不是银弹,但是市面上普遍使用率最高的方案,当然也不排除一定的kpi的水分。不过通过这种方式,能够让主工程与子app之间拥有了更大的自主性,能够相对灵活处理,有一定的扩展性。

4. 目前为止遇到的坑

4.1 路由传递

微前端化遇到多页应用时,会有切换页面的问题,一个微应用内的路由切换很容易,但是应用与应用的路由切换就比较麻烦,这种情况就会遇到路由的作用域传递情况。
路由作用域传递,可能解释不太确切,其实就是路由change,命中app,然后操作app,可以访问app内部的二级、甚至三级路由,但是路由直接通过url进入到app的二级、三级路由呢?
下图是正常的访问路径,但是如果反过来呢???

image.png
这里会存在一定的困难点,毕竟是跨不同应用,如何能够通过主应用来访问app中的不同的页面?
其实处理流程仍然是相似的:

  1. 通过activeRule,命中需要被激活的app
  2. app加载资源,并执行App Entry,通过资源执行,app已经可以独立运行了
  3. 拦截app的history事件,并且接管该app的url change事件,即劫持url change来实现主应用的路由系统,从而实现app跳转到内部的二级路由对应的页面中

4.2 模块导入

如何获取app的入口文件,并对app进行渲染?

在微前端架构下,不管基于何种形式,我们都需要获取到子app暴露的一些函数或者对象,通过这些暴露参数,我们可以控制子app渲染以及生命周期钩子的控制,所以这种情况往往需要定制一个规则,所有的app均按照这种规则进行处理,即实现主工程对app的导入。

通常我们需要借助类似umd 包格式中的 global export 方式获取子应用的导出即可,大体的思路是通过给 window变量打标记,然后打入到主应用的缓存中,初次使用通过标记渲染,再次调用使用缓存渲染。

4.3 资源chunk包加载异常

资源导入,即加载app的构建资源,即使app entry加载成功,但是现有前端工程体系中,很多app都是用了import(xxx)动态加载资源方式,通过借助webpack打包构建工具,实现对代码分割,将entry的bundle文件体积减小,统计提升入口页面加载速度。

但是当该工程作为app引入后,app在runtime阶段,当需要运行该chunk包时,就会请求该资源为404,主要原因是webpack构建时,默认的public path为/,而接触过cdn部署的同学可能会知道,在发布部署的时候,webpack需要对publicPath进行修改为cdn对应的配置。

  1. // vue.config.js
  2. module.exports = {
  3. ...
  4. publicPath: 'http://test.com/'
  5. ...
  6. }

当然除此之外,也可以利用webpack运行时的publicPath,但是这种方式不推荐,因为如果多个app激活状态,即多个app都处于运行时,publicPath会出现混乱,文件加载异常

4.4 应用隔离

微前端架构中存在两个关键的问题,就是应用中的js隔离和css隔离。由于微前端场景下,不同技术栈的子应用会被集成到同一个运行时中,所以我们必须在框架层确保各个子应用之间不会出现样式互相干扰的问题。

4.4.1 css隔离

css隔离的功能,确保子应用之间样式互相不干扰,实现方式主要以下几点:

1. Shadow DOM
在不考虑兼容性的问题下,shadow dom确实可以现实样式隔离,如果将子应用渲染到 Shadow DOM 中,那么子应用产生的所有样式都不会污染到全局,但是事实上shadow dom同样也存在一个大问题。

考虑场景如下,大部分类似 Dialog 组件的实现都是在 body 下创建一个容器节点,但是 Shadow DOM 里 Dialog 的样式无法作用到全局,因此展示出来 Dialog 就是无样式的。不过解决思想类似于,比如类似 Dialog 组件的实现能够进行优化:判断自身是否在 Shadow DOM 里,如果是的话则将容器节点创建到 Shadow DOM 里,否则创建到 body 节点下。

至少目前来说还没有Shadow DOM方案来实现样式的隔离,未来可期

2. CSS Module 、BEM
通常在项目中,几乎都通过css 前缀的方式来避免样式冲突或者直接基于css module 方案写样式。但是如果在微前端中,使用微前端的目标是解决存量应用的接入问题,也很显然对原有项目进行样式重写,而且尤其对于不同app使用了相同的ui框架,比方说element-ui,而且都进行了定制化样式重写,这种情况几乎结果是可以预料得到,app之间样式绝对会出现冲突。

3. 动态样式处理
这种方式其实理解起来很简单,我们只需要在应用失活/卸载后,同时卸载掉其样式表即可,原理是浏览器会对所有的样式表的插入、移除做整个CSSOM的重构,从而达到插入、卸载样式的目的。这样即能保证,在一个时间点里,只有一个app的样式表是生效的。

表现上可以理解一下:

<!--before-->
<head>
    <!-- 主应用资源 -->
    <link rel="stylesheet" href="app.css">

    <!-- user 应用 -->
    <link rel="stylesheet" href="http://www.test.com/app.css">
    <link rel="stylesheet" href="http://www.test.com/chunk.ddds32323.css">

    <!-- admin 应用 -->
    <link rel="stylesheet" href="http://www.admin.com/app.css">
    <link rel="stylesheet" href="http://www.admin.com/chunk.dfds343d.css">
</head>


<!--after-->
<head>
    <!-- 主应用资源 -->
    <link rel="stylesheet" href="app.css">
</head>
<body>
    <div id="user_app">
        <!-- user 应用 -->
        <link rel="stylesheet" href="http://www.test.com/app.css">
        <link rel="stylesheet" href="http://www.test.com/chunk.ddds32323.css">
        ...
    </div>
</body>

但是这种方式仍然会存在问题,即同一个时间点里,如果多个app处于激活状态,同样避免不了样式被覆盖的情况,而且由于对于现有的通过var(cssvariable)css变量的形式实现的换肤,依然也解决不了。所以css换肤一直也会是微前端的一个重点需要被攻克问题而持续存在。

4.4.2 js隔离

js隔离的功能,确保子应用之间全局变量/事件不冲突,如何确保各个子应用之间的全局变量不会互相干扰,从而保证每个子应用之间的软隔离?
能够基于浏览器端实现完全的js容器沙箱机制,将app与主应用和其他app之间隔离,都没有一些特别好的方案,而目前看各大团队实现方式来说,个人感觉qiankun实现来说,相对解决了一部分问题,但是仍然存在很多漏洞,qiankun的方法是实现了一个独立运行的js沙箱,对dom操作和js全局事件进行拦截处理。

即在应用的 bootstrap 及 mount 两个生命周期开始之前分别给全局状态打下快照,然后当应用切出/卸载时,将状态回滚至 bootstrap 开始之前的阶段,确保应用对全局状态的污染全部清零。而当应用二次进入时则再恢复至 mount 前的状态的,从而确保应用在 remount 时拥有跟第一次 mount 时一致的全局上下文。
微前端杂谈以及实践中路上遇到的坑 - 图6

目前在实践当中,也借助了该种隔离机制处理,目前感受来说,能解决基础使用场景,但是对于同时激活多个app的场景下,如果存在事件或者变量重复的场景下,也很难做到完美。

Web Worker

当然很多团队也在尝试使用 Web Worker 为 JS 创造多线程环境的能力,将第三方 JS 放入 Worker 线程在后台运行,但目前 Web Worker 存在这一些限制,比如 DOM 限制、通信联系等

5. 现有app推进现状

目前对微前端也都仅仅是探索阶段,微前端体系中还存在着很多的问题,包括开发、联调、测试、发布、部署、监控都各个过程,各大团队依然也是出于探索推进阶段,同时也在各种试错。就目前而言很多文章中,微前端体系介绍而言,可以推荐看一下:
阿里克军的微前端体系介绍
克军的微前端体系介绍的文字版

目前暂时只对vue框架基础实现进行了调研,也推进了主工程基础逻辑抽离、组件注册服务、样式隔离、路由传递以及生命周期的支持,目前来说可以实现基础的微前端的展示,但是由于坑多,涉水尚浅,很多深层次支持功能尚待逐步添加和完善,期待大家一起努力,实现微前端的落地。