最早我托管个人博客,用的还是最传统的方式:一台云服务器装好Nginx,把build出来的静态文件丢到站点目录,再配一下HTTPS证书和日志切割。说实话,站点的访问量并不高,但为了这堆静态文件,我得定期处理Nginx配置、给证书续期、盯磁盘空间、防SSH爆破,一年下来真正花在内容上的时间还没花在“维护服务器”上的时间多。
后来我第一次尝试用Serverless方式部署静态站点,把静态文件传到对象存储,开启静态网站托管,再挂上CDN和自定义域名。全程没有碰一台服务器,页面打开速度反而比原来快了不少。也是从那次之后,我把手上好几个个人项目全部迁到了这套架构上,运维工作几乎降到了零。这篇文章就围绕“Serverless部署静态站点”这条主线,把我实际部署、踩坑、调优的过程完整写一遍。内容既适合第一次接触Serverless的小白,也适合想从传统服务器迁到Serverless的开发者参考。
1. 静态站点而已,为什么要扯上Serverless
1.1 传统部署到底贵在哪
很多人觉得,一个静态站不就是一堆HTML、CSS、JS文件吗,随便找台服务器扔上去就行,何必绕一圈用Serverless。
这个想法并没错,但它只看到了“文件传输”这一步,没看到文件后面跟着的一整串运维账。我自己的经历是,一台1核2G的云服务器,一年下来CPU跑不满5%,内存闲置一大半,但该花的钱一分不少。而且最麻烦的不是钱,是时间:系统漏洞要打补丁,Nginx版本要升级,证书三个月要换一次,还要担心服务器被扫到之后的入侵风险。这些活儿本身没有任何“产出”,它们只是静态文件能稳定被人访问的前提。
Serverless方式把这一整串前提都抽走了。你的静态文件不再属于某一台机器,而是被放到对象存储这类云服务里,由云厂商保证它的持久性、可用性和吞吐能力。你所面临的“运行环境”问题直接从部署清单里消失。
1.2 Serverless静态托管帮你省掉的隐形工作
我用一个具体的场景来说。传统方案里,如果网站某天访问量突然从100涨到10万,你第一反应是加带宽还是加机器?无论哪种,都要经历“观察——决策——操作——生效”的链路,等配置完成,流量高峰可能已经过去了。
Serverless方案没有类似的扩缩容动作。对象存储本身就是海量并发设计的,CDN节点也会把你的静态文件分散缓存到离用户最近的边缘节点。用户访问的是CDN,而CDN回源到对象存储也只发生在缓存未命中时。这套链路天然能扛住突发流量,你不需要做任何弹性伸缩配置。
另外有两件容易被忽略的事:HTTPS证书和数据冗余。Serverless平台的证书通常是自动申请、自动续期,源站存储的数据也会做多副本冗余,单台磁盘损坏、甚至整个可用区出问题,只要云厂商的容灾机制在,文件就不会丢。这些在传统单机部署里都得自己操心的细节,到了Serverless架构里几乎都是默认项。
1.3 不适合Serverless化的反例,帮你冷静判断
讲完好处,也得泼一盆冷水。Serverless部署静态站不是万能的,下面几种情况你就得慎重:
- 站点后台需要执行服务端代码,比如用户登录、数据库读写、支付回调,这些不是“纯静态站”的范畴,需要另外搭配Serverless函数或后端服务。
- 如果你需要对文件做很细的读写权限控制,比如登录用户才能下载某个文件,那就要评估对象存储的权限系统和你的业务是否匹配,而不是简单开启公有读。
- 如果你所在的网络环境访问某些云服务本身就不稳定,那Serverless实际体验可能反而不如自建服务器。
判断标准其实就一条:你的站点是不是“内容为主,逻辑为辅”。只要核心是内容展示,Serverless静态部署就大概率比传统服务器更合适。
2. 部署前先回答三个问题:区域、生命周期、访问特征
2.1 区域与接入要求:选存储桶地域前先确认的事
第一次用Serverless部署时,我图省事随意选了一个地域,结果域名接入那一环得多走几步流程。后来我养成了习惯,动手之前先把区域和接入要求问清楚。
对象存储的存储桶是有地域概念的。你选的地域,决定了后续CDN回源的物理距离,也决定了这个存储桶本身所在的数据中心位置。通常建议遵循两个原则:
- 你的用户主要在哪,存储桶就选在哪。国内访问为主就选国内地域,海外访问为主就选海外地域。
- 如果同一份数据有可能被多地用户访问,不要靠一个存储桶硬撑,应该依赖CDN的多节点覆盖来解决“距离”问题。CDN会负责把内容分发到各边缘节点,源站存储桶的地域选择影响会小很多。
域名接入这件事也要提前考虑。使用自定义域名绑定存储桶和CDN时,不同地域、不同服务商对接入流程的要求并不完全相同,有的需要在国内完成域名接入登记,有的则不需要。我自己的做法是在创建存储桶之前,先把服务商官方文档里“静态网站托管”和“自定义域名绑定”两个章节通读一遍,确认没有特殊限制再往下走,省得资源建好了又推倒重来。
2.2 内容生命周期:纯静态还是“静态为主、少量动态”
第二步要分清你网站的“动态成分”到底有多少。如果纯静态,那内容永远是那一堆文件;如果是“静态为主、少量动态”,就得在设计时预留出动态数据的接入方式。
举个例子,我的一个项目是文档站,文档正文是静态的,但“搜索”功能是动态的。早期我用的是一个第三方的站内搜索脚本,直接在浏览器端索引全文,不需要后端。后来内容变多变复杂,搜索质量不够了,我就把搜索拆到Serverless函数里,由函数去查一个独立的索引服务。静态页面还是那些静态页面,但页面右上角的搜索框已经指向了动态接口。
这种“静态外壳+少量动态接口”的组合,是Serverless架构下非常推荐的做法。它保留了静态站的性能和成本优势,又通过函数计算补足了动态能力。你在规划时不妨把站点的功能拆一遍,哪些能纯静态实现,哪些必须动态,分清楚之后再选型就不会犹豫。
2.3 成本边界:从账单反推你的流量模型
很多人对Serverless有个误解,觉得它一定便宜。其实它的成本模型是“按量付费”,流量大了、请求多了,账单自然就上去了。静态站点的成本大头一般来自三块:存储费用、CDN流量费用、请求次数费用。
我自己的经验是,一个日活几百上千的个人博客,月成本通常保持在几元到几十元这个量级,确实比云服务器便宜得多。但如果你做一个图片分享站,文件又大又多,用户访问频繁,CDN流量费用很快就会成为大头。这时候就要做成本优化,比如:
- 图片统一压缩、转WebP格式,减小体积;
- CSS、JS做合并和压缩;
- 文件版本化与CDN长缓存结合,尽量让资源在用户本地就命中缓存,减少回源和CDN节点反复拉取;
- 定期清理存储桶里的旧版本文件,避免无用文件持续吃存储费用。
成本模型应该是选型的一部分。先估算一下文件总量、月均访问量、平均资源体积,再套到服务商的价格计算器里,得到的数字基本就是你未来的月账单区间。
3. 核心部署实操:存储桶、CDN、域名三步走
3.1 第一步:创建存储桶并开启静态网站托管
以主流的对象存储服务为例,比如腾讯云COS、阿里云OSS。进入控制台后创建一个存储桶,创建时有几个关键选项需要注意:
- 地域:按上面说的就近原则选;
- 访问权限:如果是公开站点,建议选择“公有读私有写”,也就是文件可以被任何人读取,但只有你有权限写入;
- 版本控制:个人站点可以不开,如果你需要防止误删,可以考虑开启,但会产生额外的存储版本费用;
- 静态网站托管:创建完成后需要在“存储桶配置”里的“基础配置”或“静态网站”相关菜单中打开开关。
静态网站托管的开关里通常有一项索引文档设置,默认是index.html,也就是用户访问目录自动加载的文件,这个一定要填。还有一个错误文档设置,默认填404.html,这个建议单独做一个带站点风格的美化404页面,而不是用浏览器默认的文本错误页。
这里有一个很重要的区别:开启“静态网站托管”后的访问域名,和你直接用“默认域名”访问存储桶里的文件,走的是两套逻辑。托管模式下,URL会按照目录索引自动加载index.html;而普通模式下,访问/默认会直接报错或列出文件列表(取决于服务商策略)。要让站点像一个真正的网站,必须确认打开的是静态网站托管模式的域名。
3.2 第二步:上传站点文件并设置正确的访问权限
本地构建好站点之后,上传方式有控制台上传、API上传、命令行工具上传和CI/CD上传几种。我推荐命令行工具,因为可重复执行、可脚本化。以腾讯云COS的命令行工具coscmd为例:
# 安装 pip install coscmd # 配置密钥和存储桶信息 coscmd config -a <你的SecretId> -s <你的SecretKey> -b <bucket-appid> -r <region> # 递归上传dist目录下的所有文件到存储桶根目录 coscmd upload -r ./dist/ / # 如果需要清理旧文件(注意:会删除所有远端文件,先确认)。 coscmd delete -r / --force阿里云OSS对应的命令工具是ossutil,命令格式略有差异,但逻辑相同:先配置凭证,再递归上传。关键的权限设置在上传前后都要检查一遍。如果你创建存储桶时选了“公有读私有写”,上传之后文件自动就具备公开访问权限,不需要再单独为每个文件设置ACL。
这里比较容易出问题的点在于文件权限和存储桶权限不一致。比如存储桶是公有读的,但个别文件可能继承了私有的权限,导致部分图片或资源访问403。上传完成后,建议用浏览器逐个访问几个关键资源确认HTTP状态码是200,而不是404或403。
3.3 第三步:绑定CDN加速与自定义域名
存储桶的托管域名通常不太好记,而且没有CDN加速。生产环境建议绑定自定义域名,并且接CDN。
CDN配置里有两层回源设置要理解清楚:CDN节点从源站拉取文件,然后缓存到边缘节点;用户请求先打CDN,CDN只有在缓存未命中时才回源。所以这里的“源站”指向上一步创建的存储桶静态网站托管地址,而不是你之前那台服务器。
控制台里创建CDN加速域名时,选择“静态网站源站”类型,填上存储桶的托管域名,回源协议建议直接选HTTPS回源。然后按照CDN服务商的要求,在域名DNS服务商处添加一条CNAME解析到CDN分配的域名,等解析生效后,访问你的自定义域名就正式走在Serverless链路上了。
到这一步,你可能还会遇到证书问题。自定义域名要支持HTTPS,需要在CDN控制台申请或上传SSL证书。现在主流平台都支持免费证书自动申请,选择自动验证方式后,平台会利用你的DNS解析完成域名所有权校验,证书可以自动续期,基本不需要人工干预。
3.4 功能验收清单:部署完成后先跑这五条
我在每次部署完之后都会按下面这个清单挨个验证,表面上看起来都是“打开页面看看”,实际上每一条背后验证的都是不同的链路:
- 访问
https://你的域名/是否正常加载首页,浏览器地址栏显示小锁标识; - 直接访问
https://你的域名/about/这类二级路径,刷新后页面是否正常(这一步验证的是SPA路由或目录索引规则,见下一节); - 控制台“Network”面板里,JS、CSS、图片等静态资源的HTTP状态码是否为200,并且有正确的
Content-Type; - 查看CDN日志或回源统计,确认哪些请求发生了回源、是否都在预期范围内;
- 用一个不存在的URL访问,确认能返回一个美观的404页面,而不是一堆XML堆栈或空白页。
如果这里第五项没有通过,别急着调CDN,先去看存储桶静态托管配置里的“错误文档”是不是没填。这个验收清单看着琐碎,但它能在你正式对外推广站点之前,把线上问题提前拦下来。
4. 最容易翻车的四个配置点,我挨个踩过
4.1 SPA前端路由刷新404,错误文档的最佳实践
第一次把Vue Router的History路由站点部署到Serverless上时,我遇到一个经典问题:从首页点进二级页面正常,但用户在二级页面按一下F5刷新,直接404。
原理不复杂:刷新二级页面时,浏览器向服务器请求/about这个路径,但存储桶里根本没有名为about的文件。传统Nginx可以通过try_files $uri /index.html把所有路径重写到index.html,而Serverless静态托管的做法通常是利用“错误文档”机制:把错误文档设置为index.html,这样遇到不存在的路径时,平台会返回index.html的内容,前端Router接管后再渲染对应页面。
这个方案能解决刷新404,但也有个副作用:真的404请求也会拿到200状态码,并加载首页。所以应用代码里要用前端路由的兜底逻辑判断URL是否匹配具体页面,不匹配就展示“内容不存在”的组件。另外,如果你在意SEO,这种“所有路径都返回index.html”的方式会让搜索引擎难以区分真实页面和错误页面,更优的做法是部署时给每个路由生成独立的HTML文件(预渲染),把真实404页面单独保留为一个HTML并配为错误文档。
4.2 HTML缓存时间过长导致发版不生效
静态资源可以放心做长缓存,但HTML文件千万别一上来就设成一年。我踩过一次最难受的坑:改了一条导航文案,执行完发布流程,页面刷了很多次都没变化。F12一看,index.html的Cache-Control是max-age=31536000,CDN节点把旧版本缓存得结结实实。
这里需要区分两种资源:
- 带版本号的静态资源(如
app.8f3a2b.js),文件名变了就是一个新URL,可以做超长缓存,完全安全; - 不带版本号的HTML文件,建议设置为不缓存或极短缓存(如
max-age=0, s-maxage=60),保证用户能尽快看到新内容。
如果你在构建工具里已经给静态资源加了hash,那HTML文件里引用的URL每次发版都会变化,这时候哪怕CDN缓存了旧HTML,也只要等它过期或手动刷新一下就能恢复。发布流程里我一般会加一步“强制刷新CDN缓存”,把HTML文件所在目录的缓存全部清理掉,顺便再触发一次预缓存,让边缘节点优先拉取一份新版本,这样发布完成后第一批用户访问也不会命中旧缓存。
4.3 CDN与源站的HTTPS回源问题:混合内容与证书不一致
CDN开启HTTPS之后,还有一个容易被忽略的环节:CDN节点到源站之间的回源协议。如果CDN用的是HTTPS,源站也要求HTTPS,但回源协议配的是HTTP,中间就可能出现内容和协议不一致的情况。
更常见的现象是混合内容阻塞:页面是通过HTTPS加载的,但HTML里某个图片或脚本却是http://开头的地址,浏览器会直接阻止这个不安全请求。排查方法是在控制台里过滤Mixed Content警告,把静态页面里所有资源引用改成相对路径或https://。
我现在的做法是三步走:构建阶段全站资源走HTTPS链接,CDN开启强制HTTPS跳转,回源协议直接选HTTPS。这三步配置好,基本不会出现混合内容问题。另外,如果站点有WebSocket长连接需求,要提前确认CDN是否支持WebSocket代理,不支持的话这部分流量需要绕过CDN直连源站或改走其他方案。
4.4 防盗链配置误伤:Referer空值和同源资源
给站点加了防盗链之后,我发现搜索结果页里用户点击跳转进来时,部分图片加载失败。查了半天,问题出在“允许空Referer”这个选项没开。
防盗链通常是根据请求头里的Referer字段判断来源域名。但很多场景下Referer是空的:浏览器直接输入URL、从本地文件打开、某些隐私模式,以及部分App内嵌浏览器。如果防盗链配置把空Referer也拦截了,就会把这个正常用户的图片请求拒掉。
另外,如果你在页面里引用了字体文件、跨域图片等静态资源,CDN需要返回正确的Access-Control-Allow-Origin头,否则字体可能加载失败。我当时的配置是“允许来源域名列表里加上自己的CDN域名和主域名,同时勾选允许空Referer”,Reefer白名单之外的情况一律拒绝。这样既能防住外部网站随便盗用图片流量,又不影响正常用户和站内资源加载。
5. 静态站点站上Serverless后的进阶玩法
5.1 用边缘函数做更细的流量控制
静态站点上了CDN之后,很多人以为就结束了。但如果你用的CDN平台支持边缘函数(有的叫Edge Function,有的叫Function at Edge),那么你可以在CDN节点上直接执行轻量代码,实现很多原来要专门部署后端才能做的事。
我自己用得比较多的是几个场景:一是给响应统一加安全响应头,比如Content-Security-Policy、X-Frame-Options、Strict-Transport-Security,这些头在边缘节点注入,不用改源站代码;二是做灰度发布辅助,通过判断用户请求头里的版本标识,把一批用户引到新版本的存储前缀下,另一批用户继续访问旧版本,验证没问题再切全量;三是简单的访问控制,比如拦截异常UA或针对某些频率过高的IP做临时限速。
边缘函数的成本通常很低,因为只在边缘节点轻量执行,时长按毫秒计费。如果你是第一次接触,建议从一个最简单的“给响应增加Header”开始,跑通流程之后再逐步增加逻辑。注意不要在边缘函数里做太重的IO或计算,它不适合跑业务逻辑,只适合做请求处理链路上的轻量干预。
5.2 接入CI/CD,让静态站持续发布
部署静态站点最大的好处之一就是发布简单,而进一步的好处是可以把发布做成全自动。我现在维护的文档站,只要往Git仓库的主分支推一次代码,流水线就会自动完成:拉代码、构建、上传对象存储、刷新CDN缓存,整个过程大概两三分钟。
流水线怎么配取决于你的代码托管平台和云厂商。我用的方式是在仓库根目录放一个CI配置文件,定义好触发器。核心步骤其实就是把第三章节里的上传命令搬到流水线里,另外加上“构建阶段”和“刷新CDN”这两步。
# 伪代码,具体语法按你使用的CI平台调整 name: deploy on: push: branches: [ main ] jobs: build-and-deploy: runs-on: ubuntu-latest steps: - name: Checkout run: git checkout ${{ github.ref }} - name: Install and build run: npm ci && npm run build - name: Upload to bucket run: coscmd upload -r ./dist/ / - name: Purge CDN cache run: curl -X POST https://cdn.api.example/purge?urls=https://yourdomain.com/这个流程跑通之后,你的手就完全从发布动作里解放出来了。以后改内容只需要管“写对提交”,剩下的事都交给流水线。对于多人协作的项目,还可以加上“推送测试分支触发预览部署”,即每次MR都自动生成一个独立的预览环境地址,方便团队评审。
5.3 预渲染解决纯静态站的SEO短板
纯静态站的SEO短板主要体现在SPA站点上。搜索引擎爬虫虽然近些年对JavaScript的解析能力提高了,但并非所有场景都稳定,而且首屏渲染依赖JS会让首次内容到达时间变长。对于内容型网站,我更推荐预渲染。
预渲染的思路很简单:构建时把每个路由对应的页面都预先渲染成独立的HTML文件,用户和爬虫拿到的都是服务端已经渲染好的内容。现在主流框架都有现成的预渲染方案,比如VuePress、Astro、Next.js的静态导出模式,基本能做到“写完Markdown自动生成全站静态页面”。
如果你用的是Vue或React这类框架,也可以在构建阶段额外安装预渲染插件,指定一份路由清单,由插件把每个路由爬一遍并生成对应的HTML。代价是构建时间会变长,路由多了之后效果比较明显。但SEO的收益是值得的:页面标题、描述、内容正文都直接出现在HTML源码里,社交分享时的卡片也能正确抓取摘要。
5.4 基础设施即代码:把整个Serverless栈管起来
最后说一个适合进阶用户的玩法:用基础设施即代码(IaC)管理你的存储桶、CDN、回源规则、缓存配置。工具方面最常用的是Terraform,主流云厂商也都有自己的IaC产品。
我第一次把Serverless静态站点用Terraform重构时,最大的感受是“可回滚”。以前在控制台里改缓存配置,误操作了只能凭记忆恢复;现在所有配置都写在一个.tf文件里,执行terraform apply之前可以先用terraform plan看变更,不满意直接改代码重来。
# 伪代码示例,具体用到的provider和resource以云厂商文档为准 resource "cos_bucket" "static" { bucket = "my-static-site" website = { index_document = "index.html" error_document = "404.html" } } resource "cdn_domain" "static" { domain = "www.example.com" origin { type = "cos" domain = cos_bucket.static.website_endpoint } https_config { switch = "on" } }这套玩法非常适合多站点管理。我有好几个子站点,配置结构几乎一样,只是域名、存储桶名字不同。用IaC之后,新增一个站点只需要复制一份配置模板,改几个变量,跑一次apply就能全部建好,效率比在控制台里一个个点击高出一个量级。
在我最初把Serverless部署静态站点当作“高级玩法”来研究的那段时间,完整跑通一遍大概花了一整天,大部分时间都耗在理解“错误文档”和“CDN缓存刷新”上。后来多部署了几个项目,同样的流程走熟了之后,基本十分钟内就能把一个新站点从零推到线上。这也正是Serverless架构的魅力:当服务器、证书、扩容这些问题都不再需要你关心,你要做的就是专注于内容本身,以及偶尔在CDN配置页里点几个按钮。
如果你正准备把现有站点迁过来,我的建议是先拿一个低风险的子站点试水,把整套流程跑通、记录下自己的验收清单,再逐步迁移核心站点。踩坑不可怕,可怕的是每次都在同一个坑里重复浪费半天时间。希望这份实操记录能让你少走几段弯路。