在去年的博客年度更新日志中,我提到本站的评论框功能过于单薄,只能输入纯文本,我一直希望能为它加入更丰富的交互体验——比如支持加粗、斜体、删除线,能上传图片、添加 Emoji 符号等。
一年后的这个国庆假期,我终于抽时间把它完善了。最终实现的效果比我最初设想的更优雅,也更具可维护性、可扩展性。本文将记录我为 WordPress 评论框增加 Emoji 弹层、以及利用 Cloudflare R2 + Pages 搭建免费图床并无缝集成到评论系统 的完整过程。
当然,除了这两个主要功能,我还在前台增加了评论的删除、编辑和点赞功能。点赞其实更像是“排序工具”——当一些文章下的评论数量过多、质量参差时,我可以通过点赞将优质评论手动置顶。至于这一部分的实现细节,后面再说~
1. 最终效果展示
各位可以先看看下面这个视频的实际展示。当然,如果你愿意的话,也推荐直接在评论区亲手试一试,并欢迎提出建议或改进意见。这次改造,我把 Emoji 弹层 和 图床上传功能 所依赖的 JavaScript 脚本全部拆分成了一个独立模块文件。
这样做的好处是:页面在初始加载时几乎不受任何额外脚本影响,保持简洁与快速。
当访客点击评论框上方的表情按钮 😊 或图片按钮 🖼️ 时,浏览器才会按需加载对应模块,实现延迟加载(Lazy Loading)效果。脚本中设置了文件大小与格式校验,符合要求的图像会自动上传到 Cloudflare 图床,上传成功后返回图片 URL,并自动在评论框中插入带有文章标题的 alt 属性的 <img> 标签。
这么做的好处是能让前台 HTML 结构保持优雅简洁,所有功能模块都按需加载,减少首屏体积,从而显著提升访问速度。评论区的图片本身并非核心内容,将它们上传到“赛博善人” Cloudflare 提供的免费 R2 存储,不仅能节省服务器带宽与流量成本,还能借助 Cloudflare 的全球 CDN 加速,让图片加载更快、更稳定。
2. 使用 CloudFlare R2 + Pages 搭建图床
使用 Cloudflare R2 + Pages 搭建图床的整个过程不需要花钱,但需要具备访问外网的条件,以及一张可用的外币信用卡用于验证身份。我最终搭建好的图床地址是 img.shephe.com,目前开放上传功能,大家可以随意体验,甚至收藏备用作为临时图床使用。


我使用的图床程序是开源项目 Cloudflare ImgBed,它是专为 Cloudflare 平台设计的轻量化图床方案,虽然界面简洁,但功能十分完整——支持图片的上传、管理、读取、删除等全链路操作,覆盖文件的完整生命周期。同时提供了鉴权、分目录存储、图片审核、随机图等实用功能,基本满足个人博客或中小站点的使用需求。
下面我将简单记录一下使用 Cloudflare R2 与 Pages 搭建图床的流程,主要包括:创建对象存储(R2 Bucket)、部署 Pages 项目、激活运行环境、以及绑定自定义域名等环节。整个过程相对直观,跟着操作一般不会出错。
2.1 创建 Cloudflare R2 对象存储
R2 是 Cloudflare 推出的对象存储服务,对于个人博客或中小型网站来说,它最大的吸引力在于 免费额度高、带宽不限速、不怕被刷流量。Cloudflare 的基础设施全球加速、稳定可靠,这使得 R2 成为非常理想的“自建图床底层”。
💡 免费额度参考(截至 2025 年):每月前 10 GB 存储免费、每月前 100 万次 A 类请求免费、出口流量 1TB/月免费(超出部分也极为便宜),这对于一个自用图床来说几乎可以忽略成本。
登录 Cloudflare 控制台后,确保你的账户已完成支付方式绑定(验证信用卡)。在左侧菜单中选择 「R2 对象存储」,然后点击右上角的 「创建存储桶」 来新建一个 Bucket。以下是关键参数设置示例(可根据截图参考):

