Web Worker——js的多线程,实现统计博客园总阅读量

Posted huanzi-qch

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Web Worker——js的多线程,实现统计博客园总阅读量相关的知识,希望对你有一定的参考价值。

  前言

  众所周知,js是单线程的,从上往下,从左往右依次执行,当我们有耗时的任务需要处理时,便会阻塞线程造成页面卡顿等问题。web worker的目的,就是为javascript创造多线程环境,允许主线程将一些任务分配给子线程。在主线程运行的同时,子线程在后台运行,两者互不干扰。等到子线程完成计算任务,再把结果返回给主线程。因此,每一个子线程就好像一个“工人”(worker),默默地完成自己的工作。更多worker的介绍请戳:JavaScript标准参考教程

  本文通过web worker 统计博客园总阅读量,来学习一下worker的使用,前段时间想看一下自己的博客有多少的阅读量,发现博客园好像没有提供这个统计功能,刚好之前有了解到worker,js的多线程,刚好适用于去统计总阅读量,又不影响我页面的渲染,主线程渲染页面,子线程负责循环请求博客园随笔列表进行统计,统计好了再将数据发送到主线程。详细思路如下:

  主线程

  1、先追加一个带id=‘statistical’的span标签,并显示“统计中...”

  2、开启worker子线程开始统计,并且开始监听onmessage事件等待子线程返回数据

  3、onmessage收到子线程返回的数据,更新id=‘statistical’的span标签的text值

  子线程

  1、循环使用XMLHttpRequest对象请求博客园随笔列表,直到最后一页(直到返回的页面没有文章数据)

  2、使用正则处理、匹配数据(每篇文章的阅读量)存入全局变量中,并且判断是否最后一页,以便跳出循环

  3、将收集到的数据进行数据清洗、相加得到总阅读量

  4、将总阅读量推送给主线程,并结束子线程

 

  代码编写

  在开始写主线程之前,我们先实现子线程的任务

 

  子线程

  根据博客园目前的链接规则,访问个人博客主页的地址如下:http://www.cnblogs.com/huanzi-qch/,分页查看随笔列表的地址如下:https://www.cnblogs.com/huanzi-qch/default.html?page=1,并根据响应回来的页面内容格式用正则 /huanzi-qch\\s+阅读[(]+[1-9]\\d+[)]/g 去匹配,当然也可以用 /阅读[(]+[1-9]\\d+[)]/g

 

  2019-08-12补充:最近博客园显示阅读那里发生改动,阅读与(123)中间多了空格,导致我们之前写的正则匹配不到,现在优化一下我们的正则

阅读(\\s*)[(]+[1-9]\\d+[)]

  \\s 表示匹配任意空白字符

  * 表示任意次数

  这样一样,不管中间有没有空格我们都能匹配到

 

  我们对子线程进行如下封装,name值在主线程new Worker的时候构造:

    console.log("我是worker 任务线程 负责统计总阅读量..");
