☰
若依前端 Jenkins 自动打包部署与 Nginx 发布实战
2026/9/29 1:47:33 网站建设 项目流程

1. 为什么前端也要接Jenkins自动部署

前端项目做自动打包部署这件事,很多团队一开始都是靠人肉操作:本地npm run build,然后打开FTP或者宝塔面板,把dist目录拖上去,再刷新一下CDN。项目小的时候还行,一旦上了若依这类前后端分离的框架,配合多人协作和频繁发版,手动部署几乎必然出问题——有人漏传了文件,有人把测试环境的配置发到了生产,还有人打包时忘了改VUE_APP_BASE_API导致接口全 404。我见过最离谱的一次,是同事把本地没提交的代码打包发到线上,排查了两个小时才发现问题。

Jenkins的价值就在这里:它把"拉代码、装依赖、打包、上传、刷新"这一整套动作固化成流水线,谁触发、什么分支、发到哪个环境,全部有据可查。前端的自动打包部署和后端的逻辑不太一样,后端一般是打包成 jar 或镜像直接跑,前端产出的是静态资源,最终要落到 Nginx 或者对象存储上,所以流水线的最后一步通常是文件同步而不是服务重启。这个差异决定了前端 Jenkins 配置的写法,后面会详细展开。

若依(RuoYi)是一套在国内相当流行的快速开发框架,它的 Vue 前端版本(ruoyi-ui)结构规整,package.json里脚本命名统一,构建产物固定输出到dist,这些特点其实非常适合作自动化。你不需要改造项目结构,只要在 Jenkins 里写清楚 Node 版本、构建命令和发布路径就行。这篇文章我会从零开始,把一套能直接落地的方案讲透,包括 Jenkins 环境准备、Node 构建环境、Git 凭据配置、流水线脚本、Nginx 衔接、以及我自己踩过的十来个坑。不管你之前有没有碰过 Jenkins,照着做都能跑通。

适合阅读的人群:负责若依前端部署的前端同学、需要给团队搭 CI/CD 的全栈或运维、以及正在从"手动拖文件"往自动化迁移的开发者。文中用的是 Jenkins 的声明式流水线(Declarative Pipeline),比传统自由风格任务更清晰,也更容易做版本管理。

2. 整体方案设计与关键选型考量

2.1 前端部署和后端部署的本质差异

先说清楚一个认知问题,否则后面很多配置你会觉得"为什么和网上后端教程不一样"。后端部署的终点是"进程重启",Jenkins 干完活通常是执行一段 shell 去kill老进程、起新 jar,或者滚动更新 K8s 的 Deployment。前端部署的终点是"静态文件替换",dist里的 HTML、JS、CSS 换了,用户的浏览器下次请求就能拿到新版本,中间不涉及任何进程生命周期管理。

这个差异带来三个直接影响。第一,前端部署天然是"覆盖式"的,老文件不删会越堆越多,所以流水线里要有清理旧目录的动作。第二,前端有浏览器缓存问题,文件名带 hash 的资源可以放心用长缓存,但index.html必须禁用缓存,否则用户拿到的还是旧入口。第三,前端不需要考虑"服务启停窗口",理论上可以做到用户无感知,前提是文件替换的过程中不能让用户访问到"半新半旧"的状态——这也是后面要用临时目录再切换的原因。

2.2 为什么选 Jenkins 而不是其他方案

市面上的选择不少:GitLab CI、GitHub Actions、Drone、以及各种云厂商的流水线。选 Jenkins 的理由很实在——它太常见了。若依本身的社区教程里 Jenkins 的案例最多,遇到问题搜得到答案;它是自托管的,内网环境、私有 Git 仓库都能适配,不依赖外部服务的网络策略;插件生态成熟,Publish Over SSH、GitLab 插件、钉钉通知插件都是一行配置的事。

当然它也有短板,Jenkins 的界面确实老派,配置项散落在各种全局工具配置里,新手第一次上手容易迷路。但只要你理解了它的核心模型——"节点(Node)执行任务(Job),任务里跑流水线(Pipeline)"——剩下的就是查插件文档的事。对若依这种规模的项目,Jenkins 完全够用,没必要为了追新去折腾更复杂的方案。

