引入giscus的评论服务
加入github维护的giscus的评论服务
首先于 github 主页Settings
中 Integrations->Application
找到app商店 Authorized GitHub Apps
, 在其中搜索栏搜索 giscus
根据提示下单(免费)后安装, 在安装好后会提示你将要开启讨论的仓库, 可以按需选择
之后可以在 Authorized GitHub Apps
旁边的 Installed GitHub Apps
中看到并配置你的App
开启 Discussions
-
在 GitHub 上,导航到开源存储库的主页
-
在您的存储库名称下,单击
Settings
-
向下滚动到
Features
, 并开启Discussions
此时, 与你
Settings
同一栏中, 便可以找到Discussions
管理你的讨论内容
giscus连接仓库
-
输入仓库名
-
映射关系: 选用
title
做映射, 会作为讨论的标题 -
Discussion分类: 选择
Announcements
推荐使用**公告(announcements)**类型的分类,以确保新 discussion 只能由仓库维护者和 giscus 创建
-
服务特性:
- 启用
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组件二次优化
-
安装
pnpm add @giscus/react
-
创建组件
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内部组件并引入自定义组件
swizzle
后需要重新运行 Docusaurus
, 才可以显示效果
对于不需要评论的文章,在前言中加入 hide_comment: true
该值默认为false, 评论开启
@docusaurus/theme-common
@docusaurus/plugin-content-blog
mitt
文档组件
- Typescript
- JavaScript
pnpm run swizzle @docusaurus/theme-classic DocItem/Layout --eject --typescript
pnpm run swizzle @docusaurus/theme-classic DocItem/Layout --eject
之后会生成 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>
);
}
博客组件
- Typescript
- JavaScript
pnpm run swizzle @docusaurus/theme-classic BlogPostPage --eject --typescript
pnpm run swizzle @docusaurus/theme-classic BlogPostPage --eject
之后会生成 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组件配置方式二次优化
- giscus功能基本参数默认&关键参数可配置
- 主题, 语言等样式可配置
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组件导入配置
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: 优化路由切换时的事件通知
issue: onRouteDidUpdate does not see new Head metadata
映射到 discuss组件
的问题, 会导致切换路由后, 仍然显示上一篇文章的评论
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` 事件,所有订阅了这个事件的部分都会得到通知
调用该模块
const config: Config = {
clientModules: [require.resolve('./src/clientModules/routeModules.ts')]
};