news 2026/9/18 5:20:08

Git Submodule实战:从添加子模块到递归克隆与踩坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Git Submodule实战:从添加子模块到递归克隆与踩坑指南

前阵子接手一个微服务仓库,里面要复用三个公共库:一份API定义、一套工具函数、一份配置模板。最初大家都是复制粘贴,结果每次一改接口,其他服务马上编译失败,喊破嗓子才对齐。后来我把这三个公共库改成 git submodule 管理,主仓库用 git submodule add 统一挂载,新人拉代码时一条 git clone --recurse-submodules 就能把子项目全部递归拉下来,再也没人跑来找我“缺文件”。这篇就当写给同样被多仓库依赖折磨的人,从添加子模块开始,到递归克隆的细节,一次讲完。

1. git子模块是什么,为什么需要它

1.1 从一个真实的项目场景说起

我先说说什么时候需要子模块。假设你现在负责一个主项目,它必须依赖另一个还在活跃开发的仓库。这个依赖不是发布到 Maven、npm 或者 Go Module 里的固定制品,而是源码本身,比如团队内部公共组件库、接口定义仓库、配置模板仓库。公共库和主项目必须保持版本匹配,甚至公共库的每次更新都会直接影响主项目的行为。

遇到这种情况,有人会选择把公共仓库直接复制到主项目里,但这种做法很快会失控:公共库一旦更新,所有复制过代码的项目都要手动同步,改漏了就是线上事故。也有人用 Git Subtree,它能把外部仓库的历史合并进当前仓库,但操作复杂度高,团队里不是每个人都熟悉那套命令。相比之下,git submodule 的思路非常直白:在主仓库里记录“依赖哪个仓库的哪个提交”,而不是记录依赖的具体内容。这个设计天然解决了“主项目需要锁定子项目版本”的核心诉求。

子模块还有一个明显优势,就是权限和协作方式完全复用 Git 本身。成员只需要有对应仓库的访问权限,就能在主项目里直接进入子模块目录改代码、提交、推送,不需要经过任何中间发布流程。这也让它特别适合“项目代码和公共代码一起演进”的场景。

1.2 子模块的底层原理:gitlink与.gitmodules

要理解子模块,必须先弄清楚两个东西:gitlink 和.gitmodules文件。

gitlink 是 Git 索引(index)中的一种特殊记录。普通文件在索引里是 blob 对象,路径后面跟着文件模式100644100755,而子模块目录在索引里变成模式160000,后面跟的不是文件内容,而是子模块仓库某个 commit 的哈希值。用git ls-files --stage看主目录,你会看到类似这样的输出:

160000 commit 0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b common-lib

这行记录的意思很明确:主仓库的common-lib路径下,应该有一个 Git 仓库,而主仓库只认0a1b2c3d...这个提交。至于这个仓库从哪里来、地址是什么,Git 会去读.gitmodules文件。

.gitmodules是主仓库根目录下的一个普通文本文件,内容大致如下:

[submodule "common-lib"] path = common-lib url = git@github.com:yourname/common-lib.git branch = main

这个文件里保存了子模块的逻辑名、路径、远程地址,如果添加时指定了分支,还会记录 branch 字段。它会被提交到主仓库,所有拉取主仓库的人都靠它来识别子模块信息。子模块本身的 Git 历史则被放在两个地方:一个是你本地.git/modules/common-lib目录,另一个是检出目录common-lib/.git,后者其实是一个文件,内容指向前者。这么设计是为了避免主仓库里出现嵌套仓库的混乱。

理解了 gitlink 和.gitmodules,你就能明白一个关键结论:主仓库永远不会保存子模块的文件内容,它保存的只是一个“指针”。这也是“子模块提交后主仓库还会显示有变化”的原因,因为指针变了。

1.3 submodule和包管理工具的定位差异

有人会问:有 npm、Maven、Go Module,为什么还要 Git 子模块?这两类东西解决的问题完全不同。

包管理工具的目标是下载一个发布版本,比如common-lib:1.2.3,然后把它作为外部依赖,放进本地依赖缓存或虚拟环境。发布物往往是构建产物,比如 jar、npm 包、编译后的二进制,普通开发者不需要直接看到源码,也不需要在主项目里改这个依赖的代码。Git 子模块则把“依赖”拉回了源码层面,你看到的是完整仓库内容,你可以直接进去改,也可以固定在某一个提交上。它更像“源码级依赖”。

