NexT 文章阅读排行(热榜)功能

写这篇文章之前,本人也了解过其他网站的做法,大部分都是基于 Leancloud 来实现,使用谷歌 Firestore(Firebase)来做的很少。国内版 Leancloud 需要实名认证,最近还需绑定自己的域名(详情)。一种方案就是使用国际版来替代,此版本只需要绑定邮箱和手机号,参考:这篇文章

主要是本人比较排斥这样搜刮隐私的行为,So 经过几次尝(tou)(ji)无果后,果断放弃!

关于 Leancloud 的相关实现,可以参考下边这篇文章,精简实用,本文也在此基础上作了参考。

hexo next 新增阅读排行页面

注意:使用 leancloud 这功能时,有以下两个前提

1、主题配置文件开启 leancloud_visitors,并设置 app_idapp_key

1
2
3
4
leancloud_visitors:
enable: true
app_id: KWIxah6ChxxxxoTi-xxxHsz #<app_id> // 你的app_id
app_key: 3wuQxxxbjUakGxxxxxsT1P #<app_key> // 你的app_key

配置文件也有 “Show number of visitors to each article” 这个说明,开启这个才能统计每篇文章的的访客数(阅读量),排行榜功能是需要用到这个数值的,所以得开启

2、leancloud 应用中创建 Class

登录leancloud控制台创建应用(也可以用已经创建好的),在所选择应用的存储选项下创建Class按钮,新建一个class,class名一定要是Counter,其它配置默认即可。若是不创建这个Class的话,上一步中的访客数(阅读量)就一直为空,排行榜功能就不能正常工作。

创建后正常访问:

n1MX5D.png

以上两点提示,重要!!!

开始进入正题:Firestore

主题配置文件,已经提前预配了 firestore

nOGNCQ.png

是不是已经给你提示了呢?而且官方文档也提到过,一共分为两步:

1、注册

nOGsET.png

2、配置

nOGbPe.png

Firebase 属于谷歌的,确保你能访问:https://console.firebase.google.com (tips:科学上网)

注册

可以直接使用 Google 帐号登录,没有的自行进行注册,此步骤略过。登录成功后,点击网页右上角:转到控制台

添加项目

输入项目名称,比如是 leafjame2019814, 点击继续,默认配置即可,完成后还可以再修改。

nOGL2d.png

创建项目完成后,点击网页左侧 setttings 按钮,如图:

nOJSVf.png

可查看到自己的项目 ID网络 API 密钥,这就是 next 主题配置文件中提到的 projectIdapiKey

nOJF2j.png

创建数据库

创建完项目后,接着需要创建存储数据的地方,如下图所示:

nOJEMn.png

有两种选项,自己用的话,选择以测试模式开始,就可以。

nOJ8MR.png

如果选择以锁定模式开始,后续自己通过 js api 的方式写入时会提示没有权限

下一步设置 Cloud Firestore 位置,有以下好几种,分为多域性区域性。具体区别可查看文档,默认选择 nam5 (us-central) 即可,点击完成,等待分配 Database。

nOJyLt.png

完成后显示是这样的,现在还没有数据。

可参考Cloud Firestore 使用入门,文档还是很全的

配置文章阅读量

做完上边的操作后,接着就是把代码集成到自己的项目中了。

next/layout/_third-party/analytics/firestore.swig 中,其实已经实现了文章阅读量的功能,只需要经过本文以上的操作,然后在 next/_config.yml 文件,启用 firestore

nOJhWQ.png

里面的 apiKeyprojectId 就是上文中创建的,collection:集合 ID,下边会讲到。

这个功能经过配置后,可用于文章的阅读次数了,当文章未被查看时,效果是这样的:

nOJbwV.png

此时 Firebase 中 Database 下还没有数据,当浏览这篇文章后,阅读次数加 1 了,如图:

nOJjW4.png

再打开 Firebase,你会发现,也有数据了:

nOYZSH.png

articles 就是上边配置的 collection 的值,即:集合 ID

这控制台能对集合、文档、字段、值进行操作,比如设置阅读量啦什么的~

设置开发环境

每个页面的阅读量有了,不过我们要做的是排行榜啊,得把所有的页面汇总排序。而且刚才的 Database 里存的数据也没有 URL 等等这些信息呀,接下来要写代码了。

新增如下的页面——「热榜」,我想大家都会了吧,不细说了。

nOYQTf.png

在这个 index.md 中,引入要用到的 Firebase 代码,可参照官方文档进行。

之前我想直接在此 md 文件中引入 firestore.swig,但启动一直报错。。(应该是没配置对的缘故)

报错截图如下:

nOY8fg.png

多次改路径尝试无果后,我觉定在 md 文件中再次引入依赖的 js 代码。

index.md 完整代码如下:

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
39
40
41
42
43

---
title: 文章热度排行
date: 2019-08-14 15:23:11
---

<div id="top" style="margin-top:80px;">

</div>

<!-- Firebase App (the core Firebase SDK) is always required and must be listed first -->
<script src="https://www.gstatic.com/firebasejs/5.10.1/firebase-app.js"></script>