- 存储桶名称:自行命名(例如
shephe-img或imgbed) - 位置:选择
亚太地区或北美洲西部,两者在国内访问速度差别不大 - 默认存储类:选择 标准(Standard) ❗注意:R2 的免费额度仅适用于标准存储类,“不频繁访问”类无法享受免费额度。
点击 「创建存储桶」 按钮后即可成功创建。咱暂时不需要立即配置域名或访问策略,可以在下一小节部署 Cloudflare Pages 模板 时统一设置。R2 只需要保持为一个空的、可用的存储桶即可备用。
2.2 创建 Cloudflare Pages 项目
Cloudflare Pages 是 Cloudflare 推出的前端托管与持续部署平台,专门用于部署静态网站或轻量 Web 应用。它支持从 GitHub、GitLab 等代码仓库直接拉取项目并自动构建发布,与 Cloudflare 的全局 CDN 网络无缝结合。对于图床这样的静态服务来说,Pages 提供了 零运维成本、自动构建、HTTPS 一键启用 的完美方案。
在创建 Pages 项目前,需要先把开源项目 Cloudflare-ImgBed 复制到自己的 GitHub 账号下。在 GitHub 上打开该项目主页后,点击右上角的 「Fork」 按钮。这样你就可以在自己的空间中独立修改、部署,而不会影响原作者的项目。这里的“Fork”直译为“叉”,理解上更像是“复制仓库到自己的账户”。
完成 Fork 后,进入 Cloudflare 控制台 → 计算(Workers) → Workers 和 Pages → 创建应用程序。选择 「Pages」 选项卡,然后点击 「导入现有 Git 存储库」。系统会提示你授权访问 GitHub,选择刚刚 Fork 的 Cloudflare-ImgBed 仓库。
随后配置如下:
- 项目名称:可自定义,如
imgbed或shephe-imgbed - 生产分支:保持默认
main - 构建命令:
npm install - 构建输出目录:不填,使用默认根目录

点击 「保存并部署」,Cloudflare 会自动拉取你的 GitHub 仓库并开始首次构建。几分钟后,你就会得到一个以 .pages.dev 结尾的测试域名,例如:test-38av.pages.dev
这时,你的图床前端已经可以通过这个域名访问(通常很慢)。接下来我们将进入关键步骤 —— 激活项目、绑定 R2 存储与自定义域名,让上传功能真正可用。
2.3 绑定 KV 数据库和 R2 对象存储
完成 Pages 项目的初次部署后,我们需要为它绑定两个核心组件:
- 一个是 KV 数据库(Key-Value 存储),用于记录图片的元数据与索引信息
- 另一个是前面创建的 R2 对象存储,用于保存实际的图片文件
首先,在 Cloudflare 仪表盘左侧菜单中找到 Workers KV,点击右上角的 「Create Instance」 来新建一个命名空间。命名建议使用:img_url,建议保持这个命名不变,因为 Cloudflare-ImgBed 项目默认配置中使用的就是这个名称。如果使用相同的名称,就无需再修改项目文件或环境变量,节省调试时间。💡
创建好 KV 命名空间后,返回 Pages 项目详情页 → 设置(Settings) → 绑定(Bindings)。在这里我们需要添加两个绑定项,分别是数据库和存储:

