使用git子模块和package.json工作区实现多项目开发
前言
随着前端技术的飞速发展,目前无论是公司还是个人基本都不需要再去从零到一去搭建一个开发框架,但即使拥有一套成熟的产品依旧避免不了客户定制化的需求,本文主要是为了解决在使用基础框架进行多项目开发的痛点。
按照以往的方式,我们会将基础框架作为模板去生成一个项目仓库进行开发,但这个过程中就会产生一个问题,就是关于基础框架内容的更新如何去同步到各个项目上,本文将介绍如何通过另一种方式解决这个问题。
一、git Submodule介绍
- 什么是子模块(Submodule)
Git子仓库是Git提供的多仓库管理机制,允许在一个主仓库中嵌套引用其他Git仓库。子模块是指向另一个仓库的指针,在主仓库中存储的是子仓库的提交哈希,而不是实际文件。子仓库保持完全独立,主仓库只记录引用。 - 为什么需要子模块?
- 传统多仓库管理的痛点
- 代码复用困难:公共组件库需要复制粘贴或手动维护
- 版本同步问题:多个项目引用同一组件,更新后需要逐个同步
- 依赖管理复杂:手动管理第三方库的版本和更新
- 历史记录分离:组件修改历史与主项目分离,难以追溯
- 子仓库的优势
- 代码复用:公共代码可被多个项目引用
- 版本控制:每个子仓库独立版本管理
- 权限隔离:不同团队可独立维护不同子仓库
- 历史记录完整:子仓库保留完整提交历史
- 传统多仓库管理的痛点
- 子模块配置
# 添加子模块到指定目录
git submodule add <repository_url> <path>
# 示例:添加utils子模块到libs/utils目录
git submodule add https://github.com/example/utils.git libs/utils
执行后会在:
- 当前目录创建libs/utils(子仓库文件)
- 根目录创建.gitmodules文件(子模块配置)
- .git/config中添加子模块配置
# 拉取指定子模块
git submodule update --init --recursive <path>
# 拉取所有子模块
git submodule update --init --remote
# 示例:拉取utils子模块
git submodule update --init --recursive libs/utils
执行后可以看到libs/utils拉取了对应仓库的代码,且包含.git文件
- 子模块拉取/提交代码
# 进入子模块目录
cd libs/utils
# 拉取最新代码
git pull origin main
# 返回主仓库,提交子模块更新
cd ../..
git add libs/utils
git commit -m "chore: update utils submodule"
- 删除子模块
git submodule deinit libs/utils
git rm libs/utils
二、package.json Workspaces介绍
- 什么是工作区(Workspaces)?
Workspaces是npm/yarn/pnpm等包管理工具提供的一种多包管理机制,允许在单个代码仓库(monorepo)中管理多个相互依赖的包,将多个子包纳入统一管理,自动提升公共依赖至根节点并建立符号链接,实现依赖去重、本地包直接引用、统一流程执行和版本协同。 - Workspaces配置
private:指示 npm 将该项目视为私有项目,禁止将其意外发布到公共的 npm 注册表,使用工作区必须指定workspaces:配置工作区路径,数组或对象格式,支持通配符模式匹配(如packages/*),路径可以是相对路径或绝对路径。配置后会根据路径去匹配,获取工作区目录下的package.json。
npm/yarn配置
根目录package.json
{
"name": "framework",
"version": "1.0.0",
"private": true,
"workspaces": [
"src/project/*"
],
"dependencies": {
"qs": "6.13.0"
}
}
工作区package.json(如:src/project/projectA/package.json),必须包含name和version
{
"name": "projectA",
"version": "1.0.0",
"dependencies": {
"lodash": "^4.17.21"
}
}
pnpm配置
根目录package.json
{
"private": true
}
根目录pnpm-workspace.yaml
packages:
- src/project/*
工作区package.json与上述相同
- 实现效果
以上面配置为示例,目录结构如下:
framework
|–src
|----project
|------projectA
|--------package.json
|--------…
|------projectB
|–package.json
虽然project目录下同时存在projectA和projectB目录,但是由于projectB下没有package.json,因此projectB不会被视为一个工作区,当我们在项目根目录下打开终端运行npm install命令,完成后我们可以看到即使根目录下的package.json没有lodash依赖,但是由于工作区的关系它也被安装了下来,因此使用workspaces我们可以很好的对不同项目特有的依赖去各自进行维护,不需要因为项目开发而污染基座框架的依赖。
- 常见问题
- 不同工作区依赖相同包的不同版本
如:packages/utils依赖 lodash@^4.0.0,packages/components依赖 lodash@^3.0.0 - 工作区包与根目录依赖版本冲突
根目录安装的共享依赖版本与工作区包声明的版本不匹配。 - 工作区之间的循环依赖
包A依赖包B,包B又依赖包A,形成循环引用。‘
- 不同工作区依赖相同包的不同版本
不同包管理器的处理机制差异
| 包管理器 | 依赖提升策略 | 冲突处理方式 | 符号链接机制 |
|---|---|---|---|
| npm | 将依赖提升到根node_modules | 优先安装满足所有包要求的最高版本 | 为工作区包创建符号链接 |
| yarn | 使用PnP或node_modules | 使用版本解析算法,可能安装多个版本 | 使用.yarn/cache和符号链接 |
| pnpm | 使用内容寻址存储 | 严格遵循版本声明,可能安装多个版本 | 使用硬链接和符号链接 |
关键区别:
- npm/yarn会尝试"依赖提升"(hoisting)到根目录
- pnpm默认不提升,每个包有独立的node_modules
- 冲突时,npm/yarn可能安装多个版本,pnpm更严格
解决方式:
- 使用overrides字段(npm 8.3+)强制统一版本
- 在根package.json中安装共享依赖
- 使用peerDependencies声明兼容版本
- 工作区常用命令
# 安装工作区依赖
npm install lodash --workspace=projectA
yarn workspace projectA add lodash
pnpm add lodash --filter projectA
# 删除工作区依赖
npm uninstall lodash --workspace=projectA
yarn workspace projectA remove
pnpm remove lodash --filter projectA
三、完整示例
- 最终目录结构
├─┬sample
│ └──┬src
│ └────┬modules
│ ─────└──┬projectA --项目A
│ ────────└──┬views
│ ────────└──┬components
│ ────────└──┬package.json
│ ─────└──┬projectB --项目B
│ ────────└──┬views
│ ────────└──┬components
│ ────────└──┬package.json
│ └───.gitmodules
│ └───package.json
- 配置子模块
git submodule add https://xxxxx.git src/modules/projectA
git submodule add https://xxxxx.git src/modules/projectB
- 配置工作区(使用pnpm为例)
根目录package.json
{
"name": "my-app",
"version": "1.0.0",
"private": true
}
根目录pnpm-workspace.yaml
packages:
- src/modules/*
- 项目A开发人员操作步骤
# 1.拉取my-app仓库
# 2.在my-app仓库根目录打开终端运行
git submodule update --init --recursive src/modules/projectA
# 3.拉取成功后进入src/modules/projectA目录下执行git命令先checkout到主分支(如:main),再创建各自的开发分支
cd src/modules/projectA
git checkout main
git checkout -b xxx/main
# 4.回到my-app根目录安装依赖
pnpm install
# 5.运行项目进行开发
pnpm run dev
# 6. 提交项目A代码
cd src/modules/projectA
git pull origin main
cd ../..
git add src/modules/projectA
git commit -m "xxxx"
四、结论
通过git子模块结合包管理工具的工作区,我们能实现在一套基础框架下进行多项目开发,既保持基础框架的简洁,又保证了公共代码可被多个项目引用的同时能持续更新,各个项目的依赖保持独立,每个项目又拥有完整的提交历史。
五、扩展
- 如使用
Jenkins等进行自动化构建工具,也可以实现分项目构建,通过添加构建选项,如项目下拉选择,再添加构建脚本,通过脚本去手动执行拉取对应项目仓库,再进行依赖安装和构建,实现分项目构建的目的。 - 对于项目包含静态资源的情况,有两种方式处理:第一种方式就是把项目的静态资源也作为一个子模块来维护,路径如
public/static/modules/projectA。第二种方式就是在静态资源放在项目目录下,再添加一个脚本,脚本种包含拉取子仓库及将静态资源移动到框架的public/static/modules/xxx下,后续通过执行脚本来拉取即可。注意需要在基础框架的.gitignore中去配置忽略这个静态资源目录,若脚本放置位置不在根目录下也需注意构建编译时不需要编译脚本,不然容易出现内存泄漏的问题。
更多推荐
所有评论(0)