<!-- Add Firebase products that you want to use -->
<script src="https://www.gstatic.com/firebasejs/5.10.1/firebase-auth.js"></script>
<script src="https://www.gstatic.com/firebasejs/5.10.1/firebase-database.js"></script>

<script src="https://www.gstatic.com/firebasejs/5.10.1/firebase-firestore.js"></script>

<script>

firebase.initializeApp({
apiKey: 'AIzaSyDKul4ZXXXXX6l6UiHzXXXXvcDsiE', //你的apiKey
projectId: 'aXXX5eXXX6b' //你的projectId
})

var title= '';
var count = 0;
var url = '';
const db = firebase.firestore();
var collection = 'articles'; //主题配置文件配置的collection //{{ theme.firestore.collection }}';
db.collection(collection).orderBy('count', 'desc').limit(10).get().then((querySnapshot) => {
querySnapshot.forEach((doc) => {
// console.log(doc.id, " => ", doc.data());
title = doc.id;
count = doc.data().count;
url = doc.data().url;
var content="<h5><p>"+"<font color='#1C1C1C'>"+"【文章热度: "+count+" ℃】"+"</font>" + '&emsp;&emsp;' + "<span'><a href='"+url+"'>"+title+"</a></span>"+"</p></h5>";
document.getElementById("top").innerHTML+=content
});
});

</script>

1、引入的 js 版本最好和 firestore.swig 中的保持一致
2、记得把 apiKey 和 projectId 换成你自己的。。
3、orderBy 这是是按 Database 中 articles 集合的 count 值降序排序
4、limit 限制返回的结果集数量
tips:Firebase 官方网站上,可全文搜索想要结果

参考文档: Firebase 读取数据Firebase 对数据进行排序和限制数量github 上 firestore 相关 JS 操作方法

完成以上步骤后,访问热榜界面,就能出现以下界面了:

nOYt6s.png

文章排行有了,但是点击文章要跳转的链接还没有呢。我们也看到了,Firebase 只保存了每个文章访问量 count 值,没有存文章 url 信息。这就是接下来要解决的问题。

保存文章url地址

前文说过,Next 整合了 firestore,那只能实现记录每篇文章浏览量的功能,要做热榜,似乎不满足需求啊,得改代码。

修改 next/layout/_third-party/analytics/firestore.swig,主要修改的地方有三处:

修改文章页判断逻辑

以 firestore.swig 之前的代码来做排行榜功能时,发现不止文章出现在排行榜页面,连左侧点击过的链接,比如分类、标签、归档等等的链接也会显示在排行榜页面。

后来博主经过多次调试、阅读代码后,发现了问题所在,原因就出现在现有的文章页和非文章页判断逻辑上:

nOYamq.png

这个逻辑只能适用于单篇文章的阅读次数统计。。不能满足我们现在的需求了,代码修改如下:

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
  //https://hexo.io/zh-tw/docs/variables.html
var isPost = '{{ page.title }}'.length > 0
var isArchive = '{{ archive }}' === 'true'
var isCategory = '{{ category }}'.length > 0
var isTag = '{{ tag }}'.length > 0

