使用 Angular 打造微前端架构的 ToB 企业级应用

这篇文章其实已经准备了11个月了,因为虽然我们年初就开始使用 Angular 的微前端架构,但是产品一直没有正式发布,无法通过生产环境实践验证可行性,11月16日我们的产品正式灰度发布,所以是时候分享一下我们在使用 Angular 微前端这条路上的心得(踩过的坑)了额,希望和 Angular 社区一起成长一起进步,如果你对微前端有一定的了解并且已经在项目中尝试了可以忽略前面的章节。

什么是微前端

微前端这个词这两年很频繁的出现在大家的视野中,较早提出这个概念的应该是在 ThoughtWork 的技术雷达,主要是把微服务的概念引入到了前端,让前端的多个模块或者应用解耦,做到让前端的子模块独立仓储,独立运行,独立部署。

那么微前端和微服务到底有什么区别呢?

下面这张图是微服务的示意图,微服务主要是业务模块按照一定的规则拆分,独立开发,独立部署,部署后通过 Nginx 做路由转发,微服务的难点是需要考虑多个模块之间如何调用的问题,以及鉴权,日志,甚至加入网关层

image.png

对于微服务来说,模块分开解藕基本就完事了,但是微前端不一样,前端应用在运行时却是一个整体,需要聚合,甚至还需要交互,通信。

image.png

为什么需要微前端(Micro Front-end)

  1. 系统模块增多,单体应用变得臃肿,开发效率低下,构建速度变慢;
  2. 人员扩大,需要多个前端团队独立开发,独立部署,如果都在一个仓储中开发会带来一些列问题;
  3. 解决遗留系统,新模块需要使用最新的框架和技术,旧系统还继续使用。

微前端的几种方案对比

方式描述优点缺点难度系数路由转发路由转发严格意义上不属于微前端,多个子模块之间共享一个导航即可简单,易实现体验不好,切换应用整个页面刷新🌟嵌套 iframe每个子应用一个 iframe 嵌套应用之间自带沙箱隔离重复加载脚本和样式🌟🌟构建时组合独立仓储,独立开发,构建时整体打包,合并应用方便依赖管理,抽取公共模块无法独立部署,技术栈,依赖版本必须统一🌟🌟运行时组合每个子应用独立构建,运行时由主应用负责应用管理,加载,启动,卸载,通信机制良好的体验,真正的独立开发,独立部署复杂,需要设计加载,通信机制,无法做到彻底隔离,需要解决依赖冲突,样式冲突问题🌟🌟🌟Web Components每个子应用需要使用 Web Components 技术编写组件或者使用框架生成面向未来不成熟,需要踩坑🌟🌟🌟

上述只是简单列举了几种实现方式的对比,当然这些方案也不是互斥的,选择哪种方案取决你的业务场景是什么,以下几个前提条件对于技术选型至关重要:

  • 是否为 SPA 单体应用?
  • 技术栈是否统一,需要支持跨框架调用吗?
  • 是否需要应用间彻底隔离?

我们是做企业级 SaaS 平台的,肯定是 SPA 单体应用,技术栈都是 Angular,应用之间不需要彻底隔离,反而需要共享通用样式和组件,避免重复加载。

所以选择的是:运行时组合 方案。

Worktile 的微前端技术选型之路

目前市面上的微前端解决方案并不多,关注度和成熟度较高的应该就是 single-spa

国内也有很多团队都有自己的微前端框架,比如开源了的基于 single-spa 的 qiankun – 可能是你见过最完善的微前端解决方案 , 还有 phodal 的 mooa 以及无数内部的解决方案(最近阿里飞冰也开源 了面向大型工作台的微前端解决方案 icestark,只支持 React 和 Vue)

