ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

用GitHub Actions实现前端自动化部署到GitHub Pages的完整指南

用GitHub Actions实现前端自动化部署到GitHub Pages的完整指南 不需要主标题直接从二级标题开始。1. 先把思路理清楚为什么需要自动化部署这套东西多数前端项目的上线流程到今天还停留在一个比较原始的状态。本地跑一遍npm run build然后手动把dist目录拖进某个面板或者用 scp 推到服务器。这套流程在最开始没什么问题但一旦项目变多、版本迭代变快痛点就非常明显容易漏文件、容易覆盖线上版本、多人协作时谁部署的说不清楚、回滚全靠记忆。GitHub Actions 解决的就是这件事。它把构建和发布变成两个可以自动执行的任务定义在仓库里的.github/workflows/目录中。只要满足触发条件比如 push 到 main 分支它就在 GitHub 托管的机器上自动执行你写好的步骤最终把构建产物推到gh-pages分支GitHub Pages 会自动托管该分支下的静态文件。我当初第一次用这套方案就是为了省掉每次改完代码要手动跑命令、手动拖文件的重复劳动。尤其是个人博客和公司内部后台这类静态项目改动频繁如果每次都要走一遍手动流程效率太低了。自动化之后commit 推上去几分钟后线上就是新版本构建失败还能在 Actions 页面直接看到日志。这个体验用过了就不想再回去。这篇文章会从原理讲到实操内容包括完整的 workflow 配置、项目页和用户页的差异、SPA 路由问题、路径配置以及我在实际项目中遇到的报错和排查过程。不管你是刚开始接触 CI/CD 的初级开发者还是已经在用 GitHub Pages 但想把手动流程改造成自动化的老手都可以参考后面的配置来落地自己的项目。2. 动手之前先弄清 GitHub Actions 和 Pages 的配合逻辑2.1 你写的 workflow 到底是怎么被执行的GitHub Actions 的底层逻辑并不复杂。它本质上是一台按照剧本执行的临时服务器。这个剧本就是.github/workflows/下的 YAML 文件。每次触发条件满足时GitHub 会创建一台干净的虚拟机拉取你的仓库代码然后一行一行执行你在 YAML 里定义的步骤。这套设计有几个关键点每次执行都是全新的环境。这次装好的依赖、生成的产物下次不会保留。所以你在 workflow 里必须把安装依赖这一步骤写进去每次构建时重新执行。执行顺序是自上而下的。steps列表里排在前面的先执行后面的依赖前面的结果。比如npm ci必须在npm run build之前。每个 step 之间默认是独立的 shell 会话。也就是上一个 step 里cd到某个目录下一个 step 并不会继承这个目录状态需要在配置里显式指定working-directory。workflow 所在的代码上下文。如果没有特殊配置Actions 默认在你仓库的默认分支上找.github/workflows/文件。这意味着如果某个 workflow 只存在于 feature 分支push 到 main 时它不会被触发。理解这几点后很多为什么我的 Actions 没跑为什么报找不到文件的问题就能想明白了。2.2 Pages 的两种发布形态直接影响路径配置GitHub Pages 本身有两种托管形态配置思路完全不同。这个坑我见过太多人踩所以先单独拿出来说。User/Organization Site用户页仓库名必须叫username.github.ioPages 会直接托管整个仓库的静态文件。访问地址就是根路径https://username.github.io/。在这种形态下应用中的所有资源引用路径可以直接写成/xxx.js不需要加前缀。Project Site项目页任何普通仓库在 Settings - Pages 里选择分支和目录后就能开启 Pages 服务。访问地址是https://username.github.io/repo-name/。注意这个/repo-name/前缀它意味着你的构建产物里所有index.html中的资源引用路径、路由路径、图片路径都要带上这个前缀否则页面加载后 CSS 和 JS 全部 404。大多数人的项目都是第二种形态所以后面我会专门用一节讲路径前缀的问题包括 Vue 和 React 项目分别怎么改。3. 一份最小可用的部署 workflow逐行拆解3.1 推荐分支结构与行动逻辑先说结论日常开发在主分支Pages 部署在gh-pages分支。主分支只保存源码gh-pages分支只保存构建产物。这样代码和站点文件是分开的就算gh-pages被误操作也不会影响源码。整个流水线的逻辑是你 push 代码到 main 分支Actions 被触发克隆仓库安装 Node.js 环境和项目依赖执行构建生成静态文件把构建产物推送到gh-pages分支GitHub Pages 自动抓取该分支完成发布这里有个细节不要手动维护 gh-pages 分支。让 Actions 每次覆盖推送它这样本地永远不需要关心这个分支的代码。即便你手动动过它下一次构建也会被整体覆盖。3.2 完整 workflow 配置与每行的含义下面的 YAML 是我目前用得比较顺手的一版适用于基于 Vite 的 Vue/React 项目Node 版本、构建命令、输出目录都可以按需调整。name: Deploy to GitHub Pages on: push: branches: [main] workflow_dispatch: permissions: contents: write jobs: build-and-deploy: runs-on: ubuntu-latest concurrency: group: pages cancel-in-progress: true steps: - name: Checkout uses: actions/checkoutv4 - name: Setup Node uses: actions/setup-nodev4 with: node-version: 20 cache: npm - name: Install dependencies run: npm ci - name: Build run: npm run build - name: Deploy to gh-pages uses: peaceiris/actions-gh-pagesv4 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: ./dist publish_branch: gh-pages force_orphan: true逐行解释一下关键配置。触发器部分on: push: branches: [main] workflow_dispatch:push定义的是代码推送触发branches: [main]是限定只有推送到 main 分支才跑这个流程。workflow_dispatch是手动触发让你可以在 Actions 页面上直接点按钮运行一次这个在调试时非常有用。如果你希望忽略某些文件的变更比如只改了 README 就不触发构建可以加 paths-ignore但我个人不建议一开始就配置先在简单模式下跑通再说。权限部分permissions: contents: write这一步很多教程会漏掉。GitHub 为了安全从 2023 年起对新仓库的默认 GITHUB_TOKEN 权限做了收紧默认是只读。如果不在 workflow 里显式声明contents: write后续向 gh-pages 分支推送时就会报 403 权限不足。checkout 步骤拉取源码。默认拉取当前分支的最新代码。setup-node 步骤- name: Setup Node uses: actions/setup-nodev4 with: node-version: 20 cache: npm这里指定 Node 版本为 20。cache: npm是告诉 GitHub Actions 缓存npm的依赖目录这样每次构建不用重新下载全部依赖能省不少时间。不同的包管理器对应不同的值npm、yarn、pnpm都支持。如果你用了 pnpm注意缓存字段要写成cache: pnpm并且还需要先启用 pnpm 的 store 配置。安装依赖与构建npm ci npm run buildnpm ci和npm install的区别值得说一下。npm ci会严格按照 package-lock.json 里的版本锁定关系安装安装速度快、结果可复现最适合 CI 场景。如果你的项目没有提交 package-lock.jsonnpm ci会报错此时可以退回npm install但后面可能会面临版本漂移导致构建结果不一致的问题。部署步骤- name: Deploy to gh-pages uses: peaceiris/actions-gh-pagesv4 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: ./dist publish_branch: gh-pages force_orphan: truepeaceiris/actions-gh-pagesv4是目前社区里用得最多的部署工具。它做的事情就是把publish_dir目录下的文件推送到指定分支。force_orphan: true表示每次都创建一个全新的孤儿分支没有历史记录。这样做的好处是 gh-pages 分支的体积不会随版本累积变大坏处是你无法快速回滚到之前的某个部署版本。如果你更想要保留历史版本以便回滚可以把force_orphan设为false这样每次部署会保留上一次的提交作为父提交gh-pages 分支就会生成历史记录回滚时只需要 checkout 到之前的 commit 即可。3.3 关于 actions 版本锁定策略写 workflow 时还有一个容易纠结的点action 版本写v4还是master我的建议是尽量使用v4这样的大版本标签而不要用master。因为master指向的是仓库最新代码作者可能随时推送破坏性变更你的流水线可能毫无预兆地坏掉。用大版本标签虽然也会跟随 minor 版本更新但至少 maintainer 不会在这个标签上做破坏性的重构。如果你对稳定性要求更高甚至可以把版本锁定到具体的 commit SHA比如uses: peaceiris/actions-gh-pages068dc1d3e722c17f3c0a0d4a0f9b6c1a3e2b8f7d这样哪怕作者删号你的 workflow 也不受影响。缺点是升级需要手动改 SHA维护成本略高。4. 从能跑到好用项目页 URL 前缀和 SPA 路由问题4.1 项目页的资源路径前缀怎么配如果是用户页username.github.io路径天然是根路径不用额外处理。但如果是项目页访问地址是https://username.github.io/repo-name/问题就来了。比如构建产物在index.html里引用了/assets/index-a1b2c3.js这个绝对路径会被浏览器解析为https://username.github.io/assets/...实际却在https://username.github.io/repo-name/assets/...结果就是资源 404页面白屏。不同框架有不同的解决方式。Vite 项目在vite.config.ts中设置export default defineConfig({ base: /repo-name/ })这样构建时所有资源引用都会自动加上/repo-name/前缀。Create React App在package.json中加一行homepage: https://username.github.io/repo-name/然后确保你使用的是相对路径的引入方式{ homepage: https://username.github.io/repo-name/ }Next.js 静态导出在next.config.js中配置module.exports { output: export, basePath: /repo-name, images: { unoptimized: true } }Vue CLI在vue.config.js中配置module.exports { publicPath: process.env.NODE_ENV production ? /repo-name/ : / }这里有一个更省事的小技巧如果你不想在不同环境下反复改路径可以改成相对路径方案。Vite 支持把base设为./这样所有引用都会变成相对路径比如./assets/index.js。页面部署在任意子路径下都能正常工作不需要关心仓库名。不过相对路径方案有个副作用如果应用使用了 BrowserRouter 这类需要修改 history 的路由刷新某个深层路由页面比如/repo-name/about时会有问题因为服务器不知道需要把所有请求回退到index.html。4.2 SPA 路由的 404 Refresh 问题GitHub Pages 本身不提供服务器端 rewrite 能力。也就是说当你访问https://username.github.io/repo-name/about时GitHub Pages 会去找/about这个路径对应的静态文件如果不存在就返回 404。这在 Vue Router 的 history 模式或 React Router 的 BrowserRouter 下是必现的问题。解决思路有几种换成 Hash 模式。Vue Router 用createWebHashHistoryReact Router 用HashRouter。URL 变成https://username.github.io/repo-name/#/about改的是 hash 部分浏览器不会向服务器发请求也就不会 404。这是最省事的方案代价是 URL 不够优雅、SEO 不友好。做一个自定义 404 页面。在项目根目录放一个404.html内容是一个重定向脚本把所有未知路径重定向到首页并在 URL 上带上原始路径参数。首页加载后再根据参数恢复路由。这个方案能保住 history 模式但需要一定的开发量。放弃在 Pages 上部署需要 history 路由的大型 SPA。如果项目对路由和 SEO 有比较高的要求建议直接上 Vercel、Netlify 或自己的服务器它们的 serverless 函数可以做 rewrite。个人经验是放在 GitHub Pages 上的项目图省事直接上 Hash 模式别纠结。4.3 缓存、并发和构建时间的优化当 workflow 跑了几次以后你会发现构建时间开始影响体验。配合 setup-node 的缓存之后安装依赖的时间能压到 10-20 秒主要时间集中在构建本身。还有几个优化点并发控制如果你快速连续 push 多次Actions 会排队执行多个相同 workflow。concurrency配置可以在新的运行开始时取消还在跑的旧运行只保留最后一次的结果。我上面的配置里已经加了concurrency: group: pages cancel-in-progress: true这样能避免无意义的重复构建尤其适合多人协作频繁 push 的场景。构建产物缓存如果你每次构建都要花很多时间在代码编译上可以尝试使用actions/cache缓存构建中间产物。但说实话对于中小型前端项目收益有限还会带来缓存失效问题优先级不高。5. 我在实际项目中遇到的 5 个报错和完整排查链路这一节分享几个我真实踩过的坑。每个问题我都会先描述现象再把排查过程写出来而不是直接甩结论因为排查思路本身才是可以复用的能力。5.1 部署成功但页面 404现象Actions 日志全绿gh-pages 分支也有文件但访问https://username.github.io/repo-name/显示 404。排查过程我先检查了 Settings - Pages 页面确认 Source 选的是Deploy from a branch分支是gh-pages目录是/ (root)配置看起来没问题。然后我直接在浏览器访问/repo-name/还是 404。接着我打开了 gh-pages 分支的文件列表发现里面确实有index.html。问题就很奇怪了。后来我注意到仓库本身的名字repo-name里包含大写字母。GitHub Pages 的 URL 路径是大小写敏感的而我的仓库名是My-Repo访问地址其实应该是https://username.github.io/My-Repo/。我输入的是https://username.github.io/my-repo/小写自然就 404 了。根因GitHub 把仓库名直接映射为 URL 路径仓库名大小写不同URL 路径也必须完全匹配。建议仓库名尽量全小写加连字符。一方面避免这种 URL 歧义问题另一方面也符合大多数静态站点的 URL 风格。如果你的仓库名已经有大写老老实实用大写路径访问。5.2 一直报 Resource not accessible by integration现象action 在 checkout 阶段正常一到部署步骤就报错日志里有Resource not accessible by integration或者 403。排查过程这个报错最经典网上搜也能搜到很多讨论。原因基本集中在 token 权限上。point 在于permissions: contents: write。一开始我的 workflow 里没有写 permissions所以 GITHUB_TOKEN 默认是只读的。而部署步骤要向 gh-pages 分支写入文件只读 token 不允许。解决在 workflow 顶层加上permissions: contents: write之后就正常了。如果你用的不是peaceiris/actions-gh-pages而是其他需要访问你仓库的 action有时候还需要在仓库 Settings - Actions - General - Workflow permissions 里把默认权限改成 read and write。不过最规范的做法还是通过permissions关键字在 workflow 里按需声明而不是全局放开。5.3 构建时 Node 版本不一致导致的编译失败现象本地npm run build没问题push 之后 Actions 里构建失败报错信息五花八门比如某个依赖不支持当前 Node 版本、或者语法错误。排查过程一步步看日志发现 setup-node 默认用的是 Node 20。而我本地项目用的 Node 18。项目里的某个构建依赖在 Node 20 下表现不同编译报错了。这个在大型项目、老项目中非常常见。解决在.nvmrc文件里锁定项目需要的 Node 版本比如18.20.4然后在 workflow 的 setup-node 步骤里把node-version改为读取.nvmrc- uses: actions/setup-nodev4 with: node-version-file: .nvmrc cache: npm这样团队成员本地和 CI 环境使用的 Node 版本会保持一致避免因为版本不一致导致的各种诡异问题。5.4 每次构建都重新装全部依赖太慢现象构建日志里npm ci每次都下载四五分钟的依赖即使没有任何代码变更。排查过程我一开始没有在 setup-node 里配置cache: npm所以 Actions 每次都在裸机环境重新下载依赖。其实 GitHub 提供了依赖缓存机制只要你在 setup-node 里声明 cache 类型它会自动读取 package-lock.json 来定位缓存 key。解决加上cache: npm。这里有个前置条件项目根目录必须有package-lock.json。如果你的项目用的是 yarn 或 pnpm对应改成cache: yarn或cache: pnpm。从日志里可以明显看到区别加上缓存后Install dependencies 步骤会先输出Found a cache from ...后面就快很多。而且peaceiris/actions-gh-pages的部署步骤也提供了cache参数可以额外缓存 Pages 需要的文件。5.5 使用自定义域名后 CSS/JS 路径错乱现象给 Pages 绑定了自定义域名访问一切正常但在某些二级路径下资源加载失败。排查过程如果你的站点绑定了自定义域名Pages 的访问 URL 会变成https://yourdomain.com此时如果还是用/repo-name/作为 base 路径构建出的资源路径就全是/repo-name/assets/xxx自然 404。解决此时 base 应该改成根路径即去掉/repo-name/前缀。这也说明一个道理base 配置应该和环境相关不能写死。建议在项目里用环境变量区分部署目标构建时按DEPLOY_TARGET决定 base 值。6. 进阶配置workflow_dispatch 手动触发与分支隔离6.1 想要手动部署指定版本怎么办默认的 workflow 只在代码 push 时执行。但有一个常见需求我想在某个历史 commit 上手动部署一次而不想动 main 分支的代码。做法就是workflow_dispatch。配置里加了它Actions 页面就会出现一个Run workflow按钮。点开后还可以选择ref分支或 commit然后手动运行。这对线上问题紧急回滚特别有用打开 Actions 页面选择对应 workflow点击 Run workflow在 ref 下拉框选择目标 commit 或分支运行完成后 gh-pages 分支被覆盖注意在执行这个操作之前main 分支的最新代码并不会因为 ref 的选择而改变所以如果你要从历史 commit 部署需要用这个 ref 参数指定。如果你的 workflow 里还残留着 push 触发那么这个历史 commit 不触发 push不会产生冲突。6.2 多环境分支的策略如果你的项目同时维护两个版本比如dev分支给测试环境用main分支给生产环境用可以用多个 workflow 文件或者在一个 workflow 里用条件判断。简单的做法是复制一份 workflow把on.push.branches改成[dev]在部署步骤里把publish_branch改成gh-pages-dev。这样 dev 的站点和 main 的站点分别发布到不同分支上互不干扰。如果你想在同一个分支上切换环境也可以用环境变量区分- name: Deploy run: | if [[ ${{ github.ref }} refs/heads/dev ]]; then echo deploy to dev else echo deploy to prod fi这种方式更灵活但可读性和维护性略差建议按团队习惯选择。7. 另一个值得考虑的发布思路Push to Another Repositorypeaceiris/actions-gh-pages推送的目标分支默认是本仓库。但有一种场景项目代码在私有仓库而展示站点需要放在公网仓库。此时可以用 deploy_key 的方式把构建产物推送到另一个公开仓库。配置方式也很简单- name: Deploy to external repo uses: peaceiris/actions-gh-pagesv4 with: deploy_key: ${{ secrets.DEPLOY_KEY }} external_repository: username/public-repo publish_dir: ./dist publish_branch: gh-pages关键在于生成一对 SSH deploy key。公钥放到目标公开仓库的 Settings - Deploy keys私钥配置到当前仓库的 Secrets。注意 deploy key 只对单个仓库生效不能同时在多个仓库使用同一把 key。如果目标仓库换了名字key 需要重新生成。我在公司里用过这个方案把内部开发的后台系统源码放在私有仓库构建后的静态页面推到公网仓库让客户直接通过 URL 查看演示版本效果很好。安全性上源码不会出现在公网环境中只有不可编辑的构建产物暴露给外部。8. 我实际用下来的一些经验总结最后说几点我踩过多次坑之后沉淀下来的心得不算什么高深理论都是能省时间的实操经验。第一workflow 的调试不要怕失败。Actions 的日志虽然不算友好但每一步的报错都是明确的。大部分问题路径、权限、版本都能从日志的前几行定位出来。遇到问题先看日志再去搜报错信息不要盲目改配置。第二构建结果尽量保持确定性。用npm ci而不用npm install锁定 Node 版本提交 package-lock.json。这些细节决定了你本地构建和 CI 构建结果是否一致。否则你会碰到一个典型的尴尬场面本地构建正常、CI 构建失败因为两者依赖版本不同。第三gh-pages 分支的目录结构要保持干净。我见过有人往 gh-pages 分支手动提交了源码和 node_modules导致 Pages 托管了不该托管的文件。只要用force_orphan: true每次整体覆盖就不会出现这种问题。第四Pages 的部署不只是跑通命令。要真正在自己的项目里落地还要处理路径前缀、路由模式、404 页面、缓存这些细节。很多人配置好了 workflow页面却白屏大多不是 workflow 的问题而是构建时路径配置不对。第五JSON 中的 Github Pages 相关配置要注意。例如如果你用 Vite 且配置了base: ./在 workflow 里publish_dir: ./dist找到的文件里index.html 的 src 是相对路径那么 GitHub Pages 及其 CDN 都会按相对路径正确解析。反过来用绝对路径时则必须保证 base 和仓库名完全匹配。第六不要把 GitHub Actions 当成万能的。如果项目需要动态资源、服务端渲染、复杂的权限控制Pages 就不合适建议用云函数或自己的服务器。GitHub Actions 的价值在于简化静态站点的发布流程而不是替代完整的后端部署。我现在几乎所有静态项目都已经切到了这套流程push 代码Actions 自动构建自动发布到 gh-pages。省下的时间可以用来干更有意义的事而不是每次都在终端里敲那几行重复命令。你可以先拿一个简单的静态页面项目试跑一遍把流程中的每一步都看清楚、想清楚然后再上比较复杂的框架项目。实践一遍之后你对 CI/CD 的理解会比看多少篇教程都深刻。
返回列表