Skip to main content

引入giscus的评论服务

加入github维护的giscus的评论服务

giscus安装

首先于 github 主页SettingsIntegrations->Application 找到app商店 Authorized GitHub Apps, 在其中搜索栏搜索 giscus 根据提示下单(免费)后安装, 在安装好后会提示你将要开启讨论的仓库, 可以按需选择

之后可以在 Authorized GitHub Apps 旁边的 Installed GitHub Apps中看到并配置你的App

开启 Discussions

  1. 在 GitHub 上,导航到开源存储库的主页

  2. 在您的存储库名称下,单击Settings

  3. 向下滚动到 Features , 并开启 Discussions

    此时, 与你 Settings 同一栏中, 便可以找到 Discussions 管理你的讨论内容

giscus连接仓库

  1. 输入仓库名

  2. 映射关系: 选用title 做映射, 会作为讨论的标题

  3. Discussion分类: 选择 Announcements

    推荐使用**公告(announcements)**类型的分类,以确保新 discussion 只能由仓库维护者和 giscus 创建

  4. 服务特性:

    • 启用 reaction
    • 评论输入框在上方
    • 懒加载
    • 默认主题

启用giscus

如上步骤执行OK后, 会得到一段 script 标签, 我们在应用中植入, 便可展示对应的评论窗口

<script src="https://giscus.app/client.js"
data-repo="electrical-killer/notebook.discus"
data-repo-id="R_kgDONXMPYA"
data-category="Announcements"
data-category-id="DIC_kwDONXMPYM4CkxZM"
data-mapping="title"
data-strict="0"
data-reactions-enabled="1"
data-emit-metadata="0"
data-input-position="top"
data-theme="dark"
data-lang="zh-CN"
data-loading="lazy"
crossorigin="anonymous"
async>
</script>

关于其中各种 id (例如 repo_id :仓库标识值) , 连接仓库的配置网址是一种得到的方式, 还有另一种 API 查询, 使用GitHub GraphQL API, 不需要登录, 直接输入后运行OK, 之后我们会得到: 仓库标识id, 分类标识id

{
repository(owner: "electrical-killer", name: "notebook.discus") {
id
discussionCategories (first: 5) {
nodes {
name
id
}
}
}
}

React组件初步实现

官方组件仓库

扩展阅读: 在 React 和 TypeScript 中使用 Web Components

优化后的组件实现, 见下方标题: Discuss组件二次优化

  1. 安装

    pnpm add @giscus/react
  2. 创建组件

    import Giscus from '@giscus/react';

    export default function Discuss() {
    return (
    <Giscus
    id="discussions"
    repo="electrical-killer/notebook.discus"
    repoId="R_kgDONXMPYA"
    category="Announcements"
    categoryId="DIC_kwDONXMPYM4CkxZM"
    mapping="title"
    term="Welcome to @giscus/react component!"
    reactionsEnabled="1"
    emitMetadata="0"
    inputPosition="top"
    theme="dark"
    lang="zh-CN"
    loading="lazy"
    />
    );
    }

swizzle内部组件并引入自定义组件

warning

swizzle 后需要重新运行 Docusaurus, 才可以显示效果

tip

对于不需要评论的文章,在前言中加入 hide_comment: true

该值默认为false, 评论开启

相关依赖
  • @docusaurus/theme-common
  • @docusaurus/plugin-content-blog
  • mitt

文档组件

pnpm run swizzle @docusaurus/theme-classic DocItem/Layout --eject --typescript

之后会生成 src/theme/DocItem/Layout 目录, 并修改 src/theme/DocItem/Layout/index.tsx 组件

src/theme/DocItem/Layout/index.tsx
import React from 'react';
import clsx from 'clsx';
import { useWindowSize } from '@docusaurus/theme-common';
import { useDoc } from '@docusaurus/theme-common/internal';
import DocItemPaginator from '@theme/DocItem/Paginator';
import DocVersionBanner from '@theme/DocVersionBanner';
import DocVersionBadge from '@theme/DocVersionBadge';
import DocItemFooter from '@theme/DocItem/Footer';
import DocItemTOCMobile from '@theme/DocItem/TOC/Mobile';
import DocItemTOCDesktop from '@theme/DocItem/TOC/Desktop';
import DocItemContent from '@theme/DocItem/Content';
import DocBreadcrumbs from '@theme/DocBreadcrumbs';
import type { Props } from '@theme/DocItem/Layout';

