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. 最初出现的现象

功能第一次上线后,连续出现了三个容易混淆的现象:

  1. 网站右下角或编辑器中的“+ 新建”没有反应;
  2. “点击选择图片”没有弹出文件窗口;
  3. 拖入图片后没有状态提示,也没有插入 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 避免覆盖旧图。

前端在上传期间显示状态,成功后自动插入:

![图片名称](https://公开媒体域名/images/对象名.png)

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 部署成功、生成产物合法、浏览器交互通过、后端写入成功、公开资源可访问。