Daybook 网站部署与图片上传排错
本页定位:记录 Daybook 在线编辑器新增图片上传功能时遇到的真实故障。重点不是保存操作过程,而是说明如何区分登录、前端、Pages Functions、R2 绑定和部署状态,并把“代码已推送”与“线上功能已验证”分开。
1. 目标与数据链路
目标是在知识库编辑器中支持两种操作:点击选择图片,或直接把图片拖进上传区域。上传成功后,编辑器自动在正文光标位置插入 Markdown 图片链接。
选择 / 拖入图片
↓
编辑器 FormData
↓
/api/editor/upload
↓
Pages Function 验证登录、类型和大小
↓
R2:oyz-daybook-media/images/
↓
返回公开 URL,并插入 Markdown
图片放在 R2,而不是塞进 Git 仓库。这样文章构建继续只处理 Markdown、HTML、CSS、JavaScript 和小型静态附件,图片上传不需要等待下一次 Pages 构建才能访问。
2. 最初出现的现象
功能第一次上线后,连续出现了三个容易混淆的现象:
- 网站右下角或编辑器中的“+ 新建”没有反应;
- “点击选择图片”没有弹出文件窗口;
- 拖入图片后没有状态提示,也没有插入 Markdown。
当时还出现过浏览器停在 /api/auth/callback 的情况。OAuth 回调页不是编辑器页面,过期的授权码也不能继续复用;但这只能解释某一次登录停滞,不能解释重新进入编辑器后所有按钮仍然失效。
3. 排错时必须分开的五层
3.1 GitHub 提交
git push 成功只说明代码到达远端仓库,不能说明 Cloudflare 已构建,更不能说明浏览器中的功能可用。
3.2 Cloudflare Pages 部署
部署列表中的生产版本必须对应预期提交,并且状态为 Active。本次曾有一次空提交触发的部署返回 Failure,重试后才成功;因此不能只看到“已触发部署”就结束检查。
npx wrangler pages deployment list --project-name oyz-daybook-git
3.3 GitHub OAuth 会话
编辑器和上传接口都需要有效的 oyz_session。未登录请求会跳转到 /api/auth/login;浏览器停在旧的 callback URL 时,应重新从 /editor/ 发起登录,不能刷新一次性授权码。
3.4 Pages Function 与 R2 绑定
上传接口通过 context.env.MEDIA 访问 R2。Pages 项目需要把 oyz-daybook-media 绑定为 MEDIA,并在绑定后重新部署。绑定属于运行时资源;前端按钮是否响应与 R2 是否绑定是两个不同问题。
3.5 浏览器中的生成后 JavaScript
本项目的编辑器不是直接维护最终 HTML,而是由 scripts/build-editor.mjs 生成。检查生成器文件能通过 node --check,不等于生成到 public/editor/index.html 里的内联脚本也合法。
这次真正的根因就在这一层。
4. 真正根因:模板中的换行被二次解释
上传成功后,需要在正文光标处补一个换行。代码位于 JavaScript 模板字符串内部,原写法中的 \n 在生成 HTML 时被外层模板先解释了一次,最终把正则拆成了真实换行:
// 生成后的错误形态
!/
$/
浏览器因此报出“正则表达式未结束”一类语法错误,整段编辑器脚本都没有执行。结果不是只有上传失效,而是依赖同一脚本的“+ 新建”、点击选图、拖放监听和状态提示一起失效。
修复原则是在生成器中对反斜杠再转义一次,使最终 HTML 中保留真正的 \n:
els.content.setRangeText(
(start && !/\n$/.test(els.content.value.slice(0, start)) ? "\n" : "")
+ markdown + "\n",
start,
end,
"end"
);
这里最重要的不是记住某一行,而是记住:生成 JavaScript 的 JavaScript 有两层语法,必须检查最终产物。
5. 图片上传实现边界
后端只接受 multipart/form-data 中的 file 字段,并执行以下限制:
- 必须是图片 MIME 类型;
- 支持 JPEG、PNG、GIF、WebP、SVG 和 AVIF;
- 单张图片不超过 10 MiB;
- 对象写入
images/前缀,并使用日期、UUID 和清理后的原文件名组成键; - 响应只返回对象键、原名称和公开 URL,不返回任何凭据;
- R2 对象使用长期缓存,文件名中的 UUID 避免覆盖旧图。
前端在上传期间显示状态,成功后自动插入:

