從頭開始寫一個 Chrome 插件

插件功能

  • 平時對我來說最浪費時間的莫過于刷「知乎」,但是手賤停不下來呀。Chrome 上面裝了個 StayFocusd,設定十分鐘之后就屏蔽 zhihu 域名。但有時候是真的要上知乎查些東西,每次還得關掉,可一關掉就會「忘記」打開。
  • 我希望能有個人能每隔5分鐘就提醒我一次,你今天刷了5分鐘了,你今天刷了10分鐘了,你今天刷了15分鐘了。。。講真,我覺得這種提醒既溫和又有效,當然,也可以變得很不溫和,比如把提醒直接發給老板。
  • 本文代碼已經放在 github 上了:dingding_robort/chrome_extension。

文件結構

  • manifest.json(插件注冊 metadata)
  • bg.js(主程序)
  • jquery-3.2.1.min.js(發送 ajax 請求用)

manifest.json

{
  "name": "block zhihu",
  "version": "0.1",
  "description": "Notify dingding every 5 minuntes for browsing zhihu",
  "permissions": [
    "tabs",
    "storage",
    "alarms",
    "idle",
    "https://oapi.dingtalk.com/"
  ],
  "background": {
    "scripts": ["jquery-3.2.1.min.js","bg.js"]
  },
  "manifest_version": 2
}
  • manifest.json:是 chrome 插件必須的一個說明文件,命名也不用改。
  • name、version、description,這些可以隨便寫。
  • "manifest_version": 2 這個也不用改,是 chrome 的 manifest 文件格式版本。
  • permissions:chrome 插件要調用 chrome 的接口,就需要在這里聲明權限,tabs、alarms、idle 都是 chrome API。storage 是 localStorage 存儲。下面那個釘釘地址是跨域請求權限。
  • background:這個列表里面的腳本會在后臺運行。要知道后臺運行的腳本和網頁本身的腳本并不在一個進程里,所以直接打開網頁審查是看不到這個后臺腳本的,如果要調試插件程序的話,要去 extension - inspect views 里面找。如果需要跟網頁腳本本身做交互的話,需要增加 content_scripts ??項。