import styles from './styles.module.css';
import Discuss from '../../../components/Discuss';

/**
* Decide if the toc should be rendered, on mobile or desktop viewports
*/
function useDocTOC() {
const { frontMatter, toc } = useDoc();
const windowSize = useWindowSize();

const hidden = frontMatter.hide_table_of_contents;
const canRender = !hidden && toc.length > 0;

const mobile = canRender ? <DocItemTOCMobile /> : undefined;

const desktop =
canRender && (windowSize === 'desktop' || windowSize === 'ssr') ? (
<DocItemTOCDesktop />
) : undefined;

return {
hidden,
mobile,
desktop
};
}

export default function DocItemLayout({ children }: Props): JSX.Element {
const docTOC = useDocTOC();
const { frontMatter } = useDoc();
const { hidediscussion } = frontMatter;

return (
<div className="row">
<div className={clsx('col', !docTOC.hidden && styles.docItemCol)}>
<DocVersionBanner />
<div className={styles.docItemContainer}>
<article>
<DocBreadcrumbs />
<DocVersionBadge />
{docTOC.mobile}
<DocItemContent>{children}</DocItemContent>
<DocItemFooter />
</article>
<DocItemPaginator />
</div>
{!hidediscussion && <Discuss />}
</div>
{docTOC.desktop && <div className="col col--3">{docTOC.desktop}</div>}
</div>
);
}

博客组件

pnpm run swizzle @docusaurus/theme-classic BlogPostPage --eject --typescript

之后会生成 src/theme/BlogPostPaget 目录, 并修改 src/theme/BlogPostPage/index.tsx 组件

src/theme/BlogPostPage/index.tsx
import React, {type ReactNode} from 'react';
import clsx from 'clsx';
import {HtmlClassNameProvider, ThemeClassNames} from '@docusaurus/theme-common';
import {
BlogPostProvider,
useBlogPost,
} from '@docusaurus/plugin-content-blog/client';
import BlogLayout from '@theme/BlogLayout';
import BlogPostItem from '@theme/BlogPostItem';
import BlogPostPaginator from '@theme/BlogPostPaginator';
import BlogPostPageMetadata from '@theme/BlogPostPage/Metadata';
import BlogPostPageStructuredData from '@theme/BlogPostPage/StructuredData';
import TOC from '@theme/TOC';
import ContentVisibility from '@theme/ContentVisibility';
import type {Props} from '@theme/BlogPostPage';
import type {BlogSidebar} from '@docusaurus/plugin-content-blog';
import Discuss from '../../components/Discuss';

function BlogPostPageContent({
sidebar,
children,
}: {
sidebar: BlogSidebar;
children: ReactNode;
}): JSX.Element {
const {metadata, toc} = useBlogPost();
const {nextItem, prevItem, frontMatter} = metadata;
const {
hide_table_of_contents: hideTableOfContents,
toc_min_heading_level: tocMinHeadingLevel,
toc_max_heading_level: tocMaxHeadingLevel,
hide_discussion: hidediscussion,
} = frontMatter;
return (
<BlogLayout
sidebar={sidebar}
toc={
!hideTableOfContents && toc.length > 0 ? (
<TOC
toc={toc}
minHeadingLevel={tocMinHeadingLevel}
maxHeadingLevel={tocMaxHeadingLevel}
/>
) : undefined
}>
<ContentVisibility metadata={metadata} />

<BlogPostItem>{children}</BlogPostItem>

{(nextItem || prevItem) && (
<BlogPostPaginator nextItem={nextItem} prevItem={prevItem} />
)}

{!hidediscussion && <Discuss />}
</BlogLayout>
);
}

export default function BlogPostPage(props: Props): JSX.Element {
const BlogPostContent = props.content;
return (
<BlogPostProvider content={props.content} isBlogPostPage>
<HtmlClassNameProvider
className={clsx(
ThemeClassNames.wrapper.blogPages,
ThemeClassNames.page.blogPostPage,
)}>
<BlogPostPageMetadata />
<BlogPostPageStructuredData />
<BlogPostPageContent sidebar={props.sidebar}>
<BlogPostContent />
</BlogPostPageContent>
</HtmlClassNameProvider>
</BlogPostProvider>
);
}