我们在做技术选型的时候首要考虑的就是 single-spa 和 mooasingle-spa 成熟度应该较高,示例文档很完善,mooa 为 Angular 打造的主从结构的微前端框架,和我们的业务和技术符合度较高,研究一段时间后最终我们还是选择了自研一套符合自己的微前端库(因为比较简单,不敢称之为框架),主要是因为我们的业务有以下几个需求在以上的框架中不满足或者说很难满足, 甚至需要高度定制。

  • 产品是主从结构的,Portal 包含左侧导航,消息通知以及子应用管理
  • 需要在多个子应用之间通信,主应用或者某个子应用需要打开其他子应用的详情页或者路由跳转
  • 子应用A的某个页面中可能会加载子应用B的某个组件
  • 基于以上2个特性,所以需要提供并存模式,即当前显示的虽然是 B 应用,但是要保证 A 应用正常可以调用,如果销毁了就无法被其他应用调用
  • 需要提供预加载功能
  • 子应用的样式也需要独立加载
  • 路由,不管是在主应用还是子应用,路由体验要和单体应用一致

我运行了 single-spa 和 mooa 的示例,主要是一些简单的渲染展示,一旦需要满足以上一些特性还是需要修改很多东西,mooa 实现应该还是比较全面也比较适合我们的,但是它的示例中路由有一些问题,页面跳转了但是路由没有变,打包已经抛弃了 Angular CLI,代码层面参考了 single-spa 的很多东西,API 可以再度简化,既然是为 Angular 定制的,我觉得应该以 Angular 的方式实现更符合,当然不排除作者想要后期支持 React 和 Vue,不可否认的是 phodal 本人对于微前端的理解的确很深,写的很多不错的微前端的文章 microfrontends, 甚至出过少数一本微前端的书《前端架构 – 从入门到微前端》,我在实现微前端的时候也借鉴参考了它的很多思想和实现方式。

使用 Angular 打造微前端应用

使用 Angular 实现微前端其实比 React 和 Vue 更加困难,因为 Angular 包含 AOT 编译,Module,Zone.js ,Service 共享等等问题,React 和 Vue 直接子应用 JS 加载渲染页面某个区域即可。

选择动态加载模块后编译还是加载整个应用

在 Angular 单体应用中,必须有一个根模块 AppModule,然后是每个特性模块 FeatureModule,每个特性模块可以有自己的路由,当然可以使用路由的惰性加载这些特性模块,但是在微前端架构中,每个子模块都是独立仓储的,如何在运行时把子模块加载到根模块就是一个技术选择难点。

  1. 名列前茅种方案就是把每个子模块当作一个特性模块,然后在打包的时候随着主应用一起打包编译,这样是最简单的,但是这个无法做到独立部署,而且每次部署都是全量更新
  2. 第二种方案还是把子模块当作一个特性模块,在主应用通过 SystemJsNgModuleLoader 加载子模块,然后编译运行,(注:SystemJsNgModuleLoader 在新版本已经遗弃)
  3. 第三种方案就是每个子模块是一个独立的应用,和主应用一样,有自己的 AppModule, 路由,选择这种方案就需要处理多个应用路由同步的问题,还有就是 Angular 目前的依赖库是无法直接运行时使用的,需要每个子应用一起编译,无法做到公共依赖库抽取(可能有其他方案)
  4. 第四种方案就是把所有的子模块编译成 Web Components 使用,我暂时没有深入研究过,选择这种方案直接使用组件肯定没有问题,但是使用 Web Components 后路由如何处理我不知道。

我们最终选择了最复杂的第三种方案,因为新的 Ivy 渲染引擎正式发布后会解决第三方依赖库运行时直接使用的问题,至于 Web Components  没有深入研究,因为目前第三种方案运行挺好的。

image.png

应用注册,加载,销毁机制

这个是所有微前端应用的基础和核心,但是我觉得反而是最简单容易实现的,主要要做的就是:

提供静态资源动态加载功能

配置好子应用的规则,包含:应用名称,路由前缀,静态资源文件

this.planet.registerApps([
  {
      name: 'app1',
      hostParent: '#app-host-container',
      routerPathPrefix: '/app1',
      selector: 'app1-root',
      scripts: ['/static/app1/main.js'],
      styles: ['/static/app1/styles.css']
  },
  // ...
]);