6. 为什么前几次“看起来验证过”仍然失败
前几次验证只覆盖了部分链路:
node --check scripts/build-editor.mjs只检查生成器本身;- Pages 显示
Active只证明构建和部署完成; - R2 binding 已填写只证明运行时有存储入口;
- 把隐藏的文件输入改成原生
<label>,只能改善点击触发,不能修复整段脚本的语法错误。
缺失的关键检查是:提取生成后 HTML 中的每段 <script>,逐段执行语法检查,并用真实浏览器内核触发上传事件。
7. 最终验证
本次在重新上线前完成了三层验证。
7.1 生成产物检查
运行编辑器构建脚本,提取 public/editor/index.html 中的内联脚本,再逐段运行 node --check。修复前能稳定复现语法错误,修复后全部通过。
7.2 浏览器内核回归
新增 scripts/test-editor-browser.mjs,使用本机 Edge 的无界面浏览器和 DevTools Protocol 验证:
- 文件输入控件真实存在;
- 选择文件会调用上传接口;
- 拖放
File会调用同一上传流程; - 成功响应会插入 Markdown;
- 页面状态显示“图片上传成功,已插入 Markdown”。
7.3 后端与远端 R2 冒烟测试
scripts/test-editor-function.mjs 增加 multipart、缺少 MEDIA 绑定、R2 写入、MIME 和公开 URL 的断言。随后向远端 oyz-daybook-media/images/ 上传临时 SVG,公开地址返回 HTTP 200,再删除测试对象。
Wrangler 的 R2 命令默认可能操作本地模拟桶。第一次未加远端参数时,命令显示 Resource location: local,公开地址自然返回 404。线上冒烟测试必须明确使用 --remote:
npx wrangler r2 object put oyz-daybook-media/images/test.svg --file .\test.svg --remote
npx wrangler r2 object delete oyz-daybook-media/images/test.svg --remote
8. 提交与部署时间线
| 提交 | 结果 | 说明 |
|---|---|---|
0d5d7e3 |
功能初版 | 增加拖放区域、上传 API 和 R2 配置,但未发现生成后脚本错误 |
b138e1c |
Pages Failure | 重新部署第一次失败,不能当作已上线 |
b7592fb |
Active | 部署重试成功,但前端脚本仍不可执行 |
5af0ce7 |
Active | 改为原生文件选择触发,仍未触及真正根因 |
798704e |
Active | 修复双层转义,浏览器、Function 和远端 R2 测试通过后上线 |
9. 以后固定执行的发布检查
-
git diff --check无格式错误; - 生成器源文件通过
node --check; - 先生成
public/,再检查最终 HTML 中的全部内联脚本; - 前端交互使用浏览器内核测试,不以“代码看起来正确”代替点击和拖放;
- Function 测试覆盖未登录、缺少 binding、格式错误、大小超限和成功写入;
- R2 冒烟测试明确区分
local与remote; - 临时对象测试后删除;
- 正常推送
main,不强制推送; - 核对远端提交;
- 核对 Pages 生产部署对应提交且状态为
Active; - 最后再用正式域名完成一次登录、上传和图片公开访问检查。
10. 结论
这次故障不是单纯的 R2、OAuth 或缓存问题,而是生成后的 JavaScript 语法错误。最有效的排错顺序是:先确认页面是否正确、再看浏览器脚本、然后检查 API 和 binding,最后验证对象存储与公开 URL。
以后判断网站功能是否完成,需要同时满足:代码进入远端、Pages 部署成功、生成产物合法、浏览器交互通过、后端写入成功、公开资源可访问。