2.3 部署架构:单机 Nginx 还是别的

如果部署架构还没定,我建议前端静态资源直接用Nginx托管,Jenkins 和 Nginx 可以在同一台机器,也可以分开。同一台机器最省事,流水线把dist传到本机某个目录,Nginx 直接指向它,配置简单到不能再简单。分开的好处是构建和运行隔离,构建时的高 CPU 占用不会影响线上服务,适合访问量稍大的场景。

如果你的若依前端要发到多个节点,别在 Jenkins 里写一堆 scp 命令逐个推送,直接用 rsync 同步到一台中转机,再由中转机分发,或者干脆上对象存储 + CDN,把分发交给云服务。手工逐个推的方式节点一多必然漏发。

选定架构之后,整条流水线的动作就确定了:拉取 Git 代码 → 切到指定分支 → 安装 npm 依赖 → 执行构建 → 产物上传到目标目录 → 备份旧版本 → 切换目录 → 重载 Nginx。下面这张表把每一步的目的和常见坑列出来,方便对照。

流水线阶段核心动作目的常见问题
Checkout拉取指定分支代码保证构建源正确凭据失效、分支写错
Installnpm/yarn/pnpm 安装依赖准备构建环境依赖源慢、lock 文件冲突
Build执行 build 脚本产出 dist内存不足、环境变量错误
Deploy上传产物替换线上文件权限不足、路径写错
Reload重载 Nginx生效新文件缓存未清、软链未切

3. Jenkins 环境准备与关键插件配置

3.1 安装方式的选择与国内镜像加速

Jenkins 的安装方式主要有三种:直接跑 war 包、用系统包管理器(yum/apt)、以及 Docker 部署。如果是新环境,我更推荐Docker 部署,理由是版本隔离干净、升级回滚方便、环境依赖少。命令大概是这样:

docker run -d --name jenkins \ -p 8080:8080 -p 50000:50000 \ -v /data/jenkins_home:/var/jenkins_home \ -v /usr/bin/docker:/usr/bin/docker \ -v /var/run/docker.sock:/var/run/docker.sock \ --restart=always \ jenkins/jenkins:lts

注意-v /data/jenkins_home:/var/jenkins_home这个挂载,容器删了数据还在,这是保命配置。另外要注意容器内 uid 的权限问题,Jenkins 容器默认用户 uid 是 1000,挂载出来的宿主机目录权限要给它写权限,否则首次启动就报错。

系统包安装的话,官方源在国内访问很慢,装插件时更是慢到怀疑人生。换国内镜像源是必须做的一步。进系统管理 → 插件管理 → 高级,把升级站点 URL 换成国内镜像(比如清华的 Jenkins 镜像地址)。这一步不做,你装一个 Git 插件可能要等十分钟。Docker 部署的情况下还记得在容器里加上时区参数-e TZ=Asia/Shanghai,否则构建日志时间和你的认知对不上。

3.2 必装插件清单

Jenkins 裸装是没法干活的,下面这几个插件基本是刚需,我按重要性排一下:

  • Git plugin / Git Client plugin:拉代码用,必装。
  • GitLab plugin(或 Gitea/Bitbucket 对应插件):如果你想让 GitLab 提交后自动触发构建,这个是关键。
  • Pipeline:声明式流水线的基础,LTS 版本一般自带。
  • NodeJS Plugin:让 Jenkins 帮你管理 Node 版本,比在宿主机装 Node 干净得多,强烈建议装。
  • Publish Over SSH:如果目标机和 Jenkins 不在一台,用它传文件。
  • DingTalk(钉钉通知插件):构建成功失败推送到群里,团队协作体验提升明显。
  • Workspace Cleanup:构建前清空工作空间,避免上一次的残留文件污染这次构建。

安装插件同样建议走国内镜像,装完之后记得重启 Jenkins 让插件完全生效。装完 NodeJS 插件,要去系统管理 → 全局工具配置里新增一个 NodeJS 安装项,给它起个名字(比如node18),选定版本,勾选自动安装。这个名字后面在流水线里要用到,别随便改。

