前言
寫部落格也有一段時間了,不知道諸位是不是跟我一樣在多個平台都有同步博文,筆者目前在掘金、csdn和簡書都有在同步文章,這個過程中發現一個問題,簡書官方沒有統計作者所有博文的總閱讀、評論、點贊等資料,隻是給出了每篇文章的對應資料,這對于習慣了在各個平台上檢視資料的筆者來說十分不友好(看着部落格閱讀數上漲是更新的巨大動力),為此筆者決定通過技術手段解決這個問題。
思考解決思路
要解決這個難點,最直覺的思路自然是扒接口,如果官方有暴露對應的api的話,那一切都簡單了。點開浏覽器檢視對應的
xhr
請求:
首屏的請求逐個點開看,發現沒有一個有相關資訊的。接着我們檢視伺服器傳回的HTML檔案:
發現簡書個人中心頁使用了後端渲染,每篇文章的内容都是server直出的,首次傳回的html裡隻有首屏會顯示的文章,後續的文章是怎麼加載的呢?我們滾動scroll,觀察接口:
我們發現在滾動之後,浏覽器會自動請求新的内容,然後添加在之前渲染的内容末尾,每篇文章的相關資料都是由伺服器計算好之後直出的,并沒有暴露出對應的api。看來要解決這個問題隻有通過最
"笨"
但是有效的DOM查詢大法了。
實戰操作
頁面Dom搜尋法
接下來我們通過chrome開發工具,檢視每篇文章的相關元素:
發現表示浏覽數的dom元素機構是固定的,有唯一的類名去修飾其樣式,這就非常友善我們使用
jquery
來擷取元素并讀取其中的内容,簡單來講,統計頁面内所有文章表示浏覽數的dom元素,然後讀取其中的數字求和就可以實作我們的統計目的。評論數和點贊數同理,核心代碼如下:
// 一個映射對象,分别聲明閱讀數、評論數和點贊數的類名關鍵字
const targetMap = {
views: 'read',
comments: 'comments',
likes: 'like'
}
// 計算的通用方法
const compute = function(type) {
const lable = targetMap[type];
let count = 0;
// ic-list-加上lable就是對應的類名 依賴jquery
$(`.ic-list-${lable}`).each(function(key, value) {
// jquery擷取所有目标元素的父元素其中的html内容
const parentNodeHtmlContent = $(this).parent().html();
// 替換掉html内容中我們不感興趣的部分,隻擷取數字并求和
count += parseInt(parentNodeHtmlContent.replace(`<i class="iconfont ic-list-${lable}"></i>`, ''));
});
return count;
}
// 輸出浏覽數,評論數和點贊數方法類似
console.log(compute('views'))
接下來還有一個問題,頁面是懶加載的,如果在部落客的所有博文沒有被加載完全的時候去統計,擷取的資料肯定是不準确的,因為沒有加載出來的内容沒有被統計。我們需要讓頁面自動滾動加載直至加載完所有内容。這個功能如何實作呢? 我們可以通過腳本讓頁面滾動到最底部,觸發頁面加載新的内容,如果此時頁面的總高度和我們滾動前計算的總高度不一緻,表示加載出了新的内容,頁面需要繼續滾動,直至頁面滾動後的高度和滾動前的高度保持一緻(這表示頁面沒有新的内容了),核心代碼如下:
let allFunc = async function() {
// 記錄頁面滾動前的初始位置
const originPositon = window.scrollY;
// 目前頁面高度
let currentDocHeight = 1;
// 滾動後的頁面高度,随便一個初始值,二者不一緻即可,觸發第一次滾動
let newHeight = 0;
const scrollFunc = async() => {
while(currentDocHeight !== newHeight) {
// 更新目前頁面高度
currentDocHeight = $(document).height();
// promise實作異步,要給網絡加載内容的時間
await new Promise((resolve) => {
// 頁面滾動
$(document).scrollTop($(document).height());
// 每次滾動間隔800毫秒,確定内容加載完畢
setTimeout(resolve, 800);
})
// 更新新的頁面高度
newHeight = $(document).height();
}
}
// 不停滾動直至加載完所有内容
await scrollFunc();
// 回到初始位置
$(window).scrollTop(originPositon);
}
api内容搜尋法
通過上述的方法我們實作了頁面資料的統計,但是方法實在笨重,要通過頁面滾動加載完使用者所有的文章之後,再統計頁面的dom,而且頁面滾動時的
setTimeout
時間不好把握,時間過短在低網速情況下可能會導緻頁面沒有加載新的内容後就開始頁面長度比較,導緻滾動操作提前停止,時間過長則會拉長等待時間,體驗也不好。那有沒有更佳的解決方案呢?觀察頁面滾動時的加載流程我們發現,頁面是通過
https://www.jianshu.com/u/xxx?order_by=shared_at&page=數字
這個get請求來拉取新的頁面内容的,那我們直接調用這個api,在傳回的html檔案中查找我們需要的資訊不就可以了?接下來的問題是如何确定已經拉取了所有内容,通過實踐發現,當拉取的頁數超過使用者釋出的所有文章數時,傳回的html将會自動切換到使用者動态頁:
(以筆者的部落格為例,部落格文章一共有三頁,請求到第四頁時,傳回的是
動态
頁的内容)
我們可以通過分析
動态頁
的html特征,确認之前文章清單請求結束。具體代碼如下:
// 簡單封裝的get請求,傳回promise
const getApiPromise = function(url) {
return new Promise((resolve, reject) => {
try {
$.get(url, function(data) {
resolve(data);
})
} catch(e) {
reject(e)
}
})
}
// 擷取頁面請求url
const getUrl = (id, page) => `https://www.jianshu.com/u/${id}?order_by=shared_at&page=${page}`;
// 通過正規表達式和傳回的html,擷取頁面各項資料
const getCount = (originContent, reg) => originContent.toString().match(reg).reduce((oldValue, newVaule) => {
return oldValue + parseInt(newVaule)}, 0)
const countThroughApi = async function() {
// 比對使用者uid
const exec = /[0-9a-z]{12}$/
const userId = window.location.href.match(exec)[0];
if (!userId) {
return 'not Find';
}
let page = 1;
let views = 0, comments = 0, likes = 0;
let res;
// 比對浏覽數的正則
const viewReg = /(?<=<i class="iconfont ic-list-read"><\/i>\s).*(?=(\s)*<\/a>)/g;
// 比對評論數的正則
const commentReg = /(?<=<i class="iconfont ic-list-comments"><\/i>\s).*(?=(\s)*<\/a>)/g;
// 比對點贊數的正則
const likesReg = /(?<=<i class="iconfont ic-list-like"><\/i>\s).*(?=(\s)*<\/span>)/g;
while (true) {
// 請求api
res = await getApiPromise(getUrl(userId, page));
// 通過動态頁中html的特征内容,确認文章頁請求完成,終止循環
if (res.includes('<!-- 發表了文章 -->') || res.includes('<!-- 發表了評論 -->')) {
break;
}
// 分别計算浏覽、評論和點贊數
views += getCount(res, viewReg);
comments += getCount(res, commentReg);
likes += getCount(res, likesReg);
// 更新頁碼
page += 1;
}
const ansString = '總閱讀數:' + views + ' 總評論:' + comments + ' 總點贊: ' + likes;
console.log(ansString);
return ansString;
}
以上是功能實作兩種方法。每次要計算結果的時候如果都把以上腳本通過
injected script
的形式在chrome的dev tool裡執行,過于繁瑣,體驗很差,為此我們需要引入chrome插件。
插件開發
有關插件開發的基礎知識我這裡不再贅述了,有一個大神有非常完備的總結帖,看完之後全網的chrome插件教程除了官方文檔,幾乎都不用看了,牆裂推薦。筆者的代碼倉庫位址會放在文末,這裡筆者隻提及我們要開發的這個插件需要的幾個關鍵點。
manifest.json檔案
{
// ...省略部分内容
"background": {
// 背景js
"scripts": ["background.js"]
},
// 前台執行的js
"content_scripts": [{
// 腳本生效的url,隻有在使用者頁下才可以統計
"matches": [
"http://www.jianshu.com/u/*",
"https://www.jianshu.com/u/*"
],
// 需要加載的js
"js": [
"jquery.js",
"computed.js"
],
// 執行模式,這裡表示頁面加載完成後再加載插件相關代碼
"run_at": "document_idle"
}],
// 權限申請,允許我們添加右鍵菜單頁和控制插件圖示
"permissions": ["contextMenus", "declarativeContent"]
}
插件背景檔案
background.js
:
// 與content_script,即computed.js進行通訊的函數
function sendMessageToContentScript(message, callback)
{
chrome.tabs.query({active: true, currentWindow: true}, function(tabs)
{
chrome.tabs.sendMessage(tabs[0].id, message, function(response)
{
if(callback) callback(response);
});
});
}
// 給頁面建立右鍵菜單
chrome.contextMenus.create({
title: "計算浏覽數-by頁面滾動統計Dom",
// 設定比對的url,在使用者頁下載下傳才建立右鍵菜單
documentUrlPatterns: ['https://www.jianshu.com/u/*'],
onclick: function(){
// 發送通信消息
sendMessageToContentScript({cmd:'dom'}, function(response)
{
// console.log('來自content的回複:'+response);
});
}
});
chrome.contextMenus.create({
title: "計算浏覽數-by請求api",
documentUrlPatterns: ['https://www.jianshu.com/u/*'],
onclick: function(){
sendMessageToContentScript({cmd:'api'}, function(response)
{
// console.log('來自content的回複:'+response);
});
}
});
// 控制插件圖示在特定時刻高亮
chrome.runtime.onInstalled.addListener(function(){
chrome.declarativeContent.onPageChanged.removeRules(undefined, function(){
chrome.declarativeContent.onPageChanged.addRules([
{
conditions: [
// 隻有打開簡書的使用者頁才顯示pageAction
new chrome.declarativeContent.PageStateMatcher({pageUrl: {urlContains: 'www.jianshu.com/u'}})
],
actions: [new chrome.declarativeContent.ShowPageAction()]
}
]);
});
});
接下來我們檢視
content_script
,即
computed.js
的相關内容:
// allFunc和countThroughApi的相關定義同之前的分析,這裡略去
// content_script監聽background.js發過來的通信請求
chrome.runtime.onMessage.addListener(async function(request, sender, sendResponse)
{
sendResponse('');
// 通過兩種不同的而方法統計資料
if (request.cmd === 'dom') {
alert(await allFunc());
} else {
alert(await countThroughApi())
}
});
接下來我們在本地測試一下效果:
可以看到右上角插件圖示亮起,表示可用,右鍵滑鼠,出現兩種計算方法的選項,點選任意一種,開始統計:
項目位址
參考文獻
小茗大神的chrome插件詳細攻略
chrome extension 官方文檔