跳转到内容
关系图谱

giscus:把评论区建在 GitHub Discussions 上

2,161 字 8 分钟

属性 1
series
VaultKit 包指南

@vaultkit/vitepress-plugin-giscusgiscus(基于 GitHub Discussions 的评论系统)的轻量 Vue 集成。它不引官方的 @giscus/vue——那层 web component 封装对这里的场景是纯开销——而是直接注入 giscus 官方 client.js,并针对 VitePress 这类 SPA 场景解决三大痛点:明暗主题热同步、路由切换重挂、视口门控保 bfcache。

组件本身框架无关:不 import vitepress,明暗态与页面标识全部由宿主经 props 驱动,任何 Vue 3 SSR 站点都能用。本文覆盖全部 props / options、giscus.app 上生成四个 ID 的完整步骤,以及三大痛点的设计细节。本站文章末尾的评论区就是它驱动的。

为什么需要它

giscus 把评论存进 GitHub Discussions:无自建后端、数据在自己仓库里、评论者用 GitHub 账号登录——技术博客的理想选择。但把官方 <script> 嵌进 VitePress 有三个坑:

  1. 主题切换不跟随——iframe 加载时固定了主题,站点切暗色后评论区还是刺眼的白
  2. SPA 路由切换评论不换——mapping: pathname 时 iframe 在加载期读父页 URL,Layout 常驻组件不销毁,评论会一直停在首次挂载的页面
  3. 首屏性能与 bfcache——评论区在文章末尾,挂载即注入会让 giscus iframe 在页面加载期就开始导航,抢首屏带宽,还会以 SubframeIsNavigating 为由破坏 bfcache 资格(前进后退失去秒开)

本包用约 130 行源码把三个坑全部填掉,零额外依赖。

前置:在 giscus.app 生成四个 ID

组件需要 repo / repoId / category / categoryId 四个参数,完整生成步骤:

准备仓库

选一个公开仓库(可以就是博客仓库,也可以专门建一个评论仓库)。私有仓库 giscus 无法读取。

安装 giscus GitHub App

访问 github.com/apps/giscus 安装,授权访问上述仓库(建议只授权所需仓库而非全部)。

开启 Discussions

仓库 Settings → General → Features,勾选 Discussions。然后在 Discussions 页建一个专用分类,官方建议用 Announcements 类型——只有仓库维护者与 giscus 能创建新讨论,防止路人手工开帖干扰映射。

到 giscus.app 生成配置

打开 giscus.app,在「仓库」一栏填入 owner/repo。校验通过后选择映射方式与 Discussion 分类,页面下方「启用 giscus」处生成的 <script> 标签里就有 data-repo-idR_ 开头)与 data-category-idDIC_ 开头)——把四个值抄进组件 props 即可,其余 data-* 属性不用抄(本组件的 options 已覆盖)。

安装

sh
pnpm add -D @vaultkit/vitepress-plugin-giscus

使用 @vaultkit/vitepress-theme-vault 主题则开箱即带,站点配置里填四个 ID 就能点亮评论区。

快速上手

推荐挂在 Layout 的 #doc-after 插槽(正文之后),用 useData / useRoute 接线明暗态与路由:

.vitepress/theme/Layout.vue
vue
<script setup lang="ts">
import { GiscusComments } from '@vaultkit/vitepress-plugin-giscus'
import { useData, useRoute } from 'vitepress'
import DefaultTheme from 'vitepress/theme'

const { isDark, frontmatter } = useData()
const route = useRoute()
</script>

<template>
  <DefaultTheme.Layout>
    <template #doc-after>
      <GiscusComments
        v-if="frontmatter.comments !== false"
        repo="owner/repo"
        repo-id="R_kgDOxxxxxxx"
        category="Announcements"
        category-id="DIC_kwDOxxxxxxx"
        :dark="isDark"
        :page-key="route.path"
        :options="{ lang: 'zh-CN' }"
        aria-label="评论"
      />
    </template>
  </DefaultTheme.Layout>
</template>

isDarkdarkroute.pathpageKey,两根线接好后主题同步与路由重挂全自动。是否渲染的门控(如 frontmatter 关评论)交给宿主 v-if

Important

本包发布的是含 .vue 源文件的产物,消费方 VitePress 项目必须把它加进 vite.ssr.noExternal,否则 SSR 构建会报错:

ts
// .vitepress/config.ts
import { defineConfig } from 'vitepress'

export default defineConfig({
  vite: { ssr: { noExternal: ['@vaultkit/vitepress-plugin-giscus'] } },
})

公开导出一览

导出形态说明
GiscusCommentsVue 组件(具名 + 默认导出)评论区本体
GiscusOptions类型options prop 的形状

Props 逐项详解

repo / repoId / category / categoryId

  • 类型:均为 string必填
  • 语义:四个 giscus 身份参数,生成方式见上文步骤。repoowner/repo 形式;repoIdcategoryId 是 GraphQL node ID(分别 R_ / DIC_ 开头),不是仓库名或分类名——填错的典型症状是评论区显示「giscus is not installed on this repository」或干脆空白。

dark

  • 类型:boolean,默认 false
  • 语义:暗色态开关,变化时经 postMessage 热同步 iframe 主题,免重载。宿主传 useData().isDark 即可。组件刻意不自己探测暗色(比如读 html.dark 类)——探测方式因主题而异,由宿主注入才是框架无关的正解。