bg.js

  • 要寫這個程序首先需要掌握一些概念:

    • JavaScript:chrome 的插件是由 JavaScript 寫的。
    • tabs 行為:這里會用到 chrome tab 的更新(onUpdate)和激活(onActivated)兩個行為。也就是 tab 里刷新頁面和點擊某個 tab。
    • windows 行為:這個程序會用到 windows 焦點狀態改變(onFocusChanged)行為。也就是當我切換程序,比如把 chrome 切到后臺,把微信切到前臺這樣的行為。
    • idle 行為:這個程序會用到 idle 的狀態變換(onStateChanged)行為,電腦休眠之類的狀態。
    • alarms:定時觸發某項任務。
    • ajax 請求:發送 get、post 等請求,這里是為了給發送消息給釘釘機器人。
    • localStorge:chrome 的本地儲存,可以看做為一個有鍵值對的字典,值只有 string 一種形式。
  • 程序邏輯結構:

    1. 判斷我是不是在去刷知乎了:當一個標簽頁刷新了 zhihu.com 域名(tab.onUpdate),或者我點到了開著 zhihu.com 的標簽頁(tab.onActivated),就說明我開始刷知乎了,計時開始。
    2. 判讀我刷了多久:當我從 zhihu.com 域名離到別的域名時(tab.onUpdate),或者當我去到別的 tab(tab.onActivated),或者干脆我焦點不在 chrome 上了(windows.onFocusChanged)或者甚至休眠、關機(idle.onStateChanged)算作計時終止。
    3. 發送釘釘請求:如果刷超過一定時間了,直接讓釘釘機器人釘你一下。或者也可以簡單的使用 alert 在 chrome 上面彈窗。
  • 代碼實現:全局常量和初始化本地存儲

    // 這里其實可以增加更多的域名,比如 youtube.com 、weibo.com之類的,畢竟能刷的又不止知乎。
    var track_sites = ["zhihu.com"]
    
    // 時間按東八區的時間來算,主要是為了在每天零點清空數據用的。
    var GMT = +8
    var MINUTE_PER_DAY = 1440
    
    // 每刷幾分鐘就給出提醒,這里是每 5 分鐘就提醒一次。
    var TIMEPACE = 5 * 60
    
    //發送釘釘機器人的鏈接
    var NOTIFY_URL = "https://oapi.dingtalk.com/robot/send?access_token="
        + "c6d5a2936381dfc29394f3c336bea5fad962d90ffd31809e92d95xxxxxxx"
    var MOBILE_NUMBER = "176xxxxx619"
    
    initLocalStorage();
    
    function initLocalStorage(){
        //初始化 localStorage
        localStorage.clear();
        localStorage["is_idle"] = "false";
        localStorage["last_site"] = "null";
        localStorage["last_time"] = timeNow();
        localStorage["total_elapsed_time"] = 0;
        localStorage["next_alarm_time"] = TIMEPACE;
        //以每個域名為 key 的每個域名訪問了多少時間
        //雖然邏輯上并不需要用這個字典,但將來可以擴展成特定的網站每天或者每周給予特定的訪問時長。
        for (var i in track_sites){
            localStorage[track_sites[i]] = JSON.stringify({"elapsed_time": 0});
        }
    }
    
    function timeNow(){
        // 返回當前時間戳
        return Math.round(Date.now()/1000) + GMT * 3600;
    }
    
    • track_sites:這里其實可以增加更多的域名,比如 youtube.com 、weibo.com之類的,畢竟能刷的又不止知乎。
    • GMT:時間按東八區的時間來算,主要是為了在每天零點清空數據用的。
    • MINUTE_PER_DAY:每天有 1440 分鐘,不解釋。
    • TIMEPACE:每刷幾分鐘就給出提醒,這里是每 5 分鐘就提醒一次。
    • NOTIFY_URLMOBILE_NUMBER:發送釘釘機器人的鏈接,為什么要用釘釘機器人: http://www.lxweimin.com/p/418e4ffbb4e3
    • 強迫癥問為什么為什么track_sites這個是小寫,其他都是大寫?因為(忘記改了,劃掉)這個以后做個接口在前臺手動增加域名的話,會是個變量。
    • initLocalStorage():清空本地存儲,然后增加一些變量,比如 is_idle 電腦是不是在休眠,last_site 上一個訪問的站點,total_elapsed_time 總共浪費了多少時間,next_alarm_time 刷到這個時間點就提醒,然后就是以每個域名為 key 的每個域名訪問了多少時間的表,雖然邏輯上并不需要用這個字典,但將來可以擴展成特定的網站每天或者每周給予特定的訪問時長。
    • timeNow():獲取當前時間戳。
  • 代碼實現:事件監聽

    function classifyDomin(domain){
        // 檢查域名是不是在黑名單里面
        var in_list = false;
        for (var i in track_sites){
            if(domain.match(track_sites[i])){
                addTimeDelta(track_sites[i]);
                in_list = true;
                break
            }
        }
        // 不在黑名單里面的域名作為 null 處理
        if(in_list == false){
            addTimeDelta("null");
        }
    }
    
    function getCurrentTabDomin(){
        // 獲取當前活躍 tab 的域名
        chrome.tabs.query({active: true, lastFocusedWindow: true}, function(tabs){
            if (tabs.length == 1){
                var url = new URL(tabs[0].url);
                var domain = url.hostname;
                classifyDomin(domain);
            } else if (tabs.length == 0){
                addTimeDelta("null");
            } else {
                console.log("奇怪,找到不止一個 tabs active?");
                console.log(tabs);
            }
        })
    }
    
    chrome.tabs.onUpdated.addListener(getCurrentTabDomin)
    chrome.tabs.onActivated.addListener(getCurrentTabDomin)
    chrome.windows.onFocusChanged.addListener(getCurrentTabDomin)
    chrome.idle.onStateChanged.addListener(function(idleState){
        if (idleState == "active"){
            // is_idle 狀態記錄是為了下面每分鐘定時 check 事件,如果 idle 了,就不再 check
            localStorage["is_idle"] = false;
            getCurrentTabDomin();
        }else{
            localStorage["is_idle"] = true;
            addTimeDelta("null");
        }
    })
    
    • checkCurrentTab(),獲取當前活躍的 tab 的域名,然后去 updateDomin() 去確認這個域名是不是在黑名單里面,然后再去 addTimeDelta()「更新瀏覽時間」,注:代碼中 addTimeDelta() 會在下文實現。
    • tab Update,Activated,還有 windows focus Changed,把checkCurrentTab() 函數綁到這三個事件上。
    • idle.onStateChanged 從電腦休眠中蘇醒和恢復時,記錄一下 idle 狀態,同時如果是從休眠到 active 的狀態,等同于 windows focus Changed 事件。
  • 代碼實現:更新瀏覽時間

    function updateLocalStorageTime(){
        // 更新 localStorage 里的訪問時間
        var domain = localStorage["last_site"];
        var site_log = JSON.parse(localStorage[domain]);
        timedelta = timeNow() - parseInt(localStorage["last_time"]);
        site_log["elapsed_time"] = parseInt(site_log["elapsed_time"]) + timedelta;
        console.log(domain, "elapsed_time: ", site_log["elapsed_time"]);
        localStorage[domain] = JSON.stringify(site_log);
        localStorage["total_elapsed_time"] = parseInt(localStorage["total_elapsed_time"]) + timedelta;
        if(parseInt(localStorage["total_elapsed_time"]) > parseInt(localStorage["next_alarm_time"])){
            fireNotification();
        }
        localStorage["last_time"] = timeNow();
    }
    
    function isElapsedTime(domain){
        // 判斷剛剛過去的時間段是不是在刷知乎
        if(localStorage["last_site"] == "null" && domain != "null"){
            localStorage["last_site"] = domain;
            localStorage["last_time"] = timeNow();
        }else if(localStorage["last_site"] != "null"){
            updateLocalStorageTime();
            localStorage["last_site"] = domain;
        }
    }
    
    • isElapsedTime() 根據上一次事件時正在訪問的站點域名,來判斷上一段時間是不是在刷知乎。
    • updateLocalStorageTime() 更新一下 localStorage 的各站點訪問時間。
  • 代碼實現:定時程序

    function minLeftMidnight(){
        // 距離 0 點還剩下多少分鐘,每日清空定時任務 init 時要用
        return MINUTE_PER_DAY - Math.round(timeNow()/60) % MINUTE_PER_DAY
    }
    
    chrome.alarms.create("mignight_clear",
            {delayInMinutes: minLeftMidnight(), periodInMinutes: MINUTE_PER_DAY});
    // 每天零點清空 localStorage
    chrome.alarms.create("minute_check", {periodInMinutes: 1})
    // 每分鐘檢查一下正在瀏覽的網站,超時發送提醒
    chrome.alarms.onAlarm.addListener(function(alarm){
        if (alarm.name == "mignight_clear"){
            console.log("clear localStorage");
            initLocalStorage();
        }else if (alarm.name == "minute_check"){
            if(localStorage["is_idle"] == true){
                console.log("minute_check");
                getCurrentTabDomin();
            }
        }
    })
    
    • 每天零點觸發清空 localStorage 程序
    • 除了特定事件發生會觸發檢查當前 tab domin 以外,每分鐘也觸發一次。
  • 代碼實現:發送提醒

    function notifyDingding(msg){
        // 發送 msg 到釘釘提醒
        $.ajax({
            type: "POST",
            beforeSend: function(request) {
                request.setRequestHeader("Content-Type",
                    "application/json; charset=utf-8");
            },
            url: NOTIFY_URL,
            data: JSON.stringify({
                "msgtype": "text",
                "text": {
                    "content": msg
                },
                "at": {
                    "atMobiles": [MOBILE_NUMBER]
                }
            }),
            success: function(return_msg){
                console.log(return_msg);
            }
        });
    }
    
    function fireNotification(){
        // 拼接 msg,彈窗并請求釘釘
        let elapsed_time = parseInt(localStorage["next_alarm_time"]) / 60
        msg = "今天你已經刷了" + elapsed_time + "分鐘知乎了。" 
        console.log(msg);
        alert(msg);
        notifyDingding(msg);
        localStorage["next_alarm_time"] = parseInt(localStorage["next_alarm_time"]) + TIMEPACE;
    }
    
    • 釘釘模塊跟 python 的差不多,不過是用 ajax 發送的。
    • fireNotification 會直接在瀏覽器上彈窗提醒,不用釘釘一樣能收到。
  • 全部代碼可以去 github 看到:

  • 為了看一下效果,我特地刷了半小時的知乎(捂臉):