- KV 命名空间绑定
- 绑定类型:KV Namespace
- Variable name:
img_url - 命名空间:选择刚刚创建的
img_url
- R2 存储绑定
- 绑定类型:R2 Bucket
- Variable name:
R2 - 存储桶:选择前面创建的 R2 存储(如
img_r2或shephe-img)
保存绑定后,Pages 项目就能在运行时通过环境变量自动调用这两个资源了。接下来,可以顺手在 自定义域(Custom Domains) 一栏中添加你自己的图床域名(包括域名解析),完成域名绑定后,回到 Pages 项目概览页,点击 「重新部署(Redeploy)」,等待几分钟部署完成即可。
不出意外的话,你现在就能通过自定义域名访问你的图床首页了 🎉。至于 ImgBed 的图床设置与使用非常简单,支持上传、删除、目录、鉴权等功能,可以参考 官方文档 获取更详细说明。
3. 将 ImgBed 图床集成到 WordPress
通过上文第 2 步,我们已经有了可用的免费图床。Cloudflare ImgBed 免费开源但功能完整(WebDAV、API 等),官方文档也清晰,因此很容易结合接口快速把“评论图片上传 + 自动插入 URL”做进 WordPress 评论框。
3.1 HTML 结构调整(comments.php)
在评论框的外层容器中预留一个绝对定位的小工具栏(初始隐藏,聚焦后显示)。下面是可直接参考的结构要点:
'comment_field' =>
'<div class="grve-form-textarea grve-border">
<div class="grve-form-inner">
<label for="comment">本站评论均由人工审核,请耐心等待…!</label>
<textarea class="grve-form-input-item" id="comment" name="comment" cols="45" rows="10" aria-required="true"></textarea>
<div id="comment-toolbar" class="hidden">
<button type="button" id="emoji-btn" class="cmt-btn"><i class="fa-regular fa-face-laugh-squint"></i></button>
<button type="button" id="img-btn" class="cmt-btn" title="插入图片..."><i class="fa-regular fa-images"></i></button>
</div>
</div>
</div>',
.grve-form-inner需position:relative;以便工具栏与 Emoji 弹层的绝对定位。#comment-toolbar初始隐藏,聚焦 textarea 后再显示。- Emoji 弹层由 JS 生成(无需写死 HTML)。
3.2 前端交互逻辑(延迟加载 JS 模块)
先在 footer.php 里临时写入这段脚本进行验证(稳定后可迁至主题核心 JS)。这段脚本实现只有用户点击工具栏按钮时,才按需加载模块,从而保持首屏轻量。以下是我的最终版:
document.addEventListener("DOMContentLoaded", function () {
fitText(".grve-single-simple-title", 40, 50, 1);
});
window.addEventListener("resize", function () {
fitText(".grve-single-simple-title", 40, 50, 1);
});
document.addEventListener("DOMContentLoaded", function () {
const commentBox = document.querySelector("#comment");
const toolbar = document.querySelector("#comment-toolbar");
if (!commentBox || !toolbar) return;
commentBox.addEventListener("focus", () => toolbar.classList.add("active"));
toolbar.addEventListener("mousedown", (e) => e.preventDefault());
commentBox.addEventListener("blur", () => {
if (!commentBox.value.trim()) toolbar.classList.remove("active");
});
});
let enhancerLoaded = false;
["#emoji-btn", "#img-btn"].forEach((sel) => {
const btn = document.querySelector(sel);
if (!btn) return;
btn.addEventListener("click", async () => {
if (!enhancerLoaded) {
enhancerLoaded = true;
const { default: initCommentEnhancer } = await import(
"/wp-content/themes/impeka/js/comment-enhancer.js"
);
initCommentEnhancer();
btn.click();
}
});
});
3.3 打通图床和 WordPress
这是本节的关键步骤,用来让 ImgBed 图床真正与 WordPress 评论系统互通。简单来说,当用户在评论框中点击图片按钮、选择本地图片后,浏览器会将图片文件上传到你搭建好的 Cloudflare 图床,图床返回图片的外链地址,最后这段链接再自动插入评论输入框中,从而实现“上传即嵌入”的效果。实现上有两条路径可以选择:
- 前端直连 Cloudflare Worker
即图片直接通过浏览器上传到 Cloudflare 的 API 接口。这种方式不依赖你的网站后端,理论上速度最快、逻辑最简单。但在国内或部分地区,Cloudflare 的 dev 域名(*.workers.dev或*.pages.dev)访问并不稳定,时快时慢,甚至可能无法打开,用户体验受限。此外,API Key 必须暴露在前端,这会带来一定安全风险。 - 通过 WordPress Ajax 代理转发上传
这也是我最终采用的方案。思路是让浏览器先把图片上传到自己服务器的一个 Ajax 接口(例如wp-admin/admin-ajax.php?action=image_upload_proxy),由该接口再通过服务器端的 cURL 请求转发到 Cloudflare 图床。这样可以利用服务器网络带宽和稳定的国际线路,大幅提高成功率;同时,API Key 被保存在服务器端,不会暴露给前端用户。
我目前采用的第二种方式,虽然这种方式会消耗服务器少量性能和上行流量,但对于大多数中小博客来说负担完全可以接受,换来的是稳定性、安全性和可控性。另外,这种方式还能方便地加入防刷机制(如登录校验、上传大小限制等),后期如果想扩展后台上传记录、统计上传次数,也更容易实现。
以下是需要添加进 functions.php 中的本地代理脚本,其中的 api_key 和上传地址根据自己的设置:
// 评论框上传到图床的本地代理
add_action('wp_ajax_image_upload_proxy', 'shephe_image_upload_proxy');
add_action('wp_ajax_nopriv_image_upload_proxy', 'shephe_image_upload_proxy');
function shephe_image_upload_proxy() {
if (empty($_FILES['file'])) {
status_header(400);
header('Content-Type: application/json; charset=utf-8');
echo json_encode(['error' => 'NO_FILE']);
wp_die();
}
$file = $_FILES['file'];
$api_key = 'xxx'; // 你的图床 Key(需含 upload 权限)
$endpoint = 'https://img.shephe.com/upload';
$query = http_build_query([
'returnFormat' => 'full',
'uploadFolder' => 'comments',
'authCode' => $api_key,
]);
$url = $endpoint . '?' . $query;
$ch = curl_init();
$postFields = [
'file' => new CURLFile($file['tmp_name'], $file['type'] ?: 'application/octet-stream', $file['name'])
];
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $postFields,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Accept: application/json',
'User-Agent: WP-ImgProxy/1.0'
],
CURLOPT_TIMEOUT => 30,
]);
$body = curl_exec($ch);
$errno = curl_errno($ch);
$error = curl_error($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
header('Content-Type: application/json; charset=utf-8');
if ($errno) {
status_header(502);
echo json_encode(['error' => 'CURL_ERROR', 'message' => $error]);
wp_die();
}
if ($code < 200 || $code >= 300) {
status_header($code);
echo json_encode(['error' => 'HTTP_'.$code, 'raw' => $body]);
wp_die();
}
echo $body;
wp_die();
}
以下是我完整的 comment-enhancer.js 文件。这个文件可以看作是评论区功能的「增强模块」,将所有与评论框相关的交互逻辑集中管理,包括 Emoji 弹层、图片上传、文件校验、提示框显示、样式注入 等等。
整个脚本的设计思路是 按需加载 + 模块化封装。默认情况下,页面不会主动加载任何与表情或图像上传相关的脚本,只有当用户真正点击评论框右上方的表情按钮 😊 或图片按钮 🖼️ 时,浏览器才会异步加载该模块文件。这样既能保持前台的极简与快速,又能在用户需要时即时启用完整功能。
// /wp-content/themes/impeka/js/comment-enhancer.js
export default function initCommentEnhancer() {
const CONFIG = {
buttonEmoji: '#emoji-btn',
buttonImage: '#img-btn',
textarea: '#comment',
// 使用 WordPress 代理上传
apiProxy: '/wp-admin/admin-ajax.php?action=image_upload_proxy',
allowedTypes: ['image/jpeg', 'image/png', 'image/webp', 'image/gif'],
maxSizeMB: 2,
stitchTool: 'https://www.shephe.com/tools/img-stitching/'
};
/* ---------- Emoji 样式内联 ---------- */
(function injectEmojiStyles() {
if (document.querySelector('#comment-enhancer-style')) return;
const style = document.createElement('style');
style.id = 'comment-enhancer-style';
style.textContent = `
.emoji-picker{
position:absolute;
top:44px;
right:10px;
width:240px;
padding:8px;
background:#fff;
border:1px solid #e5e7eb;
border-radius:10px;
box-shadow:0 8px 24px rgba(0,0,0,.12);
z-index:1000;
display:none;
}
.emoji-picker.active{display:flex;}
.emoji-grid{
display:flex;
flex-wrap:wrap;
gap:6px;
}
.emoji-item{
display:inline-flex;
align-items:center;
justify-content:center;
width:32px;
height:32px;
font-size:20px;
line-height:1;
border:0;
background:transparent;
border-radius:6px;
cursor:pointer;
transition:transform .12s ease,background .12s ease;
}
.emoji-item:hover{
background:#f3f4f6;
transform:scale(1.1);
}`;
document.head.appendChild(style);
})();
/* ---------- 工具函数 ---------- */
function insertAtCursor(el, html) {
const start = el.selectionStart ?? el.value.length;
const end = el.selectionEnd ?? el.value.length;
const before = el.value.slice(0, start);
const after = el.value.slice(end);
el.value = before + html + after;
el.selectionStart = el.selectionEnd = start + html.length;
el.focus();
}
/* ---------- 自定义提示框 ---------- */
function showNotice(msg, options = {}) {
const { type = 'info', link = null } = options;
let box = document.querySelector('.comment-notice');
if (!box) {
box = document.createElement('div');
box.className = 'comment-notice';
document.body.appendChild(box);
const style = document.createElement('style');
style.textContent = `
.comment-notice {
position: fixed;
top: 35%; left: 50%;
transform: translateX(-50%);
background: #fff;
border: 1px solid #e5e7eb;
box-shadow: 0 6px 20px rgba(0,0,0,.12);
padding: 14px 20px;
border-radius: 10px;
font-size: 15px;
z-index: 2000;
display: none;
color: #333;
line-height: 1.5;
}
.comment-notice.show { display: block; animation: fadeIn .25s ease; }
.comment-notice a { border-bottom: 1px dotted #b10b14;color: #b10b14;padding: 0 3px 2px 3px; }
.comment-notice a:hover { border-bottom: 1px solid #b10b14 }
@keyframes fadeIn { from {opacity:0; transform:translate(-50%, -10px);} to {opacity:1; transform:translate(-50%, 0);} }
`;
document.head.appendChild(style);
}
box.innerHTML = msg + (link ? ` <a href="${link}" target="_blank">在线压缩、拼接或转换图片</a>` : '');
box.classList.add('show');
setTimeout(() => box.classList.remove('show'), 5000);
}
/* ---------- Emoji Picker ---------- */
(function initEmojiPicker() {
const btn = document.querySelector(CONFIG.buttonEmoji);
const textarea = document.querySelector(CONFIG.textarea);
if (!btn || !textarea) return;
const container = btn.closest('.grve-form-inner') || textarea.closest('.grve-form-inner');
if (!container) return;
let picker = container.querySelector('.emoji-picker');
if (!picker) {
const emojis = ["😊","😂","😍","🤔","😎","😢","😡","🤮","🙏","👍","👎","🎉","👄","💔","🌹","🐱","🐶","🍕","☕","🌈","👌","🐱","🍉","🚗"];
picker = document.createElement('div');
picker.className = 'emoji-picker';
picker.innerHTML = `
<div class="emoji-grid">
${emojis.map(e => `<button type="button" class="emoji-item" data-emoji="${e}">${e}</button>`).join('')}
</div>`;
container.appendChild(picker);
}
let closeTimer = null;
let isHovering = false;
function openPicker() {
clearTimeout(closeTimer);
picker.classList.add('active');
picker.style.display = 'flex';
}
function scheduleClose() {
clearTimeout(closeTimer);
closeTimer = setTimeout(() => {
if (!isHovering) {
picker.classList.remove('active');
picker.style.display = 'none';
}
}, 2000);
}
picker.addEventListener('mouseenter', () => { isHovering = true; clearTimeout(closeTimer); });
picker.addEventListener('mouseleave', () => { isHovering = false; scheduleClose(); });
btn.addEventListener('click', (e) => {
e.stopPropagation();
if (picker.classList.contains('active')) {
picker.classList.remove('active');
picker.style.display = 'none';
} else { openPicker(); scheduleClose(); }
});
picker.addEventListener('click', (e) => {
const item = e.target.closest('.emoji-item');
if (!item) return;
const emoji = item.dataset.emoji;
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
textarea.setRangeText(emoji, start, end, 'end');
textarea.dispatchEvent(new Event('input', { bubbles: true }));
textarea.focus();
openPicker(); scheduleClose();
});
document.addEventListener('click', (e) => {
if (!container.contains(e.target)) {
picker.classList.remove('active');
picker.style.display = 'none';
}
});
})();
/* ---------- 图片上传(WP 代理,严格容错稳定版) ---------- */
(function initImageUploader() {
const btn = document.querySelector(CONFIG.buttonImage);
const ta = document.querySelector(CONFIG.textarea);
if (!btn || !ta) return;
btn.addEventListener('click', () => {
const input = document.createElement('input');
input.type = 'file';
input.accept = 'image/*';
input.onchange = async (e) => {
const file = e.target.files && e.target.files[0];
if (!file) return;
if (!CONFIG.allowedTypes.includes(file.type)) {
showNotice('仅支持 Jpg/Jpeg/Png/Webp/Gif 格式图片');
return;
}
const sizeMB = file.size / 1024 / 1024;
if (sizeMB > CONFIG.maxSizeMB) {
showNotice(`最大支持 2MB 图片,当前 ${sizeMB.toFixed(2)}MB<br>👉 `, {
link: CONFIG.stitchTool
});
return;
}
const formData = new FormData();
formData.append('file', file);
const prevText = btn.textContent;
btn.disabled = true;
btn.textContent = '⏳ 上传中…';
try {
const resp = await fetch(CONFIG.apiProxy, { method: 'POST', body: formData });
const text = await resp.text();
if (!resp.ok) {
showNotice(`❌ 上传失败:HTTP ${resp.status}`);
console.warn('Raw response:', text);
return;
}
let data;
try { data = JSON.parse(text); }
catch { showNotice('返回数据解析失败'); console.warn('Raw response:', text); return; }
// 只在明确拿到 src/url 时插入,避免把错误文本当路径
let imgUrl = null;
if (Array.isArray(data)) {
imgUrl = data[0]?.src || data[0]?.url || null;
} else if (data && typeof data === 'object') {
const d = Array.isArray(data.data) ? data.data[0] : data.data;
imgUrl = d?.src || d?.url || data.src || data.url || null;
}
if (!imgUrl) {
showNotice('上传成功但未检测到图片地址 src/url');
console.warn('Parsed object:', data);
return;
}
if (!/^https?:\/\//i.test(imgUrl)) {
imgUrl = new URL(imgUrl, 'https://img.shephe.com').toString();
}
const pageTitle = document.title.replace(/<[^>]*>?/gm, '').trim() || 'Kevin\'s Space';
insertAtCursor(ta, `<img src="${imgUrl}" alt="${pageTitle}">`);
showNotice('✅ 图片上传成功');
} catch (err) {
showNotice('⚠ 上传异常:' + err.message);
console.error(err);
} finally {
btn.disabled = false;
btn.textContent = prevText;
input.value = '';
formData.delete('file');
}
};
input.click();
});
})();
}
在功能层面上,comment-enhancer.js 主要做了以下几件事:
- 动态创建 Emoji 弹层:
- 点击表情按钮时,自动在评论框右上角生成弹层。
- 支持鼠标悬停不关闭、点击插入表情符号、自动淡出等交互。
- 样式通过脚本内联注入,不依赖外部 CSS 文件。
- 图片上传与插入:
- 点击图片按钮后弹出文件选择框,校验文件格式与大小(≤2MB)。
- 通过 WordPress Ajax 代理上传到 Cloudflare 图床。
- 上传成功后自动返回图片外链,并在评论框中插入
<img>标签。 alt属性自动填充为当前文章标题,兼顾可读性与 SEO。
- 错误与状态提示:
- 自定义了一个轻量的提示框组件(取代浏览器
alert)。 - 提示信息会自动淡出,并可带跳转链接(如图片过大时指向图片拼接工具)。
- 自定义了一个轻量的提示框组件(取代浏览器
- 安全与性能优化:
- 模块加载后自动释放文件对象,避免内存堆积。
- API Key 保留在服务器端,前端仅调用本地 Ajax 接口,确保安全性。
- 所有 DOM 注入和样式注册均带防重复判断,避免二次加载冲突。
总结来说,这个文件的定位是一个评论区扩展核心模块,它让原本只能输入纯文本的 WordPress 评论框拥有了轻量级的富媒体能力,同时保持页面整洁、加载迅速、维护方便,后期如果要增加其他富文本格式也很方便。
4. 常见问题与故障排查
在图床与 WordPress 评论框集成的过程中,虽然整体流程不复杂,但由于涉及到前端上传、跨域访问、接口代理和 Cloudflare 的安全机制,不可避免会遇到一些常见问题。比如:
4.1 CORS 报错处理(Worker / 代理)
问题表现:前端控制台出现类似以下错误:
Access to fetch at 'https://img.shephe.com/upload'
from origin 'https://www.shephe.com' has been blocked by CORS policy
原因分析:
CORS(跨域资源共享)问题通常出现在浏览器直连图床接口的情况下。Cloudflare Pages 或 Workers 默认并不会自动允许其他域名访问接口,当你的图床域名和主站域名不同(如 img.shephe.com 与 www.shephe.com),浏览器就会拦截请求。
解决方案:
- 方案 A:使用 Cloudflare Transform Rules
在 Cloudflare 控制台中,进入 Rules → Transform Rules → HTTP Response Header Modification,
为图床域名添加响应头:Access-Control-Allow-Origin: https://www.shephe.com Access-Control-Allow-Methods: GET,POST,OPTIONS Access-Control-Allow-Headers: Authorization,Content-Type让主站域名可以合法访问接口。 - 方案 B:使用 Cloudflare Pages
_headers文件
在 Pages 项目的根目录下新建_headers文件,内容如下:/* Access-Control-Allow-Origin: https://www.shephe.com Access-Control-Allow-Methods: GET,POST,OPTIONS Access-Control-Allow-Headers: Authorization,Content-Type - 方案 C:使用 WordPress Ajax 代理(推荐)
如果网络对 Cloudflare 的访问不稳定,建议直接使用服务器代理。所有上传请求由你自己的域名转发到 Cloudflare,CORS 问题自然消失。这是我最终采用的方案,也是最稳定、安全的做法。
4.2 R2 权限配置问题
问题表现:上传时返回 403 Forbidden 或提示 AccessDenied。
原因分析:Cloudflare R2 默认不对外开放写入权限。如果 Pages 项目没有正确绑定 R2 存储桶(Bucket),或者绑定时未选择正确的命名变量名,脚本调用时就无法访问 R2 API。
解决方案:
- 打开 Cloudflare 控制台 → Workers 和 Pages → 你的 Pages 项目 → 设置 → Bindings。
- 检查绑定项是否存在:
- 类型为 R2 Bucket
- 变量名为
R2(需与项目代码一致)
- 确认所选存储桶为你创建的 R2(如
imgbed)。 - 若仍提示无权限,进入 R2 存储设置 → Access Policies,确认
R2绑定的 API Key 拥有PutObject权限。 - 重新部署项目,等待生效。
4.3 405 错误的原因与解决
问题表现:上传时浏览器控制台提示:
上传失败:HTTP 405
原因分析:
405 表示 “Method Not Allowed”。这是 Cloudflare ImgBed 的常见错误类型之一。出现该问题通常是以下几种情况:
- 上传接口调用错误(如请求方法不是
POST); - 使用了错误的鉴权方式(应为
authCode参数,而不是 HTTP Header); - 图床部署在 Pages 环境中,而不是 Worker 环境时,未设置正确的路径;
- 代理层传递文件时未使用
multipart/form-data格式。
解决方案:
- 确保上传时使用
POST方法。 - 检查上传 URL 是否包含正确的参数,例如:
https://img.shephe.com/upload?returnFormat=full&uploadFolder=comments&authCode=你的 KEY - 若使用 WordPress Ajax 代理,请确保后端代码使用
curl_file_create()生成文件字段。
PHP 示例:'file' => new CURLFile($file['tmp_name'], $file['type'], $file['name']) - 确认 Pages 项目已绑定 KV 与 R2,并重新部署。
4.4 上传崩溃(fetch 内存溢出)修复思路
问题表现:
浏览器在选择图片上传后,页面卡死或直接崩溃(Chrome、Edge 均会闪退)。
原因分析:
- 该问题一般与
fetch上传大文件相关。当使用fetch携带FormData时,某些浏览器会在序列化过程中占用大量内存; - 若同时调用
response.json()自动解析响应体,就可能导致瞬时内存暴涨,尤其是当返回的错误信息或 HTML 页面内容较大时。
修复思路:
- 避免直接使用
response.json()
改为先读取response.text(),再尝试手动JSON.parse(),出现异常时安全退出。 - 改用原生 XMLHttpRequest 上传
传统的xhr.send(formData)在流式传输大文件时更稳定,不会因为流序列化而崩溃。
这也是我后来采用的稳定方案。 - 减少临时对象引用
上传完成后立即释放文件引用,例如:input.value = ''; formData.delete('file'); - 文件大小预检查
通过前端脚本提前阻止大于 2MB 的文件上传,从源头避免卡死。
通过以上几项调整,整个系统运行已非常稳定。无论是从国内访问还是海外访问,图片上传都能快速完成,接口响应时间稳定在 200~400ms 左右。这也验证了:在 Cloudflare Pages + R2 架构下,通过 WordPress Ajax 代理上传是一种性能、安全性、可维护性都兼顾的实现方式。


看到了,你的这俩东西,开了广告拦截就加载不出来了,😂