chrome插件實現簡書自動計算閱讀評論點贊數

前言

寫博客也有一段時間了,不知道諸位是否是跟我同樣在多個平臺都有同步博文,筆者目前在掘金、csdn和簡書都有在同步文章,這個過程當中發現一個問題,簡書官方沒有統計做者全部博文的總閱讀、評論、點贊等數據,只是給出了每篇文章的對應數據,這對於習慣了在各個平臺上查看數據的筆者來講十分不友好(看着博客閱讀數上漲是更新的巨大動力),爲此筆者決定經過技術手段解決這個問題。html

思考解決思路

要解決這個難點,最直觀的思路天然是扒接口,若是官方有暴露對應的api的話,那一切都簡單了。點開瀏覽器查看對應的xhr請求: jquery


首屏的請求逐個點開看,發現沒有一個有相關信息的。接着咱們查看服務器返回的HTML文件:

發現簡書我的中心頁使用了後端渲染,每篇文章的內容都是server直出的,首次返回的html裏只有首屏會顯示的文章,後續的文章是怎麼加載的呢?咱們滾動scroll,觀察接口:

咱們發如今滾動以後,瀏覽器會自動請求新的內容,而後添加在以前渲染的內容末尾,每篇文章的相關數據都是由服務器計算好以後直出的,並無暴露出對應的api。看來要解決這個問題只有經過最 "笨"可是有效的DOM查詢大法了。

實戰操做

頁面Dom搜尋法

接下來咱們經過chrome開發工具,查看每篇文章的相關元素: git

發現表示瀏覽數的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'))
複製代碼

接下來還有一個問題,頁面是懶加載的,若是在博主的全部博文沒有被加載徹底的時候去統計,獲取的數據確定是不許確的,由於沒有加載出來的內容沒有被統計。咱們須要讓頁面自動滾動加載直至加載完全部內容。這個功能如何實現呢? 咱們能夠經過腳本讓頁面滾動到最底部,觸發頁面加載新的內容,若是此時頁面的總高度和咱們滾動前計算的總高度不一致,表示加載出了新的內容,頁面須要繼續滾動,直至頁面滾動後的高度和滾動前的高度保持一致(這表示頁面沒有新的內容了),核心代碼以下:github

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

插件開發

有關插件開發的基礎知識我這裏再也不贅述了,有一個大神有很是完備的總結帖,看完以後全網的chrome插件教程除了官方文檔,幾乎都不用看了,牆裂推薦。筆者的代碼倉庫地址會放在文末,這裏筆者只說起咱們要開發的這個插件須要的幾個關鍵點。json

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的相關內容:api

// 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())
    }
});
複製代碼

接下來咱們在本地測試一下效果: promise

能夠看到右上角插件圖標亮起,表示可用,右鍵鼠標,出現兩種計算方法的選項,點擊任意一種,開始統計:

項目地址

參考文獻

小茗大神的chrome插件詳細攻略
chrome extension 官方文檔

相關文章
相關標籤/搜索