应用加载:根据当前页面的 URL 找到对应的子应用,然后加载应用的静态资源,调用预定义好的启动函数直接启动应用即可,在 Angular 中就是启动根模块 platformBrowserDynamic().bootstrapModule(AppModule)

应用的预加载:当前应用渲染完毕会预加载其他应用,并启动,并不会显示

销毁应用使用 appModuleRef.destroy();

按照上述的步骤处理简单的场景基本就足够了,但是如果希望应用共存就不一样了,我们的做法是把  bootstrapped 状态隐藏起来,而不是销毁,只有 Active 状态的应用才会显示在当前页面中。

路由

因为选择了每个子应用是独立的 Angular 应用,同时还可以共存多个子应用,那么多个应用的路由同步,跳转就成了难题,而且还要支持应用之间路由跳转,应用之间通信,组件渲染等场景。我认为路由是我们在使用微前端架构中遇到的最复杂的问题。

目前我们的做法是主应用的路由中把所有子应用的路由都配置上,组件设置成 EmptyComponent , 这样在切换到子应用路由的时候,主应用会匹配空路由状态,不会报错,每个子应用需要添加一个通用的空路由  EmptyComponent

{
        path: '**',
        component: EmptyComponent
}

除此之外还需要在切换路由的时候同步更新其他应用的路由,否则会造成每个应用的当前路由状态不一致,切换的时候会有跳转不成功的问题。

  • 主应用路由切换时,找到所有当前启动的子应用,使用 router.navigateByUrl 同步跳转
  • 子应用路由切换时,同步主应用路由,同时同步其他启动状态的子路由

我看了很多微前端框架包括  single-spa,基本上路由这一块没有处理,完全交给开发者自己去填坑,single-spa 的 Angular 示例基本就是切换就销毁了 Angular 应用,因为没有并存,所以也就不需要处理多个应用路由的问题了,当然它作为和框架无关的微前端解决方案,也只能做到这一步了吧。

这个等 Ivy 渲染引擎正式发布后,可以把子应用编译成直接可以运行的模块,整个应用如果只有一个路由会简化很多。

共享全局服务

对于一些全局的数据我们一般会存储在服务中,然后子应用可以直接共享,比如:当前登录用户多语言服务等,简单的数据共享可以直接挂载在 window 上即可,为了让每个子应用使用全局服务和模块内服务一致,我们通过在主应用中实例化这些服务,但后在每个子应用的 AppModule 中使用 provide 重新设置主应用的 value,当然这些不需要子应用的业务开发人员自己设置,已经封装到业务组件库中全局配置好了。

{
  provide: AppContext,
  useValue: window.portalAppContext
}

应用间通信

应用间通信有很多中方式,我们底层使用浏览器的 CustomEvent ,在这之上封装了 GlobalEventDispatcher 服务做通信(当然你也可以使用在 window 对象上挂载全局对象实现),场景就是某个子应用要打开另外一个子应用的详情页

// App1
globalEventDispatcher.dispatch('open-task-detail', { taskId: 'xxx' });

// App2
globalEventDispatcher.register('open-task-detail').subscribe((payload) => {
    // open dialog of task detail
});

应用间组件互相调用

在我们的敏捷开发子产品中,一个用户故事的详情页,需要显示测试管理应用的关联的测试用例和测试执行情况,那么这个测试用例列表组件放在 测试管理 子应用是最合适的,那么用户故事详情页肯定在敏捷开发应用中,如何加载测试管理应用的某个组件就是一个问题。

这一块使用了 Angular CDK 中的 DomPortalOutlet  动态创建组件,并指定渲染在某个容器中,这样保证了这个动态组件的创建还是 测试管理 模块的,只是渲染在了其他应用中而已。

const portalOutlet = new DomPortalOutlet(container, componentFactoryResolver, appRef, injector);
const testCasesPortalComponent = new ComponentPortal(TestCasesComponent, null);
portalOutlet.attachComponentPortal(testCasesPortalComponent);

