为什么选 Twikoo
静态博客最大的痛点之一就是没有评论区。GitHub 上评论方案不少,最后我选了 Twikoo,原因很直接:
- 零成本:EdgeOne Makers 免费额度对个人博客完全够跑
- 不用买服务器:网页控制台拖个 ZIP 就部署完,数据库自动提供
- 前端集成简单:一个 JS 文件 + 几行初始化代码
- 功能齐全:表情贴图、邮件通知、管理员后台、评论审核都有
下面是我从 EdgeOne Makers 部署到 Astro 前端懒加载集成的完整踩坑过程。
整体架构
先看清楚整个评论系统是怎么跑起来的:

访客在你的博客文章页输入评论 → Twikoo 前端 JS 把数据发到 EdgeOne Makers 上的云函数 → 云函数把评论存进平台自动提供的 KV 存储 → 你在 Twikoo 管理后台审核和回复。
整个链路里,你只需要管两个东西:EdgeOne Makers 上的 Twikoo 项目(后端) 和 博客页面里的一段初始化代码(前端)。
第一步:下载 Twikoo EdgeOne 部署包
- 去 Twikoo 官方文档 下载
twikoo-edgeone-makers.zip - 不要解压,这个 ZIP 直接用于下一步上传
这个包只有三个文件(函数入口、SMTP 桥接源码、依赖声明),云函数实现由平台从 npm 上自动取回,后续升级只需在项目里点「重新部署」。
第二步:上传到 EdgeOne Makers
- 进入 EdgeOne Makers 控制台,在「项目」标签页点击直接上传
- 填写项目名称(比如
twikoo,仅小写字母、数字和连字符) - 选择加速区域(选「全球可用区(含中国大陆)」)
- 把刚才下载的 ZIP 拖入上传区域,点击开始部署
- 等待状态变为成功
评论数据存放在平台自动提供的 Blob KV 中,首次写入时自动创建命名空间,不需要任何额外配置,也不用自备数据库。


第三步:修改 Node.js 版本(关键!)
部署完后,有一个必改项:
- 进入项目的项目设置
- 把 Node.js 版本改为
24.18.0并保存 - 回到构建部署页点一次重新部署
为什么要改? 平台默认的 Node.js 版本偏低,Twikoo 2.0 的产物和依赖要求 Node 24+,不改的话函数会跑不起来。

第四步:绑定自定义域名
EdgeOne Makers 的默认域名(*.edgeone.cool)只有 3 小时预览有效期,必须绑自己的域名才能长期使用。
- 进入项目的域名管理页,点击添加自定义域名
- 输入你的子域名(比如
twikoo.yourblog.com),环境选生产 - 等待状态变为「请添加 CNAME」,复制页面上的 CNAME 值
- 到你的 DNS 服务商(比如腾讯云 DNSPod、阿里云),为该域名添加一条 CNAME 记录
- 回到域名管理页,点击 HTTPS 配置 → 选择申请免费证书 → 自动验证 → 保存
- 等待状态变为已生效
绑好后,浏览器访问 https://twikoo.yourblog.com/,应该看到「Twikoo 云函数运行正常,请参考…」,这个地址(含 https://,不带路径)就是前端的 envId。


