Spade2是我前段时间整理的一个纯前端小项目:一套聚合了文本批量清理、JSON格式化、时间戳转换的小工具页面集合。做完之后我面临一个非常现实的问题——怎么把它分享给身边的朋友用。压缩包发过去显得太原始,传文件工具又撑不了长期访问,花钱买服务器的话为了这几个静态页面又确实有点浪费。后来我直接用GitHub Pages把这套HTML推上线,整个过程比我预想中顺畅得多,但同时也踩了不少文档里不会写清楚的坑。这篇文章就把Spade2从一个本地文件夹到线上可访问的完整过程,包括分支选择、Actions构建、路径白屏、域名绑定这些环节全部梳理出来,如果你手里刚好也有类似静态页面想低成本分享出去,这篇应该能帮你少走几条弯路。
1. 为什么我把Spade2放到了GitHub Pages而不是服务器上
1.1 静态站点托管方案的横向对比
动手部署之前,我先把市面上常见的几种托管方式摆在一起做过一轮筛选。Spade2没有后端逻辑,所有页面都是HTML、CSS、JavaScript,数据存在浏览器本地或者通过接口调用,本质上是一套纯静态资源。这种项目最常见的部署选项也就三个:
| 托管方式 | 月成本 | 运维复杂度 | HTTPS证书 | 全球CDN加速 | 适合场景 |
|---|---|---|---|---|---|
| 传统VPS | 几十到几百 | 需要自己维护Nginx、防火墙、进程守护 | 需要手动申请续期 | 一般没有 | 需要后端接口、数据库 |
| 对象存储+CDN | 流量少时几块钱 | 需要配置存储桶策略、CDN回源 | 证书一键配置 | 自带 | 流量较大的正式项目 |
| GitHub Pages | 0 | 几乎为零 | 免费且自动续期 | 自带 | 纯静态页面、项目文档、演示站点 |
这个表做完之后答案就很明显了。Spade2一周可能只有几十次访问,为一个低频展示型页面去维护一台机器完全没必要。对象存储虽然也很便宜,但是需要单独配置Bucket、CDN加速域名、证书那些东西,对这种个人项目来说有点过度设计。GitHub Pages的优势不在于性能指标多亮眼,而在于它把仓库、代码、发布这三个环节绑定在了一起:我推送代码就是发布站点,不需要额外的发布动作。
1.2 免费托管背后容易被忽略的能力边界
不过免费不等于无限,GitHub Pages有它自己的使用限制,这点文档写得很隐晦,但实际使用中必须心里有数。官方建议发布站点的总大小不超过1GB,单个被Pages发布的文件不能超过100MB,每月的带宽配额是100GB左右,构建频率也有限制——如果你用的是默认的Actions构建,每小时大概只能跑十次左右的构建任务。
这些限制放到Spade2身上完全够用,因为整套项目加起来可能也就几MB,而且更新频率不会高到哪里去。但如果是想拿它存高清图片、视频或者每次构建都会产生几十MB产物的项目,那就要考虑是不是该换成对象存储了。实际上身边有朋友用Pages放企业内部的静态原型图,结果因为单张设计稿超过100MB导出,Pages直接拒绝发布,排查了半天才找到原因。所以我建议你在往Pages仓库里塞大文件之前,先确认文件大小在合理范围内。
1.3 GitHub Pages能做什么、不能做什么
还有一个经常被误解的点。GitHub Pages本质上是静态资源托管,它不执行任何服务端代码。你在本地打开HTML文件是什么效果,部署上去基本就是什么效果,但如果你指望它去跑一段PHP或者执行服务端的Node逻辑,那就完全走不通了。
Spade2在开发时所有的数据持久化都用浏览器localStorage,跨设备同步则依赖用户手动导出导入文件,为的就是不依赖服务端。如果你的项目和Spade2不同,含登录鉴权、业务接口这类服务端能力,那Pages只能作为前端静态层,真正的逻辑还是要部署在服务器或者云函数上。这也是我把Spade2定位为“纯工具页集合”的原因,用Pages托管这种项目,刚好是能力圈范围内最舒服的用法。
2. 本地目录与仓库初始化的关键细节
2.1 Spade2的目录结构设计
项目在本地其实就是一个非常朴素的文件目录,我先把结构列出来:
Spade2/ ├── index.html ├── tools/ │ ├── json-formatter.html │ ├── timestamp.html │ └── text-cleaner.html ├── assets/ │ ├── css/ │ │ └── common.css │ ├── js/ │ │ ├── common.js │ │ └── util.js │ └── images/ └── .gitignore这个结构没什么特别的,但有一点很重要:目录层级要扁平化。我见过很多项目页面嵌套三四层目录,结果部署到Pages之后,资源路径一旦从绝对路径变成相对路径,全部乱套。Spade2把所有CSS和JS统一放在assets目录,HTML页面之间相互跳转也只隔一层tools目录,这样就算后面要整体调整路径策略,改动范围也可控。
另外,我在目录里放了一个.gitignore文件,用于忽略本地缓存目录、临时文件、操作系统生成的Thumbs.db、.DS_Store这类垃圾文件。这个文件不显眼,但非常有用,否则这些系统文件被提交到仓库后,会污染仓库历史,看着非常难受,后面想清理还要重写Git历史。
2.2 仓库名对URL的影响有多直接
创建GitHub仓库这一步我踩过一个不大不小的坑:仓库名字会直接决定访问路径。如果你新建一个仓库叫Spade2,然后用户名的Pages域名是yourname.github.io,那么Pages发布之后,这个站点的默认访问地址就是:
https://yourname.github.io/Spade2/除非后面绑定自定义域名,否则这个名字会一直挂在URL上,非常显眼。我当时创建仓库时随手起了个Shovel-Project,之后怎么读怎么别扭,只好删除仓库重新建。好在当时还没有推送任何内容,删掉重来没有负担。所以建议在初始化仓库之前,先想好这个项目将来要以什么身份出现在别人面前,仓库名的命名最好和Pages站点整体规划一起定下来。
2.3 main与master分支,以及push之前要注意的提交规范
第一次把代码推到远端时还会遇到一个很实际的问题:你的Git版本和仓库默认分支是main还是master,可能会和网上很多旧教程对不上。2020年之后GitHub新仓库的默认分支已经变成main,但不少教程、旧工具链还在用master,复制别人命令的时候就要特别注意,不要盲目执行,先看一眼当前仓库默认分支到底是什么。
我的做法是本地分支保持和远端一致。在推送之前,先执行:
git init git add . git commit -m "chore: 初始化Spade2项目结构" git branch -M main git remote add origin git@github.com:yourname/Spade2.git git push -u origin main这里git branch -M main的意思是把当前分支强制重命名为main,确保本地和远端的默认分支对齐。很多人在这步会踩坑,本地还是master,远端是main,结果push到远端后产生两个头部分支,部署配置里选来选去,浪费不少时间。提前统一,后面做任何自动化构建都会省心非常多。
3. 三条部署路径实测:分支直推、Actions自动构建、gh-pages分支
3.1 第一种:在Repository Settings里手动选择分支
最原始的Pages部署方式是直接在仓库的Settings中找到Pages面板,然后在Build and deployment部分选Source为“Deploy from a branch”,再选好分支和根目录,保存之后等一两分钟,Pages就会把这个分支的内容发布出去。
这种方式的优点是操作门槛低,完全不需要接触命令行和配置文件,适合一个简单的纯静态项目,比如临时把一份演示文档推上去给同事看。缺点是没有构建环节,你仓库里长什么样,线上就是什么样。如果项目到了后面引入了Sass、ES6模块打包、压缩混淆这些构建步骤,那就没法用这种方式直接发布了,除非你把构建后的产物提交到仓库里。
Spade2在最开始的阶段就是用的这个方式,因为最初确实没有任何构建步骤,HTML、CSS、JS都是可直接运行的原生代码。这种方式在测试期救急很不错,算是零成本起步的第一步。
3.2 第二种:用GitHub Actions实现Push即部署
当Spade2的页面数量开始增加之后,我决定把构建流程前置到发布之前,于是从手动选择分支切换到了GitHub Actions自动部署。Actions的思想很简单:在仓库里放一个工作流描述文件,GitHub检测到push事件之后,自动在远端服务器上执行你定义好的步骤,构建项目,再把产物上传到Pages环境。
我用的是官方推荐的静态站点部署工作流,核心内容如下:
name: Deploy static content to Pages on: push: branches: ["main"] workflow_dispatch: permissions: contents: read pages: write id-token: write concurrency: group: "pages" cancel-in-progress: false jobs: deploy: environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Setup Pages uses: actions/configure-pages@v5 - name: Upload artifact uses: actions/upload-pages-artifact@v3 with: path: '.' - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@v4如果你使用的是纯静态HTML项目,上面这份配置把path: '.'改成path: './dist'之后,再把构建步骤插到Checkout和Upload artifact之间,就能覆盖绝大多数静态站点的发布场景。这个工作流把构建和部署环节固化在配置文件里,日后再也不用手动进Settings去切换分支了,每次push都自动完成从代码到上线的过程。
另外,权限那三行permissions不是摆设。GitHub Pages的部署动作需要写入Pages环境,需要读取仓库内容,还需要一个短暂的id-token用于身份认证,缺任何一个都可能在运行时报错。如果你把别人配置里的权限段不小心删掉了,很容易出现“Action运行成功但没有触发Pages更新”这种诡异现象。
3.3 第三种:gh-pages分支模式,以及它的适用场景
还有一类部署方式是通过gh-pages分支。它的逻辑是:把构建产物单独放在一个名为gh-pages的分支上,Pages从那个分支读取文件。很多前端打包工具内部都集成这种发布方式,比如老牌的npm部署包就默认把构建结果推送到gh-pages分支。
在实际测试中,gh-pages分支和Actions两种方案在最终线上效果上没有本质区别,它们都能实现自动部署。区别主要在于:gh-pages分支的方式通常需要工具帮你完成分支切换和推送动作,有一定的本地耦合度;而Actions方案把一切收口在云端配置文件里,本地只需要push一个main分支即可。如果团队里有多个协作者,大家都需要频繁发版,我建议优先采用Actions方案,因为每个协作者只要会提交代码,发布流程就会自动运转,不需要每个人都在本地安装部署工具、配置身份认证。
4. Spade2首版的白屏事故:相对路径与缓存排查全过程
4.1 现象:页面能打开,但CSS和JS全部404
Spade2部署完成后第一次访问,页面结构渲染出来了,但样式和脚本全部失效,打开控制台看到一排红色404错误。
错误信息指向的是CSS和JS文件全部按域名根路径去请求了,比如:
GET https://yourname.github.io/assets/css/common.css 404 (Not Found)但实际文件是放在https://yourname.github.io/Spade2/assets/css/common.css路径下的。这明显是路径问题。排查过程我一步一步来:
- 先在浏览器地址栏手动访问
/Spade2/index.html,能打开,说明页面文件被成功发布。 - 再手动访问
/Spade2/assets/css/common.css,也能打开,说明CSS文件本身存在。 - 但页面上所有引用都指向了根路径
/assets/...,说明我在HTML里写死了绝对路径。
4.2 原因:绝对路径与子路径的冲突
造成这个问题的根源很容易理解。当Pages仓库不是以用户名.github.io命名时,访问路径会带上一段子路径前缀/Spade2/。这种情况下,如果HTML里的资源引用以/开头,浏览器会认为这是域名根目录下的资源,自然找不到。
Spade2在前期的快速原型阶段,很多页面是从本地直接双击打开调试的,本地文件系统没有域名概念,写/assets/css/common.css没有任何问题,但推到Pages后,从根路径找资源就全盘出错。
4.3 解决:把所有资源引用改成相对路径
我当时采用的方案是把所有资源引用统一改成相对路径,也就是去掉开头的斜杠。修改后:
<link rel="stylesheet" href="assets/css/common.css"> <script src="assets/js/common.js"></script> ```s html入口在仓库根目录时这样写没有问题。但要注意,如果你的页面不是放在根目录,而是放在`tools/json-formatter.html`这样的子目录里,相对路径就要多算一层,写成`../assets/css/common.css`。所以我后来干脆在HTML的head里加了一个`base`标签: ```html <base href="./">加了base标签之后,页面里所有相对路径都会以当前目录为基准解析。这样即使页面换到子目录,只要目录结构和仓库一致,依然能保证资源请求不跑偏。这是纯静态方案里最省心的做法之一。
如果Spade2本身是一个使用了构建工具的项目,那还有更优雅的解法,比如在Vite配置中设置base: './',在Vue CLI中设置publicPath,或在Next.js中设置assetPrefix,让构建工具自动把所有资源引用转成相对路径,完全不用手动改HTML。这也就是我开始引入构建流程的原因之一——自动化能避免大量人为疏忽。
4.4 白屏事故后的一课:浏览器缓存与CDN缓存
把资源引用改完之后,页面基本恢复,但我又碰到另一个迷惑性更强的问题——明明已经重新部署了,线上却还是旧版本。
刷新一次甚至两次都没有变化,这时就会想到缓存。GitHub Pages背后有CDN加速,它对HTML文件通常不会长缓存,但对带哈希值的静态资源会缓存得非常久。如果你改的是CSS或JS文件,文件名没有变化,浏览器和CDN很有可能继续使用旧版本。
我当时做的验证方法是:在浏览器里强制刷新(Mac下是command+shift+R),再看Network面板里CSS响应头是否为200 from memory cache或304。为了彻底解决这种缓存拖延问题,我后来在构建阶段给所有静态资源加上了内容哈希,形成类似common.css?id=8h32kL这样的版本号,每次内容变更文件名就会变化,浏览器自然就会去拉取新版本,这个问题才算是从根上解决了。
5. 绑定自定义域名与HTTPS证书的完整配置记录
5.1 为什么考虑自定义域名
Spade2线上能访问之后,我发现默认域名虽然简洁,但以yourname.github.io/Spade2的形式发出去始终不够正式。尤其是想把它作为个人工具站推荐给更多人用的时候,URL里的仓库名和平台名会显得非常业余。这个时候就可以考虑绑定一个自定义域名,让站点以tools.自己的域名的形式给别人访问,体验上会好很多。
5.2 DNS解析类型选择:CNAME与ALIAS/ANAME的区别
绑定自定义域名有两件事要做:第一件事是在仓库中放一个CNAME文件,内容写上你的域名;第二件事是在域名DNS后台做解析。
DNS记录的类型选择这里很容易踩坑。GitHub Pages的官方文档推荐使用CNAME记录,但CNAME有一个限制:它不能和你的主域名同时使用。也就是说,如果你的主域名是example.com,你想用www.example.com指向Pages,那通常可以;但如果你想直接让example.com指向Pages,大部分DNS服务商那里CNAME和主域名A记录是冲突的。
我这边的解决方式是,给主域名用的DNS服务商提供了ALIAS或ANAME记录,可以理解为“扁平化的CNAME”,它允许让主域名也指向Pages的IP地址,同时也不影响其他记录共存。如果你用的服务商不支持ALIAS/ANAME,还有一种笨办法:先查一下当前Pages站点的IP地址,然后在DNS里配一条A记录指向这个IP。但Pages的IP在极少情况下会变化,所以还是优先用CNAME或ALIAS。
Spade2的配置如下:
类型:CNAME 主机记录:tools 记录值:yourname.github.io对应的CNAME文件内容:
tools.example.com说完DNS,再说说CNAME文件的细节。它必须放在仓库根目录,并且文件内容就是你绑定的域名,不带https://。如果你绑定的域名是tools.example.com,那CNAME文件里就写tools.example.com。这个文件存在的意义是防止Pages在部署时把你的自定义域名配置丢掉,如果你手动改了DNS却忘记添加CNAME文件,等下一次部署来临时,Pages有可能丢失自定义域名绑定,站点又退回默认URL。
5.3 HTTPS证书的自动签发与强制开启
配置完成之后,Pages会自动为自定义域名申请HTTPS证书,这个过程一般需要几分钟到几小时不等。在证书签发完成之前,浏览器访问域名可能会提示不安全,这是因为证书还没生效。
我在实际操作中遇到过一个现象:DNS解析和CNAME文件都配好了,但仓库Settings的Pages面板里“Enforce HTTPS”开关还是灰色,无法勾选。等了一段时间之后,我刷新页面,开关才变成可点击状态。这个开关最好手动确认打开,因为它会让所有HTTP请求自动跳转到HTTPS。如果不开启,有时候用户通过旧链接访问http版本会降低整体安全性,浏览器也会给出不友好提示。
有一个比较隐晦的注意点:如果你同时使用了Cloudflare这类代理服务,建议在Cloudflare的SSL/TLS模式里选择“Full (strict)”,而不是“Flexible”。因为GitHub Pages本身已有证书,如果Cloudflare那边用Flexible模式,用户的请求到Cloudflare时是HTTPS,但Cloudflare到Pages时会退回HTTP,这种情况下Pages可能返回重定向循环或报错。为了减少排查复杂度,我当时直接把解析放在DNS服务商原生,不在中间套代理服务,这个方案在Pages上最干净。
6. Spade2从纯HTML走向构建流程:多环境预览与自动化发布
6.1 压垮手工流程的最后一根稻草
Spade2前期的发布链路是:修改代码 → 本地测试 → push → Actions部署 → 手工打开线上页面确认。这套流程对于两三个页面的小项目来说还能接受,但随着页面数量增加,我发现越来越多的时间消耗在“本地起了服务之后忘了改端口”“CSS变量改了一个,结果忘了同步到另一个文件”这类手工维护上。
更重要的一点是,多个工具页面复用了同一套CSS和公共脚本,每次改公共文件都要全局回归测试一遍,纯手工维护的成本越来越高。于是我给Spade2引入了构建工具,用组件化思路把公共头部、页脚、CSS变量统一抽离,每次发布前都会执行一次编译、压缩和路径重写。
6.2 构建流程接入后的发布链路
引入构建工具之后的目录变成了src和dist两层的结构:
Spade2/ ├── src/ # 源码目录,日常开发在这里 ├── dist/ # 构建产物目录,由构建命令自动生成 ├── .github/workflows/ # Actions工作流 └── package.jsonActions工作流也相应地改成先安装依赖、构建、再上传artifact:
- name: Install dependencies run: npm ci - name: Build run: npm run build - name: Upload artifact uses: actions/upload-pages-artifact@v3 with: path: './dist'这里有个关键点:npm ci和npm install是有区别的。npm ci会严格按照package-lock.json文件安装依赖,保证每一次远端构建时依赖版本一致。如果只用npm install,当锁文件存在时它也可能更新依赖版本,这会导致“本地构建没问题,远端构建报错”的诡异问题。我在接入Actions的初期就被这个问题坑过一次,本地一切正常,推上去后Actions那边因为某个依赖小版本升级而打包报错,后来统一改成npm ci之后就稳定了。
6.3 用Actions做Pull Request预览环境
除了main分支的自动部署,我还给Spade2配置了一个Pull Request预览环境。它的逻辑是:当有人发起PR时,Actions会把这次分支改动后的站点构建出来,部署到一个临时的Pages URL上,这样在合并之前就可以直接点击预览看效果,不用拉代码到本地再跑一遍。
预览环境用到了同一个工作流文件,只不过通过触发条件来区分:
on: pull_request: branches: ["main"]然后在jobs中增加一个上传artifact的逻辑,配合actions/deploy-pages的临时环境返回临时URL。这样每次PR产生时,PR界面会自动出现一个“Preview”链接,所有参与评审的人都能点击访问。虽然不是所有个人项目都需要这个功能,但如果你在团队里维护的项目未来会有人协作,这个能力几乎是刚需。它不需要额外付费,只是多写一段配置,性价比很高。
6.4 自动化之后少操的那些心
把构建和部署全部自动化之后,我日常的操作就变成了:本地改代码,提交PR,看一眼预览环境,合并到main,剩下的都由Actions完成。
我个人的体会是,GitHub Pages真正的价值不是省了一台服务器的钱,而是用一套标准化流程把“从代码到可访问页面”之间的所有手工动作全部消除掉了。Spade2到今天已经迭代了几轮工具页面,包括后来加入的Base64编解码、正则测试面板,都是走这一套流程上线,整个过程非常顺滑。
最后分享一个小技巧:在push之前,如果你在本地装了类似serve这样的静态文件服务器,先跑一遍serve dist,确认线上将要展示的内容能正常访问,再提交推送。这个习惯看起来多了一步动作,但它能帮你避免“线上白屏”这类问题的再次出现,尤其是在项目接入构建工具之后,本地预览和线上发布两种环境的路径解析逻辑要一致,这一点很重要。