//我的博客园地址名称,要是读取不到this.name的值,默认设置我的博客名称
    var myCnblogsName = this.name ? this.name : "huanzi-qch";

    //监听主线程发送过来的数据
    //this.addEventListener(\'message\', function (e) {
    //  this.postMessage(\'主线程发送过来的数据: \' + e.data);
    //}, false);

    //监听发送报错
    //this.addEventListener(\'messageerror \', function (e) {
    //  this.postMessage(\'发送数据到主线程报错: \' + e.data);
    //}, false);

    //加载其他 JS 脚本。
    //this.importScripts(""):

    //任务线程内部的全局变量数组,用于保存数据
    var statisticsArray = [];

    //发送ajax请求博客园
    function getReadData(page){
        //是否还要继续
        var flag = false;
        
        //使用XMLHttpRequest对象请求博客园 
        var xhr = new XMLHttpRequest();            
        xhr.open(\'GET\', "https://www.cnblogs.com/"+myCnblogsName+"/default.html?page=" + page, false);//同步
        xhr.setRequestHeader("Content-Type", "text/html; charset=utf-8"); //设置响应格式
        xhr.onreadystatechange = function() {
          // readyState == 4说明请求已完成
          if (xhr.readyState == 4 && xhr.status == 200 || xhr.status == 304) { 
            //使用正则处理HTML字符串,需要设置全局标识
                //var myRe = /huanzi-qch(\\s*)阅读(\\s*)[(]+[1-9]\\d+[)]/g;
                var myRe = /阅读(\\s*)[(]+[1-9]\\d+[)]/g;
                var resultArray = xhr.responseText.match(myRe);
                
                //合并到全局变量数组中
                statisticsArray = statisticsArray.concat(resultArray);
                
                //判断这个即可:resultArray.length > 0     如果还有文章集合,则返回true
                if(resultArray && resultArray.length > 0){
                    flag = true;
                }
          }
        };
        xhr.send();
        
        return flag;
    }


    //循环调用getReadData,默认最大页数 100 (100页,每页10条记录,相对于1000篇博客,已经够多了吧?)
    for(var i = 1;i < 100;i++){
        //如果返回false则立即跳出循环
        if(!getReadData(i)){ break;}
    }

    //处理全局数组
    for(var i = 0;i < statisticsArray.length;i++){
        if(statisticsArray[i]){
            //只保留数字部分
            statisticsArray[i] = statisticsArray[i].match(/[1-9]\\d+/)[0];
        }else{
            statisticsArray.splice(i, 1);
        }
    }

    //数组求和,需要返回主线程的最终值
    //向产生这个 Worker 线程发送消息。
    var count = eval(statisticsArray.join("+"));
    this.postMessage(count);
    
    console.log("统计结束,总阅读量为:"+count);

    //关闭 Worker 线程
    this.close();

 

   主线程

   刚开始我是想将子线程单独放在一个js文件里,上传到博客园后台管理的文件里,然后引入创建worker对象,不成想博客园门户地址跟保存用户上传文件的地址不同源,而worker受同源限制,导致无法创建对象

  只能将子线程的代码放在同一个页面了,通过<script id="worker" type="app/worker"></script>包起来,通过读取这个script的内容成Blob二进制对象,然后二进制对象转为URL,再通过这个URL创建worker。

  最后代码如下:

        // 先追加一个显示标签
        $("#profile_block").append("总阅读量:<span id=\'statistical\' style=\'color: #464646;\'>统计中...</span><br/>");
           
        //创建一个Blob,读取同个页面中的script标签
         var blob = new Blob([document.querySelector(\'#worker\').textContent]);

        //这里需要把代码当作二进制对象读取,所以使用Blob接口。然后,这个二进制对象转为URL,再通过这个URL创建worker。
        var url = window.URL.createObjectURL(blob);

        //创建worker对象
        var worker = new Worker(url ,{ name : \'huanzi-qch\'});

        //监听任务线程返回的数据
        worker.onmessage = function (event) {
            //设置总阅读量
            $("#statistical").text(event.data);
        }

        //error 事件的监听函数。
        worker.onerror = function (event) {
          console.log(\'error:\' + event);
        }

        //messageerror 事件的监听函数。发送的数据无法序列化成字符串时,会触发这个事件。
        worker.onmessageerror = function (event) {
          console.log(\'messageerror:\' + event);
        }

        //发送数据到任务线程
        //worker.postMessage(\'Hello World\');

 

  效果演示

  将所有代码都添加到 博客侧边栏公告 并保存

 

  小扩展:既然添加了总阅读量,不如把积分、排名也放一起显示吧!

  先前往 博客设置 --> 选项 勾选上“积分与排名”,然后加入以下js代码

        //隐藏博客园提供的积分与排名标签,并将内容迁移到指定位置
        $("#sidebar_scorerank").hide();
        $("#profile_block").append("积分:<span style=\'color: #464646;\'>"+$("#sidebar_scorerank").find(".liScore").text().match(/[1-9]\\d+/)[0]+"</span><br/>");
        $("#profile_block").append("排名:<span style=\'color: #464646;\'>"+$("#sidebar_scorerank").find(".liRank").text().match(/[1-9]\\d+/)[0]+"</span><br/>");

 

        

 

  总结

  通过这个小例子,我们以后看自己的博客情况也更加方便了,访问有侧边公告栏的页面都会统计总阅读量(不过这样会无形增加博客园服务器的压力 <手动羞涩脸>),并且也充分的感受到了worker的威力,之前js受限于单线程模型,无法充分发挥js的潜力,现在有了worker多线程,我们可以解锁更多姿势了!

  更多对worker的介绍请戳:JavaScript标准参考教程

 

  统计任意博客总阅读量

  我们直接用子线程的代码去统计别人的博客的总阅读量,不需要大幅度改动,直接将myCnblogsName的值改成对应的博客地址名称,我们进行简单封装成一个function,然后跑去博客主页打开F12在控制台运行代码然后调用function即可,简单方便,即开即用

/**
    输入别人的博客园地址名称
*/
function statistical(myCnblogsName){
    console.log("我是worker 任务线程 正在统计 "+myCnblogsName+" 的博客的总阅读量..");

    //任务线程内部的全局变量数组,用于保存数据
    var statisticsArray = [];

    //发送ajax请求博客园
    function getReadData(page){
        //是否还要继续
        var flag = false;

        //使用XMLHttpRequest对象请求博客园
        var xhr = new XMLHttpRequest();            
        xhr.open(\'GET\', "https://www.cnblogs.com/"+myCnblogsName+"/default.html?page=" + page, false);//同步
        xhr.setRequestHeader("Content-Type", "text/html; charset=utf-8"); //设置响应格式
        xhr.onreadystatechange = function() {
          // readyState == 4说明请求已完成
          if (xhr.readyState == 4 && xhr.status == 200 || xhr.status == 304) { 
            //使用正则处理HTML字符串,需要设置全局标识
                //var myRe = /huanzi-qch(\\s*)阅读(\\s*)[(]+[1-9]\\d+[)]/g;
                var myRe = /阅读(\\s*)[(]+[1-9]\\d+[)]/g;
                var resultArray = xhr.responseText.match(myRe);

                //合并到全局变量数组中
                statisticsArray = statisticsArray.concat(resultArray);

                //判断这个即可:resultArray.length > 0     如果还有文章集合,则返回true
                if(resultArray && resultArray.length > 0){
                    flag = true;
                }
          }
        };
        xhr.send();

        return flag;
    }


    //循环调用getReadData,默认最大页数 100 (100页,每页10条记录,相对于1000篇博客,已经够多了吧?)
    for(var i = 1;i < 100;i++){
        //如果返回false则立即跳出循环
        if(!getReadData(i)){ break;}
    }

    //处理全局数组
    for(var i = 0;i < statisticsArray.length;i++){
        if(statisticsArray[i]){
            //只保留数字部分
            statisticsArray[i] = statisticsArray[i].match(/[1-9]\\d+/)[0];
        }else{
            statisticsArray.splice(i, 1);
        }
    }

    //数组求和,需要返回主线程的最终值
    var count = eval(statisticsArray.join("+"));

    console.log("统计结束,总阅读量为:"+count);
}

   比如查询我的博客总阅读量,在控制台执行上面的方法定义后,再执行,so easy

statistical("huanzi-qch");

 

  我们去统计一下推荐博客排行榜中的部分大佬看一看他们的总阅读量是多少

 

  看了一下他们的随笔数量,一个是六百多,一个是一百多,我们定义的循环次数100是够用的,其实改成for(;;)也没有问题,因为我们已经设置了break的条件

    

  然后去他们的博客主页打开控制台,运行代码,然后调用statistical方法

    

 

   不愧是大佬啊,总阅读量一个是七百万,一个是三百万

   

 

  总评论量、总推荐量

  2020-07-16更新

  今天心血来潮,既然已经统计了总阅读量,为什么不连总评论量跟阅读量一起统计呢?说干就干!

  由于评论跟推荐不同,单篇文章数量可以为0,不想阅读量,我自己发布完文章总是要进行看下布局有没有乱,因此单篇阅读量不可能为0,所以正则表达式得换一换

/阅读(\\s*)[(]+[1-9]\\d+[)]/g

  换成

/阅读(\\s*)[(]+\\d+[)]/g

  同理,下面进行数值匹配计算时的正则也要换,换成

/\\d+/

 

  完整代码

<script id="worker" type="app/worker">
/**************** worker 任务线程 负责统计总阅读量 ********************/
    console.log("我是worker 任务线程 负责统计总阅读量..");
    //我的博客园地址名称,要是读取不到this.name,默认是我的博客名称
    var myCnblogsName = this.name ? this.name : "huanzi-qch";

    //监听主线程发送过来的数据
    //this.addEventListener(\'message\', function (e) {
    //  this.postMessage(\'主线程发送过来的数据: \' + e.data);
    //}, false);

    //监听发送报错
    //this.addEventListener(\'messageerror \', function (e) {
    //  this.postMessage(\'发送数据到主线程报错: \' + e.data);
    //}, false);

    //加载其他 JS 脚本。
    //this.importScripts(""):

    //任务线程内部的全局变量数组,用于保存数据 阅读
    var statisticsArray_read = [];
    
    //任务线程内部的全局变量数组,用于保存数据 评论
    var statisticsArray_comment = [];
    
    //任务线程内部的全局变量数组,用于保存数据 推荐
    var statisticsArray_recommend = [];

    //发送ajax请求博客园
    function getReadData(page){
        //是否还要继续
        var flag = false;
        
        //使用XMLHttpRequest对象请求博客园 
        var xhr = new XMLHttpRequest();            
        xhr.open(\'GET\', "https://www.cnblogs.com/"+myCnblogsName+"/default.html?page=" + page, false);//同步
        xhr.setRequestHeader("Content-Type", "text/html; charset=utf-8"); //设置响应格式
        xhr.onreadystatechange = function() {
          // readyState == 4说明请求已完成
          if (xhr.readyState == 4 && xhr.status == 200 || xhr.status == 304) { 
                var text = xhr.responseText;
                //使用正则处理HTML字符串,需要设置全局标识
                var myRe_read = /阅读(\\s*)[(]+\\d+[)]/g;
                var resultArray_read = text.match(myRe_read);
                
                //合并到全局变量数组中
                statisticsArray_read = statisticsArray_read.concat(resultArray_read);
                
                var myRe_comment = /评论(\\s*)[(]+\\d+[)]/g;
                var resultArray_comment = text.match(myRe_comment);
                
                //合并到全局变量数组中
                statisticsArray_comment = statisticsArray_comment.concat(resultArray_comment);
                                
                var myRe_recommend = /推荐(\\s*)[(]+\\d+[)]/g;
                var resultArray_recommend = text.match(myRe_recommend);
                
                //合并到全局变量数组中
                statisticsArray_recommend = statisticsArray_recommend.concat(resultArray_recommend);
                
                
                //判断这个即可:resultArray.length > 0     如果还要文章集合,则返回true
                if(resultArray_read && resultArray_read.length > 0){
                    flag = true;
                }
          }
        };
        xhr.send();
        
        return flag;
    }


    //循环调用getReadData,默认最大页数 100 (100页,每页10条记录,相对于1000篇博客,已经够多了吧?)
    for(var i = 1;i < 100;i++){
        //如果返回false则立即跳出循环
        if(!getReadData(i)){ break;}
    }

    //处理全局数组
    for(var i = 0;i < statisticsArray_read.length;i++){
        if(statisticsArray_read[i]){
            //只保留数字部分
            statisticsArray_read[i] = statisticsArray_read[i].match(/\\d+/)[0];
        }else{
            statisticsArray_read.splice(i, 1);
        }
        
        if(statisticsArray_comment[i]){
            //只保留数字部分
            statisticsArray_comment[i] = statisticsArray_comment[i].match(/\\d+/)[0];
        }else{
            statisticsArray_comment.splice(i, 1);
        }
                
        if(statisticsArray_recommend[i]){
            //只保留数字部分
            statisticsArray_recommend[i] = statisticsArray_recommend[i].match(/\\d+/)[0];
        }else{
            statisticsArray_recommend.splice(i, 1);
        }
    }

    //数组求和,需要返回主线程的最终值
    //向产生这个 Worker 线程发送消息。
    var count_read = eval(statisticsArray_read.join("+"));
    var count_comment = eval(statisticsArray_comment.join("+"));
    var count_recommend = eval(statisticsArray_recommend.join("+"));
    
    console.log("统计结束,总阅读量为:"+count_read);
    console.log("统计结束,总评论量为:"+count_comment);    
    console.log("统计结束,总推荐量为:"+count_recommend);

    this.postMessage("{\\"count_read\\":\\""+count_read+"\\",\\"count_comment\\":\\""+count_comment+"\\",\\"count_recommend\\":\\""+count_recommend+"\\"}");

    //关闭 Worker 线程
    this.close();
</script>

<script>  
    $(function($){
        // title提示
        $("#calendar").css("display","none");
        $(".diggit").attr("title","谢谢点赞~~");
        $(".buryit").attr("title","雅蠛蝶~~");

               $("#div_digg .diggit").click(function(){
                      tip.msg("谢谢点赞~~");
               });
        
        //引入图片
        $("#blog-news").prepend("<img style=\'width: 100px;\' src=\\"https://pic.cnblogs.com/avatar/1353055/20180830212901.png\\">");
        //$("#blog-news").append("<img title=\'一刀流居和·狮子歌歌\' style=\'width: 260px;\'  src=\\"https://files-cdn.cnblogs.com/files/huanzi-qch/20181126141626.bmp\\">");

                //隐藏博客园提供的积分与排名标签,并将内容迁移到指定位置
                $("#sidebar_scorerank").hide();
        $("#profile_block").append("积分:<span style=\'color: #464646;\'>"+$("#sidebar_scorerank").find(".liScore").text().match(/[1-9]\\d+/)[0]+"</span><br/>");
        $("#profile_block").append("排名:<span style=\'color: #464646;\'>"+$("#sidebar_scorerank").find(".liRank").text().match(/[1-9]\\d+/)[0]+"</span><br/>");



        /**************** worker  主线程  ********************/
        // 先追加一个显示标签
        $("#profile_block").append("总阅读量:<span id=\'count_read\' style=\'color: #464646;\'>统计中...</span><br/>");
        $("#profile_block").append("总评论量:<span id=\'count_comment\' style=\'color: #464646;\'>统计中...</span><br/>");
        $("#profile_block").append("总推荐量:<span id=\'count_recommend\' style=\'color: #464646;\'>统计中...</span><br/>");
           
        //创建一个Blob,读取同个页面中的script标签
         var blob = new Blob([document.querySelector(\'#worker\').textContent]);

        //这里需要把代码当作二进制对象读取,所以使用Blob接口。然后,这个二进制对象转为URL,再通过这个URL创建worker。
        var url = window.URL.createObjectURL(blob);

        //创建worker对象
        var worker = new Worker(url ,{ name : \'huanzi-qch\'});

        //监听任务线程返回的数据
        worker.onmessage = function (event) {
            var data = JSON.parse(event.data)
            //设置总阅读量
            $("#count_read").text(data.count_read);
            $("#count_comment").text(data.count_comment);
            $("#count_recommend").text(data.count_recommend);
        }

        //error 事件的监听函数。
        worker.onerror = function (event) {
          console.log(\'error:\' + event);
        }

        //messageerror 事件的监听函数。发送的数据无法序列化成字符串时,会触发这个事件。
        worker.onmessageerror = function (event) {
          console.log(\'messageerror:\' + event);
        }

        //发送数据到任务线程
        //worker.postMessage(\'Hello World\');
        /**************** worker  主线程   end  ********************/

                //github、gitee
                $("#profile_block").append("github:<a href=\\"https://github.com/huanzi-qch\\" target=\\"_blank\\">huanzi-qch</a><br/>");
                $("#profile_block").append("gitee:<a href=\\"https://gitee.com/huanzi-qch\\" target=\\"_blank\\">huanzi-qch</a><br/>");

                //QQ交流群
                $("#profile_block").append("QQ交流群:1015379123<br/>");
    })
</script>
View Code

  放在博客侧边栏公告(支持HTML代码) (支持 JS 代码)

 

 

  效果

 

以上是关于Web Worker——js的多线程,实现统计博客园总阅读量的主要内容,如果未能解决你的问题,请参考以下文章

NodeJs多线程、多进程、定时任务

html5之Web Worker -- js多线程编程

web worker

js中的worker使用及多线程改单线程实现

Web Worker

Celery Worker 中的多线程