从Microrepo与Monorepo发微到微前端:前端项目架构学习笔记
Author:zhoulujun Date:
前端项目管理
单体应用(Monolithic):
对于简单的系统,只用一个仓库来管理项目就够了。但随着业务逐渐复杂,这种架构使得项目变成巨石应用。
导致很多问题:
各个功能互相耦合,灵活性低;
一个错误会导致整个应用崩溃,可靠性降低;
项目体积庞大、编译速度变慢,部署效率低。
多仓多模块(git repo/Microrepo/Multirepo):
于是产生了多仓多模块管理的方式,此时按照业务和模块来拆分为多个仓库(用git SubModules/SubTree来管理,推荐SubTree。二者的区别推荐阅读《Git 工具 - 子模块: submodule与subtree的使用》),这样每个仓库都能独立进行各模块的编码、测试和发版。
Multirepo:多仓库
在Multirepo策略中,每个项目或组件都有自己的代码库(one-repository-per-module)。这种方式提倡把项目按模块分为多个代码库,通过科学的分解,可是实现每个repo独立开发和测试,提高开发效率。同时也方便理解各个模板业务
优点:
自主性:团队可以独立管理自己的发布周期和依赖。
灵活性:项目独立,容易适应各自不同的工具链和CI/CD流程。
分散风险:一个项目的问题不会直接影响到其他项目。
缺点:
代码复用:代码重用可能需要额外的版本管理和分发机制。
比如在多个repo间涉及公共组件、公共函数或公共配置(如各种config)时候,代码复用就成了问题。
管理成本:每个项目都需要独立管理CI/CD流程、版本控制等。
各个项目的工作流是割裂的,因此每个项目需要单独配置开发环境、配置 CI 流程、配置部署发布流程等等,甚至每个项目都有自己单独的一套脚手架工具,然而很多情况下这些项目里的基建和逻辑是有很大部分重复的,而且各个项目间存在构建、部署和发布的规范不能统一的情况下维护成本也高了起来。
依赖管理:跨项目的改动可能更难协调。
比如说刚开始一个工具包版本是 v1.0.0,有诸多项目都依赖于这个工具包,但在某个时刻,这个工具包发了一个break change版本,和原来版本的 API 完全不兼容。而事实上有些项目并没有升级这个依赖,导致一些莫名的报错
Microrepo: 独立的可扩展性和自治性
Microrepo并不是一个被广泛认知和定义的术语,可能被视为多仓库管理策略的一种形式。
基于上下文的假设,Microrepo可能指代细粒度分割的代码仓库,即每个小功能或组件都有自己的repository。
Microrepo是亚马逊和Netflix等公司青睐的策略,遵循微服务的理念,将服务代码分离到单独的仓库中。虽然提供了独立的可扩展性,但也可能带来治理方面的挑战。
Microrepo的关键特点包括:
每个服务一个仓库:每个服务或组件都在自己的仓库中,实现自治的开发和可扩展性。
权限和构建配置:权限和构建配置通常在仓库级别管理,使团队拥有其代码库的所有权和控制权。
局部依赖:依赖在每个仓库内部受控,使企业可以根据自己的时间表升级版本。
治理考虑:Microrepo在仓库数量增加时可能引起治理上的问题,需要进行有效的管理和协调。
如果按照这种解释,Microrepo的优缺点与Multirepo相似,但更加强调细粒度的控制和组件化管理。
Monorepo vs. Microrepo
Monorepo适用于大型、相互关联的代码库,而Microrepo更适合较小、独立服务组成的项目。
协作要求:Monorepo促进了代码共享和协作,而Microrepo允许团队自治地进行开发。
可扩展性需求:Microrepo实现了独立的可扩展性,而Monorepo确保了整个代码库的均衡扩展。
治理和管理偏好:Microrepo赋予了自治性,但需要有效的治理和协调,而Monorepo确立了一致的标准和实践。
工具和生态系统:Microrepo支持的工具范围更广,包括Maven、Gradle、NPM和CMake,而Monorepo受益于Bazel和Buck等专用工具链。
导致很多问题:
管理复杂性增加:随着微仓库数量的增加,管理这些仓库(包括权限管理、版本跟踪和依赖配置)的复杂性呈线性上升。
依赖管理挑战:如果多个微仓库间存在依赖关系,任何一个库的更新都可能需要协调更新其他依赖该库的微仓库,这可能导致依赖地狱(Dependency Hell)。
重复工作:完整的 CI/CD 流程可能需要在每个微仓库上独立设置,这可能意味着相似或重复的配置工作。
代码共享困难:代码重用变得更为复杂,需要适当的包管理或其他机制来共享公共代码。
跨仓库协作:当需要实现涉及多个微仓库的跨功能或跨服务特性时,协作可能更为困难,需要跨多个代码库来协调变更。
版本同步问题:保持相关微仓库中的改动同步可能很困难,需要严格的发布流程和版本管理策略。
资源开销:每个微仓库可能都需要自己的开发、测试和部署环境,这可能会导致资源的冗余分配和开销。
权限管理:为每个微仓库设置细粒度的权限可能变得繁琐,尤其是执行跨团队协作时。
失去大局观:由于代码分散在多个仓库中,开发人员可能很难获得系统的整体视图,了解各个部分的协同作用。
git 本身坑
用于切换分支的git checkout命令不再可靠。
子仓库与主仓库同步问题(多人协作的情况下,分仓库要先提)
并不是所有团队都会同时遇到上述问题,但是或多或少总会遇到一些!
单仓多模块(Monorepo):
即Monorepo,其核心是将多个项目放到同一个仓库里面进行管理,不拆分仓库,统一管理各个模块的构建流程、发布流程等等。
如前端项目,可以避免大量的冗余node_module冗余。
单仓多模块并不等同与巨石应用,它通过目录划分为多个子项目,每个项目都可以拆分出来独立使用。在编译构建时,各模块可以并行构建,提高执行效率提升。
Monorepo: 集中化的协作和代码一致性
Monorepo将所有服务和组件集中存放在一个仓库中,如Linux和Windows所示。前端领域,比较知名的像Vue3、babel、npm7都是在用Monorepo方式进行管理的。
谷歌内部的工具链和严格的编码标准有助于其可扩展性和代码一致性。
Monorepo的关键特点包括:
服务组织:每个服务在仓库中表示为一个文件夹,便于代码共享和重用。
权限和所有权:每个服务成员负责其指定的文件夹,具备BUILD配置和OWNERS权限控制。
共享依赖:整个代码库共享依赖,有助于统一版本升级并减少兼容性问题。
代码质量和审核:Monorepo通过严格的代码审核流程鼓励高标准的代码质量。
Monorepo的缺点:
体积问题:因为所有code都在一个repo下,这就导致了随着项目越来越复杂,整个repo的体积会变得很大。据说某大公司员工想要修改项目中的某个小样式,需要将数以十G的repo拉到本地,听起来确实是个噩梦。当然对此问题也可借助git本身提供的一些机制进行优化,有兴趣的可以了解下稀疏检出和浅克隆
权限问题:Monorepo模式下的权限是开放的。代码安全,文档安全,都会是一个需要好好考虑的问题。这个方面如果处理不好的话,对整个团队整个项目带来的后果可能是灾难性的
版本控制:仓库变得太大,对版本控制技术会有很大的挑战。因为 Git 社区建议的是使用更多更小的代码库,Git 本身并不适合单个巨大的代码库
更多的缺点参看:《前端 monorepo 之殇 》
multirepo与monorepo优缺点对比梳理
场景 | multirepo | monorepo |
项目代码维护 | ❌ 多个仓库需要分别download各自的node_modules,像这种上百个包的,多少内存都不够用 | ✅ 代码都只一个仓库中,相同依赖无需多分磁盘内存。 |
代码可见性 | ❌ 包管理按照各自owner划分,当出现问题时,需要到依赖包中进行判断并解决 ✅ 对需要代码隔离的情况友好,研发者只关注自己核心管理模块本身 | ✅ 每个人可以方便地阅读到其他人的代码,这个横向可以为团队带来更好的协作和跨团队贡献,不同开发者容易关注到代码问题本身 ❌ 但同时也会容易产生非owner管理者的改动风险 ❌ 不好进行代码可视隔离 |
代码一致性 | ❌ 需要收口eslint等配置包到统一的npm包,再到各自项目引用,这就允许每个包还能手动调整配置文件 | ✅ 当您将所有代码库放在一个地方时,执行代码质量标准和统一风格会更容易。 |
代码提交 | ❌ 底层组件升级,需要通知到所有项目依赖的相关方,并进行回归 ❌ 每个包的修改需要分别提交 | ✅ API 或共享库中的重大更改能够立即公开,迫使不同的开发者需要提前沟通并联合起来。每个人都必须跟上变化。 ✅ 提交使大规模重构更容易。开发人员可以在一次提交中更新多个包或项目。 |
唯一来源 | ❌ 子包引用的相同依赖的不同版本的包 | ✅ 每个依赖项的一个版本意味着没有版本冲突,也没有依赖地狱。 |
开发 | ✅ 仓库体积小,模块划分清晰。 ❌ 多仓库来回切换(编辑器及命令行),项目一多真的得晕。如果仓库之间存在依赖,还得各种 npm link。 | ✅ 只需在一个仓库中开发,编码会相当方便。 ✅ 代码复用高,方便进行代码重构。 ❌ 项目如果变的很庞大,那么 git clone、安装依赖、构建都会是一件耗时的事情。 |
工程配置 | ❌ 各个团队可能各自有一套标准,新建一个仓库又得重新配置一遍工程及 CI / CD 等内容。 | ✅ 工程统一标准化 |
依赖管理 | ❌ 依赖重复安装,多个依赖可能在多个仓库中存在不同的版本,npm link 时不同项目的依赖可能会存在冲突问题。 | ✅ 共同依赖可以提取至 root,版本控制更加容易,依赖管理会变的方便。 |
代码管理 | ✅ 各个团队可以控制代码权限,也几乎不会有项目太大的问题。 | ❌ 代码全在一个仓库,如果项目一大,几个 G 的话,用 Git 管理可能会存在问题。 ❌ 代码权限如果需要设置,暂时不支持 |
部署(这部分两者其实都存在问题) | ❌ multi repo 的话,如果各个包之间不存在依赖关系倒没事,一旦存在依赖关系的话,开发者就需要在不同的仓库按照依赖先后顺序去修改版本及进行部署。 | ❌ 而对于 mono repo 来说,有工具链支持的话,部署会很方便,但是没有工具链的话,存在的问题一样蛋疼。(社区推荐pnpm、lerna) |
持续集成 | ❌ 每个repo需要定制统一的构建部署过程,然后再各自执行 | ✅ 可以为 repo 中的每个项目使用相同的CI/CD部署过程。 ✅ 同时未来可以实现更自动化的部署方式,一次命令完成所有的部署 |
Monorepo和Multirepo都是管理组织代码的方式,这两者的核心区别可以归结为你相信怎样的哲学能让团队在一起工作的效率最高(多元化 vs 集中管理)!
为什么 Monorepo 现在前端非流行呢?
在大型前端项目中,通常会有很多公共的组件和库被不同的应用或团队使用。
随着时间的沉淀,模块数量也在飞速增长。Multirepo 这种方式虽然从业务逻辑上解耦了,但也同时增加了项目的工程管理难度。组件化的前期可以忽略不计,当模块量到达一定体量程度下,这个问题会逐渐明显。比如:
代码和配置很难共享:每个仓库都需要做一些重复的工程化能力配置(如 eslint/test/ci 等)且无法统一维护,当有工程上的升级时,没能同步更新到所有涉及模块,就会一直存在一个过渡态的情况,对工程的不断优化非常不利。
依赖的治理复杂:模块越来越多,涉及多模块同时改动的可能性急剧增加。如何保障底层组件升级后,其引用到的组件也能同步更新到位。这点很难做到,如果没及时升级,各工程的依赖版本不一致,往往会引发一些意想不到的问题。
存储和构建消耗增加:假如多个工程依赖 pkg-a,那么每个工程下 node_modules 都会重复安装 pkg-a,对本地磁盘内存和本地启动都是个很大的挑战,增加了开发时调试的困难。而且每个模块的发布都是相对独立的,当一次迭代修改较多模块时,总体发布时效就是每个发布流程的串联。对发布者来说是一个非常大的负担。
Monorepo 架构使得共享和重用代码变得更容易,因为所有的代码都存储在同一个版本库中。
monorepo相较于Multirepo(git submodules)最大的优势在于统一了工作流,去除了冗余的node_module(非常占用磁盘空间)等。
目前很多开源项目都用了 monorepo,比如react、Angular、 babel、nuxtjs 都使用了 lerna 来管理项目。
Microsoft: 通过使用 Monorepo, 在其 TypeScript、Visual Studio Code 等项目中保持了代码的一致性和同步。Windows Terminal、Fluid Framework也采用Monorepo,Rush(Rush Stack)专门为管理Monorepo设计
Google: Angular、TensorFlow.js、Material Components Web (MDC Web)等
Facebook: React、Jest、React Native 等。
Alibaba:Arale(包括Alibaba UI、Rax、Rax-Component)、umi、egg等
Google 和 Meta,他们都是大仓实践的典范。Twitter、Airbnb、Uber等大公司纷纷跟风monorepo
在实际场景来落地 Monorepo,需要一套完整的工程体系来进行支撑,因为基于 Monorepo 的项目管理,绝不是仅仅代码放到一起就可以的,还需要考虑项目间依赖分析、依赖安装、构建流程、测试流程、CI 及发布流程等诸多工程环节,同时还要考虑项目规模到达一定程度后的性能问题,比如项目构建/测试时间过长需要进行增量构建/测试、按需执行 CI等等,在实现全面工程化能力的同时,也需要兼顾到性能问题。
Monorepo 只是一个管理概念,实际上它并不代表某项具体的技术,更不是所谓的框架。开发人员需要根据不同场景、不同的研发习惯,使用相应的技术手段或者工具,来达到或者完善它的整个流程,从而达到更好的开发和管理体验。
怎么使用Monorepo?
一般主流采用的都是yarn的workspace和lerna来管理monorepo。
之前前端缺少类似 .net 的 project 和 solution 的概念。一个 project 是一个独立代码库库,多个 project 组成一个 solution。
在Java的生态系统中
Maven:使用POM(Project Object Model)文件来描述项目配置、依赖和构建的生命周期。在Maven中,一个单独的项目由一个pom.xml文件定义。Maven也支持多模块项目,其中一个父项目(包含一个pom.xml文件)可以管理多个子项目
Gradle:Gradle是另一种流行的构建和项目自动化工具。它使用Groovy或Kotlin DSL来描述项目配置。类似于Maven,Gradle也可以支持多个项目构成的构建,被称作多项目构建(multi-project builds)。在Gradle中,根目录下的settings.gradle(或settings.gradle.kts)文件用来包含子项目(子目录中通常有单独的build.gradle或build.gradle.kts文件)
前端就是一个库一个项目,然后一些通用逻辑,组件等都是跟着项目走的,要想跨项目无非就是 npm 包或者拷贝代码了。
Maven和Gradle,它们集成了编译、打包、测试、部署等一系列构建过程,而且拥有中央仓库来管理依赖。Java发展到现在,Maven以及Gradle功不可没。前端这方面可以说是稀烂!
前端的包管理
Node.js 没有传统的隔离环境,每个项目的 node_modules 自成体系。
java的Maven诞生于2004年 ,应该是各语言的依赖管理工具中早的。
优点:统一的项目模型,生命周期管理,插件生态丰富,依赖管理自动化,跨平台项目构建能力强,持续集成支持良好。
缺点:配置文件复杂,XML格式繁琐,学习曲线相对陡峭,构建速度可能因为依赖解析而变慢。
Ruby的gems也是2004年出现的,但gem离完备的依赖管理工具还差些,直到Ruby的bundler出现。
Python的pip出现的更晚,在2008年。
Node.js 的npm最初发布于2010年。
PHP 的 Composer,在2012年推出。
Ruby、PHP、Python 依赖冲突问题都比较恶心!
我们可以看下npm的发展史:
初创期及扩张 (2010 - 2014)
2010年:npm 首次发布,随 Node.js 一起,由 Isaac Z. Schlueter 开发,最初是作为 Node.js 的一个组件被创建出来。
2011年:npm 1.0 发布,新增了 npm link 等命令,重视核心性能,支持全局和本地安装,并提供了版本管理功能。
成熟与优化 (2015 - 2017)
2015年:npm 3.0 推出,引入了平级依赖(peer dependencies),尝试解决依赖树中的版本冲突问题,并首次实现了扁平化的 node_modules,减少了重复安装相同模块的情况。
2016年:引进了安全机制,npm 开始扫描和报告已知的安全漏洞,并逐渐对这些功能进行升级。
2017年:认识到了以前版本对于锁文件的缺失,npm 5.0 发布,这是 npm 的一个重大更新,新增 package-lock.json 文件来确保依赖版本的一致性,并通过更快的安装速度和改进的缓存机制来提高性能。
竞争与特性重塑 (2018 - Present)
2018年:npm 6.0 旨在提高性能和安全性,内建 npm audit 命令,并支持自动修复漏洞。
2020年:npm 7.0 发布,重要的更新包括了对工作区(workspaces)的支持,允许开发者在单个项目中管理多个包;增强了 package-lock.json,以支持新的锁文件版本;改进了自动冲突解决和 peer dependencies 的处理。
2021年:npm 7 开始内置在 Node.js 发布版本中,npm 队伍还致力于增强性能、改进用户体验,并解决长期存在的问题。例如,npm 被 GitHub 收购,这有可能意味着它的社区和企业支持将会得到加强。
我们可以用npm把各个功能模块封装为package,做工程化管理,如果最常用的包改动后会影响大量依赖此包的项目
lerna
lerna 主流应用在处理版本、构建工作流以及发布包等方面都比较优秀,既兼顾版本管理,还支持全量发布和单独发布等功能。
在前端领域,它是最早出现也是相当长一段时间 monorepo 方案的事实标准,具有统治地位,很多后来的工具的概念或者 workspaces 结构都借鉴了 lerna,是 lerna 的延续。
Lerna 现在已经被很多著名的项目组织使用,如:Babel, React, Vue, Angular, Ember, Meteor, Jest 。
但没有一套构建、测试、部署的工具链,整体 Monorepo 功能比较弱,但要用到业务项目当中,往往需要基于它进行顶层能力的封装,提供全面工程能力的支撑。
在业界实践中,比较多的时间上,都是采用 Yarn 配合 lerna 组合完整的实现了 Monorepo 中项目的包管理、更新到发布的全流程。
这里关于他不过多介绍,直接参看官网:https://lerna.js.org/
使用lerna bootstrap --hoist可以将子项目的node_modules提升到顶层,解决node_modules重复的问题。
但是lerna bootstrap --hoist在提升时如果遇到各个子项目引用的依赖版本不一致,会提升使用最多的版本,从而导致少数派那个找不到正确的依赖,发生错误。
可以使用yarn workspace,他也会提升用的最多的版本,但是会为少数派保留自己的依赖在自己的node_modules下面。
Turborepo
上述提到传统的 Monorepo 解决方案中,项目构建时如果基于多个应用程序存在依赖构建,耗时是非常可怕的。Turborepo 的出现,正是解决 Monorepo 慢的问题。
Turborepo 是一个用于 JavaScript 和 TypeScript 代码库的高性能构建系统。通过增量构建、智能远程缓存和优化的任务调度,Turborepo 可以将构建速度提高 85% 或更多,使各种规模的团队都能够维护一个快速有效的构建系统,该系统可以随着代码库和团队的成长而扩展。
推荐导读:https://vercel.com/blog/vercel-acquires-turborepo
pnpm + lerna-lite + turborepo
推荐阅读:《Monorepo,大型前端项目管理模式实践 》
Rush
rushstack,提供从初始化、开发、构建、测试到部署的全流程能力,有一套比较完整的 Monorepo 基础设施,适合直接拿来进行业务项目的开发。
这个正在研究,准备单独放一篇文章!
微前端/Monorepo
微前端的概念是由ThoughtWorks在2016年提出的,它借鉴了微服务的架构理念,将Web应用由单一的单体应用,转变为多个小型前端应用聚合为一的一种手段,一种面向垂直划分系统的前端集成。
核心在于将一个庞大的前端应用拆分成多个独立灵活的小型应用,每个应用都可以独立开发、独立运行、独立部署,再将这些小型应用融合为一个完整的应用,或者将原本运行已久、没有关联的几个应用融合为一个应用。
微前端既可以将多个项目融合为一,又可以减少项目之间的耦合,提升项目扩展性,相比一整块的前端仓库,微前端架构下的前端仓库倾向于更小更灵活。
微前端方案,可以大幅降低接入旧项目的成本,使用新技术开发新的功能或重构部分旧的功能模块。更多推荐阅读《微前端学习笔记》。
微前端不是新技术,是构建web应用的策略和方法,也就是前端架构管理的一种模式;
微前端鼓励团队拆分,各司其职,各个团队负责的模块可独立开发、构建、测试和部署;
微前端是将体量大、复杂难以管理的单体应用,拆分成多个微型前端应用,且这些微应用功能上是相互关联的,最终重新聚合成一个统一应用。
概念图
微前端的思路是,把巨石应用拆分成多个微型应用,每个微应用都有自己独立的构建体系。任务并行执行,最大限度的使用线程,以空间换时间,构建速度的问题自然就解决了。
Monorepo 可以协助微前端项目,因为它可以提供一个集中式的代码库和版本控制系统,使得多个微前端应用可以共享代码和资源,并且可以更容易地进行协作和集成。通过 Monorepo,可以更容易地管理共享的组件、库和工具,以及更方便地进行测试、构建和部署。
文章以及超过数据库预先设置了,项目落地还是单独放一篇文章写。
Micro Frontends with Module Federation in Monorepo
Module Federation 是 webpack5 提供的用于应用之间共享模块的机制——不仅可以动态地加载代码,同时还可以共享依赖!
只要用 ModuleFederationPlugin 声明 exposes 的模块,另一个应用里用 ModuleFederationPlugin 声明 remotes 导入的模块,就可以直接用别的应用的模块了。这就是它为什么会叫模块联邦。
Module Federation 由Webpack5 核心开发者 Zack Jackson 提出的
Module Federation is a Webpack 5 super power plugin which offers an improved approach to micro frontends in both developer experience and application performance. The setup is rather straightforward and enables dynamic imports from other micro frontends in runtime.
Module Federation 是天生的模块级微前端! 具体应用推荐阅读《探索 webpack5 新特性 Module federation 在腾讯文档的应用》
相比qiankun,MF 理论上还是基于 webpack 打包后代码的异步调用,又是共用运行时,远程模块只是作为组件使用,在性能上损耗较少。
Module Federation是去中心化部署应用集群的思想,比较适合多个相对独立的应用,但又需要互相调用对方核心能力的场景,例如 APP 内存在多个 H5 应用,这些 H5 在大多时候都是独立运行的,组成了 APP 内丰富的二级页面,但有时候又需要业务之间又需要互相调用对方的能力(例如某业务模块和订单模块独立部署,但业务有时候需要在页面中展示购买页面)
qiankun 由于是应用级别的加载,又由于沙箱的存在,其性能会有较大的损失,更适合在 PC 端拥有较好性能浏览器中使用。而 H5 的单个页面内容相对较少,对性能要求更高,因遵循“最小模块”加载的原则,不论从功能上还是性能上都不适合使用 qiankun,而作为模块加载 MF 相比之下可能会有更大的发挥空间。
参考文章:
Monorepo vs. Microrepo: 选择适合你的代码仓库策略 https://blog.csdn.net/weixin_37604985/article/details/131198986
"分"与"合"的哲学碰撞,Monorepo vs Multirepo https://juejin.cn/post/6949882490324516894
架构之路-你不知道的git Submodules,monorepo,Module Federation https://juejin.cn/post/7017817146314981413
前端 Monorepo 在字节跳动的实践 https://zhuanlan.zhihu.com/p/640021345
前端monorepo大仓权限设计的思考与实现 https://cloud.tencent.com/developer/article/2378150
漫谈依赖管理工具:从Maven,Gradle到Go https://cloud.tencent.com/developer/article/1122163
现代前端工程为什么越来越离不开 Monorepo? https://juejin.cn/post/6944877410827370504
Monorepo,大型前端项目管理模式实践 https://tech.taobao.org/news/ulamrkootb5ii314
为什么说 webpack 的 Module Federation 天生是模块级的微前端? https://zhuanlan.zhihu.com/p/614907086
从零开始打造基于Module Federation的微前端架构(一)——Monorepo架构与远程模块集成 https://juejin.cn/post/7265524106817503295
使用mono-repo实现跨项目组件共享 https://dennisgo.cn/Articles/Engineering/mono-repo.html
https://michalzalecki.com/micro-frontends-module-federation-monorepo/
https://www.wobushi.top/2021/微前端框架初探,对比qiankun和Module-Federation/
转载本站文章《从Microrepo与Monorepo发微到微前端:前端项目架构学习笔记》,
请注明出处:https://www.zhoulujun.cn/html/webfront/engineer/Architecture/9028.html