Docusaurus 添加评论功能

406 阅读3分钟

更好的阅读体验可查看我的博客原文

获取 Giscus 配置参数

首先根据 Giscus 官网 的步骤配置安装并获取配置后的参数。

选择 giscus 连接到的仓库。请确保:

  1. 此仓库是公开的,否则访客将无法查看 discussion。
  2. giscus app 已安装否则访客将无法评论和回应。
  3. Discussions 功能已在你的仓库中启用。

安装所需包

yarn add @giscus/react mitt

封装评论组件

配置生命周期函数

由于 Docusaurus bug 导致 Giscus 有时获取的仍然是上一篇文章的评论,为解决这一问题我们创建一个 clientModule

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
}

:::info 相关 issue :::

创建一个组件

import React, { forwardRef, useEffect, useState } from 'react';
import BrowserOnly from '@docusaurus/BrowserOnly';
import Giscus, { GiscusProps } from '@giscus/react';
import { useThemeConfig, useColorMode, ThemeConfig } from '@docusaurus/theme-common';
interface CustomThemeConfig extends ThemeConfig {
  giscus: GiscusProps & { darkTheme: string };
}

export const Comment = forwardRef<HTMLDivElement>((_props, ref) => {
  const { giscus } = useThemeConfig() as CustomThemeConfig;
  const { colorMode } = useColorMode();
  const { theme = 'light', darkTheme = 'dark_dimmed' } = giscus;
  const giscusTheme = colorMode === 'dark' ? darkTheme : theme;
  const [routeDidUpdate, setRouteDidUpdate] = useState(false);

  useEffect(() => {
    function eventHandler(e) {
      setRouteDidUpdate(true);
    }

    window.emitter.on('onRouteDidUpdate', eventHandler);

    return () => {
      window.emitter.off('onRouteDidUpdate', eventHandler);
    };
  }, []);

  if (!routeDidUpdate) {
    return null;
  }

  return (
    <BrowserOnly fallback={<div>Loading Comments...</div>}>
      {() => (
        <div ref={ref} id="comment" style={{ paddingTop: 50 }}>
          <Giscus
            id="comments"
            mapping="title"
            strict="1"
            reactionsEnabled="1"
            emitMetadata="0"
            inputPosition="bottom"
            lang="zh-CN"
            loading="lazy"
            {...giscus}
            theme={giscusTheme}
          />
        </div>
      )}
    </BrowserOnly>
  );
});

export default Comment;

Swizzling Docusaurus 内部组件

Docusaurus 页面分为文档和博客,自己根据需求 Swizzling 对应的页面组件。

Swizzling 文档页面对应组件

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

:::caution

因为我的项目是基于 typescript 的,如果你的项目是 javascript 的话,则不需要加后面的 --typescript

:::

Swizzling 后会生成 src/theme/DocItem/Layout 目录,我们需要对 src/theme/DocItem/Layout/index.tsx 进行修改

import React from 'react';
import clsx from 'clsx';
import { useWindowSize } from '@docusaurus/theme-common';
// @ts-ignore
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';
// highlight-add-line
import Comment from '../../../components/comment';

/**
 * 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();
  // highlight-add-start
  const { frontMatter } = useDoc();
  const { hide_comment: hideComment } = frontMatter;
  // highlight-add-end

  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>
        // highlight-add-line
        {!hideComment && <Comment />}
      </div>
      {docTOC.desktop && <div className="col col--3">{docTOC.desktop}</div>}
    </div>
  );
}

Swizzling 博客页面对应组件

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

同样修改文件

import React, { type ReactNode } from 'react';
import clsx from 'clsx';
import { HtmlClassNameProvider, ThemeClassNames } from '@docusaurus/theme-common';

import {
  BlogPostProvider,
  useBlogPost
  // @ts-ignore
} from '@docusaurus/theme-common/internal';
import BlogLayout from '@theme/BlogLayout';
import BlogPostItem from '@theme/BlogPostItem';
import BlogPostPaginator from '@theme/BlogPostPaginator';
import BlogPostPageMetadata from '@theme/BlogPostPage/Metadata';
import TOC from '@theme/TOC';
import type { Props } from '@theme/BlogPostPage';
import type { BlogSidebar } from '@docusaurus/plugin-content-blog';
// highlight-add-line
import Comment from '../../components/comment';

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,
    // highlight-add-start
    hide_comment: hideComment
    // highlight-add-end
  } = frontMatter;
  return (
    <BlogLayout
      sidebar={sidebar}
      toc={
        !hideTableOfContents && toc.length > 0 ? (
          <TOC
            toc={toc}
            minHeadingLevel={tocMinHeadingLevel}
            maxHeadingLevel={tocMaxHeadingLevel}
          />
        ) : undefined
      }
    >
      <BlogPostItem>{children}</BlogPostItem>
      {(nextItem || prevItem) && <BlogPostPaginator nextItem={nextItem} prevItem={prevItem} />}
      // highlight-add-line
      {!hideComment && <Comment />}
    </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 />
        <BlogPostPageContent sidebar={props.sidebar}>
          <BlogPostContent />
        </BlogPostPageContent>
      </HtmlClassNameProvider>
    </BlogPostProvider>
  );
}

:::caution

Swizzling 后需要重新运行 Docusaurus ,不然是无法看到效果的

:::

:::tip

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

:::

配置 Giscus

module.exports = {
  themeConfig: {
    giscus: {
      repo: 'xxx',
      repoId: 'xxx',
      category: 'Announcements',
      categoryId: 'xxx'
    }
  },
  clientModules: [require.resolve('./src/clientModules/routeModules.ts')]
};

黑夜模式

根据自己喜好修改配置

giscus: {
  theme: 'light_high_contrast',
  darkTheme: 'dark_tritanopia'
},

:::info 默认的主题分别是:light, dark_dimmed :::

大功告成

完成以上步骤后你就可以在每篇博客和每篇文档的底部看到评论功能了