5 种复用项目业务模块的方案

在不同项目中复用业务模块,主要有封装 npm package、Git submodule、Monorepo、依赖外部化(external)+ CDN 引入、模块联邦(Module Federation)等几种解决方案。

各种方案的实现原理不同,不同方案有各自的优缺点,需要根据实际需求进行选择,同时也需要注意如何处理版本同步、配置差异等问题。

封装 NPM package
原理

将业务模块的代码、依赖和配置打包成一个可重用的 NPM package,并上传到公共或私有的 NPM 仓库中。然后在需要的项目中引用该 package。

npm 包的发布更新大概流程 ⛳️
优点
  • 简单易用,适用于较大的业务组件或组件库等大多数场景。
缺点
  • 开发效率问题,每次改动都需要发布新版本,相关项目则需要重新安装新依赖,不适合高度动态的业务模块。流程比较复杂,需要考虑如何进行版本控制和发布管理。有时也限制了业务模块的可配置性和差异性。
  • 构建产物较大,引入了公共库之后,公共库的代码都需要打包到项目最后的产物中,导致产物体积偏大,构建速度相对较慢。(tree shaking能解决一些体积的问题)。
Git submodule
原理

将业务模块作为 Git 的子模块加入到项目中,并将其导入到需要的特定位置,通过 Git 管理多个不同的仓库。

优点
  • 仓库相互独立,主仓库和子仓库都有各自的 git 记录,克隆主仓库时不会克隆子仓库的文件,只是克隆一个快链(gitlink)。
缺点
  • 学习成本较高,需要熟练掌握 Git 子模块的使用和管理,也需要确保团队成员都具有相应的 Git 知识。
  • 项目结构复杂,需要维护多个仓库之间的版本和依赖关系。(版本更新整体流程与npm包没有太大的区别。子应用更新提交到 Git 远程仓库,主应用通过git命令更新子仓库内容,然后进行联调。)
Monorepo
原理

Monorepo 是一种项目管理方式,将多个项目以及共享代码、库、配置和工具等资源放在同一个 Git 仓库中进行管理,并使用特定的工具集进行管理和维护。

优点
  • 统一项目和代码库的管理,避免冗余和版本冲突。
  • 代码复用方便,互相依赖的子项目通过软链的方式进行调试。如果有依赖的代码变动,那么用到这个依赖的项目当中会立马感知到。
缺点
  • 学习成本较高,这种方法需要熟悉 monorepo 的概念和工具,如 Lerna 或 Yarn Workspaces 等,同时也需要考虑如何管理多个项目之间的依赖关系,以及如何在不同的项目中处理可配置性和差异性等问题。
  • 改造成本较高,如果是旧有项目,并且每个应用使用一个 Git 仓库的情况,那么使用 Monorepo 之后项目架构调整会比较大,改造成本也比较高。
  • 构建产物较大,跟发npm包的方案一样,所有的公共代码都需要进入项目的构建流程中,产物体积还是会偏大。
  • 开发效率问题,Monorepo 本身也存在一些天然的局限性,如项目数量多起来之后依赖安装时间会很久、项目整体构建时间会变长等等,我们也需要去解决这些局限性所带来的的开发效率问题。而这项工作一般需要投入专业的人去解决,如果没有足够的人员投入或者基建的保证,Monorepo 可能并不是一个很好的选择。
依赖外部化(external)+ CDN 引入
原理

依赖外部化 + CDN 引入方案是一种通过网络引入第三方库和框架代码的方法。将应用程序和第三方库分离开来,应用程序只包含自己的代码和其他公用的库,而第三方库通过 CDN 或其他网络资源准备好并提供引入。

具体步骤 ⛳️
  1. 将第三方库的 JS、CSS 资源从应用程序中独立出来,形成一个单独的 bundle
  2. 将该 bundle 上传至 CDN 并根据 CDN 的 URL 进行引入
  3. 在应用程序中去掉待引入的 CDN 对应库的 import 引入语句
  4. 在 HTML 页面中通过 script 或 link 标签引入第三方库所对应的 URL
  5. 运行应用程序时,浏览器会从 CDN 上下载第三方库,而不是从应用程序中加载它

Vite中,先使用external排除要打包的依赖

// vite.config.ts export default defineConfig({ build: { rollupOptions: { external: ['react', 'react-dom'], }, }, });

然后在index.html中通过<script>引入 CDN 中的第三方依赖,从而达到模块复用的效果

<!-- index.html --> <script src="https://cdn.jsdelivr.net/npm/react@17.0.2/index.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/react-dom@17.0.2/index.min.js"></script>
优点
  • 节省网页加载时间和增加了并发请求数,由于使用了 CDN,第三方库的文件也存储在许多地方,因此每个用户下载的版本都可以进行缓存和复用。另外,开发者也可以使用最新版本的库,因为 CDN 在发布的同时会立即更新版本。
缺点
  • 兼容性问题,并不是所有的依赖都有UMD格式的产物,因此这种方案不能覆盖所有的第三方npm
  • 依赖顺序问题,我们通常需要考虑间接依赖的问题,如对于antd组件库,它本身也依赖了reactmoment,那么reactmoment也要进行外部依赖处理,并且在HTML中引用这些包,同时也要严格保证引用的顺序,比如说moment如果放在了antd后面,代码可能无法运行。而第三方包背后的间接依赖数量一般很庞大,如果逐个处理,对于开发者来说简直就是噩梦
  • 产物体积问题,因为是全量引入模块代码,就没有办法通过Tree Shaking去除无用代码,会导致应用的性能有所下降
  • 网络故障问题,引入的库可能会被 CDN 网络故障影响,此时需要考虑备选 CDN 的使用
  • 限定了第三方库参数的可配置性,我们无法对库进行配置,因此,在项目中使用该方案需要根据需求进行权衡
模块联邦(Module Federation)
原理

模块联邦(Module Federation)是一种新型的前端代码共享方案,可用于将来自不同应用程序的代码集成到一个单一应用程序中。

  • 每个应用程序可以按照其自身的业务需求进行独立构建和部署,同时将一部分模块作为域导出(expose),供其他应用程序使用。
  • 对于需要导入模块的应用程序,可以通过远程加载(remotes)的方式动态加载其他应用程序中相应的模块,并按需渲染到自己的应用程序中。

它可以让每个应用程序拥有自己的代码处理逻辑,同时可以将其他应用程序中的功能作为模块直接集成到自己的应用程序中,使它们之间可以轻松共享模块和代码。

优点
  • 可以灵活地进行代码共享和部署,实现真正的动态模块共享,适用于微前端架构
  • 可以显著减少重复的代码下载和解析,降低应用程序的网络请求和延迟时间
缺点
  • 对于一些需要进行深度集成和沟通的应用程序,需要在架构层面进行适当的调整
  • 目前仅适用于 Webpack 5 与 Vite3 及以上版本,与其他构建工具的兼容性有限
https://juejin.cn/post/7147331959650254862#heading-0