工程化

使用微前端开发应用不仅仅要解决 Angular 的技术问题,还有一些开发,协作,部署等工程化的问题需要解决,比如:

  • 公共依赖库抽取
  • 本地如何启动开发
  • 如何打包部署,生成的 hash 资源文件如何通知主应用

应用公共依赖库抽取避免类库重复打包,减少打包体积,这就需要自定义 Webpack Config 实现,起初我们是完全自定义 Webpack 打包 Angular 应用,一旦这么做就会失去很多 CLI 提供的方便功能,偶尔发现了一个类库 angular-builders ,他的作用其实就是在 Angular CLI 生成的 Webpack Config 中合并自定义的 Webpack Config,这样就做到了只需要写少量的自定义配置,其余的还是完全使用 CLI 的打包功能,差一点就要自己写一个类似的工具了。
在主应用中把需要公共依赖包放入 scripts 中,然后在子应用中配置 externals,比如:moment lodash rxjs 这样的类库。

const webpackExtraConfig = {
    optimization: {
        runtimeChunk: false // 子应用一定要设置 false,否则会报错
    },
    externals: {
        moment: 'moment',
        lodash: '_',
        rxjs: 'rxjs',
       'rxjs/operators': 'rxjs.operators',
        highcharts: 'Highcharts'
    },
    devtool: options.isDev ? 'eval-source-map' : '',
    plugins: [new WebpackAssetsManifest()]
};
return webpackExtraConfig;

WebpackAssetsManifest 主要作用是生成 manifest.json 文件,目的就是让生成的 Hash 文文件的对应关系,让主应用加载正确的资源文件。

本地开发配置 proxy.conf.js 代理访问每个子应用的资源文件,同时包括 API 调用。

基于 Angular 的微前端库 ngx-planet

以上是我们在使用 Angular 打造微前端应用遇到的一些技术难点和我们的解决方案,调研后最终选择自研一套符合我们业务场景的,同时只为 Angular 量身打造的微前端库。

Github 仓储地址:ngx-planet
在线 Demo:http://planet.ngnice.com

不敢说 “你见过最完善的微前端解决方案” ,但至少是 Angular 社区目前我见过完全可用于生产环境的方案,API 符合 Angular Style ,国内很多大厂做微前端方案基本都忽略了 Angular 这个框架的存在,Worktile 四个研发子产品完全基于 ngx-planet  打造开发,经过接近一年的踩坑和实践,基本完全可用。

image.png

希望 Angular 社区可以多一些微前端的解决方案,一起进步,我们的方案肯定也存在很多问题,也欢迎大家提出改进的建议和吐槽,我们也将继续在 Angular 微前端的路上继续深耕下去,如果你正在寻找 Angular 的微前端类库,不妨试试 ngx-planet。

将来会调研在 Ivy 渲染引擎下的优化和改进方案。

文章标题:使用 Angular 打造微前端架构的 ToB 企业级应用,发布者:刘佳,转载请注明出处:https://worktile.com/kb/p/6514

(2)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
刘佳刘佳认证作者
上一篇 2022年3月20日 上午1:20
下一篇 2022年3月20日 上午1:27