var urlPath = '{{ page.path }}';
var urlFullPath = '{{ page.permalink }}';
var indexPath = 'index.html'; //首页链接
var isMenu = false;
{% for name, path in theme.menu %}
{# 判断当前链接是否是左侧菜单栏链接 #}
var menuLink = '{{ url_for(path.split('||')[0]) | trim }}';
if(urlPath.indexOf(menuLink) > 0 || urlPath == indexPath){
isMenu = true;
}
{% endfor %}

// if (isPost) { //is article page
if (!isMenu) { // 非菜单页、非主页(即在某篇文章链接里)
var title = '{{ page.title }}'
var doc = articles.doc(title)

// getCount(doc, true).then(appendCountTo($('.post-meta')))
getCount(doc, urlFullPath, true).then(appendCountTo($('.post-wordcount')))
}
// else if (!isArchive && !isCategory && !isTag) { //is index page
else if (urlPath == indexPath) { // 主页
var titles = [] //array to titles

修改前后对比如下:

nOY61J.png

注意了:

1
var indexPath = 'index.html'; //首页链接

这是我在站点配置文件中修改后的结果

nOYcc9.png

之前的 permalink: :year/:month/:day/:title/,在做 SEO 优化改成了 permalink: :title.html,所以这里能直接用来做判断了。当然你不改的话,这里的 if 判断就得改成你现在的逻辑。

修改getCount方法

我也尝试过在 articles.doc(title) 中追加设置链接的方法,参考了 Github,比如:

1
articles.doc(title).set({'uri': urlPath, 'url': urlFullPath});

不过这样操作后,在 getCount 方法中就会出现其它的错误,改动比较多,偷懒一下,我就把 url 地址直接以参数的形式传递到 getCount 方法

1
getCount(doc, urlFullPath, true)

这样改动的就相对少了。

nOYgXR.png

这里用了 window.localStorage 来保存整个网站的数据,以 title 为 key,数据没有过期时间,直到手动去删除。

修改文章阅读次数样式(非必须)

最后的修改是为了改下文章阅读次数的样式

firestore.swig 中的 $('.post-meta') 替换成 $('.post-wordcount');将图标 $('<i>').addClass('fa fa-users') 替换成 $('<i>').addClass('fa fa-eye'),效果如下:

nOYf76.png


附上 firestore.swig 完整代码:

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
{% if theme.firestore.enable %}
<script src="https://www.gstatic.com/firebasejs/4.6.0/firebase.js"></script>
<script src="https://www.gstatic.com/firebasejs/4.6.0/firebase-firestore.js"></script>
{% if theme.firestore.bluebird %}
<script src="https://cdnjs.cloudflare.com/ajax/libs/bluebird/3.5.1/bluebird.core.min.js"></script>
{% endif %}
<script>
(function () {

firebase.initializeApp({
apiKey: '{{ theme.firestore.apiKey }}',
projectId: '{{ theme.firestore.projectId }}'
})

function getCount(doc, url, increaseCount) {
//increaseCount will be false when not in article page

return doc.get().then(function (d) {
var count
if (!d.exists) { //has no data, initialize count
if (increaseCount) {
doc.set({
count: 1,
'url': url
})
count = 1
}
else {
count = 0
}
}
else { //has data
count = d.data().count
if (increaseCount) {
if (!(window.localStorage && window.localStorage.getItem(title))) { //if first view this article
doc.set({ //increase count
count: count + 1
})
count++
}
}
}
if (window.localStorage && increaseCount) { //mark as visited
localStorage.setItem(title, true)
}

return count
})
}

function appendCountTo(el) {
return function (count) {
$(el).append(
$('<span>').addClass('post-visitors-count').append(
$('<span>').addClass('post-meta-divider').text('|')
).append(
$('<span>').addClass('post-meta-item-icon').append(
// $('<i>').addClass('fa fa-users')
$('<i>').addClass('fa fa-eye')
)
).append($('<span>').text('{{ __("post.visitors")}} ' + count + ' 次'))
)
}
}

var db = firebase.firestore()
var articles = db.collection('{{ theme.firestore.collection }}')

//https://hexo.io/zh-tw/docs/variables.html
var isPost = '{{ page.title }}'.length > 0
var isArchive = '{{ archive }}' === 'true'
var isCategory = '{{ category }}'.length > 0
var isTag = '{{ tag }}'.length > 0

var urlPath = '{{ page.path }}';
var urlFullPath = '{{ page.permalink }}';
var indexPath = 'index.html'; //首页链接
var isMenu = false;
{% for name, path in theme.menu %}
{# 判断当前链接是否是左侧菜单栏链接 #}
var menuLink = '{{ url_for(path.split('||')[0]) | trim }}';
if(urlPath.indexOf(menuLink) > 0 || urlPath == indexPath){
isMenu = true;
}
{% endfor %}

// if (isPost) { //is article page
if (!isMenu) { // 非菜单页、非主页(即在某篇文章链接里)
var title = '{{ page.title }}'
var doc = articles.doc(title)

// getCount(doc, true).then(appendCountTo($('.post-meta')))
getCount(doc, urlFullPath, true).then(appendCountTo($('.post-wordcount')))
}
// else if (!isArchive && !isCategory && !isTag) { //is index page
else if (urlPath == indexPath) { // 主页
var titles = [] //array to titles

var postsstr = '{% for post in page.posts %}titles.push("{{ post.title }}");{% endfor %}' //if you have a better way to get titles of posts, please change it
eval(postsstr)

var promises = titles.map(function (title) {
return articles.doc(title)
}).map(function (doc) {
return getCount(doc)
})
Promise.all(promises).then(function (counts) {
// var metas = $('.post-meta')
var metas = $('.post-wordcount')
counts.forEach(function (val, idx) {
appendCountTo(metas[idx])(val)
})
})
}
})()
</script>
{% endif %}

结尾

至于热榜页面的样式布局,可在其 index.md 文件中修改即可

经过博主三四天攻坚,以参阅 Google 官方文档为主,至此,文章热榜功能已经全部完成!👏👏👏

最终效果:

nOYOBt.png

基于 Firestore 做的排行榜也有个缺点,对于不能访问谷歌的用户来说,这个页面是不能正常显示的。

虽然 Firebase 具有离线访问数据功能,不过这是针对短期不能联网的情况。

如果需要做到国内用户普遍都能访问,那好像就得依赖于 Leancloud 实现了,不知道大家有什么其他方案,欢迎留言讨论。

(本文完)

点击查看

本文标题:NexT 文章阅读排行(热榜)功能

文章作者:北宸

发布时间:2019年08月16日 - 21:30:23

最后更新:2023年08月19日 - 13:26:00

原始链接:https://www.liaofuzhan.com/posts/781439527.html

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

-------------------本文结束 感谢您的阅读-------------------
🌞