前言
想掌握網(wǎng)站文章歡迎度、閱讀排行?那么「熱榜」功能就不可或缺
在寫(xiě)這篇文章以前,本人也了解過(guò)其他博客的做法,無(wú)外乎是基于Leancloud實(shí)現(xiàn)的。很少有用谷歌Firestore(Firebase)來(lái)做的,這篇文章咱就來(lái)說(shuō)說(shuō)Firestore。
PS:Leancloud現(xiàn)在需要上傳
手持身份證照片進(jìn)行實(shí)名。。。
主要是本人比較排斥這樣搜刮隱私的行為??,So經(jīng)過(guò)幾次嘗(tou)試(ji)無(wú)果后,果斷放棄。如果你也有遇到這種情況,希望此文能對(duì)你有所幫助。
關(guān)于Leancloud的相關(guān)實(shí)現(xiàn),可以參考這篇文章,精簡(jiǎn)實(shí)用,本文也在此基礎(chǔ)上作了參考。
正文
/themes/next/_config.yml文件,已經(jīng)提前預(yù)配了firestore:

是不是已經(jīng)給你提示了呢?而且next官方文檔也提到過(guò),一共分為兩步:
1、注冊(cè)

2、配置

Firebase屬于谷歌的,確保你能訪問(wèn):https://console.firebase.google.com ? (tips:github)
步驟
注冊(cè)
可以直接使用Google 帳號(hào)登錄,沒(méi)有的自行進(jìn)行注冊(cè),此步驟略過(guò)。。。
登錄成功后,點(diǎn)擊網(wǎng)頁(yè)右上角:轉(zhuǎn)到控制臺(tái)
添加項(xiàng)目
輸入項(xiàng)目名稱(chēng),比如我用的是leafjame2019814, 點(diǎn)擊繼續(xù),繼續(xù),用默認(rèn)的配置即可,完成后還可以再修改。

創(chuàng)建項(xiàng)目完成后,點(diǎn)擊網(wǎng)頁(yè)左側(cè)setttings按鈕,如圖:

即可查看到自己的項(xiàng)目ID和網(wǎng)絡(luò) API 密鑰,這就是next主題配置文件中提到的projectId和apiKey

創(chuàng)建數(shù)據(jù)庫(kù)
創(chuàng)建完項(xiàng)目后,接著需要?jiǎng)?chuàng)建存儲(chǔ)數(shù)據(jù)的地方,如下圖所示:

有兩種選項(xiàng),自己用的話,選擇以測(cè)試模式開(kāi)始,就可以。

如果選擇
以鎖定模式開(kāi)始,則后續(xù)自己通過(guò)js api的方式寫(xiě)入會(huì)提示沒(méi)有權(quán)限。。(可能需要配置權(quán)限什么的吧,我自己也沒(méi)搞懂)
下一步設(shè)置Cloud Firestore位置,有一下好幾種,分為多域性和區(qū)域性。具體區(qū)別可查看文檔,默認(rèn)選擇nam5 (us-central)即可,點(diǎn)擊完成,等待分配Database。

完成后顯示是這樣的,現(xiàn)在還沒(méi)有數(shù)據(jù)。
可參考Cloud Firestore 使用入門(mén),文檔還是很全的
配置文章閱讀量
做完上邊的操作后,接著就是把代碼集成到自己的項(xiàng)目中了。
在/themes/next/layout/_third-party/analytics/firestore.swig中,其實(shí)已經(jīng)實(shí)現(xiàn)了文章閱讀量的功能,只需要經(jīng)過(guò)本文以上的操作,然后在/themes/next/_config.yml文件,啟用firestore:

apiKey和projectId就是上文中創(chuàng)建的。collection:集合ID,下邊會(huì)講到。
這個(gè)功能經(jīng)過(guò)配置后,可用于文章的閱讀次數(shù)了,當(dāng)文章未被查看時(shí),效果是這樣的:

此時(shí)的Firebase中的Database下也沒(méi)有數(shù)據(jù),當(dāng)瀏覽這篇文章后,
閱讀次數(shù)加1了。如下圖:

再打開(kāi)Firebase,你會(huì)發(fā)現(xiàn),也有數(shù)據(jù)了:

articles就是上邊配置的collection的值,即:集合ID
這控制臺(tái)能對(duì)集合、文檔、字段、值進(jìn)行操作,比如設(shè)置閱讀量啦什么的。。??
設(shè)置開(kāi)發(fā)環(huán)境
每個(gè)頁(yè)面的閱讀量有了,不過(guò)我們要做的是排行榜啊,得把所有的頁(yè)面匯總排序。而且剛才的Database里存的數(shù)據(jù)也沒(méi)有URL等等這些信息呀,接下來(lái)要寫(xiě)代碼了。。。
新增如下的頁(yè)面——「熱榜」,我想大家都會(huì)了吧,不細(xì)說(shuō)了。

在這個(gè)index.md文件中,引入要用到的Firebase代碼,可參照官方文檔進(jìn)行。
之前我想直接在此md文件中引入
firestore.swig,但啟動(dòng)一直報(bào)錯(cuò)。。(我也是新手??????,應(yīng)該是沒(méi)配置對(duì),知道答案的麻煩請(qǐng)留言告知下~)
報(bào)錯(cuò)截圖如下:

