在上文中,笔者介绍了 Monorepo 这一项目组织方式、Lerna 这个用于便捷管理 monorepo 项目的工具以及它们在开源项目中的应用。那这一次就来聊聊 Monorepo 如何在 WebApp 中落地。
优点与缺点
对于大型开源项目来说,MonoRepo 是利远大于弊的,这也使得 babel、react-router 等知名开源项目纷纷采用 MonoRepo 来进行项目管理;但是对 WebApp 来说,利与弊乍一看似乎很难讲清楚。但两者之间依然存在共通之处:
- 多模块可以更好的实现功能解耦,让模块可以独立开发、测试、发布和部署
- 跨模块的调用采用依赖包引用的方式进行而不是相对/绝对路径引用的方式,避免了文件搬运时的路径修改文件
此外,WebApp 使用 MonoRepo 进行项目管理,还有以下好处:
- 当其他 Web 项目与当前项目存在一定共通之处时,可以方便的让其他项目使用对应模块的功能(如工具函数),而不需要进行代码复制
- 可以将某一个模块作为
模板
模块,在开发新功能时通过 CLI 或其他方式快速创建模块原型,节省前置准备时间
当然,在 WebApp 中使用 MonoRepo 并不是一件简单的事情,相比常规的项目组织方式,Monorepo 会在不少方面对开发者提出一些难题与挑战:
- 雷同的配置(test、webpack)会在各模块间存在多份,因此需要将这些配置也封装为一个模块,并被各模块引用;或者使用
create-react-app
(以下简称为cra
)等工具来减少手动配置,但这类工具也会限制开发者 - 模块间需要小心的进行版本管理,避免引用错误的兄弟模块导致报错
项目结构
我们已经知道,一个典型的 MonoRepo 项目具有以下项目结构:
1 | ├── packages |
在开源项目中,pkg1、pkg2 与其他子模块可能都是独立发布的依赖包,而根(root)目录的package.json
文件中一般只包含风格检查(lint)、测试(test)、编译(compile)、打包(build)等相关任务的描述和脚本,所有具体的功能都在子模块中完成;而 WebApp 中则有所不同——子模块具有不同的类型——我们可以把子模块按功能、职责分为以下类型:
- 应用入口(entry):作为项目部署的直接目标,包含路由配置(router)、模板实现(layout)、静态资源(asset)等组成单位,但不涉及具体功能的实现。
- 功能模块(module):可以独立运行、部署的功能模块,每个功能模块负责某一部分功能的实现。
- 公共依赖(common):同时会被应用入口与功能模块所依赖的项目级基础设置,如通用配置(config)、工具函数(utils)、基础布局(layouts)、通用组件(components)、持久层基类与启动配置(core)。
- 模板模块(template):作为功能模块的模板,用于在新增功能模块时可以快速创建模块原型。
在这个基础上,我们可以将项目结构细化为:
1 | ├── packages |
基础实现
技术栈:TypeScript+React+Mobx+React-Router
为了简化实现,我们采用cra
作为模板进行模块创建,并在此基础上做一些调整
具体实现的源码,可以参考monorepo-react
应用入口
一个应用入口模块应该具有以下基本目录结构:
1 | ├── app |
我们在入口的package.json
添加对其余模块的依赖,保证 lerna 在运行编译脚本时可以根据依赖关系控制顺序,且 app 模块的基本逻辑如下:
1 | // src/App.tsx |
可以看到,app 模块以依赖包的形式引用了兄弟子模块中的layout
与template
,用于生成路由配置和导航菜单。同时通过修改 webpack 配置中的resolve.mainFields
字段值,扩展出一个自定义字段,让 app 模块可以直接引用配置了该字段的功能模块
的入口组件,而不是编译生成的代码。在这样的机制下,就能像正常 spa 项目那样开发页面级的功能模块了
模板模块
模板模块的功能是为其他功能模块(或者说页面)提供一个代码原型,借助 cli 等工具可以快速创建新的功能模块,并且可以根据项目的迭代随时优化模板的实现逻辑,以适应项目的需要。
模板模块(以及基于模板产生的功能模块)会包含自身所需的各个组成部分,能够独立的运行、开发、测试、编译、部署,也可以作为入口模块的一部分使用
一个模板模块大致会具有以下结构
1 | ├── template |
其中,针对单模块启动 devServer 时,入口文件为index
;而将模板/功能模块作为入口模块的一个页面时,入口文件为App
(需要在package.json
中指定)。
对于模板/功能模块的持久层技术栈,笔者这里选择了跟多模块天然贴切的mobx
全家桶,当然redux
或者其他持久层技术栈也是可以的,但需要一些额外的步骤(比如将子模块的持久层作为单一 store 树中的一个节点)进行处理,这里就不展开讲解了,有兴趣的朋友可以自己尝试。关于模板模块中各个部分的实现思路,可以参考React组件设计模式(2)文章中的介绍。
基础模块
core
core 模块是整个项目中整合核心逻辑的一个库,在笔者的 demo 项目中,主要是包含了依赖注入的相关实现、全局 loading 的实现以及对使用的 ui 组件库中经常发生交互与状态变更的组件(如 antd 的 Table、Form)的外置状态类的实现
cra
也可以命名为 config 或 scripts 等,主要是包含了入口模块、模板模块与各个功能模块需要的 webpack 等工具的配置,以及基于模板模块创建功能模块的 cli 脚本,作用类似于create-react-app
提供的react-scripts
库。若直接使用官方 cra 创建项目,那这里基本就是包含了基于customize-cra
工具的一些用于修改 cra 基础配置的工具函数
总结
基于 Monorepo 的 WebApp 可以享受到这一系统设计方案的种种利好,并通过一些额外的处理逻辑来规避可能出现的缺陷,让整个系统的开发效率和可维护性得到极大的提高。同时,这种设计思路也许可以作为微前端的雏形,通过进一步的框架调整来实现真正的微前端应用。