注意:envId 是域名根目录,不是某个子路径。因为部署包把云函数映射到了根路径
/。
第五步:验证后端并设置管理员
- 浏览器访问
https://twikoo.yourblog.com/,看到正常提示即部署成功 - 打开 Twikoo 管理后台:
https://twikoo.js.org/admin.html - 输入你的 envId,第一次登录会让你设置管理员密码,记好
第六步:Astro 前端集成
这一步是重点。网上大部分教程都是直接引入 Twikoo JS 然后初始化,但这样会在每个页面都加载评论脚本,影响博客首页速度。我做了懒加载处理:只有进入文章详情页时才动态加载评论脚本。
6.1 配置项
先在 src/config.ts 里加配置:
// src/config.ts
const config = {
// ... 其他配置
Comment: {
Twikoo: {
enable: true,
envId: 'https://twikoo.yourblog.com', // 你的自定义域名
script: 'https://cdn.jsdelivr.net/npm/twikoo@2.0.15/dist/twikoo.nocss.js',
style: 'https://cdn.jsdelivr.net/npm/twikoo@2.0.15/dist/twikoo.css',
}
},
}
6.2 文章页嵌入评论容器
在 src/pages/posts/[slug].astro 的文章末尾,加评论区的 HTML 结构:
---
<Base title={post.title} commentStyles={true}>
<!-- ... 文章内容 ... -->
{config.Comment.Twikoo.enable && (
<section class="entry-comments" id="comments">
<h3>
<span class="comment-heading-icon">●</span> 评论
<span class="comment-heading-count" id="comment-count">0</span>
</h3>
<!-- 加载中状态 -->
<div class="comment-loading" id="comment-loading">
<span class="comment-loading-spinner"></span>
<span>评论加载中…</span>
</div>
<!-- Twikoo 挂载点,默认隐藏,脚本加载完成后再显示 -->
<div id="tcomment"
data-env-id={config.Comment.Twikoo.envId}
data-script={config.Comment.Twikoo.script}
hidden>
</div>
</section>
)}
</Base>
6.3 懒加载初始化脚本
关键在这一段:评论脚本不是页面一打开就加载,而是等到用户进入文章页、Twikoo 的 DOM 挂载点存在时才动态插入 <script> 标签。同时处理了加载失败、表单渲染等待、评论数统计等边界情况。
<script>
let twikooScript: Promise<void> | undefined;
document.addEventListener('astro:page-load', () => {
const mount = document.getElementById('tcomment');
if (!mount) return;
const envId = mount.dataset.envId || '';
const loading = document.getElementById('comment-loading');
// 显示评论表单:Twikoo 渲染是异步的,用 MutationObserver 等表单出现
let formObserver: MutationObserver | undefined;
const showForm = () => {
if (!mount.querySelector('.tk-submit textarea.tk-textarea__inner')) return;
formObserver?.disconnect();
mount.hidden = false;
if (loading) loading.hidden = true;
};
// 加载失败处理
const showError = () => {
formObserver?.disconnect();
if (loading) loading.hidden = true;
mount.hidden = false;
mount.textContent = '评论加载失败,请刷新页面重试。';
};
// 只在第一次时加载 Twikoo 脚本,后续页面切换复用
if (!twikooScript) {
twikooScript = (window as any).twikoo
? Promise.resolve()
: new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = mount.dataset.script || '';
script.async = true;
script.onload = () => resolve();
script.onerror = () => reject(new Error('评论脚本加载失败'));
document.body.appendChild(script);
});
}
twikooScript.then(() => {
mount.replaceChildren();
formObserver = new MutationObserver(() => showForm());
formObserver.observe(mount, { childList: true, subtree: true });
// 初始化 Twikoo
Promise.resolve((window as any).twikoo!.init({
envId,
el: '#tcomment',
path: location.pathname,
onCommentLoaded: () => showForm(),
})).then(() => showForm(true)).catch(showError);
// 单独拉取评论数,显示在标题旁边
(window as any).twikoo!.getCommentsCount({
envId,
urls: [location.pathname],
includeReply: true,
}).then((items: Array<{ count: number }>) => {
const count = document.getElementById('comment-count');
if (count) count.textContent = String(items[0]?.count ?? 0);
}).catch(() => {});
}).catch(() => {
twikooScript = undefined;
showError();
});
});
</script>
为什么这么写?
twikooScript用了模块级变量缓存,博客站内切换文章页时不会重复加载脚本astro:page-load事件是 Astro 视图过渡的标准钩子,SPA 切换页面时会重新触发path: location.pathname保证每篇文章的评论是独立的,不会串
最终效果
部署完之后,文章页底部就是这样的:

评论数会显示在「评论」标题旁边,加载过程中有 loading 动画,加载失败有提示,整个过程不阻塞页面渲染。
常见问题
Q:EdgeOne Makers 免费吗? A:个人项目免费额度内够用。评论数据存在平台 KV 里,不用自己买数据库。
Q:为什么我部署完访问域名一直 401?
A:默认的 *.edgeone.cool 域名只有 3 小时预览有效,必须绑自定义域名才能长期访问。
Q:为什么函数跑不起来 / 报错? A:检查项目设置里的 Node.js 版本是不是 24.18.0,改完要重新部署。
Q:评论会被垃圾评论刷爆吗? A:Twikoo 后台可以设置评论默认隐藏,审核通过才显示。注意 EdgeOne 版不支持 Akismet 垃圾评论检测。
Q:邮件通知能用吗?
A:可以,但 EdgeOne 函数运行时不支持直连 SMTP,需要用 SendGrid、MailChannels,或者用部署包自带的 SMTP 桥接(在环境变量里配 TWIKOO_SMTP_BRIDGE_TOKEN)。
Q:想换评论系统怎么办? A:Twikoo 管理后台支持导出评论数据,可以迁移。
整个部署过程从下载 ZIP 到绑定域名,快的话 20 分钟搞定。后端就是拖个文件上传,真正花时间的是前端怎么优雅地加载评论脚本,不拖慢博客主页面的速度。

● 评论 0