pageKey

  • 类型:string,默认 ''
  • 语义:页面标识,变化时整体重建评论区。宿主传 useRoute().path。这是 SPA 集成的命门(原理见设计机制一节),不接这根线的后果是:切换文章后评论区还是上一篇的。

options

  • 类型:GiscusOptions,默认 {}
  • 语义:其余 giscus data-* 配置的可覆盖子集,八个字段逐一说明:
字段类型默认值说明
mappingstring'pathname'页面 ↔ Discussion 的映射方式。pathname 最稳(URL 即身份);也可用 url / title / og:title 等 giscus 支持的值
strictbooleantrue严格匹配(哈希校验),防止标题相近的页面串评论。注意本组件缺省开启,比 giscus 官方缺省更严
reactionsEnabledbooleantrue主帖顶部的表情反应行
inputPosition'top' | 'bottom''top'输入框在评论列表上方还是下方
langstring'en'giscus 界面语言,中文站点传 'zh-CN'
loading'lazy' | 'eager''lazy'iframe 的加载策略(在组件自己的视口门控之上再叠一层浏览器级懒加载)
lightThemestring'light'亮色主题名(giscus 内置主题名或自托管 CSS 的 URL)
darkThemestring'transparent_dark'暗色主题名。缺省透明底,融入站点自身的暗色背景

定制示例:

vue
<GiscusComments
  repo="owner/repo"
  repo-id="R_xxx"
  category="Announcements"
  category-id="DIC_xxx"
  :dark="isDark"
  :page-key="route.path"
  :options="{
    lang: 'zh-CN',
    inputPosition: 'bottom',
    reactionsEnabled: false,
    darkTheme: 'noborder_dark',
  }"
/>

另外有一项被组件写死:data-emit-metadata 恒为 '0'——元数据回传对评论区场景没有用处,关闭省一条消息通道。

ariaLabel

  • 类型:string,默认 'Comments'
  • 语义:外层 <section> 容器的 aria-label,屏幕阅读器的地标导航靠它定位评论区。中文站点传 aria-label="评论"

设计机制:三大痛点的解法

痛点一:主题热同步(postMessage,免重载)

giscus iframe 支持运行时接收配置消息。组件 watch 计算出的主题名(darklightTheme / darkTheme 共同决定),变化时向 iframe postMessage

ts
iframe?.contentWindow?.postMessage(
  { giscus: { setConfig: { theme: theme.value } } },
  'https://giscus.app',
)

切换明暗时评论区原地换肤,不重新加载 iframe、已输入一半的评论草稿不丢。target origin 锁死 https://giscus.app,消息不会泄漏给其他源。

痛点二:路由重挂(pageKey → 重建)

mapping: pathname 时,giscus iframe 在加载期读取父页 URL 来决定挂哪条 Discussion。VitePress 的 Layout 是常驻组件,SPA 路由切换时不会销毁重建——iframe 一旦加载完就再也不看 URL 了。所以组件 watch(pageKey),变化时清空容器、重新走一遍注入流程,让 iframe 以新 URL 重新加载。这就是宿主必须接 :page-key="route.path" 的原因。

痛点三:视口门控(IntersectionObserver 保 bfcache)

评论区在文章末尾,绝大多数读者的首屏根本看不到它。挂载即注入 script 有两笔隐性成本:giscus iframe 在页面加载期就开始导航,跟正文图片抢带宽;且加载中的子 iframe 会让 Chrome 以 SubframeIsNavigating 为由取消页面的 bfcache 资格——用户点出去再按返回键,本该秒开的页面变成完整重载。

组件的解法是 IntersectionObserver 布防:容器进入「视口下方 500px」范围才真正注入 script(提前量保证滚到评论区时它已就绪);pageKey 变化后重置门控,新页面的评论区同样等待滚近;不支持 IntersectionObserver 的老环境直接注入兜底。

SSR 安全

组件模板只有一个空 <section> 容器,SSR 输出确定性 HTML,script 注入只在客户端视口门控后发生——没有水合差异,也没有服务端请求 giscus 的可能。

高级与边界

  • 自托管主题 CSSlightTheme / darkTheme 可以传完整 URL(如 https://example.com/giscus-custom.css),做到评论区与站点视觉完全统一
  • 评论数据迁移:从 utterances / gitalk(基于 Issues)迁移时,GitHub 支持把 Issue 转换为 Discussion,转换后只要标题与映射匹配,giscus 自动接管旧评论
  • 门控策略:组件不内置「哪些页面显示评论」的逻辑,建议宿主用 frontmatter 开关(如本站的 frontmatter.blogPost + 配置门控)
  • 多实例:同页多个评论区没有意义,giscus 官方也不支持——一页一实例

常见问题

Q:评论区空白 / 报「not installed」? 逐项核对:仓库公开、giscus app 已安装并授权该仓库、Discussions 已开启、四个 ID 与 giscus.app 生成值完全一致(repoIdR_ 开头的 node ID,不是仓库名)。

Q:切换暗色后评论区没跟上?dark prop 没接线。确认传的是响应式的 isDarkuseData() 解构出的 ref),而不是一次性布尔值。

Q:切换文章后评论还是上一篇的?pageKey 没接线,传 route.path 即可。

Q:为什么滚到底才加载评论? 这是刻意的视口门控(提前 500px),保首屏带宽与 bfcache。想恢复「挂载即加载」目前没有开关——门控的收益远大于成本。

相关链接