相关推荐

  • oa办公系统哪个

    OA(Office Automation)办公系统是企业提高办公效率、降低成本、改善工作方式的重要工具。它包括文档管理、电子邮件、日程安排、工作流程管理等基本功能。 目前市场上的主流OA办公系统包括1、微软Office 365;2、钉钉;3、腾讯WeCom(原企业微信);4、用友U8+;5、金山WP…

    2024年1月11日
    23200
  • 数据清洗的重要性何在

    摘要:数据清洗在数据分析和决策过程中扮演着至关重要的角色。它确保了数据的质量、一致性、准确性,为最终分析提供了坚实的基础。数据清洗的重要性体现在提高数据分析的准确性1、优化数据存储2、提升运营效率3、增强决策信心4。清洗过的数据可以减少误导性的信息,避免错误的商业决策。它还能优化数据存储,通过删除不…

    2023年12月14日
    47700
  • Scrum Master和项目经理的区别是什么

    Scrum Master和项目经理的区别是:1、职责不同;2、工作方式不同;3、关注重点不同;4、项目阶段不同。 Scrum Master的主要职责是推动团队的自组织和高效工作,关注团队的需求和问题;而项目经理的职责是规划、执行和交付项目,负责项目的整体管理和监控。 一、Scrum Master S…

    2023年7月30日
    1.0K00
  • 什么样的企业需要项目管理

    企业需要项目管理的一些迹象:1、多个部门之间需要协作;2、项目需要管理;3、企业需要提高效率;4、企业需要控制成本;5、企业需要提高质量。项目管理需要对资源、时间和成本进行管理,以确保项目按计划完成。 什么是项目管理? 项目管理是一种管理方法,它涉及到组织、规划、执行和控制项目的过程。项目管理的目标…

    2023年3月2日
    34400
  • 面向对象、面向服务、面向组件三种编程模式有什么区别

    区别是:面向对象编程是一种计算机编程架构。面向对象程序设计以对象为核心,该方法认为程序由一系列对象组成。面向服务编程是一种体系结构,目标是在软件代理交互中获得松散耦合。面向组件编程技术建立在面向对象技术之上,它是面向对象技术的进一步发展。 面向对象 面向对象程序设计(Object Oriented …

    2023年2月16日
    1.9K00
  • 产品管理中的用户故事拆分技巧是什么

    开篇即进入核心议题,用户故事拆分技巧涉及到1、小而具体化处理、2、基于价值优先级划分、3、维持故事独立性、4、保证可测试性、5、适时地沟通与迭代中的每一环节都至关重要,具体到某个领域例如小而具体化处理,意味着应确保单个用户故事的精简性,确保团队能够在短周期内完成,且便于理解、估时与实施。 一、小而具…

    2024年1月19日
    23500
  • 资源管理策略包括哪些

    资源管理策略包括:一、人力资源管理;二、资金管理;三、项目管理;四、设备管理;五、信息管理;六、时间管理。人力资源管理是资源管理策略中的一个重要方面。它包括招聘、培训、绩效评估、激励措施和离职等方面。 一、人力资源管理 人力资源管理是资源管理策略中的一个重要方面。它包括招聘、培训、绩效评估、激励措施…

    2023年4月30日
    1.2K00
  • 什么是职业化管理

    职业化管理是:职业化就是一种工作状态的标准化、规范化、制度化,包含在工作中应该遵循的职业行为规范(Code of Conduct),职业素养,和匹配的职业技能。即在合适的时间、合适的地点,用合适的方式,说合适的话,做合适的事,不为个人感情所左右,冷静且专业。 一、什么是职业化管理 职业化就是一种工作…

    2023年5月5日
    59500
  • 云原生应用的API管理如何实现

    云原生应用的API管理的实现步骤包括API定义和设计、API开发和管理、API测试和文档化、API网关、API生命周期管理、API目录和管理平台、API安全性和合规性等。详细介绍:1、API定义和设计,在云原生应用的开发过程中,API的定义和设计是非常重要的一步,良好的API设计可以提高应用程序的可维护性、可扩展性和可重用性;2、API开发和管理,在确定了API的设计后,可以开始进行API的开发和管理等等。

    2023年10月27日
    41000
  • 如何进行有效的员工激励和奖励

    有效的员工激励和奖励体系包括以下几个关键步骤:1、了解员工需求和期望;2、设定清晰明确的目标和标准;3、实施公平的奖励机制;4、提供多元化激励手段;5、定期评估和调整激励方案。其中,实施公平的奖励机制是核心,确保每个员工根据工作绩效和贡献获得相应的回报。员工激励和奖励有助于提高员工的工作满意度和积极…

    2023年8月21日
    73600

发表回复

登录后才能评论
注册PingCode 在线客服
站长微信
站长微信
电话联系

400-800-1024

工作日9:30-21:00在线

分享本页
返回顶部