介绍我用纯JS实现的一个静态站点评论系统,以及实现过程中的心得体会。

前言

我的博客最早是使用 Disqus 来实现评论功能的。Disqus 被墙了之后,改成了多说。今年年初,多说也正式关闭了,于是我被逼着又开始寻找其他的替代评论系统。

我先是试用了网易云跟贴、畅言等几种类似的社会化评论系统。畅言要求站点必须备案,而我实在没有为了评论去申请备案的动力。网易云跟贴的管理后台上有很多不明觉厉的功能,但好像都没多大用处。最致命的问题是我不小心把我的站点绑定到了另一个网易账户,而不是我常用的微博账户。这样的话,我每次回贴就得退登到微博账户,要管理贴子的时候又得切回管理员账户,非常不方便。然而网易云跟贴并没有提供解绑的功能。于是我给他们提了需求,然而一直到现在都没有回复。再加上有了多说作为前车之鉴,我对国内的免费评论服务已经失去了信心。今天把A换成B,难以保证日后B也关闭了,被逼着又换到C,实在是懒得折腾下去啊。于是,我放弃了换用类似的评论系统的念头。

之后我找到了 isso 项目,它是一个 Python 实现的开源评论服务。这个服务需要搭建在自己的服务器上。官方的简介简明扼要:“a Disqus alternative”。出于对 Python 的好感,我把站点的评论功能迁移到了 isso 。然而,我对 isso 也并不是很满意。首先它的功能其实也非常弱,不支持 Markdown 语法,不支持 Gravatar 头像,也没有一个像样的管理后台,搭建和配置的过程也比较费时,远达不到开箱即用的程度。再加上 isso 需要服务器运营,为了一个评论系统而去购买服务器确实太奢侈了。用了几个月后,我又萌生了换掉它的念头。

项目介绍

我的想法来源于一些基于 Github issue 的博客。其实 Github 的 issue 本身就是一个非常完善的评论系统,有完善的管理后台,灵活的通知设置,而且 Github 是开放 API 的。只要我能把 Github 的 issue 与博客的页面打通,把 issue 上的内容显示在我的博客上,然后在需要评论的时候点击跳转到 Github 的 issue 页,就实现了一个基本可用的评论系统了。

comment.js 就是基于这个想法实现的一个评论系统,它的核心代码只有 400 行左右,却能够用来实现评论会话和最新评论列表的两个功能。比起已有的社会化评论系统,它有如下几个优点:

  1. 完善的评论管理系统。基于 issue 的评论,支持 Markdown ,支持 Gravatar。
  2. 开箱即用的邮件通知功能。Github 的邮件通知功能非常完善,不像 isso 那样还得配置邮件通知服务。
  3. 无需搭建后台。直接用现成的 issue 作为后端,不像 isso 那样还需要自己搭个后台,搞定数据库。
  4. 接入简单。获取评论会话和获取最新列表各自对应一个函数。
  5. 代码简单。这意味着你也可以很快上手脚本代码,对这个脚本进行定制。
  6. 除了 Github issue 之外,comment.js 也支持使用 OSChina issue 作为后端[1],即使 Github 被墙,也能通过修改参数迅速切换到其他备选站点,比起说关闭就关闭的评论服务可靠多了。

接入方法

comment.js 依赖几个 JS 前端库:

  • jQuery - 用于 Ajax 请求以及将评论内容插入到页面中。
  • marked - Markdown 支持。
  • timeago.js - 时间文本格式化。
  • spin - 用于在加载评论数据前先绘制一个 loading 动画(可选)。
  • highlight.js - 用于代码高亮(可选)。

0. 添加静态资源文件

在页面中添加这些资源:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<!-- stylesheet -->
<link rel="stylesheet" href="path_to_comment_css/comment.css">

<!-- for IE support -->
<!--[if lte IE 9]>
<script src="https://cdnjs.cloudflare.com/ajax/libs/es5-shim/4.5.9/es5-shim.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/es5-shim/4.5.7/es5-sham.min.js"></script>
<![endif]-->

<!-- javascripts -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.0.3/jquery.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/marked/0.3.6/marked.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/timeago.js/3.0.2/timeago.min.js"></script>

<!-- loading spin indicator(optional) -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/spin.js/2.3.2/spin.min.js"></script>