多次改路徑,嘗試無(wú)果后。。。我覺(jué)定在md文件中再次引入依賴(lài)的js代碼。。
index.md完整代碼如下:
---
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>" + '  ' + "<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限制返回的結(jié)果集數(shù)量
tips:Firebase官方網(wǎng)站上,可全文搜索想要結(jié)果(都是谷歌網(wǎng)站的js包,必須能訪問(wèn)外網(wǎng)。。)
參考文檔: Firebase讀取數(shù)據(jù)、 Firebase對(duì)數(shù)據(jù)進(jìn)行排序和限制數(shù)量、 github上firestore相關(guān)JS操作方法
完成以上步驟后,訪問(wèn)熱榜界面,就能出現(xiàn)以下界面了:

文章排行有了,但是點(diǎn)擊文章要跳轉(zhuǎn)的鏈接還沒(méi)有呢。我們也看到了,F(xiàn)irebase只保存了每個(gè)文章訪問(wèn)量count值,沒(méi)有存文章的url信息。這就是接下來(lái)要解決的問(wèn)題。。。
保存文章url地址
前文說(shuō)過(guò),Next為我們整合了firestore,那只能實(shí)現(xiàn)記錄每篇文章瀏覽量的功能,要做熱榜,似乎不滿足需求啊,這就要我們改代碼了。。??
修改/themes/next/layout/_third-party/analytics/firestore.swig,主要修改的地方有三處:
修改文章頁(yè)判斷邏輯
以firestore.swig之前的代碼來(lái)做排行榜功能時(shí),發(fā)現(xiàn)不止文章出現(xiàn)在排行榜頁(yè)面,連左側(cè)點(diǎn)擊過(guò)的鏈接,比如分類(lèi)、標(biāo)簽、歸檔等等的鏈接也會(huì)顯示在排行榜頁(yè)面。。。
后來(lái)博主經(jīng)過(guò)多次調(diào)試、閱讀代碼后,發(fā)現(xiàn)了問(wèn)題所在,原因就出現(xiàn)在現(xiàn)有的文章頁(yè)和非文章頁(yè)判斷邏輯上:

這個(gè)邏輯只能適用于單篇文章的閱讀次數(shù)統(tǒng)計(jì)。。不能滿足我們現(xiàn)在的需求了,代碼修改如下:
//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'; //首頁(yè)鏈接
var isMenu = false;
{% for name, path in theme.menu %}
{# 判斷當(dāng)前鏈接是否是左側(cè)菜單欄鏈接 #}
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) { // 非菜單頁(yè)、非主頁(yè)(即在某篇文章鏈接里)
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) { // 主頁(yè)
var titles = [] //array to titles
修改前后對(duì)比如下:

注意了:
var indexPath = 'index.html'; //首頁(yè)鏈接
這是我在站點(diǎn)配置文件中修改后的結(jié)果

之前的:permalink: :year/:month/:day/:title/,在做SEO優(yōu)化改成了:permalink: :title.html,所以這里能直接用來(lái)做判斷了。
當(dāng)然你不改的話,這里的if判斷就得改成你現(xiàn)在的邏輯。
修改getCount方法
我也嘗試過(guò)在articles.doc(title)中追加設(shè)置鏈接的方法,參考了Github,比如:
articles.doc(title).set({'uri': urlPath, 'url': urlFullPath});
不過(guò)這樣操作后,在getCount方法中就會(huì)出現(xiàn)其它的錯(cuò)誤,改動(dòng)比較多。
偷懶一下,我就把url地址直接以參數(shù)的形式傳遞到getCount方法
getCount(doc, urlFullPath, true)
這樣改動(dòng)的就相對(duì)少了。。。??

這里用了
window.localStorage來(lái)保存整個(gè)網(wǎng)站的數(shù)據(jù),以title為key,保存的數(shù)據(jù)沒(méi)有過(guò)期時(shí)間,直到手動(dòng)去刪除。
修改文章閱讀次數(shù)樣式(非必須)
最后的修改是為了改下文章閱讀次數(shù)的樣式
將firestore.swig中的$('.post-meta')替換成$('.post-wordcount');
將圖標(biāo)$('<i>').addClass('fa fa-users')替換成$('<i>').addClass('fa fa-eye'),效果如下:

附上firestore.swig完整代碼:
{% 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'; //首頁(yè)鏈接
var isMenu = false;
{% for name, path in theme.menu %}
{# 判斷當(dāng)前鏈接是否是左側(cè)菜單欄鏈接 #}
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) { // 非菜單頁(yè)、非主頁(yè)(即在某篇文章鏈接里)
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) { // 主頁(yè)
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 %}
結(jié)尾
至于熱榜頁(yè)面的樣式布局,可在其index.md文件中修改即可
經(jīng)過(guò)博主三四天攻堅(jiān),以參閱Google官方文檔為主,至此,文章熱榜功能已經(jīng)全部完成。。。??????
最終效果:

基于Firestore做的排行榜也有個(gè)缺點(diǎn),那就是對(duì)于不能訪問(wèn)谷歌的用戶來(lái)說(shuō),這個(gè)頁(yè)面是不能正常顯示的。。。
雖然Firebase具有離線訪問(wèn)數(shù)據(jù)功能,不過(guò)這是針對(duì)短期不能聯(lián)網(wǎng)的情況。。??????
如果需要做到國(guó)內(nèi)用戶普遍都能訪問(wèn),那好像就得依賴(lài)于Leancloud實(shí)現(xiàn)了,不知道大家有什么其他方案,歡迎留言討論。
(本文完)