3.3 凭据管理与安全边界

拉私有仓库要凭据,这一步千万别用明文密码写在脚本里,构建日志里一旦打印出来就是安全事故。正确做法是在系统管理 → 凭据里新建一个"用户名密码"类型的凭据,填 Git 账号和访问令牌,给它起个 ID,比如gitlab-ruoyi-cred。流水线里通过这个 ID 引用,密码永远不会出现在日志里。

用访问令牌(Personal Access Token)比用登录密码更好,一是可以单独撤销,二是权限可以限定到只读仓库。令牌泄漏了随时换一个就行,密码泄漏的代价大得多。

Nginx 目标目录的写权限也要提前规划。Jenkins 执行部署的用户(Docker 里默认是jenkins用户)必须对发布目录有写权限,否则流水线跑到最后一步报Permission denied,前面白干。常规做法是把发布目录的属主改成 jenkins 用户,或者把 jenkins 用户加进一个专门的分组并给目录分组写权限。这一步在第一次部署时就要配好,别等到出问题再回头查。

4. 若依前端项目的构建要点

4.1 项目结构与构建命令确认

若依的 Vue 前端(ruoyi-ui)目录结构比较固定,package.json里的 scripts 通常是长这样的:

{ "scripts": { "dev": "vue-cli-service serve", "build:prod": "vue-cli-service build", "build:stage": "vue-cli-service build --mode staging" } }

这里有个关键点很多人会忽略:构建环境是靠--mode参数区分的,它决定了加载哪个.env文件。.env.production里的VUE_APP_BASE_API就是你打包后前端请求后端的地址。流水线里如果直接跑npm run build,默认走 production 模式;如果你们有独立的测试环境,就要用对应的--mode。

我先给你一个判断口诀:构建命令要和目标环境严格对应,不能靠改代码来切环境。见过有人为了发测试环境,手动去改.env.production里的接口地址然后提交,第二次发生产忘了改回来,直接酿成事故。正确姿势是在 Jenkins 里根据不同分支或不同参数选择不同的构建命令。

另外若依新版本有些是基于 Vite 的(ruoyi-vue3或相关衍生版本),构建命令是vite build,产物目录一般还是dist,但构建速度比 Webpack 版本快很多。不管哪个版本,你要做的是打开package.json确认脚本名和产物目录,这两个信息写错,后面全错。

4.2 Node 版本与依赖安装的坑

Node 版本是个高频雷区。若依的老版本(Vue2 + vue-cli)在 Node 16 上跑得好好的,换到 Node 18 可能要开NODE_OPTIONS=--openssl-legacy-provider才能构建,否则报ERR_OSSL_EVP_UNSUPPORTED。而若依的 Vue3 版本又要求 Node 16+。所以第一步是确认项目要求的 Node 版本,一般看package.json里的engines字段,或者看 README。

在 Jenkins 里指定 Node 版本有两种方式。一是用 NodeJS 插件,在流水线的tools段里声明:

tools { nodejs 'node18' }

二是不用插件,在宿主机装好 Node,直接用系统环境里的。第一种更可控,建议用第一种。

依赖安装这一步的坑集中在包管理器选择和依赖源上。若依老版本一般用 npm,新版本有些仓库会带pnpm-lock.yaml。必须跟着 lock 文件走:有package-lock.json就用npm ci,有pnpm-lock.yaml就用pnpm install --frozen-lockfile,有yarn.lock就用yarn install --frozen-lockfile。所谓 frozen 就是严格按 lock 文件装,不自动升级版本,保证每次构建产物一致。

npm install和npm ci的区别一定要搞清楚:前者会尝试更新 lock 文件里的依赖版本,后者严格照 lock 来。CI 环境必须用npm ci(或者带 frozen 参数的其他包管理器),否则每次构建可能装到不一样的依赖,出现"我本地能跑线上不行"的玄学问题。

还有个国内开发者的老问题——npm 官方源慢。在流水线里可以临时指定镜像源:

npm config set registry https://registry.npmmirror.com npm ci --no-audit --no-fund

