1. 从一次静态站点部署的纠结说起
Cloudflare Pages 这个玩意儿,我第一次认真用是在给一个前端小项目找落脚点的时候——纯静态产物、没有数据库、没有后台服务,就一堆 HTML、JS、CSS 外加几张图片。看着简单,真要选个地方放上去,麻烦事一堆:构建流水线要自己搭,每次改一行文案就得重新走一遍上传,团队里两个人同时改还得手动合并产物,客户要看预览版只能打个压缩包发过去。那阵子我前后试过三种托管方式,每种都能跑,但每种都有一两处让人别扭的地方,直到把项目迁到 Cloudflare Pages 上,这套流程才算真正顺下来。
Cloudflare Pages 本质上是面向前端产物的托管服务:你把代码仓库连上去,它按你给的命令跑一次构建,把输出目录里的文件推到它分布在全球的边缘节点上,然后给你一个可以直接访问的域名。听起来跟其他同类服务差别不大,但它把几件事揉在了一起——分支自动生成预览地址、构建产物与函数逻辑放在同一个项目里、静态资源和边缘逻辑共用一套域名与证书配置。适合的人群也很明确:写前端但不想碰服务器的人、需要给非技术同事快速看效果的人、想用一份配置文件把静态站和少量后端接口一起管起来的人。哪怕你只会npm run build这一条命令,也能在十几分钟内把一个站点跑起来。
我用它的这段时间,踩过的坑不算少,但绝大多数都不是它本身的问题,而是我对"构建"和"部署"这两个阶段的边界没想清楚。下面就把这套东西从设计思路到实操细节完整拆一遍,尽量把每一步"为什么这么做"讲透,让你少走我走过的弯路。
2. 整体设计思路与选型拆解
2.1 "构建产物 + 边缘分发"这条主线
用 Pages 之前,得先在心里建立一个清晰的模型:它管的是产物,不管你的源码长什么样。你在代码托管平台上放的是什么框架、用什么包管理器、跑几条构建脚本,它都不关心;它只关心一件事——你把命令写对之后,输出目录里有没有一个可以直接被浏览器读取的文件树。这条主线决定了后面所有的配置项:根目录决定它在哪个子目录里执行命令,构建命令决定产物怎么生成,输出目录决定它去哪里捡文件。这三件事想清楚了,剩下的都是锦上添花。
这个模型带来的最大好处是产物即契约。本地跑npm run build生成的dist目录,和线上跑出来的是同一套东西。我习惯在上线前先把本地构建产物用静态服务器起一遍,确认无误再推代码,这样能把九成以上的"线上白屏"问题挡在门外。反过来说,如果你的项目依赖了只在特定环境才有的变量,或者构建脚本里偷偷读了本地文件,那这套模型就会立刻暴露问题——因为它只认命令执行的结果。
另一个容易忽略的点是:构建和分发是两个独立阶段。构建失败的时候,线上还是上一次成功的产物,站点不会挂;分发阶段出问题(比如文件太大、路径大小写不一致),你会在部署日志里看到明确的报错。把这两个阶段分开看,排查问题的速度会快很多,我后面整理的排查表就是按这个顺序组织的。
2.2 几类托管方案的横向对照
选型这事没有绝对优劣,只有场景匹配。我把自己实际用过的几类方案按几个关键维度列了一张表,都是我在真实项目里比较在意的点:
| 维度 | Cloudflare Pages | 对象存储直传 | 自建轻量服务器 |
|---|---|---|---|
| 构建流水线 | 内置,连仓库即用 | 需自己写脚本 | 需自己写脚本或搭流水线 |
| 分支预览地址 | 每个分支自动生成 | 需额外配置 | 需额外配置 |
| 回滚到历史版本 | 控制台点一下即可 | 需自己管理版本目录 | 靠备份文件,麻烦 |
| 边缘缓存 | 默认内置,按文件粒度 | 需手动配缓存策略 | 需自己配 CDN |
| 少量接口逻辑 | 同项目内写函数 | 需另找服务 | 自己写服务进程 |
| 证书与域名 | 绑定域名后自动签发 | 需自行处理 | 需自行续期 |
对象存储直传的优势是便宜、可控、不依赖构建;缺点是每次发布都得自己写上传脚本、处理缓存刷新、处理文件删除(增量同步很容易留垃圾文件)。自建服务器的优势是自由度最高,能装任何东西;缺点是安全补丁、证书续期、进程守护这些事永远在消耗你的时间。Pages 处在中间:它把流水线和分发替你做了,代价是你得接受它的一些约定,比如构建环境是干净的容器、构建时长和并发次数有配额。我的判断标准很简单——如果这个项目未来半年内不太可能需要长连接或本地磁盘,那就别自建。
2.3 免费额度的真实边界在哪里
很多人上来就问免费额度够不够用,我的回答通常是:对个人项目和小团队内部站来说,绰绰有余;对流量型商业站,得先算清楚构建次数。写这篇文章时的免费档大致是这样,具体数字以控制台和官方文档为准,因为这类配额会调整:
| 配额项 | 免费档大致量级 | 实际影响 |
|---|---|---|
| 每月构建次数 | 数百次 | 提交频繁的项目要留意 |
| 同时进行的构建 | 1 个 | 多人同时推代码会排队 |
| 单站点文件数 | 两万级 | 图片多的站要提前规划 |
| 单文件大小 | 二十几 MiB | 大视频别往这里放 |
| 静态请求与流量 | 不单独计量 | 静态站基本不用担心 |
| 函数请求 | 每天十万级 | 接口调用量大的要注意 |
真正会卡住人的不是流量,是构建次数。我有个项目当时把每次推送都触发构建,一段时间里提交了几百次小改动,配额很快就见底了。后来改成"功能分支推代码不构建、合并到主分支才构建",配额一下就宽松了。这个细节后面第 6 节会展开。
提示:配额数字会变,动手前先去控制台的用量页面看一眼当期数值,别拿旧文章的数字当准绳。
3. 核心概念与关键配置要点
3.1 构建命令、输出目录与根目录三件套
这三个配置项决定了构建能不能跑通,我见过的新手问题里至少一半出在这里。根目录是相对仓库根的位置,如果你的前端代码在web/子目录里,这里就得填web,否则它在仓库根执行命令,会找不到package.json。构建命令就是你在本地跑的那条命令,npm run build、pnpm build、yarn generate都行,但要注意包管理器要和仓库里的锁文件一致——仓库里有pnpm-lock.yaml你就别写npm install,否则依赖版本可能对不上,本地好好的线上炸。
输出目录是最容易写错的一个。不同框架的默认产物目录完全不一样:有的叫dist,有的叫build,有的叫public,还有的生成到.output/public这种嵌套路径里。我第一次用的时候就填错了,构建日志显示成功,访问却是一个空白页或者 404,折腾了半天才发现文件全在另一个目录。判断方法很简单:本地跑一次构建,然后看哪个目录里生成了index.html,那个目录就是输出目录。
注意:输出目录填的是相对仓库根(或根目录配置)的路径,不是绝对路径。填
/dist这种写法大概率会报错。
还有一个隐性坑:有些框架默认会把源码也输出到产物目录里,或者生成体积很大的 source map。上线前扫一眼产物大小,几百 KB 的 JS 是正常的,几十 MB 就说明配置有问题,先压缩、分包、关掉生产环境的 map,再推上去。
3.2 环境变量与多环境隔离
环境变量这块,Pages 的设计思路是"按环境分别配置"。生产环境和预览环境可以有两套完全不同的变量值,这个设计非常实用——你的预览环境可以指向测试接口、测试数据源,生产环境指向正式接口,两边互不干扰。配置位置在项目的设置页里,分生产环境和预览环境两组,添加时还可以选择是明文还是加密存储。
加密存储这个选项很多人不用,我觉得凡是带密钥性质的都该勾上。明文变量在构建日志里可能会被打印出来(尤其是你在构建脚本里echo调试的时候),而加密变量在任何界面都不会回显原值,只能覆盖不能查看。我自己有过一次教训:为了排查一个接口地址拼接错误,在构建脚本里加了一行打印全部环境变量的命令,跑完之后立刻删掉了,但那一次日志里已经留下了完整内容。从那以后凡是密钥类的一律加密。
变量命名上有个小约定值得遵守:前端框架通常只把带特定前缀的变量注入到客户端代码里(比如NEXT_PUBLIC_、VITE_这类),不带前缀的只在构建阶段可用,不会被打包进浏览器。这个机制是保护措施,别为了图省事把所有变量都加上前缀,等于把自己的密钥明码发到浏览器里。判断标准是:这个值如果被任何访客按 F12 看到,会不会出问题?会,就别加前缀。
3.3 分支预览与预览地址的用法
分支预览是我最喜欢的功能,没有之一。你推一个分支上去,它自动给这个分支生成一个独立的访问地址,指向这次提交的构建产物。产品同事要看效果,把链接发过去就行,不用等合并、不用手动部署。更妙的是每次往同一个分支推新提交,这个地址的内容会自动更新,等于一个随代码走的演示环境。
实际用的时候有几个细节值得注意。第一,预览地址和正式域名是分开的,预览地址默认带一段随机子域,不要把它当成正式入口发给外部用户。第二,如果某个分支你不想让它构建(比如纯文档改动、依赖升级的实验分支),可以在设置里配置分支包含/排除规则,只让特定分支触发构建,这是省配额最直接的手段。第三,预览环境同样可以绑定自定义域名,团队内部站可以给它配一个preview.开头的子域,方便记忆。
提示:把"预览地址自动贴在代码评审里"做成习惯,评审的人点一下就能看到实际效果,比看代码 diff 高效得多。
我在团队里推这套流程的时候,还额外定了一条规矩:任何涉及视觉改动的提交,必须在描述里附上预览链接和一张关键区域截图。这条规矩执行了几个月之后,来回沟通的成本明显下降。
3.4 Functions:把少量后端逻辑贴在边缘
Pages 提供了一层函数能力,把接口逻辑和静态产物放在同一个项目里,路径以/functions目录为约定。你在functions/api/hello.js里导出一个处理函数,它就会响应/api/hello这个路径的请求。这个能力的定位很清楚——处理轻量逻辑:读取请求头做重定向、校验一个签名、调用一个外部接口并做一层数据裁剪、给静态页面注入一点动态内容。它不是给你跑长时间任务或者连数据库连接池的。
一个最小的处理函数长这样:
// functions/api/hello.js export async function onRequestGet(context) { const { request, env } = context; const url = new URL(request.url); const name = url.searchParams.get('name') || '访客'; return new Response(JSON.stringify({ hello: name }), { headers: { 'content-type': 'application/json; charset=utf-8' } }); }env里能拿到你在项目设置里配的环境变量,这一点很关键——函数用的变量和构建用的变量是同一套,但要注意函数的变量是在运行时读取的,改了之后不需要重新构建就能生效,这是它和构建期变量最大的区别。我刚开始没意识到这点,改完变量还傻等了一次构建。
在这里写逻辑有两条经验。一是别把重活放进来,单个请求的执行时间是有上限的,跑批量数据迁移这种事一定会超时,那种任务该放到别的地方。二是做好错误兜底,函数抛异常时返回的是通用错误页,体验很差,我习惯在最外层包一层 try/catch,出错时返回一个结构化的 JSON 和合适的状态码。
3.5 自定义域名、_headers 与 _redirects
绑定自定义域名这一步没什么难度,在项目设置里添加域名,按提示在域名解析那边加一条记录就行,证书会自动签发和续期。需要注意的只有一点:如果你同时有根域和www子域,建议选一个做正式入口,另一个用重定向指过去,避免出现两份内容被搜索引擎当成重复页面。
真正体现功力的是产物根目录下的两个特殊文件:_headers和_redirects。它们不是标准协议的一部分,是这套服务认的约定文件,放在输出目录根部就会生效。
_headers用来给路径批量设置响应头。下面这段是我常用的模板,第一段给所有资源加上基础安全头,第二段给带哈希的静态资源设置长缓存:
/* X-Content-Type-Options: nosniff X-Frame-Options: DENY Referrer-Policy: strict-origin-when-cross-origin /assets/* Cache-Control: public, max-age=31536000, immutable_redirects用来做跳转和单页应用的路由回退。单页应用刷新子路由会 404,这是最常见的抱怨,加一行规则就解决了:
/old-page /new-page 301 /* /index.html 200注意最后这条是 200 而不是 301,意思是"内部返回 index.html 但地址栏不变",这是单页应用回退的标准写法。写成 301 会让所有子路由都跳到首页地址,用户体验直接崩掉。这个坑我踩过一次,当时还以为是前端路由配置写错了。
_headers里还有一个实用技巧:给特定路径单独设置缓存策略。带内容哈希的构建产物(文件名里有一串随机字符)可以设一年缓存并标记为不可变,HTML 入口文件则要设短缓存甚至不缓存,否则用户会一直看到旧页面。这个"入口短缓存、资源长缓存"的组合是所有静态站缓存策略的通用套路。
4. 从零上线一个项目的完整实操
4.1 上线前本地自检
我现在的习惯是,任何项目上线前先在本地把构建产物当成品跑一遍,而不是只跑开发服务器。开发服务器和构建产物之间的差异比想象中大:环境变量注入方式不同、代码压缩可能引入问题、静态资源路径在生产模式下可能变成绝对路径。具体操作就是构建完之后,在产物目录下起一个本地静态服务器看一眼。
npm run build npx serve ./dist打开提示的地址,把主要页面点一遍,重点看三处:首屏样式有没有加载、控制台有没有报错、刷新一个子路由会不会 404。第三点尤其重要,很多人只在开发服务器里点链接,从来没试过直接刷新子路由,上线之后才发现需要配回退规则。
这一步还有个小作用:能提前发现产物体积异常。我有个项目本地构建出来单文件三十多 MB,原因是把一个数据文件当模块引入了,构建工具把它整个打进了主包。这种问题在开发服务器上看不出来,在构建产物上一眼就能发现。
4.2 Git 集成方式的操作步骤
这是最省心的接入方式,适合长期维护的项目。思路是把代码托管平台上的仓库授权给 Pages,之后每次推送自动触发构建。落地步骤大致如下:
- 在控制台新建项目,选择连接代码仓库,按提示完成授权,选中目标仓库。
- 选择要跟踪的分支(通常是主分支),这一条决定了哪些分支的提交会产出正式版本。
- 填写根目录、构建命令、输出目录三项,也就是 3.1 节说的三件套。
- 展开环境变量区域,把生产和预览两套变量分别填好,密钥类勾选加密。
- 点保存并触发首次构建,盯着日志从头看到尾。
第一次构建我建议逐行看日志,别只看最后那个成功标记。日志里会告诉你实际用的运行时版本、装了哪些依赖、产物有多少个文件。我曾经因为在仓库里放了一个.nvmrc指定了一个较老的运行时版本,导致构建工具报语法错误,而报错信息出现在日志很靠前的位置,只看结尾根本发现不了。
4.3 命令行直传方式与 wrangler 配置
有些场景不适合连仓库:产物是本地生成的、你在做一次性迁移、或者你想在流水线里手动控制发布时机。这时候用官方提供的命令行工具直传更方便。项目里放一个配置文件,把项目名、产物目录、兼容日期写清楚:
# wrangler.toml name = "demo-site" pages_build_output_dir = "./dist" compatibility_date = "2024-09-01"然后在本地执行:
npx wrangler pages deploy ./dist --project-name=demo-site --branch=main--branch这个参数决定了这次部署算正式版还是预览版。用main之类的正式分支名就是生产部署,用别的名字就是预览部署,会生成独立地址。这个机制在手动发版时特别有用:想先给别人看一眼,就用一个临时分支名传上去,确认没问题再传一次正式分支。
本地调试函数也在这个工具里:
npx wrangler pages dev ./dist它会起一个本地服务,把functions目录下的逻辑一起挂上,行为和线上接近。我一般改函数之后先在这里验证,省得反复推代码。
注意:命令行直传不会触发构建,你传什么上去就是什么。所以本地一定要先构建好、验证好,别把开发模式的产物传上去。
4.4 上线后的验收清单
部署完成不代表事情结束,我有一套固定的验收动作,五分钟左右能走完,能挡掉绝大多数线上事故:
| 检查项 | 怎么看 | 不合格的表现 |
|---|---|---|
| 首屏渲染 | 无痕窗口打开首页 | 白屏、样式缺失 |
| 子路由刷新 | 直接在子路由地址上按刷新 | 返回 404 |
| 资源缓存头 | 开发者工具看响应头 | HTML 被设了长缓存 |
| 环境变量 | 页面里展示的接口地址 | 指向了测试环境 |
| 函数响应 | 直接访问接口路径 | 返回通用错误页 |
| 移动端 | 手机或模拟器打开 | 布局错乱、点击区域过小 |
我特别强调"无痕窗口"这一步。普通窗口里可能有旧缓存、有登录态、有扩展注入的脚本,很多问题在普通窗口里根本复现不出来。我有一次线上样式错乱,折腾了半小时,最后发现是自己浏览器缓存了旧的 CSS 文件,无痕窗口一开就是好的。
5. 常见问题与排查技巧实录
5.1 构建阶段的失败
构建失败的原因高度集中在三类。第一类是依赖装不上:锁文件和包管理器不匹配、私有依赖没配访问凭据、某个包在构建环境里没有对应的二进制版本。这类问题的特征是日志里明确出现ERR或not found,处理办法是按日志提示改配置,实在不行就在本地用干净的目录重新装一遍复现。
第二类是命令执行出错:比如构建脚本里引用了某个只在本地存在的文件、或者用了只有本地才装的全局命令。构建环境是一个干净容器,你本地全局装的工具它没有,所以任何构建依赖都必须写进项目的依赖声明里。我遇到过一次,本地因为之前装过某个格式化工具所以能跑,线上直接报"命令不存在",最后把那工具加进了开发依赖。
第三类是资源超限:单文件太大、文件总数太多、构建时间太长。这类问题的日志往往不够直白,需要你自己核对产物。处理思路是拆分和裁剪——大图做压缩和按需加载,把不必要的东西从产物目录里移出去,关掉生产环境的 source map。
5.2 路由与 404 的典型症状
404 是这套东西上用起来最容易撞的问题,但症状不同,原因完全不同,得分清楚:
- 首页正常,子路由直接访问 404:缺单页应用回退规则,加
/* /index.html 200。 - 首页正常,某个具体路径 404 但文件明明在:文件名大小写不一致。本地文件系统不区分大小写,线上区分,
About.js被引用成about.js就找不到。 - 所有页面都 404,但日志显示构建成功:输出目录填错了,它去空目录里捡文件。
- 资源请求 404,页面结构还在但没样式:静态资源的基础路径配置有问题,检查构建配置里的 base 路径。
大小写这个问题我被坑过两次,现在处理办法很土但有效:在本地做一次全量检查,把所有引用路径和实际文件名对一遍。更省事的做法是在项目里加一条提交前检查,专门查这个。
5.3 缓存与更新延迟
"我明明改了,怎么还是旧的"——这个问题分两种情况。第一种是浏览器缓存,用户本地存了旧文件。解决办法不是让用户清缓存,而是从一开始就把缓存策略设计对:入口 HTML 短缓存,带哈希的资源长缓存。这样用户每次拿到的都是新的入口文件,入口里引用的资源哈希也变了,自然就拿到新资源。
第二种是你看到的确实是旧版本,部署本身没生效。判断方法很简单:看部署记录里的时间戳和提交号,对不上的话就是构建没触发或者触发了但失败了。构建失败时线上仍然是上一次成功的产物,这个设计是好事,但也会让人误以为"改了没生效"。
还有一种更隐蔽的情况:文件内容变了但文件名没变,缓存策略又设了长缓存,用户就一直拿旧的。这正是"带哈希的文件名"存在的意义——内容一变,哈希就变,文件名就变,缓存自然失效。如果你用的构建工具没做这个,得手动配。
5.4 问题速查表
把上面这些整理成一张表,出问题的时候按顺序对一遍,比漫无目的地翻日志快得多:
| 症状 | 最可能原因 | 第一步动作 |
|---|---|---|
| 构建失败,依赖相关报错 | 包管理器与锁文件不一致 | 统一命令与锁文件 |
| 构建失败,命令不存在 | 依赖未写进项目 | 补进开发依赖 |
| 部署成功但首页 404 | 输出目录配置错误 | 核对产物目录层级 |
| 子路由刷新 404 | 缺回退规则 | 加_redirects规则 |
| 部分资源 404 | 路径大小写不一致 | 逐条比对引用路径 |
| 改了内容不生效 | 入口文件被长缓存 | 调整_headers策略 |
| 函数返回通用错误页 | 逻辑抛异常未兜底 | 加 try/catch 与状态码 |
| 函数读不到变量 | 变量配在构建环境未配运行时 | 检查环境变量分组 |
提示:排查顺序建议固定为"部署记录 → 构建日志 → 产物目录 → 响应头 → 页面控制台"。从上游往下游查,比一上来就怀疑代码有效得多。
6. 成本控制与工程化的一点经验
6.1 构建次数是怎么被消耗掉的
配额里最先见底的通常是构建次数,而消耗它的往往不是"代码改得多",是"触发得太随意"。我梳理过自己一个月的构建记录,接近四成是文档改动、README 更新、依赖版本试探这类完全不需要构建的提交。找到这个规律之后我做了两件事:一是配了分支过滤规则,只让主分支和feature/*触发构建,docs/*这类分支直接不构建;二是把一些频繁的小改动攒一攒再推。
还有一个容易忽略的点是同时进行的构建数量。免费档通常只允许一个构建同时跑,多人同时推代码就会排队,排在后面的等得久,很容易让人误以为卡住了。团队里我的做法是约定主干合并的时间窗口,避免几个人同时往主分支推。
真正需要"每次提交都构建"的项目其实不多。判断标准是:这次构建的产物会不会有人看?如果答案是不会,那这次构建就是纯消耗。
6.2 预览环境保护与团队协作
预览环境默认是公开可访问的,这一点很多人在接外部客户项目时会忽略。如果你的预览版本里有未发布的文案、测试数据、内部链接,直接发个地址出去是有风险的。处理办法有两层:一是利用访问控制能力,把预览环境限制在团队范围内;二是在预览构建里注入一套"测试数据"环境变量,让预览版本本身就不含真实内容。
我在团队里还加了一条约定:预览地址的命名要能看出用途。默认生成的随机子域很难辨认,我们统一用一个固定前缀加上分支名,这样几十个预览地址挂在书签栏里也不至于搞混。同时约定预览环境不绑定正式域名,避免有人误把它当成正式入口分享出去。
团队协作上还有个小技巧值得分享:把构建状态和预览链接接进日常沟通流程。我们用的方式是在代码评审的模板里固定留一栏"预览地址",提交时填上,评审的人点开就能看到实际效果。这个动作看起来只是省了一次复制粘贴,实际效果是让"看效果"变成了评审的默认环节,而不是可选项。
这套东西用到现在,我最大的体会是:它把"部署"从一个需要专门安排时间的事,变成了一个顺手就完成的事。真正的门槛不在工具本身,而在于你有没有把构建命令、输出目录、环境变量这三件事想清楚,以及有没有把缓存策略和路由回退这些细节提前配好。这些地方花半小时配一次,后面能省下几十次排查。最后再分享一个我一直在用的小习惯:每个项目的产物根目录里放一个简单的说明文件,写清楚构建命令、输出目录、特殊配置文件的用途,隔三个月再回来改的时候,这份说明能救你不少时间。