微前端和微应用框架 (转载)

3,191 阅读19分钟

微前端概念

微前端 是一种构建现代Web app的技术、策略和方法,多个前端技术团队可以独立发布功能特性。

“微前端”这一技术名词最早出现在2016年底的ThoughtWorks Technology Radar中,将传统后端技术的“微服务”概念带到了前端领域,旨在将各个独立的前端应用,可以相互关联,组合,形成一个大型的前端应用。

微前端背后的想法是将一个大型网站或者web程序分解成一个个独立的开发单位所拥有的功能模块组成的。每个开发单位只会关心自己的微应用(也就是功能模块,任务领域),当然这一个个微应用,是可以独立开发,测试以及部署。

4615bcb09299cc562adee0312b174fd7f63efbf1.png@942w_540h_progressive.webp

微前端的实践方法

  • 使用HTTP服务器的路由来重定向多个应用
  • 在不同的框架之上设计通讯、加载机制,诸如 Mooa 和 Single-SPA
  • 组合多个应用(app)或者多个视图组件(widget)组成一个单体应用
  • 纯web-component 构建应用
  • 结合 web-component来构建应用

微前端为什么重要 微前端 现在已经普遍被各大互联网公司的前端团队所采用。但为什么是微前端,为什么微前端 架构方案在现在的工业界有这么重要的地位?

巨石应用  首先,在之前的前端开发过程中,SPA(单页面应用)虽然作为一个高性能、流行的web项目形态,           但是项目逐渐迭代,功能逐渐增加,导致项目代码量越来越大,尤其是企业B端的大型的中后台应用,技术栈很难  会去更迭,往往是改一处而动全身,由此带来的发版成本也越来越高,造成了很多的不便。

所以我们使用 微前端方案,最优先解决的应该就是解构巨石应用。为其解构过程中常会遇到的隔离性、依赖去重、通信、应用编排 等问题提供解决方案。

技术栈无关  技术栈无关,在我来看,恰恰是微前端最为核心的价值。往往,微前端被challenge的一个最大的问题在于:“同样是要解构巨石应用,如果是 widget 级别,那么微前端跟业务组件的区别在哪里?微前端到底是因何而生?”对于很多企业的软件项目来说,尤其是B端的应用,可能存在一套框架、体系需要用上3~5 年,甚至之上,我们该怎么确保我们的遗产代码可以顺利平滑地迁移,过上几年依旧可以在老项目中用上最新技术栈的代码?

微前端是最好的方案,「技术栈无关」成为了微前端架构上的准绳,具体到实现时,对应的就是:应用之间不应该有任何直接或间接的技术栈、依赖、以及实现上的耦合。

(但这里不得不提一点,有很多前端团队采用技术栈统一的策略,所以在微前端的具体实现中采用了微应用方案,微应用方案 是没有“技术栈无关”这一特点的)

91a3f251594910531a4c910144c99c59967cbfad.jpg@942w_530h_progressive.webp

独立性强  采用微前端的架构方案,由于各为独立的微应用,所以应用都有自己的持续交付流水线(包括构建、测试并部署到生产环境),并且要能独立部署,不必过多考虑其它代码库和交付流水线的当前状态。

d065610304c65abc6db9ae247f8331bfd3ab6d64.png@942w_317h_progressive.webp

为什么不是iframe?

大部分微前端方案又不约而同放弃了 iframe 方案。不考虑体验问题,iframe 几乎是最完美的微前端解决方案了。

iframe 最大的特性就是提供了浏览器原生的硬隔离方案,不论是样式隔离、js 隔离这类问题统统都能被完美解决。但他的最大问题也在于他的隔离性无法被突破,导致应用间上下文无法被共享,随之带来的开发体验、产品体验的问题。

