如何通过亚马逊S3存储桶 + CloudFront + Cloudflare 部署 H5 项目
最近折腾了一次 H5 项目的上线,原本以为这种纯前端静态项目会很简单,结果真正麻烦的地方根本不在打包,而是在部署链路上:S3 目录权限、CloudFront 的 SPA 回退、Cloudflare 的域名解析切换、还有 App 内 WebView 的缓存问题。
这篇文章就把这次完整过程整理一下,专门讲如何通过 Amazon S3、CloudFront 和 Cloudflare 部署 H5 项目,以及上线过程中最容易踩的坑和对应处理方式。
1. 先说结论:H5 静态部署真正难的不是上传文件
很多人第一次部署 H5,会以为流程很简单:
- 打包出
dist - 上传到 S3
- 绑定域名
- 结束
但实际线上出问题时,往往不是文件没上传成功,而是下面这些地方出了问题:
- 前端路由是 SPA,直接访问子路径会 403 或 404
- 新建了 S3 目录,但 CloudFront 回源读不到
- CloudFront 还缓存着旧的
index.html - App 内 WebView 缓存了旧入口页,导致脚本加载报错
- 新版本上线时把旧的 hash 静态资源删掉了,结果老缓存直接炸了
所以如果想把这件事做稳,不能只盯着 S3 上传成功没成功,而是要把整条链路看成一个整体。
2. 整体架构是什么样的
我这里的部署架构比较典型:
- S3:负责存静态文件
- CloudFront:负责对外访问、缓存和 HTTPS
- Cloudflare:负责 DNS 解析
对应关系如下:
flowchart LR
User[浏览器 / App WebView] --> DNS[Cloudflare DNS]
DNS --> CDN[CloudFront]
CDN --> Bucket[S3 Bucket]
Bucket --> AppFiles[index.html]
Bucket --> Assets[assets js css images]
flowchart LR
User[浏览器 / App WebView] --> DNS[Cloudflare DNS]
DNS --> CDN[CloudFront]
CDN --> Bucket[S3 Bucket]
Bucket --> AppFiles[index.html]
Bucket --> Assets[assets js css images]
flowchart LR
User[浏览器 / App WebView] --> DNS[Cloudflare DNS]
DNS --> CDN[CloudFront]
CDN --> Bucket[S3 Bucket]
Bucket --> AppFiles[index.html]
Bucket --> Assets[assets js css images]flowchart LR
User[浏览器 / App WebView] --> DNS[Cloudflare DNS]
DNS --> CDN[CloudFront]
CDN --> Bucket[S3 Bucket]
Bucket --> AppFiles[index.html]
Bucket --> Assets[assets js css images]
如果再细化一点,实际请求链路可以理解成这样:
sequenceDiagram
participant U as 用户/App
participant CF as Cloudflare
participant CDN as CloudFront
participant S3 as Amazon S3
U->>CF: 访问 h5-app.example.com
CF->>CDN: DNS 解析到 CloudFront
CDN->>S3: 请求 index.html 或静态资源
S3-->>CDN: 返回文件
CDN-->>U: 返回页面与资源
sequenceDiagram
participant U as 用户/App
participant CF as Cloudflare
participant CDN as CloudFront
participant S3 as Amazon S3
U->>CF: 访问 h5-app.example.com
CF->>CDN: DNS 解析到 CloudFront
CDN->>S3: 请求 index.html 或静态资源
S3-->>CDN: 返回文件
CDN-->>U: 返回页面与资源
sequenceDiagram
participant U as 用户/App
participant CF as Cloudflare
participant CDN as CloudFront
participant S3 as Amazon S3
U->>CF: 访问 h5-app.example.com
CF->>CDN: DNS 解析到 CloudFront
CDN->>S3: 请求 index.html 或静态资源
S3-->>CDN: 返回文件
CDN-->>U: 返回页面与资源sequenceDiagram
participant U as 用户/App
participant CF as Cloudflare
participant CDN as CloudFront
participant S3 as Amazon S3
U->>CF: 访问 h5-app.example.com
CF->>CDN: DNS 解析到 CloudFront
CDN->>S3: 请求 index.html 或静态资源
S3-->>CDN: 返回文件
CDN-->>U: 返回页面与资源
这里面三个服务各干各的事:
- S3 只负责存文件,不负责聪明地理解你的前端路由。
- CloudFront 负责把静态文件作为站点发布出去,同时承担缓存和回退逻辑。
- Cloudflare 只是把域名指向 CloudFront,它不负责修复 S3 和 CloudFront 的配置问题。
3. S3 目录应该怎么规划
如果线上只有一个目录,每次都直接覆盖,例如:
|
|
那每次上线都会有几个风险:
- 覆盖到一半,用户可能拿到半新半旧的资源
- 新版本有问题时回滚麻烦
- App 或 WebView 还缓存着旧入口页时,旧静态资源可能已经被删掉
所以我更推荐一开始就把目录拆开,例如:
|
|
含义很简单:
h5-app/:当前正式版本h5-app-next/:新版本、预发布版本
这样做的好处是:
- 新版本可以先独立部署、独立验证
- 正式切换时,不需要直接覆盖当前线上目录
- 旧版本始终保留,便于快速回滚
这其实就是一个很轻量的蓝绿发布思路。
4. 新建 S3 目录后,为什么明明上传成功了却访问不了
这个坑我觉得很值得单独拿出来说,因为非常容易忽略。
很多时候我们会在 S3 里新建一个目录,比如:
|
|
然后把新版本文件传进去,CloudFront 也指过去了,结果一访问还是报错,常见表现有:
403 AccessDenied- 通过 CloudFront 分配域名访问不了
- 静态资源加载失败
这类问题经常不是“目录没建对”,而是权限没配对。
4.1. 为什么会这样
S3 里的“目录”其实只是对象前缀,不是真正的文件夹。你看到的 h5-app-next/,本质上只是对象 key 的一部分。
也就是说,目录存在不等于可访问。真正决定它能不能被访问的是:
- Bucket 的公共访问设置
- Bucket Policy
- CloudFront 是否有权限读取这个目录下的对象
4.2. 如果你是公开 Bucket 的方式
如果当前用的是“公开读取的 S3 + CloudFront”,那新增目录后要确认:
- 打开 S3 Bucket
- 进入
Permissions - 检查
Block public access - 检查 Bucket Policy
至少要保证对象可以被读取,例如:
|
|
如果整桶被公共访问限制拦住了,那么你即使把文件传进去了,通过 CloudFront 也可能还是读不到。
4.3. 如果你是 CloudFront 私有回源方式
如果你们是通过 CloudFront 的 OAC 或 OAI 回源 S3,那也一样需要检查:
- CloudFront 的源站访问身份是否正确
- Bucket Policy 是否允许对应 CloudFront 读取对象
- 新目录下的对象是否同样处于可读取范围内
一句话总结就是:
在 S3 里新增目录并上传文件之后,一定要顺手检查 Bucket 公共访问权限或者 CloudFront 回源权限,否则目录虽然存在,但就是访问不了。
5. CloudFront 最重要的不是加速,而是把 H5 站点“发布正确”
很多人提到 CloudFront,会先想到 CDN 和加速,但对于 H5 项目来说,它还有一个更重要的作用:把一个“静态文件集合”正确发布成一个“可以访问的前端站点”。
对于 H5 项目,CloudFront 至少要做三件事:
- 提供 HTTPS 和域名绑定
- 做缓存
- 做 SPA 路由回退
5.1. 默认根对象
CloudFront 的默认根对象建议设置成:
|
|
否则访问域名根路径时,可能不能自动返回首页。
5.2. SPA 路由回退为什么必须配
H5 项目往往会有很多前端路由,例如:
/dl/lucky-gifts/recharge/faq/invitation
这些路由并不是 S3 里的真实文件路径。
这就意味着:
- 从首页点进去通常没问题,因为页面已经加载,前端路由自己接管了。
- 直接访问这些路径时,如果 CloudFront 不处理,S3 会去找对应对象。
- 找不到对象就会返回
403或404。
所以必须在 CloudFront 配置自定义错误页回退:
403 -> /index.html -> 200404 -> /index.html -> 200
这样用户直接打开 /dl 时,CloudFront 会把它兜到底层的 index.html,再让前端路由接管。
5.3. 为什么会看到 AccessDenied
如果没有配这一步,直接访问页面子路径时,经常就会看到:
AccessDeniedNoSuchKey
这并不一定说明你的前端代码有问题,而是说明 S3 正在认真地找一个并不存在的对象路径。
6. Cloudflare 在这套方案里的角色是什么
Cloudflare 在这里更像一个“域名入口层”,负责把业务域名解析到 CloudFront。
例如:
h5-app.example.com-> 正式 CloudFronth5-app-next.example.com-> 预发布 CloudFront
这样做的好处是:
- 正式环境和测试环境可以分开
- 切换时只需要改解析或迁移域名归属
- 回滚也更方便
这里要注意一个点:Cloudflare 只是负责 DNS,不负责替代 CloudFront 做 SPA 回退,也不负责修复 S3 权限问题。
7. 推荐的上线方式:蓝绿发布,而不是直接覆盖线上目录
如果每次上线都直接拿新的文件覆盖旧的 h5-app/,短期看似省事,长期一定踩坑。
我更推荐的方式是:
- 旧正式版本放在
h5-app/ - 新版本放在
h5-app-next/
上线流程大致是:
- 新版本先上传到
h5-app-next/ - 新的 CloudFront 指向
h5-app-next/ - 用
h5-app-next.example.com做完整测试 - 验证没有问题后,再切正式域名
这样做的价值很明显:
- 正式流量不受影响
- 可以提前测试真实线上环境效果
- 有问题随时回滚
8. 正式切换时,域名应该怎么切
假设正式域名是:
|
|
预发布域名是:
|
|
推荐切换步骤如下。
8.1. 切换前准备
- 新版本已经上传到
h5-app-next/ - 新的 CloudFront 源站已经指向
h5-app-next/ h5-app-next.example.com已经完整测试通过- 新 CloudFront 的证书已经覆盖
h5-app.example.com - 新 CloudFront 已经配好 SPA 路由回退
8.2. 不推荐的做法
不要这么做:
- 先去旧 CloudFront 删除
h5-app.example.com - 再去新 CloudFront 加上这个域名
- 最后再去 Cloudflare 改解析
这种做法中间会有空窗期,正式域名可能短暂不可用。
8.3. 推荐的做法
更稳的方式是通过 CloudFront 的“移动备用域名”能力,把正式域名从旧 distribution 迁到新 distribution。
迁移完成后,再去 Cloudflare 中把:
|
|
这条 CNAME 的值改成新的 CloudFront 域名。
也就是说,“切 DNS”真正改的是:
|
|
不是把它改成 h5-app-next.example.com,而是改成预发布 CloudFront 背后的分配域名。
9. invalidation 到底是什么
部署过程中另一个高频问题是:S3 上明明已经是新文件了,为什么线上访问还是旧页面?
答案通常是:CloudFront 还缓存着旧内容。
invalidation 的作用就是让 CloudFront 的缓存失效。失效后,下次请求会重新回源拉最新文件。
9.1. 什么时候需要做 invalidation
以下场景建议做:
- 发布新版本后,希望用户尽快拿到最新首页
- 正式域名切换到新的 CloudFront 后,希望立刻生效
- 线上已经出现资源错配、页面异常、白屏等问题时
9.2. 一般刷哪些路径
常规发布时,至少建议刷新:
|
|
如果已经出现明显异常,建议直接刷新:
|
|
10. 为什么不能随便删除旧的静态资源
这一点是我这次踩坑之后感受最深的地方之一。
现代前端打包后,静态资源文件通常都带 hash,例如:
|
|
如果发布新版本时直接执行这种命令:
|
|
那旧版 hash 静态资源就会被删掉。
一旦用户设备里还缓存着旧版 index.html,它就会继续请求这些旧资源。如果这些资源已经不存在,页面就会异常。
10.1. 推荐的上传策略
更稳妥的顺序是:
- 先上传新的
assets/ - 不删除线上已有的旧
assets/ - 最后再上传新的
index.html - 然后执行 CloudFront invalidation
这样做的好处是:
- 旧入口页仍然能拿到它依赖的旧资源
- 新入口页也能拿到新资源
- 可以显著减少新旧资源错配
11. 为什么浏览器正常,App 内 WebView 却报错
这类问题非常常见。
常见表现是:
- 浏览器里打开 H5 正常
- App 内打开同一个 H5 页面报错
- 错误类似:
|
|
11.1. 根本原因是什么
通常不是页面代码本身坏了,而是 App 内 WebView 缓存了某一版旧的 index.html。
旧的 index.html 会去请求某些旧的 chunk 文件,例如:
|
|
如果这些旧 chunk 在发布时已经被删除,或者当前目录只保留了新的一套资源,就会出现:
- WebView 继续请求旧 JS
- 服务器找不到该文件
- 由于启用了 SPA 回退,又返回了
index.html - 浏览器把 HTML 当成 JS 解析
- 最终报出 MIME type 错误
11.2. 为什么切回旧域名解析也不一定立刻恢复
这是很多人会疑惑的点。
DNS 切回旧环境,只能影响之后的新请求,但并不能保证:
- App WebView 会丢掉本地缓存的旧入口页
- CloudFront 或 Cloudflare 没有缓存旧结果
- 用户设备不会继续请求已经失效的旧 chunk
所以就算解析切回去了,App 里也不一定立刻恢复。
12. 遇到 MIME type 报错时怎么止血
如果线上已经出现:
|
|
我更推荐先做服务端补救,而不是第一时间让用户去清缓存或者重装 App。
12.1. 推荐止血步骤
- 保留当前线上目录,不要先删除旧文件
- 将当前最新版本的
assets/上传到正式目录 - 不要删除已有的旧
assets/ - 再上传当前最新版本的
index.html - 如果手里还有前一版本的
assets/,也一并补回正式目录 - 对正式 CloudFront 执行:
|
|
12.2. 为什么这招有效
因为它的本质是让旧版和新版的 hash 静态资源同时存在。
只要用户设备里缓存的旧入口页仍然能拿到它依赖的旧 JS,页面通常就能恢复,不一定非要让用户重装 App。
13. WebView 缓存一般会持续多久
这个问题没有统一答案。
WebView 的缓存时长通常取决于:
Cache-ControlExpiresETag- App 是否做了额外缓存
也就是说:
- 有可能几分钟
- 有可能几小时
- 有可能几天
- 甚至一直到用户清缓存、重装 App 或客户端主动刷新
所以不能指望“等一会儿就好了”,更稳妥的方式还是服务端兼容旧资源。
14. 推荐的缓存策略
为了尽量减少 H5 发布后的缓存问题,入口页和静态资源最好使用不同的缓存策略。
14.1. index.html
建议短缓存或不缓存:
|
|
14.2. assets/*.js、assets/*.css
建议长缓存:
|
|
前提是这些静态资源文件名带 hash。
15. 一次完整的标准流程
最后给出一份我比较推荐的完整流程。
15.1. 预发布
- 将新版本上传到
s3://example-h5-bucket/h5-app-next/ - 确认新的 CloudFront 指向
h5-app-next/ - 检查 S3 公共访问权限或 CloudFront 回源权限
- 确认
h5-app-next.example.com可访问 - 测试关键路由:
/、/dl、/lucky-gifts、/recharge/faq、/invitation
15.2. 正式切换
- 确认新 CloudFront 证书已覆盖正式域名
- 迁移正式备用域名到新 distribution
- 在 Cloudflare 中将正式域名的 CNAME 指向新 CloudFront 域名
- 执行 invalidation
- 再次验证正式页面和关键路由
15.3. 回滚
如果新版本有问题:
- 将 Cloudflare 中正式域名的 CNAME 改回旧 CloudFront
- 对旧 CloudFront 执行 invalidation
- 验证首页和关键页面
- 保留新环境继续排查,不要立即删除新旧资源
16. 关键结论
最后总结几条我觉得最值得记住的结论。
16.1. H5 部署真正难的不是上传文件,而是缓存和路由
- S3 只负责存静态文件
- CloudFront 负责把静态文件发布成一个真正可访问的站点
- Cloudflare 只负责 DNS,不负责替你修前端路由问题
16.2. 新建 S3 目录后,权限一定要顺手检查
- 目录上传成功不代表能访问
- 要检查 Bucket 公共访问权限或 CloudFront 回源权限
- 否则新的目录即使存在,也可能始终 403
16.3. 正式发布推荐蓝绿思路
- 新版本先放
h5-app-next - 测通过后再切正式域名
- 保留旧环境,方便回滚
16.4. 不要急着删旧静态资源
- 旧入口页可能还在用户 WebView 里缓存
- 旧资源一旦删除,App 内非常容易直接报错
- 新旧资源共存通常比“立刻清理旧资源”稳得多
16.5. 遇到问题时,优先补资源和刷 CDN
解决顺序更推荐这样:
- 先补齐旧版和新版静态资源
- 再做 CloudFront invalidation
- 再看是否需要让客户端清缓存
这样通常比一上来就要求用户重装 App 更稳妥。