加--no-audit --no-fund是为了跳过安全审计和捐赠提示,构建能快好几秒,日志也干净。

4.3 构建内存与超时控制

前端构建,尤其是 Webpack 打包,是吃内存的。若依的项目规模不算大,一般 2GB 内存够用,但如果服务器上同时跑着 Jenkins 容器和其他服务,构建时 OOM(内存溢出)导致 Node 进程被 kill 是常见故障,报错信息通常是JavaScript heap out of memory或者进程莫名其妙退出,日志断在打包中途。

解决办法是给 Node 加大堆内存上限,在构建命令里加环境变量:

export NODE_OPTIONS="--max-old-space-size=4096" npm run build:prod

4096 是 4GB,按你服务器的实际内存调整,别设得超过物理内存,否则拖垮整机。另外构建超时也要设,流水线里默认没有任何超时保护,一个卡死的构建能把执行器一直占着。声明式流水线可以这样加:

options { timeout(time: 30, unit: 'MINUTES') buildDiscarder(logRotator(numToKeepStr: '30')) }

timeout是构建超时上限,buildDiscarder是只保留最近 30 次构建记录,防止磁盘被日志和产物撑爆。这两条几乎是我每个前端流水线的标配。

5. 完整流水线脚本拆解

5.1 拉取代码与分支策略

先明确一个原则:构建哪个分支要能一眼看出。参数化构建是最直观的做法,在任务的General里勾选"参数化构建过程",加一个字符串参数BRANCH,默认值写master或者你们的主干分支名。这样每次构建时,界面上有一个输入框可以填分支,历史记录里也能看到这次构建用的是哪个分支。

拉代码的流水线片段是:

stage('Checkout') { steps { checkout([ $class: 'GitSCM', branches: [[name: "${params.BRANCH}"]], userRemoteConfigs: [[ url: 'git@your-gitlab.com:group/ruoyi-ui.git', credentialsId: 'gitlab-ruoyi-cred' ]] ]) } }

如果你在拉取时遇到 "Permission denied (publickey)",说明用的是 SSH 地址但没配密钥。两条路:一是把凭据换成 SSH 私钥类型,把 Jenkins 的公钥加到 GitLab 的部署密钥里;二是直接用 HTTPS 地址配用户名密码凭据。HTTPS 更简单,SSH 更省去密码管理,团队内网推荐 SSH。

用 SSH 方式时,Jenkins 首次连接会提示 host key 验证,非交互环境下会直接失败。稳妥做法是提前在 Jenkins 的 known_hosts 里加上目标 Git 服务器的公钥指纹,或者用插件提供的跳过验证配置。别在第一次构建时才去处理,那样你会对着一个莫名其妙的报错发呆很久。

5.2 依赖安装与构建阶段的写法

把 Node 环境、依赖源、内存参数、构建命令都整合进去,构建阶段大概是这样的:

stage('Build') { tools { nodejs 'node18' } steps { sh ''' export NODE_OPTIONS="--max-old-space-size=4096" npm config set registry https://registry.npmmirror.com npm ci --no-audit --no-fund npm run build:prod ls -lh dist/ ''' } }

最后那句ls -lh dist/是刻意加的,用来在日志里确认产物到底生成了没有、大小是否正常。前端构建"成功但 dist 是空的"这种情况真的存在——比如.env文件里配了错误的输出路径,或者构建命令其实是 lint 而不是 build。加上这一句,日志里能直观看到产物。

如果需要多环境,把构建命令改成参数化:

sh "npm run ${params.BUILD_CMD}"

然后在参数里给BUILD_CMD一个可选列表(build:prod、build:stage),下拉选择。这样测试和生产各用各的构建脚本,互不干扰。

5.3 产物上传与目录切换

这是前端部署最有讲究的一步。最粗糙的做法是直接rm -rf /var/www/ruoyi && cp -r dist /var/www/ruoyi,问题在于删除和复制的间隙里目录是不存在的,用户正好访问就会 404。更好的做法是"先传新目录,再原子切换":

