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 官方文档

相关文章
相关标签/搜索