适用子模块的典型场景包括:多个微服务共同依赖一份接口定义;文档仓库要嵌入多个子项目的文档;固件或客户端项目依赖硬件抽象层仓库;或者你想把一个代码库按目录拆成多个独立仓库,但又希望主仓库能一键拉取全部。反过来,如果你的依赖本来就是纯产物,也不需要跟着主项目版本一起演进,那老老实实用包管理工具就好,没必要引入子模块的复杂度。

2. submodule add子模块添加实战

2.1 添加前的准备

执行git submodule add之前,先把环境检查到位,不然会浪费很多时间在莫名其妙的报错上。

第一,确认 Git 版本。--recurse-submodules-j这些参数依赖较新的 Git 特性,我建议至少用 2.13 以上版本,越新越省心。Windows 下多注意,自带的旧版 Git 可能有些行为不一致。可以运行git --version看一眼。

第二,确认主仓库已经初始化并至少提交过一次。空的、还没提交的主仓库执行git submodule add往往会报error: 'xxx' does not have a commit checked out之类的错。原因是子模块需要在索引里登记 gitlink,而索引在首次提交前状态不够稳定。先进主仓库目录,写好.gitignore,提交一个初始 commit 再继续。

第三,确认子模块仓库地址能正常访问。如果是团队私服,建议统一用 SSH 地址,免密配置好之后 CI 和本地操作都少一层麻烦;如果只给外部协作者用,HTTPS 地址会更通用。无论哪种,至少要先手动执行一次git ls-remote <url>,确认远程仓库可达。这一步能过滤掉大部分权限和网络问题。

另外提醒一句:子模块将要检出的路径必须是空目录,或者目录不存在。如果目录里已经有一个名不副实的同名仓库,Git 会拒绝操作。想强制覆盖旧记录,可以配合-f,但我不推荐上来就用,先看清楚目录里的东西再说。

2.2 核心命令实操:git submodule add 的完整流程

假设主仓库叫my-app,要添加一个公共库common-lib,并放在libs/common-lib路径下。完整的操作流程是这样的:

cd my-app git submodule add git@github.com:yourname/common-lib.git libs/common-lib

执行完毕后,Git 会自动完成几件事:

  • 按照 URL 克隆子模块仓库到libs/common-lib目录;
  • 在根目录生成或更新.gitmodules文件;
  • 把子模块当前的 HEAD 提交写入主仓库索引,也就是生成 gitlink 记录;
  • 什么都不需要你额外配置,但不要急着把libs/common-lib手动 add 进主仓库,直接执行git status看看效果。

你会看到类似下面的状态:

Changes to be committed: (use "git restore --staged <file>..." to unstage) new file: .gitmodules new file: libs/common-lib

注意,libs/common-lib显示为 “new file”,但它不是普通文件目录,而是一个 commit 指针。这时候提交主仓库:

git add .gitmodules libs/common-lib git commit -m "add common-lib as submodule" git push

这里有个容易踩的坑:有人会直接在子模块目录里git add .再提交,把子模块内部的文件误加进主仓库。记住,你不需要也不应该在主仓库里提交子模块的内部文件,主仓库只认子模块的 commit 哈希。

如果添加时想指定默认跟踪分支,可以加-b参数:

git submodule add -b main git@github.com:yourname/common-lib.git libs/common-lib

这样.gitmodules里会多出一行branch = main,后面配合git submodule update --remote更新时会按这个分支来拉取。

2.3 常用参数与分支控制

git submodule add参数不多,但每一个都有明确的使用场景。我给你列一个速查表。

参数作用典型使用场景
-b <branch>为子模块指定跟踪分支,写入.gitmodules希望后续用--remote更新到指定分支最新版
-f/--force如果路径下已有 gitlink 记录或文件冲突,强制覆盖重新添加路径时使用,但要小心别覆盖掉本地改动
--name <name>指定子模块的逻辑名称,默认是路径名路径和逻辑名不一致时,方便维护
--depth <depth>创建浅克隆,只拉取指定深度提交子模块历史很长,而主仓库只需要最新代码时
--reference <repository>使用本地已有仓库作为对象来源节省网络带宽和磁盘空间,但需要注意--reference-if-able才更安全