TS=$(date +%Y%m%d%H%M%S) TARGET=/var/www NEW_DIR=$TARGET/ruoyi-$TS # 上传新版本到独立目录 mkdir -p $NEW_DIR cp -r dist/* $NEW_DIR/ # 原子切换软链接 ln -sfn $NEW_DIR $TARGET/ruoyi

这里用软链接切换是关键:ln -sfn是原子操作,切换的瞬间新的软链就指向新目录,任何时刻 Nginx 都能读到完整版本,不存在半新半旧的窗口。Nginx 的root配置指向/var/www/ruoyi,实际是根据软链找到最新的目录。

如果 Jenkins 和目标机不在同一台,就用rsync传输,它自带增量同步和断点续传能力,比 scp 稳:

rsync -avz --delete dist/ deploy@target:/var/www/ruoyi-$TS/

注意dist/后面的斜杠不能省略,有无斜杠行为完全不同:有斜杠是同步目录内容,没斜杠是把目录本身放进去。这个细节坑过无数人。

保留历史版本目录是故意的,出问题时可以一键切回来。但别无限保留,磁盘会满。加个清理逻辑,只留最近 5 个版本:ls -dt /var/www/ruoyi-* | tail -n +6 | xargs rm -rf。

5.4 重载 Nginx 与缓存处理

静态文件替换完成后,一般不需要重启 Nginx,但要让浏览器拿到新的index.html。如果你的index.html被 Nginx 也设了长缓存,用户不会主动去拿新版本。正确的缓存策略是这样的:

文件类型缓存策略原因
index.html不缓存或极短缓存入口文件,必须让用户拿到最新
带 hash 的 JS/CSS长缓存(一年)文件名随内容变,无需担心旧版
图片、字体长缓存内容变化频率低
manifest.json等短缓存可能随版本变

对应的 Nginx 配置片段:

location / { root /var/www/ruoyi; try_files $uri $uri/ /index.html; add_header Cache-Control "no-cache, no-store, must-revalidate"; } location ~* \.(js|css|png|jpg|jpeg|gif|svg|woff2?)$ { root /var/www/ruoyi; expires 1y; add_header Cache-Control "public, immutable"; }

try_files $uri $uri/ /index.html这一行是 Vue Router 的 history 模式必需的,否则用户直接访问/system/user这类深链会 404。很多人部署完发现"首页能进,刷新子页面 404",就是漏了这行。

重载命令就一句:

nginx -t && nginx -s reload

nginx -t先做配置检查,通过了再重载,避免配置写错直接把服务搞挂。这个顺序别颠倒。

6. 常见问题排查与避坑实录

6.1 构建失败类问题速查

构建阶段的报错花样最多,我把高频的整理成一张表,遇到直接对号入座。

报错信息根本原因解决方式
ERR_OSSL_EVP_UNSUPPORTEDNode 17+ 与老版 Webpack 的加密兼容问题加NODE_OPTIONS=--openssl-legacy-provider或降 Node 版本
JavaScript heap out of memory构建内存不足加大--max-old-space-size
Cannot find module 'xxx'依赖没装全或 lock 文件冲突用npm ci重装,检查包管理器是否统一
npm ERR! network/ timeout依赖源访问不畅切国内镜像源
command not found: npmNode 环境没配到流水线里检查 tools 段或全局工具配置
Failed to fetchNode 自动安装下载失败配置国内 Node 下载镜像,或改用宿主机 Node

关于 Node 自动安装失败,补充一个细节:NodeJS 插件的自动安装是从外部下载的,国内网络经常失败。可以在系统管理 → 全局工具配置的 NodeJS 安装项里,把"自动安装"的下载地址改成国内镜像。如果嫌麻烦,直接在宿主机装好 Node,流水线不声明 tools,用系统的也行,只是版本管理没那么灵活。

6.2 部署环节的权限与路径问题

部署阶段最常见的两个报错:Permission denied和No such file or directory。

Permission denied几乎都是目录属主问题。查清楚 Jenkins 用哪个用户执行(容器里是jenkins),然后确认目标目录对这个用户的写权限。快速排查命令:

