Utterances 实现博客评论功能

Utterances利用github上issue的功能,来完成对评论的存储和分类,映射到不同的博客文章url上。

在标准的html-js网站中,只需要在对应的github仓库安装 utterances GitHub app ,再在需要评论的页面引入下面脚本即可。

<script src="https://utteranc.es/client.js"
        repo="[ENTER REPO HERE]"
        issue-term="pathname"
        label="comment"
        theme="github-light"
        crossorigin="anonymous"
        async>
</script>

mdbook 覆盖主题

由于mdbook是用handlebars来写模板页面的,想要评论主题随着博客主题同时变化所以还需进一步操作。

mdbook有theme覆盖的功能,即可以用同名的文件来覆盖原有的前端代码。 使用mdbook init --theme生成包含theme文件夹的初始工程,之后把其中的theme文件夹复制到当前的博客目录中,在book.toml中指定用此文件夹来覆盖原有的theme。我们只需变动index.hbs文件,所以theme目录中的其他文件可以删除了。再创建一个用于增加评论的脚本文件comments.js

[output.html]
theme = "theme"
additional-js = ["theme/comments.js"]

js实现

comments.js主要根据当前的博客主题动态地生成引入utterances的<script>标签。loadComments函数实现了这个功能。

function loadComments() {
    // console.log("loading comments.");
    const page = document.querySelector(".page");

    const isLight = document.querySelector('html').getAttribute('class').indexOf('light') != -1;

    const commentScript = document.createElement('script')
    const commentsTheme = isLight ? 'github-light' : 'github-dark'
    commentScript.async = true
    commentScript.src = 'https://utteranc.es/client.js'
    commentScript.setAttribute('repo', 'Sugar-Coder/Sugar-Coder.github.io')
    commentScript.setAttribute('issue-term', 'pathname')
    commentScript.setAttribute('id', 'utterances')
    commentScript.setAttribute('label', 'comment')
    commentScript.setAttribute('theme', commentsTheme)
    commentScript.setAttribute('crossorigin', 'anonymous')

    page.appendChild(commentScript);
}

loadComments();

为了监听用户改变博客主题,使用 MutationObserver 来监听html的class属性变动。如果发生了从明亮主题到暗色主题的变动,那么就重新加载comments。

function removeComments() {
    const page = document.querySelector(".page");
    page.removeChild(page.lastChild);
}

(function observeChange() {
    const html=document.querySelector('html')
    const options={
        attributes:true,//观察node对象的属性
        attributeFilter:['class']//只观察class属性
    }
    let prevIsLight = document.querySelector('html').getAttribute('class').indexOf('light') != -1;
    var mb=new MutationObserver(function(mutationRecord,observer){
        let isLight = document.querySelector('html').getAttribute('class').indexOf('light') != -1;
        // console.log(`prevIsLight:${prevIsLight}, isLight:${isLight}`)
        if (prevIsLight != isLight) {
            removeComments();
            loadComments();
            prevIsLight = isLight;
        }
    })
    mb.observe(html,options)
})();

这样就实现了动态评论主题。

使用基于React的方式增加utterances

Note: 这个方法现在已经不用了,多引入了很多依赖,我现在使用上面的纯js方法来完成评论的生成。

向基于react构建的博客加入utterances可以参考这片文章

Step1: Add a DOM Container to the HTML

index.hbs中增加一个空的 <div> 容器,来放React生成的元素。

<div id="content" class="content">
    <!-- rendering post content -->
</div>
<!-- react DOM container -->
<div id="react-app"></div> 

我把上面这个DOM Container放到了#content的同级位置,让评论能在文章内容底部出现。

Step2: Add the Script Tags

为了使用React,就需要一些依赖脚本,首先是react和react-dom。

babel是为了编译包含JSX语法的js文件(post_footer.js),如果不加babel,就会出现unexpected token的报错

第四个script就是引入自己写的脚本,这个地方用了handlebars的语法来增加所有在book.toml中配置的additional_js文件。

{{!-- The react --}}
<!-- Load React. -->
<!-- Note: when deploying, replace "development.js" with "production.min.js". -->
<script src="https://unpkg.com/react@18/umd/react.development.js" crossorigin></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js" crossorigin></script>
<!-- Babel Script -->
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<!-- Custom JS scripts -->
{{#each additional_js}}
<script type="text/jsx" src="{{ ../path_to_root }}{{this}}"></script>
{{/each}}

Step3: Create a React Component

由于使用<script>方式引入的React在全局作用域中,在post_footer.js中就可以直接使用React了。 首先找到要用React的<div>容器,在这个容器中渲染要加入的元素PostFooter。

const e = React.createElement;

const domContainer = document.querySelector('#react-app');
const root = ReactDOM.createRoot(domContainer);
root.render(e(PostFooter));

接着来定义PostFooter这个React Component。 首先定义组成PostFooter的每一个comment,用React的forwardRef来定义,似乎是为了组件复用,在父组件中引用。

const Comment = React.forwardRef((props, commentBox) => {
    return <div ref={commentBox} className="comments" />
});

之后就生成引入utterances的<script>标签。该标签的属性可以根据当前的theme改变,我这边是用html标签的class属性是否包含light关键字来判断的。

因为希望评论的主题和博客的主题保持一致,所以希望在这个react组建加载的时候进行判断,完成对应的评论主题生成。使用useEffect来完成组件加载时的执行逻辑。 useEffect函数最后返回的是用于清空当前渲染出来的组件的。

这种方法现在只能通过切换url来完成评论主题的更改,不能在更改博客主题时马上更改评论主题。

const PostFooter = () => {
    const commentBox = React.createRef();
    const isLight = document.querySelector('html').getAttribute('class').indexOf('light') != -1;
    React.useEffect(() => {
      const commentScript = document.createElement('script')
      const commentsTheme = isLight ? 'github-light' : 'github-dark'
      commentScript.async = true
      commentScript.src = 'https://utteranc.es/client.js'
      commentScript.setAttribute('repo', 'Sugar-Coder/Sugar-Coder.github.io')
      commentScript.setAttribute('issue-term', 'pathname')
      commentScript.setAttribute('id', 'utterances')
      commentScript.setAttribute('label', 'comment')
      commentScript.setAttribute('theme', commentsTheme)
      commentScript.setAttribute('crossorigin', 'anonymous')
      if (commentBox && commentBox.current) {
        commentBox.current.appendChild(commentScript)
      } else {
        console.log(`Error adding utterances comments on: ${commentBox}`)
      }

      const removeScript = () => {
        commentScript.remove();
        document.querySelectorAll(".utterances").forEach(el => el.remove());
      };
      return () => {
        removeScript();
      };
    }, [])
    return (
      <>
        <Comment ref={commentBox} />
      </>
    )
}