Discuss组件配置方式二次优化

  1. giscus功能基本参数默认&关键参数可配置
  2. 主题, 语言等样式可配置
src/components/Discuss/index.tsx
import BrowserOnly from '@docusaurus/BrowserOnly'
import type { ThemeConfig } from '@docusaurus/preset-classic'
import { useColorMode, useThemeConfig } from '@docusaurus/theme-common'
import useDocusaurusContext from '@docusaurus/useDocusaurusContext'
import Giscus, { type GiscusProps, type Theme } from '@giscus/react'

export type GiscusConfig = GiscusProps & { darkTheme: Theme }

const defaultConfig: Partial<GiscusProps> & { darkTheme: string } = {
id: 'discuss',
mapping: 'title',
reactionsEnabled: '1',
emitMetadata: '0',
inputPosition: 'top',
lang: 'zh-CN',
theme: 'light',
darkTheme: 'dark_dimmed',
loading: "lazy",
}

export default function Discuss(): JSX.Element{
const themeConfig = useThemeConfig() as ThemeConfig & { giscus: GiscusConfig }
const { i18n } = useDocusaurusContext()

// merge default config
const giscus = { ...defaultConfig, ...themeConfig.giscus }

if (!giscus.repo || !giscus.repoId || !giscus.categoryId) {
throw new Error('You must provide `repo`, `repoId`, and `categoryId` to `themeConfig.giscus`.')
}

giscus.theme = useColorMode().colorMode === 'dark' ? giscus.darkTheme : giscus.theme
giscus.lang = i18n.currentLocale

return <BrowserOnly fallback={<div>Loading discussion...</div>}>{() => <Giscus {...giscus} />}</BrowserOnly>
}

Discuss组件导入配置

docusaurus.config.ts
import type { GiscusConfig } from './src/components/Discuss'

const config: Config = {
themeConfig: {
// 配置giscus
giscus: {
repo:'electrical-killer/notebook.discus',
repoId:'R_kgDONXMPYA',
category:'Announcements',
categoryId:'DIC_kwDONXMPYM4CkxZM',
theme: 'light',
darkTheme: 'dark_dimmed',
} satisfies Partial<GiscusConfig>,
};

相关issue: 优化路由切换时的事件通知

danger

issue: onRouteDidUpdate does not see new Head metadata

映射到 discuss组件 的问题, 会导致切换路由后, 仍然显示上一篇文章的评论

src/clientModules/routeModules.ts
import mitt from 'mitt';
import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';

const emitter = mitt();

if (ExecutionEnvironment.canUseDOM) {
window.emitter = emitter;
}

export function onRouteDidUpdate() {
if (ExecutionEnvironment.canUseDOM) {
setTimeout(() => {
window.emitter.emit('onRouteDidUpdate');
});
}
// https://github.com/facebook/docusaurus/issues/8278
}
上述方法, 逻辑注释
1. 依赖:
- `mitt` 是一个轻量级的事件发射器库,用于简化事件的发布和订阅
- `ExecutionEnvironment` 是 Docusaurus 提供的一个工具,用于判断当前运行环境是否支持 DOM

2. 创建事件发射器:
- `const emitter = mitt();` 通过 `mitt` 创建了一个事件发射器实例

3. 判断环境:
- `if (ExecutionEnvironment.canUseDOM)` 通过这个条件判断当前环境是否可以使用 DOM
- 如果可以,代码将事件发射器 `emitter` 赋值给 `window.emitter`,使其在全局范围内可访问

4. 定义路由更新处理函数:
- `onRouteDidUpdate` 函数是一个事件处理函数,当路由发生更新时会被调用
- 如果支持 DOM,函数会使用 `setTimeout` 来异步触发 `onRouteDidUpdate` 事件,所有订阅了这个事件的部分都会得到通知

调用该模块

docusaurus.config.ts
const config: Config = {
clientModules: [require.resolve('./src/clientModules/routeModules.ts')]
};