分支控制可能是团队里最容易问到的问题。子模块默认是“锁提交”的,也就是说,不管远程分支怎么更新,主仓库里的指针不会自动动。如果你想跟随某个分支的最新提交,必须在添加时用-b指定分支,之后配合git submodule update --remote来更新,这一点后面会展开。

--depth我要多说一句。它确实能让克隆变快,但会让子模块里的历史变浅。如果后续你需要在子模块里切换分支、查看完整 log 或者从某个旧提交开分支,浅克隆会带来额外麻烦。CI 环境可以用,本地开发尽量别用。

2.4 add之后仓库里发生了什么

真正搞明白git submodule add做了什么,比背命令有用得多。你把子模块添加好之后,可以亲自检查这两个地方。

第一个是.gitmodules。它会按子模块逻辑名分节,路径和 URL 是必不可少的。添加时如果指定了分支,就会有branch字段。这个小文件是主仓库的一部分,提交后团队成员都能读到。

第二个是 Git 索引里的 gitlink。运行:

git ls-files --stage

输出中会多一条160000模式的记录。160000在 Git 中叫 commit 模式,它表示这一项不是普通文件也不是目录,而是一个“仓库引用”。记录里只有 commit 哈希,没有子模块内单个文件的信息。这就是为什么主仓库的.git目录体积不会因为子模块变大多少,因为真正的对象都存放在.git/modules里。

再说一个实际收益。正因为主仓库锁的是 commit,所以你可以在主仓库的任何分支、任何历史版本上精确重建当时的完整代码状态。只需要:

git clone --recurse-submodules <主仓库地址>

就能把主仓库和所有子模块恢复到完全一致的状态。这种可重现性,是复制粘贴方案永远给不了你的。

3. --recurse-submodules递归克隆子项目

3.1 新克隆与已有仓库拉取的差异

子模块添加后,最大的困惑通常出现在“别人怎么把代码拉下来”。这里有两个典型场景,必须分开说。

场景一是从零克隆主仓库。此时主仓库里只记录了 gitlink,并没有子模块实际内容,所以必须额外告诉 Git 把子模块一并拉下来。命令是:

git clone --recurse-submodules git@github.com:yourname/my-app.git

--recurse-submodules会在主仓库克隆完成后,自动读取.gitmodules,逐个克隆子模块,并 checkout 到主仓库记录的 commit。一条命令解决所有问题。

场景二是主仓库已经存在,只是子模块目录是空的,或者需要重新初始化。这时候不需要重新 clone,执行:

git submodule update --init --recursive

这条命令里的--init表示根据.gitmodules初始化子模块配置,然后开始拉取;--recursive表示如果子模块里还有子模块,继续一直递归下去。很多老手已经把这条命令当口头禅了,它和clone --recurse-submodules本质上是同一套流程,只是一个发生在克隆时,一个发生在克隆后。

两种方式没有优劣,按场景选即可。但有一条建议:团队文档里最好同时给这两条命令,一个给新成员克隆用,一个给老成员更新用,能少建很多工单。

3.2 --recurse-submodules常见组合用法

--recurse-submodules不是孤立使用的,项目规模一大,你会想给它配上各种参数。

并行拉取是最实用的。子模块数量多的时候,默认串行克隆会很慢。Git 2.8 之后支持:

git clone --recurse-submodules -j8 git@github.com:yourname/my-app.git

-j8表示最多同时拉取 8 个子模块。对于十几个子模块的仓库,体验完全是两个级别。已有仓库更新时同样可以配合:

git submodule update --init --recursive -j8

这个-j参数在子模块多的时候几乎可以无脑加,只要网络带宽够,不会带来副作用。

另一个组合是--remote-submodules

git clone --recurse-submodules --remote-submodules git@github.com:yourname/my-app.git

这个参数会改变子模块的 checkout 逻辑:不是 checkout 主仓库记录的 commit,而是把每个子模块都更新到远程跟踪分支的最新提交。听上去很酷,但我不建议默认使用。它会让主仓库记录的版本失去意义,团队之间很容易出现“我拉下来代码和你不一样”的诡异局面。除非项目明确要求所有子模块永远跟随远程最新,否则还是让子模块锁定在主仓库记录的 commit 上更可控。

