waline评论与docusaurus自定义集成
2023-06-14
以前一直不太理解, 静态博客系统是如何支持评论的? 静态博客都是存粹的html/js/css脚本, 原理上就不支持进行输入交互. 后面看了几个博客插件系统, 才发现原来都是额外挂载的评论插件, 背后其实有web server后台支撑. 有的是主流评论托管站点, 比如disqus; 有的非常奇思妙想, 竟然使用了github的issue模块用来保存评论, 比如gittalk; 有的支持自己部署, 比如valine和waline. 其实想找的评论系统, 是能够支持微信扫码登录进行评论, 交互起来也非常方便, 用户信息也不会被伪造, 但是没有找到, 估计需要自己开发了.
github issue这条路感觉不是正道, disqus国外站点又太慢了, 于是决定用足够简单的valine, 界面也好看. 后面一搜索, 发现valine竟然还有安全漏洞,说的主要是xss, 用户随便输入没有做过滤, 非常容易做出xss攻击. 国内新出的有个叫waline的, 说是解决了安全性问题, 于是决定使用. 这时候出现了一个问题, 国内估计用docusaurus的非常少, 竟然没有对应的waline插件, 网络搜索内容也少得可怜. 好久没写前端了, 对react也不熟悉, 靠自己摸索集成估计要花费大量时间. 本来打算放弃了, 后面看到下面这博客文章的介绍, 成功集成了waline, 而且站点本身也是docusaurus站点. 珠玉在前, 决定用开会的时间来摸索下, 最终实现了docusaurus集成waline评论, 同时还了解了docusaurus的自定义改造方法.
qileq-评论系统
https://qileq.com/about/site/remark/
至于为什么要开放评论? 主要是静态站点孤零零挂载在外, 有什么bug也不知道, 有评论则至少提供了一个反馈的机制.
docusaurus 自定义主题
评论系统是额外挂载的, 需要找个地方挂载到页面中, 因此需要修改docusaurus的文档页面. 但是页面都是安装主题包自带的, 要修改就要动源代码了, 想想都挺麻烦. 看了介绍文档, 发现docusaurus竟然支持自定义任意页面. 有个 swizzling 功能, 可以支持把主题的某些页面代码展示在用户代码前台, 然后直接进行修改, 真是非常巧妙的设计.
https://docusaurus.io/docs/swizzling
- 查看支持 swizzle 的模块
npm run swizzle -- --list
- 修改博客页面
npm run swizzle @docusaurus/theme-classic BlogPostPage -- --eject
- 修改doc文档页面
npm run swizzle @docusaurus/theme-classic DocItem/Layout -- --eject
执行上述命令后, 自动在项目里增加以下文件:
- src/theme
theme
├── BlogPostPage
│ ├── Metadata
│ │ └── index.js
│ └── index.js
└── DocItem
└── Layout
├── index.js
└── styles.module.css
waline 插件 react 改造
参考脚本 https://github.com/orgs/walinejs/discussions/1045, 新增脚本 src/components/Comment.tsx.
import React, { useEffect, useRef } from "react";
import { init } from "@waline/client";
import type { WalineInstance, WalineInitOptions } from "@waline/client";
import '@waline/client/dist/waline.css';
export type WalineOptions = Omit<WalineInitOptions, "el"> & { path: string };
export const Waline = (props: WalineOptions) => {
const walineInstanceRef = useRef < WalineInstance | null > (null);
const containerRef = React.createRef < HTMLDivElement > ();
useEffect(() => {
walineInstanceRef.current = init({
...props,
el: containerRef.current,
});
return () => walineInstanceRef.current?.destroy();
}, []);
useEffect(() => {
walineInstanceRef.current?.update(props);
}, [props]);
return <div className="comment-area" ref={containerRef} />;
};
对于熟悉 react 语法的前端开发者来说是个简单的功能, 但是很久没写前端, react语法也是每次查后即忘, 调试还是花了很多时间.
重点在于了解useEffect的语法, 第一个参数是执行的命令, return语句是在整个页面退出的时候才会执行的回调函数, 并不是每次都会被执行, 也可以没有return函数; 第二个参数是用于触发useEffect的参数, 空列表代表在整个生命周期只执行一次, [props]则代表在props变更的时候才会触发.
知道这个语法后, 这段代码就非常清晰了. 整个页面会对waline进行一次初始化, 挂载在useRef的current节点中; 每次 waline 被调用的参数变化, 则会继续调用 update 接口. 整个代码挂载在 comment-area的 div block中. 调用这个模块, 传入的参数是WalineOptions.
Tips:
- 需要使用tsx格式, 使用js的话
npm run start会报错无法识别语法
Module build failed (from ./node_modules/babel-loader/lib/index.js):
SyntaxError: /projects/muddy/src/components/Comment.js: Unexpected token, expected "from" (3:12)
1 | import React, { useEffect, useRef } from "react";
2 | import { init } from "@waline/client";
> 3 | import type { WalineInstance, WalineInitOptions } from "@waline/client";
| ^
4 | import '@waline/client/dist/waline.css';
5 |
6 | export type WalineOptions = Omit<WalineInitOptions, "el"> & { path: string };
需要明确导入 waline的css 文件, 不然搭载后没有css格式.
参考了一个docusaurus-react插件的代码(
https://github.com/bigbigDreamer/montage/tree/main)import '@waline/client/dist/waline.css'
改造 doc 和 blog 页面
有几个很特别的改造点:
支持任意markdown元数据
支持在markdown里通过frontmatter配置comments参数, 用于决定是否展示评论模块.
原作者看来确实研究了docusaurus的解析机制, 而且通过这个模版, 以后也可以自己改造支持任意的markdown元数据.
支持页面统计
可以在 html bock中增加任意支持的waline参数, 比如pageview="true"参数自动激活了页面统计, 后台数据库可以看到生效, 展示在前台则还需要改造下页面.
支持黑白模式主题
- docusaurus的黑白模式, 摸索发现可以通过
dark='html[data-theme="dark"]'这个参数进行配置.
自定义样式
通常网站会通过两种方式启用暗黑模式支持:
- 使用 @media 选择器通过 prefers-color-scheme 来根据设备颜色模式状态自动切换
- 通过修改 dom 根元素 (html 或 body) 的属性与 class 来动态应用或取消暗黑模式的颜色样式。
博客页面改造
src/theme/BlogPostPage/index.js
import React from 'react';
import clsx from 'clsx';
import {HtmlClassNameProvider, ThemeClassNames} from '@docusaurus/theme-common';
import {BlogPostProvider, useBlogPost} 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 { Waline } from '@site/src/components/Comment';
function BlogPostPageContent({sidebar, children}) {
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,
+ comments = true,
} = 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} />
)}
+ {comments && (<Waline serverURL="https://waline.gee.cool" pageview="true"
dark='html[data-theme="dark"]' />)}
</BlogLayout>
);
}
export default function BlogPostPage(props) {
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>
);
}
文档页面改造
src/theme/DocItem/Layout/index.js
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 styles from './styles.module.css';
+ import { Waline } from '@site/src/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;
+ const comments = frontMatter.comments == undefined ? true : frontMatter.comments;
return {
hidden,
mobile,
desktop,
+ comments,
};
}
export default function DocItemLayout({children}) {
const docTOC = useDocTOC();
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 />
+ {docTOC.comments && (<Waline serverURL="https://waline.gee.cool" pageview="true" />)}
</div>
</div>
{docTOC.desktop && <div className="col col--3">{docTOC.desktop}</div>}
</div>
);
}
css 自定义
src/css/custom.css
增加这个 css 配置后, 间隔比较自然.
/* waline comment custom block */
.comment-area {
margin: 2em 0;
}
评论展示
- dark模式
- white模式
- 支持 markdown, 图片, gif 表情包