WX20170823-151825.png
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,527評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,687評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,640評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,957評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,682評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,011評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,009評論 3 449
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,183評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,714評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,435評論 3 359
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,665評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,148評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,838評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,251評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,588評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,379評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,627評論 2 380

推薦閱讀更多精彩內容

  • 轉載自http://blog.csdn.net/qq295445028/article/details/79930...
    WebSSO閱讀 2,967評論 0 3
  • 架構 總括:Manifest:程序清單Background:插件運行環境/主程序Pop up:彈出頁面Conten...
    程序員小逗逼閱讀 10,389評論 2 18
  • 大家都知道七巧板用很多圖形(例如:等腰直角三角形,平行四邊形,正方形)做成的,因為上課時老師講完了三角形如何...
    top丶浩鍋閱讀 1,821評論 2 8
  • 雨后初晴,城市的風暖暖地拂過,像極了去年在珠海那個潮濕海風吹過的夜晚。 我和丹丹過完關,踏入陌生的...
    藍曦玉閱讀 285評論 0 0
  • 憑欄倚戶春風渡, 庭院小樓桃花開。 春色滿園春常在, 紅杏出墻山門外。 微雨細潤庭院處, 百花斗艷爭雨露。 曉看紅...
    星辰溥天閱讀 452評論 0 2