持续集成是开源项目质量保障的基础设施。过去大量React开源项目选择Travis CI,主要是因为它对公开仓库完全免费、配置简单。但随着Travis CI调整收费策略,免费额度大幅缩水,社区项目被迫寻找替代方案。GitLab CI凭借其开箱即用的Runner机制和与代码仓库深度集成的特性,成为迁移的热门选择。本文将以一个典型的React项目为例,完整演示从.travis.yml迁移到.gitlab-ci.yml的全过程。

Travis CI与GitLab CI的核心差异
在动手改配置之前,先理解两个平台的设计差异,能让迁移事半功倍。Travis CI采用的是SaaS托管模式,你只需要提交.travis.yml文件,Travis的服务器会自动拉取代码并执行。而GitLab CI采用流水线(Pipeline)加执行器(Runner)的架构,Runner可以是GitLab官方提供的共享实例,也可以是你自己注册的私有机器。
配置层面,Travis用language: node_js和node_js版本列表声明环境,GitLab CI则通过Docker镜像来控制运行环境,例如image: node:18。这种基于镜像的方式更灵活,你可以锁定任意版本的Node甚至操作系统。另一个重要区别是缓存机制:Travis自动缓存node_modules目录,而GitLab CI需要显式声明缓存路径,但支持更精细的缓存Key策略,例如按分支或按提交哈希区分缓存。
此外,GitLab CI的阶段概念(stages)比Travis的jobs更强调顺序执行,同一阶段内的任务可以并行跑在不同Runner上,这在需要同时测试多个Node版本时特别有用。
编写基础版gitlab-ci.yml配置
假设原来的Travis配置是这样的:
language: node_js
node_js:
- "18"
- "20"
install:
- npm ci
script:
- npm test
- npm run build
cache:
directories:
- node_modules
迁移到GitLab CI后,等价的最小配置如下:
stages:
- test
- build
test_job:
stage: test
image: node:18
script:
- npm ci
- npm test
cache:
paths:
- node_modules/
build_job:
stage: build
image: node:18
script:
- npm ci
- npm run build
cache:
paths:
- node_modules/
artifacts:
paths:
- build/
注意几个关键点。npm ci比npm install更适合CI环境,它严格按照package-lock.json安装,速度更快且结果可复现。artifacts用来把构建产物(React项目的build或dist目录)保存下来,供后续阶段或下载使用,这是Travis缓存机制没有的能力。缓存路径末尾的斜杠表示缓存整个目录,务必确认.gitignore忽略了node_modules,否则缓存上传会异常缓慢。
如果需要像Travis那样测试多个Node版本,可以利用并行矩阵配置:
test_job:
stage: test
parallel:
matrix:
- NODE_VERSION: ["18", "20", "22"]
image: node:$NODE_VERSION
script:
- npm ci
- npm test
缓存优化与依赖安装加速
GitLab CI的缓存默认在所有Runner之间共享,但如果共享Runner来自不同机器,缓存命中率可能不理想。优化手段是给缓存设置更精准的Key,常见做法是结合依赖锁文件的哈希值:
cache:
key:
files:
- package-lock.json
paths:
- .npm/
before_script:
- npm config set cache .npm
- npm ci --prefer-offline
这种方案将npm自身的缓存目录指向项目内的.npm,只要锁文件不变,缓存Key不变,第二次安装时npm可以直接从本地缓存读取压缩包,速度提升非常明显。相比直接缓存node_modules目录,缓存.npm更不容易出问题,因为不同Runner的操作系统差异可能导致二进制依赖失效。
对于使用yarn的项目,可以把缓存路径改成.yarn/cache/,并通过yarn config set cache-folder .yarn/cache指定位置,思路完全一致。
测试报告、覆盖率与产物发布
React项目通常使用Jest做单元测试。要在GitLab的流水线页面直接展示测试结果,可以生成JUnit格式的报告:
test_job:
stage: test
image: node:18
script:
- npm ci
- npm test -- --ci --reporters=default --reporters=jest-junit
artifacts:
when: always
reports:
junit: junit.xml
paths:
- coverage/
coverage: '/Lines\s*:\s*(\d+\.\d+)%/'
其中jest-junit需要在package.json的开发依赖中安装,并在Jest配置里声明输出文件名。when: always确保即使测试失败也能拿到报告,方便排查。正则表达式coverage用于从测试输出中提取覆盖率数字,显示在流水线详情页,这对应Travis中借助第三方徽章的做法。
如果你的项目要发布npm包或部署文档站点,可以增加一个只在打tag时触发的发布阶段:
publish_job:
stage: build
image: node:18
rules:
- if: $CI_COMMIT_TAG
script:
- npm ci
- npm run build
- echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > ~/.npmrc
- npm publish
rules语法取代了Travis的deploy块,条件控制能力更强。NPM_TOKEN等敏感信息应配置在GitLab仓库的Settings菜单下的CI/CD Variables中,并勾选Masked选项,避免明文泄露。
迁移常见问题排查
第一个常见报错是内存不足。React项目跑测试时V8默认内存限制可能在容器内被触发,报JavaScript heap out of memory。解决办法是在script中设置环境变量:
export NODE_OPTIONS="--max-old-space-size=4096" npm test
第二个问题是共享Runner队列等待时间长。GitLab官方对免费项目的共享Runner有额度限制,如果流水线频繁排队,可以自建Runner:在一台服务器上执行gitlab-runner register,按提示填入仓库提供的注册令牌,选择shell或docker执行器即可。自建Runner没有时长限制,响应也更迅速。
第三个问题是构建产物过大导致流水线变慢。检查artifacts的expire_in参数,合理设置过期时间,例如expire_in: 1 week,避免仓库存储持续膨胀。同时只把真正需要的目录列为产物,不要把整个工作区都打包。
完成以上步骤后,删除仓库根目录的.travis.yml,推送新的.gitlab-ci.yml,观察第一次流水线执行情况。整体来看,GitLab CI的初始学习成本略高,但其阶段化流水线、精细缓存和内建产物管理,对中大型React开源项目来说长期收益明显优于Travis CI。