ls -ld /var/www id jenkins

如果 jenkins 用户不在目录的属主组里,加组或者chown。

No such file or directory一般是路径写错,或者目录还没创建。用 rsync 时如果目标目录不存在,rsync 会尝试创建,但多级父目录不存在还是会失败,所以保险起见先mkdir -p。

还有一种情况比较隐蔽:打包成功,但页面白屏。这种通常不是部署问题,而是资源路径问题。若依的vue.config.js里如果publicPath设成了相对路径(./),再配合前端路由的 history 模式,某些部署结构下会找不到资源。检查构建日志里的资源引用路径,和 Nginx 的实际访问路径对不对得上。默认若依的publicPath是/,部署在根路径下没问题,如果部署在子路径(比如/ruoyi/),必须同步改publicPath和路由的base,两处要一致。

6.3 触发方式与通知机制

自动化的最后一块拼图是"怎么触发"。最省心的当然是代码提交后自动构建,配 GitLab 的 Webhook 即可:GitLab 项目里配好 Jenkins 的回调地址,Jenkins 任务里勾选"GitLab 项目构建触发器",生成 Token 填回 GitLab。这样每次 push 就自动跑。但生产环境的部署我建议不要全自动,用"参数化构建手动触发",避免有人误提交就发到线上。测试环境可以全自动,生产环境手动确认,这是很实用的折中。

构建完成后的通知也值得配。装个钉钉插件,构建成功失败都推到群里,附上构建日志链接。团队里谁能不知道发布了什么,出了问题也能第一时间看到。配置在任务的后置步骤里加一段,具体格式插件文档有示例,主要是填 Webhook 地址和消息模板。

通知里建议带上分支名、构建人、构建持续时间、以及是成功还是失败。光说"构建失败了",别人还得点进去查是哪个分支,效率太低。信息给全,接收的人一眼就知道要不要关注。

6.4 版本升级与回滚策略

因为前面用了软链接 + 历史目录的保留策略,回滚就变得非常简单:把软链指回上一个版本目录就行。

ln -sfn /var/www/ruoyi-20240101120000 /var/www/ruoyi

即时生效,不用重新构建。这是"保留历史版本目录"这个设计的最大回报。我建议在部署脚本里顺手生成一个回滚脚本放到目标机上,出问题时运维同志直接执行,不用等开发来操作。

关于 Jenkins 本身的升级,Docker 部署的情况下就是换镜像重启,数据都在挂载卷里,风险可控。但升级前一定记得备份JENKINS_HOME,插件兼容性问题偶有发生,备份是最后的保险。

7. 我个人在实际操作中的几点体会

把这套流水线在几个若依项目上跑了之后,有些经验是不写成文档、光看教程学不到的。

第一条,先手动跑通一遍再上自动化。很多人一上来就写流水线脚本,结果报错一堆,分不清是脚本问题还是环境问题。正确顺序是:手动 SSH 到 Jenkins 所在的机器,手动执行一遍git clone、npm ci、npm run build、上传、切软链,确保每个命令都能跑通,再把这些命令翻译成流水线。流水线的本质就是自动化执行你已经验证过的命令,命令本身没验证过,自动化只会放大问题。

第二条,把环境变量集中管理。构建命令、目标路径、服务地址这些别散落在脚本各处,用 Jenkins 的全局环境变量或者参数统一管。每次要发布新环境时,改一处就行,改多处必然漏。我用的是参数化 + 环境变量结合,主路径固定,环境相关的走参数。

第三条,日志要能看懂。流水线里适当加echo输出当前阶段和关键变量值,比如打印当前分支、目标目录、构建命令,出问题时看日志一眼就知道卡在哪。构建成功但线上没更新这种情况,八成是分支错了或者目标路径写错了,日志里有这些信息就能立刻定位。

第四条,别追求一步到位。第一版流水线能把代码拉下来、打包上传、切软链,就已经解决了 80% 的问题。通知、回滚自动化、多环境并发这些可以后续慢慢加。我就见过有团队为了做一套"完美"的流水线拖了两个月,最后还是手动部署。先跑起来,再优化。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询