<!-- syntax highlighting(optional) -->
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/styles/default.min.css">
<script src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/highlight.min.js"></script>
<script type="text/javascript">
marked.setOptions({
highlight: function (code, lang) {
return hljs.highlightAuto(code).value;
}
});
function Highlighting(){
var markdowns = document.getElementsByClassName('markdown');
for(var i=0;i<markdowns.length;i++){
if(markdowns[i].innerHTML) markdowns[i].innerHTML =marked(markdowns[i].innerHTML);
}
}
window.addEventListener('DOMContentLoaded', Highlighting, false);
window.addEventListener('load', Highlighting, false);
</script>

<!-- comment.js -->
<script src="path_to_comment_js/comment.js"></script>

1. 注册 OAuth App

为了避免 API 被恶意滥用,Github API (以及 OSChina API)设定了一个API调用频率限制。为了提高频率限额,建议 [注册一个 Oauth App](Register a OAuth application](https://github.com/settings/applications/new)。

完成注册后,你将得到一个 client id 以及一个 client_secret ,先将这两个值记下来,后面我们会用到。

(提示:注册 App 的时候你可能会对 Authorization callback URL 这一项目感到困惑,一般填写你的站点地址即可。例如 http://hahack.com

2. 获取评论会话

第一步,在页面中添加一个 DIV ,用于展示评论会话内容。

1
<div id="comment-thread"></div>

第二步(可选),如果希望在加载完数据前先展示一个loading动画,还可以添加一个用于动画的 DIV :

1
<div id="loading-spin"></div>

最后,调用 getComments() 方法,获取该页面对应的 issue 包含的所有评论,然后展示到我们指定的 DIV 中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<script type="text/javascript">
var opt = {
type: "github",
user: "wzpan",
repo: "comment.js",
no_comment: "No comments yet. Press the button and go to comment now!",
go_to_comment: "Go to comment",
issue_id: "1",
btn_class: "btn",
comments_target: "#comment-thread",
loading_target: "#loading-spin",
client_id: "xxxxxx",
client_secret: "xxxxxx"
};
getComments(opt);
</script>

参数说明:

  • type: 要作为后端的站点。目前支持 GithubOSChina
  • user: 您的 Github 用户名。
  • repo: 您用作评论后端的仓库名。
  • no_comment: 当没有评论时,展示的提示消息。
  • go_to_comment: “去留言” 按钮的按钮文本。
  • issue_title: 您当前页面对应的 issue 标题。也可以使用 issue_id ,二者只选其一。
  • issue_id: 您当前页面对应的 issue id。也可以使用 issue_title,二者只选其一。
  • btn_class: “去留言”按钮的 CSS 样式名。
  • comments_target: 用于展示评论内容的容器。例如我们上面所写的 comment-thread DIV 。
  • loading_target(可选):用于展示 loading 动画的容器。例如我们上面所写的 loading-spin DIV 。
  • client_id(可选但建议):您注册的 OAuth App 的 client id。
  • client_secret(可选但建议):您注册的 OAuth App 的 client secret。

效果参见本页面下方的留言区。

3. 获取最新评论列表

评论列表用于获取你最近的若干条评论,效果可以参见 站点首页 右侧的最新留言区。

要获取最新评论列表的方法也大同小异。首先写一个 DIV 用于加载获取得到的评论列表数据:

1
<div id="recent-comments"></div>

之后可以调用 getRecentCommentsList() 方法,获取最近评论列表并展示到指定的 DIV 中。

1
2
3
4
5
6
7
8
9
10
11
12
<script type="text/javascript">
var opt = {
type: "github",
user: "wzpan",
repo: "comment.js",
recent_comments_target: "#recent-comments",
count: 5,
client_id: "xxxxxx",
client_secret: "xxxxxx"
};
getRecentCommentsList(opt);
</script>

参数说明:

  • type: 要作为后端的站点。目前支持 GithubOSChina
  • user: 您的 Github 用户名。
  • repo: 您用作评论后端的仓库名。
  • recent_comments_target: 用于展示最新评论列表的容器。例如我们上面所写的 recent-comments DIV 。
  • count: 列表的最大长度。
  • client_id(可选但建议):您注册的 OAuth App 的 client id。
  • client_secret(可选但建议):您注册的 OAuth App 的 client secret。

开发心得

下面照例总结下项目的开发心得。虽然整个项目只有几百行的代码,但这个过程中还是不可避免的遇到一些困难。

关于选型和项目命名

一开始的想法只是给 Hexo 写一个插件,让其能够实现评论功能。最理想的情况是类似 hexo-generator-search 那样,npm install 一下,然后 _config.yml 里添加下配置就完事。通过阅读 Hexo 的文档后我发现 helper 似乎比较适合用作这个目的:把核心功能写成一个 helper ,然后在模板文件里直接执行这个 helper ,得到的数据还能进一步再模板中调诸如 markdown 等其他现成的 helper, 这样还能实现 Markdown 支持。于是我最初的项目仓库名叫做 hexo-helper-github-comment 。

等我实现了 getComments() 方法后,我发现我的想法是错误的:helper 只适用于同步执行的操作,不适合网络请求这种异步操作。这带来的问题就是模板文件里已经成功执行了 helper 了,也返回了数据,但此时 renderer 早已经完成了模板的渲染了,而异步返回的评论数据却不再能够被渲染。

之后我想在 NodeJS 中加入 jQuery,用 jQuery 来操纵 DOM ,而不再依赖 renderer 。但这个方案似乎也不可行。因为在模板文件中,DOM 还没有创建,jQuery 拿不到实际的 DOM 。

所以最终我改成了纯 JS 的方案,把请求的方式也从 request-promise 改成了 AJAX ,然后在模板文件中直接跑 JS ,让 JS 完成请求,此时的 DOM 是已创建的,可以使用 jQuery 来操纵页面。虽然这样做就不能直接用 Hexo 现成的 markdown helper 了,但由于是纯 JS 实现,这个库也就可以在任何静态站点中使用,变得更加通用了。于是我把仓库名改成了 github-comment 。

又后来,我准备开源的前一天,在微博上先公开了关于这个项目的信息。有些人也表示了 Github 将来也可能被墙的质疑。于是我花了几分钟时间,也加入了对 OSChina 的支持。这个仓库名似乎也不只是基于 Github 了,于是我又把仓库名改成了 comment.js 。

关于取舍

我最纠结的部分,在于要不要把评论框也写进来。

直接在页面中写评论,减少了页面的跳数,当然是一大收益。但这样做也有几个问题:

  1. 功能可用性和项目的复杂度的取舍。Github 的编辑框其实包含了非常多的功能,例如支持拖拽的附件添加、表情、预览、快捷键等等,如果不把这些功能加进来,编辑框的功能就显得很鸡肋,远不如在 Github 中评论有趣;如果加进来,整个项目的代码就远不止 400 行这么简单了。
  2. 通用性和专用程度的取舍。为了避免 Github 单点问题,comment.js 还支持 OSChina 作为备选评论系统。加入 Github 的这些编辑功能,是否会影响对其他站点后端的兼容性又是个问题。
  3. 界面美观程度和版权的取舍。现在的评论会话界面几乎照搬了 Github 的样式,因为点击“去留言”按钮实际上直接跳到了 Github ,相当于为 Github 做了引流,给了一个大大的版权说明,也就没有了侵权的担忧。如果界面完全隔离了 Github,也隐藏了 Github 的版权信息,反而有点滥用平台的感觉[2]

有意思的是,当我刚发布 comment.js 的时候,我才发现几个月前已经有人做了一个类似的项目:gitment,真是心有灵犀啊。这个项目与我的项目的最大区别就在于它实现了内置的编辑框,并且目前只支持 Github 。如果你认为评论框必不可少,那么建议使用 gitment;反之如果你觉得点击按钮跳到 Github 页面似乎也还能接受,担心 Github 单点问题,而且觉得保证代码的简单和通用性更重要的话,那么不妨使用 comment.js 。


  1. 目前 OSChina 的 API 在浏览器端会出现 CORS 错误。我已经给 OSChina 提交了工单,待后台添加 CORS 支持后就可以使用 OSChina 作为后端。 ↩︎

  2. 话说回来直接照搬界面确实不太好,后面我会对评论会话的UI进行调整,避免侵权。 ↩︎

Comments