还有一个--shallow-submodules,只在克隆时对子模块做浅克隆。它能显著降低初始拉取体积,适合 CI 构建环境;但本地开发如果要在子模块内做历史搜索,浅克隆会有局限,注意权衡。

3.3 递归操作的内部执行流程

很多人用过--recurse-submodules但没想过它背后做了什么。把内部流程拆开,遇到报错时排查起来就容易多了。

当克隆主仓库时,Git 先正常拉取主仓库代码,此时子模块目录在 checkout 后是空的。然后--recurse-submodules开始工作,大致顺序如下:

  1. 读取主仓库根目录的.gitmodules,拿到所有子模块的路径和 URL;
  2. 对每个子模块执行git clone <url> <path>
  3. 克隆完成后,checkout 到主仓库索引里记录的 commit SHA,也就是那个 gitlink 指向的版本;
  4. 检查每个子模块自己的.gitmodules,如果它也有子模块,则重复上述过程;
  5. 如果有-j参数,则并行处理多个子模块。

这里最关键的是第三步。子模块默认 checkout 到“记录的提交”,而不是某个分支的最新提交,所以进入子模块目录后你通常会看到HEAD detached at 0a1b2c3。这不是错误,而是 Git 在忠实地执行“锁版本”策略。如果你想在子模块里开发,后面我会讲如何处理分离头指针。

从克隆日志里也能看到这个过程。主仓库克隆完成只用了零点几秒,但子模块一个个开始Cloning into 'common-lib'...,这就是递归逻辑在执行。假如某个子模块 URL 写错了,这里就会中断,而且因为 Git 会把子模块路径留在磁盘上,你经常需要清理后重新拉取。

3.4 子模块嵌套场景的递归行为

子模块里再套子模块,听着复杂,但 Git 处理得很统一。只要你在顶层命令加了--recursive--recurse-submodules,它就会一路递归到底。

嵌套场景下最常见的坑,是 URL 权限不统一。主仓库大家都能拉,但某个二级子模块只在内部网络可访问。新成员执行git clone --recurse-submodules时,主仓库拉完了,一级子模块也拉完了,结果卡在二级子模块上报错。排查起来特别费劲,因为报错上下文很可能只显示Unable to fetch submodule 'nested-lib'

所以团队需要提前约定:子模块的 URL 尽量使用所有人都能访问的地址格式。另外,git submodule sync是一个重要工具,它会把.git/config中的子模块 URL 重新同步成.gitmodules里的值。每次子模块 URL 或远程仓库迁移后,都应该让所有人执行:

git submodule sync --recursive git submodule update --init --recursive

先同步 URL,再更新内容。这个顺序不能反,否则会在旧的或者错误的地址上反复尝试连接。

另外提一个冷知识:.gitmodules中的 URL 可以写成相对路径,比如../common-lib.git,Git 会根据主仓库远程 URL 自动推导完整地址。这对从同一台 Git 服务器拉取多个相关仓库很方便。但相对路径在git submodule add时只对克隆场景生效,如果你直接复制一个本地主仓库,没有配置远程地址,相对 URL 就可能解析失败。建议在模板仓库、CI 脚本里多测试一次。

4. 实际踩坑与问题排查

4.1 常见报错速查表

子模块的报错信息五花八门,但大部分都有固定套路。我把实际遇到频率最高的几类整理成了一张表,方便你对照排查。

报错特征常见原因推荐处理方式
fatal: repository 'xxx' not foundURL 写错、没有访问权限、仓库被迁移git submodule sync后重试;检查 URL 和凭据
server certificate verification failedHTTPS 证书校验失败,常见于私有自签名服务在可信网络下使用 Git 服务;或者临时配置http.sslVerify=false,但只在排查时用
Unable to find current revision in submodule path子模块记录的 commit 在远程已经被强制推送清除联系子模块维护者恢复引用,或把子模块切换到存在的分支并重新提交指针
Pathspec 'xxx' is in submodule主仓库里误用了子模块路径下的文件操作进入子模块目录操作;需要修改主仓库记录时,先处理子模块内部提交
error: Server does not allow request for unadvertised object浅克隆的子模块缺少历史对象去掉--depth参数,重新完整拉取

