前言
眾所周知,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>
放在博客側邊欄公告(支持HTML代碼) (支持 JS 代碼)
效果