———— kuitos 微前端的技术圆桌(语雀:www.yuque.com/kuitos/gky7…

使用iframe进行隔离,会包含下面几个问题:

url 不同步。浏览器刷新 iframe url 状态丢失、后退前进按钮无法使用。

UI 不同步,DOM 结构不共享。想象一下屏幕右下角 1/4 的 iframe 里来一个带遮罩层的弹框,同时我们要求这个弹框要浏览器居中显示,还要浏览器 resize 时自动居中..

全局上下文完全隔离,内存变量不共享。iframe 内外系统的通信、数据同步等需求,主应用的 cookie 要透传到根域名都不同的子应用中实现免登效果。

慢。每次子应用进入都是一次浏览器上下文重建、资源重新加载的过程。

微应用概念

如果说微前端被称作是一种架构模式的话,前端的微应用就是微前端概念在工程、技术上的一种具体实现。

微前端方案的具体技术实现其实有很多种,路由分发应用、前端微服务化、微件化。但就微应用方案具体来说,即在开发时,应用都是以单一、微小应用的形式存在,而在运行时,则通过构建系统合并这些应用,组合成一个新的应用,往往会是通过业务作为主目录,而后在业务目录中放置相关的组件,同时拥有一些通用的共享模板。这很适合技术栈统一的前端团队。

1a0c2f9c5d49dc835fd8062857e6606fa6bd0a9f.jpg@942w_576h_progressive.webp

那么“微应用”通常在前端技术上的实现大致是怎样的呢?我们可以从以下的几个方面来考虑:

架构模式

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

基座模式。通过一个主应用,来管理其它应用。设计难度小,方便实践,但是通用度低。

自组织模式。应用之间是平等的,不存在相互管理的模式。设计难度大,不方便实施,但是通用度高。

路由系统

既然我们要把不同的应用协调起来,我们需要整个框架中,规定一个类似于“应用注册表”的东西。这个注册表应该包含每个应用的内容信息,以及相关的入口。在前端中,这种注册表最好的呈现方式就是“路由”。微应用方案里应当规定json文件,用于描述不同微小应用间的路由联系。

构建时集合 vs 运行时集合

通常将不同的微小应用集合起来的方式就是构建时集合。但要做到各个微应用的独立性增强(最重要保证独立部署),构建时集合就不能满足。运行时集合则可以胜任。

15735119c84c523acee2ad6b6f331db3e4d908db.png@942w_210h_progressive.webp

两个方案也包含了优缺点:

ed6b182e8cb6bdcc6818fa9e6005df83a5b6fcb9.png@942w_240h_progressive.webp

JS Entry or HTML Entry

当然,很多微应用方案为保证每个微应用的独立性,统统采用了运行时集合的方案,既然使用了运行时集合,那么落实运行时集合则需要考虑到底是JS entry还是HTML entry?

JS Entry 的方式通常是子应用将资源打成一个 entry script,比如 single-spa 的 example 中的方式。它可以很好地运用在主应用(基座应用)的初始化加载中。但这个方案的限制也颇多,如要求子应用的所有资源打包到一个 js bundle 里,包括 css、图片等资源。除了打出来的包可能体积庞大之外的问题之外,资源的并行加载等特性也无法利用上。

HTML Entry 更加灵活,直接将子应用打出来 HTML 作为入口,主框架可以通过 fetch html 的方式获取子应用的静态资源,同时将 HTML document 作为子节点塞到主框架的容器中。这样不仅可以极大的减少主应用的接入成本,子应用的开发方式及打包方式基本上也不需要调整,而且可以天然的解决子应用之间样式隔离的问题(后面提到)。

应用生命周期

因为在使用到了微应用框架的web app中,应用之间存在着交互(包含着页面应用切入和卸载)。所以,我们要规定页面的生命周期,例如,应用何时被切入,何时渲染完成,应用何时被卸载。这样更有利于不同,众多应用之间的管理。

通常生命周期会这么去设计:

Load,决定加载哪个应用,并绑定生命周期

bootstrap,获取静态资源

Mount,安装应用,如创建 DOM 节点

Unload,删除应用的生命周期

Unmount,卸载应用,如删除 DOM 节点、取消事件绑定

样式css 隔离

微应用的应用场景下,主应用和子应用之间可能会出样式冲突的情况,(甚至一些微前端应用场景里,不同技术栈的子应用之间集成在同一个运行时里面)。

样式css 隔离的方式有很多,CSS module、Shadow DOM、BEM开发规范、CSS-in-JS,我们重点讲解前两种:

Shadow DOM:基于Web Components的Shadow DOM能力,其设计的初衷是将Web Compnent的自定义组件,包装成一个黑盒,外部的DOM不能够看到其内部代码(样式),内部的DOM与外部的DOM相互之间不影响。很多微前端的解决方案采用了这种方法,这么做的最大好处在于:将每个子应用包裹到一个 Shadow DOM 中,保证其运行时的样式的绝对隔离。

aa9fccd4f006597ad823b8411d38e33e9ca873ea.png@942w_450h_progressive.webp

CSS Modules:还有很多方案的实践是通过约定 css 前缀的方式来避免样式冲突,即各个子应用使用特定的前缀来命名 class,或者直接基于 css module 方案写样式。这样不同的应用之间,样式就不会存在冲突了。(但这种方法并不适合使用了老旧技术栈的遗产项目,且可能会发生组件库样式版本不兼容的情况)

4325dac0ec5a6562fbdd82671f1ffd0389f413ea.png@675w_507h_progressive.webp

JavaScript 隔离

微应用方案成功落地还需要解决的一个关键问题就是:“如何确保各个子应用之间的全局变量不会互相干扰,从而保证每个子应用之间的软隔离?”

这个问题,简单的解决方案是给一些全局副作用加上各种前缀从而避免变量之间冲突,但这种简单的方法,增加了开发约束,无疑增加了前端团队的成本。

另一种方法解决方法就是自主实现一个JS 沙盒(sandbox),JS 沙盒机制的核心是让局部的JavaScript运行时,对外部对象的访问和修改处在可控的范围内,即无论内部怎么运行,都不会影响外部的对象。

我们这里来介绍一下 蚂蚁集团 qiankun 微前端框架 的 原创的 JS 沙盒 实现:

架构图所示:

b5b4fd7acb298eca306012d11dbdc557e521ab60.jpg@873w_801h_progressive.webp

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

当然沙箱里做的事情还远不止这些,其他的还包括一些对全局事件监听的劫持等,以确保应用在切出之后,对全局事件的监听能得到完整的卸载,同时也会在 remount 时重新监听这些全局事件,从而模拟出与应用独立运行时一致的沙箱环境。

————  kuitos 《可能是你见过最完善的微前端解决方案》知乎:zhuanlan.zhihu.com/p/78362028

应用间通信

既然是多个应用组合、拼接成一个大应用。应用之间一定需要存在通信,交互的功能。这些功能其实具体地来说,可以是打开另一个微应用,关闭另一个微应用,向另一个微应用传递参数params。

这些微应用之间的通信的传递方式,我们应当根据具体的使用场景来选择。

微应用方案的优势

微应用方案在众多微前端方案有什么很好的优势?其实我们在解构巨石应用的时候,划分众多微小的应用需要一个明确的尺度标准,这个尺度标准可能会有很多:

按照业务拆分。

按照权限拆分。

按照变更的频率拆分。

按照组织结构拆分。利用康威定律来进一步拆分前端应用。

跟随后端微服务划分。实践证明, DDD 与事件风暴是一种颇为有效的后端微前端拆分模式,对于前端来说,它也颇有有效——直接跟踪后端服务。

每个项目都有自己特殊的背景,切分微前端的方式则不一样。即使项目的类型相似,也存在一些细微的差异。就以我们盒马产品技术部举例,在盒马业务中,大部分中后台前端大型应用都是“业务驱动”,通过业务可以很轻松地将业务进行模块划分,业务开发也会更加“内聚”。

hippo-app

hippo-app 微应用 是服务于 阿里巴巴集团盒马产品技术部 中后台前端项目 的内部微应用框架。它是一个能具备完整业务功能, 能独立提供服务的应用级模块。如果把 Legion 工作台当作一个 Web 端的操作系统,那么微应用就是系统上的应用程序,就如“小程序”之于支付宝、微信一样。

我们以一个具体的案例来看。以盒马配送域的 B2C Setup 为例,它分为  “站点列表、配置详情、配置表单、配置预览” 等 4 个页面,在旧技术体系下需要创建 4 个对应的页面,如下图所示:

666e3ceffd50913b9cbc6fc2bc8960be0cdb6b04.png@942w_728h_progressive.webp

虽然在业务上他们是一个整体,但由于四个页面之间是独立的,导致难以复用数据模型、远端服务、代码模块,但在微应用体系下就完美解决了这个问题。

可以看到,hippo-app 微应用主要在架构层面为工作台带来了新的应用形态,它的核心思想是“以业务为中心的应用架构”,让业务开发更加内聚。(由于是内部工具,本文不过多赘述,对新零售业务感兴趣可以投简历,加入我们,HC多多)

其他微前端方案

Single SPA

single spa就是用于 前端微服务化 Javascript解决方案,可以通过核心的import-map技术,将不同的独立的spa应用进行整合,构成一个完善的大型应用。

什么是import-map?

import-map可以在现代浏览器中,通过script标签将 js模块 进行引入,但需要借助system.js。我们看下面例子:

<script type = "systemjs-importmap" >
     {
        "imports": {
            "module": "https://[cdn-link].js",
        }
    } 
</script>
<script src="https:/ / cdn.jsdelivr.net / npm / systemjs / dist / system.js "></script>

或者更加优化的写法:

<script type = "systemjs-importmap" src = "https://[bucket]/import-map.json" > </script>
<script src="https:/ / cdn.jsdelivr.net / npm / systemjs / dist / system.js "></script>

import -map.json

{
    "imports": {
        "module": "https://[cdn-link].js",
    }
}

若要使用single-spa,落地完整的微前端,我们则需要将import-map所引入的模块,改成CDN文件,通过这种方式,将各个微小的子应用进行连接。

single-spa的优缺点:

(优点)

加载快,可以将所有系统共用的模块提取出来,实现按需加载,一次加载,其他的复用。(system.js)

可以修改子系统(子应用)的样式,不需要代理服务器,直接修改

用户体验好,快,内容的改变不需要重新加载整个页面,避免了不必要的跳转和重复渲染

http请求少,减轻了服务器的压力

(缺点)

css和js需要去制定规范,进行隔离,负责会造成全局污染

在路由部分,需要对子系统进行一系列的改动

关于使用single-spa去落地微前端框架应用,具体可以看这几篇文章:

juejin.im/post/684490… ——————《从0实现一个前端微服务(上)》

juejin.im/post/684490… —————— 《从0实现一个前端微服务(中)》

juejin.im/post/684490… —————— 《每日优鲜团队微前端改造》

蚂蚁集团 umijs + qiankun

聊微前端/微应用的解决方案,其实不能单聊qiankun,qiankun的本质其实是一个微应用加载器,它提供了将各个独立的前端应用进行整合,通信,接受发出事件的能力而已,但真正的微前端落地实践方案,本文给出了蚂蚁集团的umijs加@umijs/plugin-qiankun插件的结合。

Umijs(乌米)是由蚂蚁集团研发的可扩展的企业级前端应用框架。Umi 以路由为基础的,同时支持配置式路由和约定式路由,保证路由的功能完备,并以此进行功能扩展。然后配以生命周期完善的插件体系,覆盖从源码到构建产物的每个生命周期,支持各种功能扩展和业务需求。

1fa060c2541bc75acdfd62eb5e05b2c4242d3411.png@942w_477h_progressive.webp

qiankun作为Umi的一个插件,使Umi的开发者提供了能够使用微前端的能力。从微应用配置到 应用的 加载/卸载 时序,子应用自定义生命周期,应用通信都提供了完善的解决方案。

@umijs/plugin-qiankun 主应用 配置/装载 子应用

a. 使用路由绑定的方式

建议使用这种方式来引入自带路由的子应用。

假设我们的系统之前有这样的一些路由:

export default {
    routes: [{
        path: '/',
        component: '../layouts/index.js',
        routes: [{
            path: '/app1',
            component: './app1/index.js',
            routes: [{
                path: '/app1/user',
                component: './app1/user/index.js',
            }, ],
        }, {
            path: '/',
            component: './index.js',
        }, ],
    }, ],
}

我们现在想在 /app1/project 和 /app2 这两个路径时分别加载微应用 app1 和 app2,只需要增加这样一些配置即可:

export default {
    routes: [{
        path: '/',
        component: '../layouts/index.js',
        routes: [{
                path: '/app1',
                component: './app1/index.js',
                routes: [{
                        path: '/app1/user',
                        component: './app1/user/index.js',
                    },
                    // 配置微应用 app1 关联的路由         
                    {
                        path: '/app1/project',
                        microApp: 'app1',
                    },
                ],
            },
            // 配置 app2 关联的路由     
            {
                path: '/app2',
                microApp: 'app2'
            },
            {
                path: '/',
                component: './index.js',
            },
        ],
    }, ],
}

b. 使用 组件的方式

建议使用这种方式来引入不带路由的子应用。 否则请自行关注微应用依赖的路由跟当前浏览器 url 是否能正确匹配上,否则很容易出现微应用加载了,但是页面没有渲染出来的情况。

我们可以直接使用 React 标签的方式加载我们已注册过的子应用:

import { MicroApp } from 'umi';
export function MyPage() { 
    return (
                <div>     
                    <div>+ <MicroApp name="app1" /> </div>  
                </div>  
            )
}

@umijs/plugin-qiankun 子应用 配置 运行时生命周期钩子

在Umi中,如果你需要在子应用的生命周期期间加一些自定义逻辑,可以在子应用的 src/app.ts 里导出 qiankun 对象,并实现每一个生命周期钩子,其中钩子函数的入参 props 由主应用自动注入。

export const qiankun = { 
    // 应用加载之前
    async bootstrap(props) {console.log('app1 bootstrap', props);},
    // 应用 render 之前触发
    async mount(props) {console.log('app1 mount', props);},
    // 应用卸载之后触发
    async unmount(props) {console.log('app1 unmount', props);},
};

@umijs/plugin-qiankun 父子应用通信

有两种方式可以实现:

配合 useModel 使用(推荐)

需确保已安装 @umijs/plugin-model 或 @umijs/preset-react

主应用使用下面任一方式透传数据:

a. 如果你用的 MicroApp 组件模式消费微应用,那么数据传递的方式就跟普通的 react 组件通信是一样的,直接通过 props 传递即可:

function MyPage() {
    const [name, setName] = useState(null);
    return <MicroApp name = {
        name
    }
    onNameChange = {
        newName => setName(newName)
    }
    />
}

b. 如果你用的 路由绑定式 消费微应用,那么你需要在 src/app.ts 里导出一个 useQiankunStateForSlave 函    数,函数的返回值将作为 props 传递给微应用,如:


function useQiankunStateForSlave() {
    const [masterState, setMasterState] = useState({});
    return {
        masterState,
        setMasterState,
    }
}

微应用中会自动生成一个全局 model,可以在任意组件中获取主应用透传的 props 的值。

import {
    useModel
} from 'umi';

function MyPage() {
    const masterProps = useModel('@@qiankunStateFromMaster');
    return <div> {JSON.strigify(masterProps)} </div>;
}

或者可以通过高阶组件 connectMaster 来获取主应用透传的 props

import { connectMaster} from 'umi';

function MyPage(props) {
    return <div> {JSON.strigify(props)}</div>
}

export default connectMaster(MyPage);

和 的方式一同使用时,会额外向子应用传递一个 setLoading 的属性,在子应用中合适的时机执行 masterProps.setLoading(false),可以标记微模块的整体 loading 为完成状态。

基于 props 传递

类似 react 中组件间通信的方案

主应用中配置 apps 时以 props 将数据传递下去(参考主应用运行时配置一节)

 const qiankun = fetch('/config').then(config => {
     return {
         apps: [{
             name: 'app1',
             entry: '//localhost:2222',
             props: {
                 onClick: event => console.log(event),
                 name: 'xx',
                 age: 1,
             },
         }, ],
     };
 });

子应用在生命周期钩子中获取 props 消费数据(参考子应用运行时配置一节)

(参考来源:umijs.org/plugins/plu… qiankun插件的官方文档)

微前端的未来

前面介绍了这么长时间有关于微前端,微应用的一些解决方案(像qiankun,hippo-app),但他们其实并不算完全地解决了问题。

技术上,他仅仅只是解决了不同独立的前端应用,整合的时候,路由router,样式CSS和全局变量Proxy的问题。

但是,对于微前端各个应用之间,应用的划分标准并不明晰;并且微前端应用和后台微服务没有一个很好的对应关系,到底应该保持同粒度的 1:1 对应的拆分,还是加上一个 BFF 层做聚合,做一个 1:N 的模式?

所以,目前前端界所提供的微前端解决方案,它们都只是各自独立的应用系统(有老旧的历史遗留系统,也有新的技术栈写的系统)聚合到了一个portal页面上。(类似于一个iframe的较好的替代方案) 作者:Masayel www.bilibili.com/read/cv8190… 出处:bilibili