遇到报错不要慌,先看错误出现在主仓库阶段还是子模块阶段。一个笨但有效的办法:用GIT_TRACE=1GIT_TRACE2=1重新执行命令,Git 会打印每一步实际操作,你能看到它正在尝试连哪个地址、读哪个文件。比如:

GIT_TRACE=1 git submodule update --init --recursive

通过 trace 输出,你能迅速确认是否卡在 URL 解析,还是网络连接,还是凭据认证。

4.2 分离头指针到底怎么处理

几乎所有第一次接触子模块的人都会遇到HEAD detached at ...,然后一脸懵。这个状态不是错误,但确实容易让人误操作。

主仓库记录的是一串 commit SHA,所以在git submodule update --init之后,Git 会直接让子模块 checkout 到这个 commit,而不会自动切换到你印象中的maindevelop分支。这样设计就是为了保证“所有人拿到的提交一致”。此时你如果直接git add子模块目录,主仓库会认为自己记录的 commit 没变,不会有任何可提交内容。

如果你只是想在子模块里改代码并提交,正确的步骤是:

cd common-lib git checkout -b fix/update-config # 修改文件 git add . git commit -m "fix: update config" git push origin fix/update-config

然后回到主仓库,重新把子模块的新提交更新到主仓库索引里:

cd .. git add common-lib git commit -m "chore: bump common-lib to latest fix"

这时候主仓库的 gitlink 才会指向你子模块的新 commit。要记住一个核心顺序:先在子模块里完成提交,再回主仓库记录指针。反了就会把主仓库提交的指针留在旧位置,子模块改动看起来“丢失”。

日常只拉取不修改的话,分离头指针完全不用管。但如果你进入子模块后发现它是独立仓库,想切到某个开发分支继续干活,可以执行git checkout main,也可以直接git switch main。这样你的本地子模块就停留在分支上,后续git pull也正常。

4.3 更新和删除子模块的正确姿势

更新子模块和更新普通 Git 仓库不一样,不只是进入目录git pull就结束了,主仓库的指针也要跟着动。

如果你想获取子模块远程分支的最新提交,推荐用update --remote

git submodule update --remote common-lib

这个命令会进入子模块,拉取远程分支(就是.gitmodules里的branchmaster默认分支)的最新提交,然后自动 checkout。但主仓库的 gitlink 仍然是旧值,所以你还得回到主仓库提交一次:

git add common-lib git commit -m "update common-lib to latest"

如果只想恢复到主仓库记录的版本,用:

git submodule update --recursive

这个命令不会拉取远程分支,而是从本地对象库里 checkout 到记录的 commit,适合刚克隆完或想丢弃子模块内的本地改动时使用。

删除子模块是另一个让很多人大呼崩溃的操作。子模块在磁盘上、索引里、.gitmodules.git/config.git/modules里都有残留,靠手动删目录一定会留尾巴。推荐三步走:

git submodule deinit -f common-lib rm -rf .git/modules/common-lib git rm -f common-lib

第一行把子模块从.git/config中移除,并清空工作区里的子模块目录;第二行删除本地缓存的对象库;第三行从索引和.gitmodules中移除记录,并把改动放入暂存区。最后记得提交:

git commit -m "remove common-lib submodule"

这是目前最干净的处理方式,比搜一堆命令拼出来靠谱得多。

4.4 团队协作中必须约定的几个习惯

如果只有你自己用子模块,节奏再乱也能兜住。最怕的是整个团队多人多仓库共用子模块,没有统一约定。我这里给出几个用血泪经验换来的习惯。

第一,新成员拉代码,统一提供一条命令。在仓库 README 最显眼位置写清楚:

git clone --recurse-submodules git@github.com:yourname/my-app.git

或者已有的本地仓库:

git submodule update --init --recursive

不要指望每个人都主动知道子模块的存在。子模块目录空着的时候,主仓库看起来完全正常,不跑一遍更新根本发现不了“缺模块”。

第二,子模块的 URL 变更后,马上广播一句git submodule sync --recursive。很多团队在迁移 Git 服务器之后,主仓库拉取正常,但子模块一直报旧地址连不上。原因就是.git/config还保留着旧 URL,而git pull不会自动修改它。执行一次 sync 就能解决。

