美文网首页
Hexo评论功能的实现:Gitalk

Hexo评论功能的实现:Gitalk

作者: 看远方的星 | 来源:发表于2023-01-22 20:36 被阅读0次

    需求:
    1、用Gitalk实现评论功能
    2、去除之前的Valine实现的评论功能

    可在主题配置文件搜索comment system,查看支持的评论系统;个人使用Gitalk进行配置;

    image

    Gitalk评论功能

    1、注册OAuth application

    1. 在 github 中进行注册,进入 https://github.com/settings/profile
    2. 点击左侧 Developer settings
    3. Register a new application
    image
    Application name:   #应用名称 
    Homepage URL:       #网站URL(填自己的博客主页地址)  
    Application description     #描述  
    Authorization callback URL: #网站URL(填自己的博客主页地址)  
    
    1. 注册完成之后,会得到:Client IDClient Secret[1]

    2、新建存放博客评论仓库

    可以在 github 中建一个项目,专门用来存储你的博客评论

    3、配置 Next 主题文件

    编辑主题配置文件:themes\next\ _config.yml,找到有关 gitalk的相关配置进行填写:

    gitalk:
      enable: true 开启gitalk评论,不需要配置
      owner: github用户名
      admin: github用户名
      repo: 博客的仓库名称(注意不是地址)
      ClientID: 上面生成的Client ID
      ClientSecret: 上面生成的Client Secret
      labels: 'gitalk' github issue 对应的issue标签(新建一个)
      distractionFreeMode: true  无干扰模式,不需要更改
    
    

    这是我的配置:


    image

    进入到 themes\next\layout\post.swig(我的博客是基于 Next,如果有差异,替换路径中的 next 即可),添加 gitalk 模板文件的导入[2]

    <!-- {### Line 357,如果行数有差异,只需要在 POST END 文章结束后添加即可 ###} -->  
    {% if theme.git_talk.enabled and not is_index %}  
    <div>{% include 'git-talk.swig' %}</div>  
    {% endif %}
    

    然后添加 git-talk.swig 文件(themes\next\layout\git-talk.swig),文件内容如下:

    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/gitalk@1/dist/gitalk.css" />  
    <script src="https://cdn.jsdelivr.net/npm/gitalk@1/dist/gitalk.min.js"></script>  
      
    <div id="gitalk-container"></div>  
      
    <script type="text/javascript">  
      var gitalk = new Gitalk({  
        clientID: "{{theme.git_talk.clientID}}",  
        clientSecret: "{{theme.git_talk.clientSecret}}",  
        repo: "eminoda.github.io", // 博客仓库地址  
        owner: "eminoda", // github 用户名  
        admin: ["eminoda"], // github 用户名  
        perPage: 20,  
        id: location.pathname.slice(0, 50), // 查找 issus 的条件,后面将对 id 有针对逻辑  
        title: "{{page.title}}",  
        body: "🚀 " + location.href + "\n\n欢迎通过 issues 留言 ,互相交流学习😊", // 初始化后,issues 的内容  
      });  
      gitalk.render("gitalk-container");  
    </script>
    

    以上操作完成后,打开文章即可看到以下页面,需要登录github账号初始化;每篇文章都需要进行登录初始化才可以使用;

    未登录 初始化后

    4、全部文章批量初始化Issues

    对于一个刚起步的博客站点没有任何问题,新增一篇文章,初始化下issue,顺手的事情。

    但对于一个历史站点,里面可能有百篇文章,如果希望看到别人阅读的回复,则需要人工每篇进行初始化,不太现实,则需要程序来批量初始化。[2]

    4.1 开启 OAuth 认证

    需要在 Developer Setting 开启 Personal access tokens[3]

    4.2 安装项目依赖
    npm i request xml-parser blueimp-md5 moment hexo-generator-sitemap -S
    

    需要的包:request、xml-parser、 blueimp-md5、 moment、 hexo-generator-sitemap

    4.3 修改 hexo-generator-sitemap 配置

    项目根目录配置文件 _config.yml 添加配置[4]

    #Sitemap
    sitemap:
      path: sitemap.xml
      template: ./sitemap_template.xml
      rel: false
      tag: true
      category: false
    

    项目根目录新建文件 sitemap_template.xml ,内容如下:

    <?xml version="1.0" encoding="UTF-8"?>  
    <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">  
      {% for post in posts %}  
      <url>  
        <loc>{{ post.permalink | uriencode }}</loc>  
        {% if post.updated %}  
        <lastmod>{{ post.updated.toISOString() }}</lastmod>  
        {% elif post.date %}  
        <lastmod>{{ post.date.toISOString() }}</lastmod>  
        {% endif %}  
        <date>{{ post.date }}</date>  
        <title>{{ post.title + ' | ' + config.title }}</title>  
        {# nunjucks 模版语法 https://github.com/mozilla/nunjucks #}  
        <desc>{{ post.description | default(post.excerpt) | default(post.content) | default(config.description) | striptags | truncate(200, true, '') }}</desc>  
      </url>  
      {% endfor %}  
    </urlset>
    
    4.4 执行 hexo generate 命令,生成 sitemap
    npm run build
    

    此命令执行成功之后, public 目录下应该有生成 sitemap.xml 文件,如果没有此文件,请检查包是否安装成功。

    4.5 添加自动初始化程序

    项目根目录新建文件 talk-auto-init.js ,内容如下[5]

    const fs = require('fs');
    const path = require('path');
    const url = require('url');
    
    const request = require('request');
    const xmlParser = require('xml-parser');
    const md5 = require('md5');
    
    // 配置信息
    const config = {
      username: 'toimc', // GitHub repository 所有者,可以是个人或者组织。对应Gitalk配置中的owner
      repo: "toimc.github.io", // 储存评论issue的github仓库名,仅需要仓库名字即可。对应 Gitalk配置中的repo
      token: 'xxxxxx', // 前面申请的 personal access token
      sitemap: path.join(__dirname, './public/sitemap.xml'), // 自己站点的 sitemap 文件地址
      cache: true, // 是否启用缓存,启用缓存会将已经初始化的数据写入配置的 gitalkCacheFile 文件,下一次直接通过缓存文件判断
      gitalkCacheFile: path.join(__dirname, './gitalk-init-cache.json'), // 用于保存 gitalk 已经初始化的 id 列表
      gitalkErrorFile: path.join(__dirname, './gitalk-init-error.json'), // 用于保存 gitalk 初始化报错的数据
    };
    
    const api = 'https://api.github.com/repos/' + config.username + '/' + config.repo + '/issues';
    
    /**
    * 读取 sitemap 文件
    * 远程 sitemap 文件获取可参考 https://www.npmjs.com/package/sitemapper
    */
    const sitemapXmlReader = (file) => {
      try {
        const data = fs.readFileSync(file, 'utf8');
        const sitemap = xmlParser(data);
        let ret = [];
        sitemap.root.children.forEach(function (url) {
          const loc = url.children.find(function (item) {
            return item.name === 'loc';
          });
          if (!loc) {
            return false;
          }
          const title = url.children.find(function (item) {
            return item.name === 'title';
          });
          const desc = url.children.find(function (item) {
            return item.name === 'desc';
          });
          const date = url.children.find(function (item) {
            return item.name === 'date';
          });
          ret.push({
            url: loc.content,
            title: title.content,
            desc: desc.content,
            date: date.content,
          });
        });
        return ret;
      } catch (e) {
        return [];
      }
    };
    
    // 获取 gitalk 使用的 id
    const getGitalkId = ({
      url: u,
      date
    }) => {
      const link = url.parse(u);
      // 链接不存在,不需要初始化
      if (!link || !link.pathname) {
        return false;
      }
      if (!date) {
        return false;
      }
      return md5(link.pathname);
    };
    
    /**
    * 通过以请求判断是否已经初始化
    * @param {string} gitalk 初始化的id
    * @return {[boolean, boolean]} 第一个值表示是否出错,第二个值 false 表示没初始化, true 表示已经初始化
    */
    const getIsInitByRequest = (id) => {
      const options = {
        headers: {
          'Authorization': 'token ' + config.token,
          'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36',
          'Accept': 'application/json'
        },
        url: api + '?labels=' + id + ',Gitalk',
        method: 'GET'
      };
      return new Promise((resolve) => {
        request(options, function (err, response, body) {
          if (err) {
            return resolve([err, false]);
          }
          if (response.statusCode != 200) {
            return resolve([response, false]);
          }
          const res = JSON.parse(body);
          if (res.length > 0) {
            return resolve([false, true]);
          }
          return resolve([false, false]);
        });
      });
    };
    
    /**
    * 通过缓存判断是否已经初始化
    * @param {string} gitalk 初始化的id
    * @return {boolean} false 表示没初始化, true 表示已经初始化
    */
    const getIsInitByCache = (() => {
      // 判断缓存文件是否存在
      let gitalkCache = false;
      try {
        gitalkCache = require(config.gitalkCacheFile);
      } catch (e) {}
      return function (id) {
        if (!gitalkCache) {
          return false;
        }
        if (gitalkCache.find(({
            id: itemId
          }) => (itemId === id))) {
          return true;
        }
        return false;
      };
    })();
    
    // 根据缓存,判断链接是否已经初始化
    // 第一个值表示是否出错,第二个值 false 表示没初始化, true 表示已经初始化
    const idIsInit = async (id) => {
      if (!config.cache) {
        return await getIsInitByRequest(id);
      }
      // 如果通过缓存查询到的数据是未初始化,则再通过请求判断是否已经初始化,防止多次初始化
      if (getIsInitByCache(id) === false) {
        return await getIsInitByRequest(id);
      }
      return [false, true];
    };
    
    // 初始化
    const gitalkInit = ({
      url,
      id,
      title,
      desc
    }) => {
      //创建issue
      const reqBody = {
        'title': title,
        'labels': [id, 'Gitalk'],
        'body': url + '\r\n\r\n' + desc
      };
    
      const options = {
        headers: {
          'Authorization': 'token ' + config.token,
          'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36',
          'Accept': 'application/json',
          'Content-Type': 'application/json;charset=UTF-8'
        },
        url: api,
        body: JSON.stringify(reqBody),
        method: 'POST'
      };
      return new Promise((resolve) => {
        request(options, function (err, response, body) {
          if (err) {
            return resolve([err, false]);
          }
          if (response.statusCode != 201) {
            return resolve([response, false]);
          }
          return resolve([false, true]);
        });
      });
    };
    
    
    /**
    * 写入内容
    * @param {string} fileName 文件名
    * @param {string} content 内容
    */
    const write = async (fileName, content, flag = 'w+') => {
      return new Promise((resolve) => {
        fs.open(fileName, flag, function (err, fd) {
          if (err) {
            resolve([err, false]);
            return;
          }
          fs.writeFile(fd, content, function (err) {
            if (err) {
              resolve([err, false]);
              return;
            }
            fs.close(fd, (err) => {
              if (err) {
                resolve([err, false]);
                return;
              }
            });
            resolve([false, true]);
          });
        });
      });
    };
    
    const init = async () => {
      const urls = sitemapXmlReader(config.sitemap);
      // 报错的数据
      const errorData = [];
      // 已经初始化的数据
      const initializedData = [];
      // 成功初始化数据
      const successData = [];
      for (const item of urls) {
        const {
          url,
          date,
          title,
          desc
        } = item;
        const id = getGitalkId({
          url,
          date
        });
        if (!id) {
          console.log(`id: 生成失败 [ ${id} ] `);
          errorData.push({
            ...item,
            info: 'id 生成失败',
          });
          continue;
        }
        const [err, res] = await idIsInit(id);
        if (err) {
          console.log(`Error: 查询评论异常 [ ${title} ] , 信息:`, err || '无');
          errorData.push({
            ...item,
            info: '查询评论异常',
          });
          continue;
        }
        if (res === true) {
          // console.log(`--- Gitalk 已经初始化 --- [ ${title} ] `);
          initializedData.push({
            id,
            url,
            title,
          });
          continue;
        }
        console.log(`Gitalk 初始化开始... [ ${title} ] `);
        const [e, r] = await gitalkInit({
          id,
          url,
          title,
          desc
        });
        if (e || !r) {
          console.log(`Error: Gitalk 初始化异常 [ ${title} ] , 信息:`, e || '无');
          errorData.push({
            ...item,
            info: '初始化异常',
          });
          continue;
        }
        successData.push({
          id,
          url,
          title,
        });
        console.log(`Gitalk 初始化成功! [ ${title} ] - ${id}`);
        continue;
      }
    
      console.log(''); // 空输出,用于换行
      console.log('--------- 运行结果 ---------');
      console.log(''); // 空输出,用于换行
    
      if (errorData.length !== 0) {
        console.log(`报错数据: ${errorData.length} 条。参考文件 ${config.gitalkErrorFile}。`);
        await write(config.gitalkErrorFile, JSON.stringify(errorData, null, 2));
      }
    
      console.log(`本次成功: ${successData.length} 条。`);
    
      // 写入缓存
      if (config.cache) {
        console.log(`写入缓存: ${(initializedData.length + successData.length)} 条,已初始化 ${initializedData.length} 条,本次成功: ${successData.length} 条。参考文件 ${config.gitalkCacheFile}。`);
        await write(config.gitalkCacheFile, JSON.stringify(initializedData.concat(successData), null, 2));
      } else {
        console.log(`已初始化: ${initializedData.length} 条。`);
      }
    };
    
    init();
    

    以上代码需改动的地方:


    image

    修改博客根目录下的package.json,新增命令:

    "scripts": {  
      "talk": "node talk-auto-init.js"  
    },
    

    注意观察文件格式,若放在最后一个,前面需要一个逗号,个人配置如下:


    image

    项目的 package.json 是配置和描述如何与程序交互和运行的中心。[6]

    4.6 执行命令
    npm run talk
    

    若出现以下情况,则成功啦:


    image
    4.7 命令合并

    修改 package.json 中的 build 命令,将自动初始化添加到 build 之后,这样每次执行 build 命令就会自动执行初始化命令。

    "scripts": {  
      "build": "hexo generate && node talk-auto-init.js"  
    },
    

    去除valine评论系统:

    编辑themes\next\ _config.yml文件:将enable选项改为false即可

    image

    某个页面要不要评论

    可以单独关闭某个页面的评论,在页面的 Front-matter 中添加 comments 字段,设为 false。比如标签页不想要评论,则在标签页面中做如下设置[7]

    title: xxxxxxxxx
    date: 2022-03-06 17:05:24
    type: "tags"
    comments: false
    

    报错及解决

    image

    修改package.json少了个逗号;

    image

    原因:talk-auto-init.js有误,之前借鉴的是这篇文章:hexo gitalk 评论自动初始化里的talk-auto-init.js,造成错误,适用于我的是这篇文章的talk-auto-init.jshexo主题next中gitalk配置与评论初始化本文贴出的也是这篇talk-auto-init.js

    解决方案:修改 talk-auto-init.js

    image image

    修改之后还是有一些成功了,有一些还是报错,直接删除用第一篇的文章的talk-auto-init.js,用第二篇文章的talk-auto-init.js,再改个人配置即可;

    参考文章


    1. Hexo Next主题 添加文章评论功能

    2. 如何在 hexo 博客中,集成 gitalk 评论插件

    3. hexo gitalk 评论自动初始化

    4. hexo next

    5. hexo主题next中gitalk配置与评论初始化

    6. package.json 详解

    7. 【Hexo】nexT主题使用攻略基础——添加评论功能

    相关文章

      网友评论

          本文标题:Hexo评论功能的实现:Gitalk

          本文链接:https://www.haomeiwen.com/subject/lgakhdtx.html