第三,谨慎使用git submodule update --remote的自动提交习惯。有人喜欢在 CI 里直接跑--remote --recursive然后自动提交指针,这省事,但也容易把还没验证好的子模块变更带进主仓库。我建议至少留一个人工 review 的环节,让子模块指针的更新像普通代码变更一样经过审核。

最后一个经验:子模块数量一旦超过三个,建议把常用命令封装成脚本或 Makefile 目标。比如make init执行完整拉取,make sub-update执行更新并提示你回到主仓库提交。团队用起来负担小,也就更愿意遵守规范。

我个人在实际项目里的体会是:git submodule 的复杂不是来自命令本身,而是来自“主仓库锁指针、子模块单独演进”这种双仓库思维。只要你把.gitmodules、gitlink、分离头指针这几个概念真正理解了,平时用起来比想象中顺手。最后再分享一个小技巧:如果你经常需要同时操作多个子模块,可以偷懒用foreach命令,比如git submodule foreach 'git checkout main && git pull'一键把所有子模块切到指定分支并拉取最新,但记得回到主仓库重新提交指针。这个命令在子模块很多时会帮你省下大量重复操作。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/18 5:19:36

实测DeepSeek 4.1 Flash:轻量级模型的能力边界与正确用法

最近有技术群里的朋友让我测一下 DeepSeek 4.1 Flash&#xff0c;说这个版本听起来“又快又轻”&#xff0c;很适合接入线上业务。我本来不想碰&#xff0c;因为“Flash”这种后缀在模型产品线里通常意味着妥协&#xff0c;但群里催得紧&#xff0c;我还是专门花了一个下午&…

作者头像 李华
网站建设 2026/9/18 5:18:23

SAP Cloud Integration OData API客户端证书认证完整指南

你们有没有遇到过这种场景&#xff1a;外部系统要调用你搭在 SAP Cloud Integration 上的 OData API&#xff0c;对方张口就要用户名密码&#xff0c;你心里却特别不踏实。Basic Auth 确实简单&#xff0c;可凭据一旦从某个日志里漏出去&#xff0c;整个接口就等于裸奔。尤其遇…

作者头像 李华
网站建设 2026/9/18 5:14:22

Maven settings.xml 配置原理与企业私服实战指南

1. 为什么你写的 Maven 项目总在下载依赖时卡住&#xff1f;真相不是网速问题我第一次在客户现场部署一个 Spring Boot 项目时&#xff0c;整整等了 27 分钟——就为了下载spring-boot-starter-web-3.1.0.jar。开发环境 3 秒搞定&#xff0c;生产服务器却像卡在泥潭里。运维同事…

作者头像 李华
网站建设 2026/9/18 5:14:20

电信信用评级:机器学习模型优化与实践

1. 电信信用评级现状与挑战电信行业每天产生海量用户数据&#xff0c;但传统信用评估模型存在明显局限。我在运营商大数据部门工作六年&#xff0c;亲眼目睹了这些痛点&#xff1a;人工审核效率低下、规则引擎误判率高、新用户缺乏历史数据难以评估。最头疼的是&#xff0c;某些…

作者头像 李华
网站建设 2026/9/18 5:12:06

移动硬盘打造Ventoy+微PE+Win10双系统实战指南

手里这块移动硬盘&#xff0c;能干嘛&#xff1f;存点资料、做个备份、偶尔倒腾一下大文件&#xff0c;大多数人用到这一步就停了。前阵子我把它重新折腾了一遍&#xff0c;现在这块盘插到电脑上&#xff0c;开机弹出Ventoy启动菜单&#xff0c;里面躺着微PE&#xff0c;随时能…

作者头像 李华
网站建设 2026/9/18 5:11:23

Nginx proxy_pass末尾斜杠:URI替换规则与404排查指南

Nginx 的proxy_pass看起来是再简单不过的一条指令&#xff0c;但我几乎每个月都会在群里或评论里看到同一个问题&#xff1a;为什么我配了反向代理&#xff0c;访问/api/xxx就 404&#xff0c;访问/xxx却正常&#xff1f;为什么同事的配置抄过来&#xff0c;换个 location 前缀…

作者头像 李华