從頭開(kāi)始寫(xiě)一個(gè) Chrome 插件

插件功能

  • 平時(shí)對(duì)我來(lái)說(shuō)最浪費(fèi)時(shí)間的莫過(guò)于刷「知乎」撩穿,但是手賤停不下來(lái)呀界阁。Chrome 上面裝了個(gè) StayFocusd令漂,設(shè)定十分鐘之后就屏蔽 zhihu 域名疮跑。但有時(shí)候是真的要上知乎查些東西桑谍,每次還得關(guān)掉,可一關(guān)掉就會(huì)「忘記」打開(kāi)祸挪。
  • 我希望能有個(gè)人能每隔5分鐘就提醒我一次锣披,你今天刷了5分鐘了,你今天刷了10分鐘了贿条,你今天刷了15分鐘了雹仿。。整以。講真胧辽,我覺(jué)得這種提醒既溫和又有效,當(dāng)然公黑,也可以變得很不溫和邑商,比如把提醒直接發(fā)給老板。
  • 本文代碼已經(jīng)放在 github 上了:dingding_robort/chrome_extension凡蚜。

文件結(jié)構(gòu)

  • manifest.json(插件注冊(cè) metadata)
  • bg.js(主程序)
  • jquery-3.2.1.min.js(發(fā)送 ajax 請(qǐng)求用)

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 插件必須的一個(gè)說(shuō)明文件人断,命名也不用改。
  • name朝蜘、version恶迈、description,這些可以隨便寫(xiě)谱醇。
  • "manifest_version": 2 這個(gè)也不用改暇仲,是 chrome 的 manifest 文件格式版本。
  • permissions:chrome 插件要調(diào)用 chrome 的接口副渴,就需要在這里聲明權(quán)限奈附,tabs、alarms煮剧、idle 都是 chrome API斥滤。storage 是 localStorage 存儲(chǔ)将鸵。下面那個(gè)釘釘?shù)刂肥强缬蛘?qǐng)求權(quán)限。
  • background:這個(gè)列表里面的腳本會(huì)在后臺(tái)運(yùn)行中跌。要知道后臺(tái)運(yùn)行的腳本和網(wǎng)頁(yè)本身的腳本并不在一個(gè)進(jìn)程里咨堤,所以直接打開(kāi)網(wǎng)頁(yè)審查是看不到這個(gè)后臺(tái)腳本的,如果要調(diào)試插件程序的話漩符,要去 extension - inspect views 里面找一喘。如果需要跟網(wǎng)頁(yè)腳本本身做交互的話,需要增加 content_scripts ??項(xiàng)嗜暴。

bg.js

  • 要寫(xiě)這個(gè)程序首先需要掌握一些概念:

    • JavaScript:chrome 的插件是由 JavaScript 寫(xiě)的凸克。
    • tabs 行為:這里會(huì)用到 chrome tab 的更新(onUpdate)和激活(onActivated)兩個(gè)行為。也就是 tab 里刷新頁(yè)面和點(diǎn)擊某個(gè) tab闷沥。
    • windows 行為:這個(gè)程序會(huì)用到 windows 焦點(diǎn)狀態(tài)改變(onFocusChanged)行為萎战。也就是當(dāng)我切換程序,比如把 chrome 切到后臺(tái)舆逃,把微信切到前臺(tái)這樣的行為蚂维。
    • idle 行為:這個(gè)程序會(huì)用到 idle 的狀態(tài)變換(onStateChanged)行為,電腦休眠之類(lèi)的狀態(tài)路狮。
    • alarms:定時(shí)觸發(fā)某項(xiàng)任務(wù)虫啥。
    • ajax 請(qǐng)求:發(fā)送 get、post 等請(qǐng)求奄妨,這里是為了給發(fā)送消息給釘釘機(jī)器人涂籽。
    • localStorge:chrome 的本地儲(chǔ)存,可以看做為一個(gè)有鍵值對(duì)的字典砸抛,值只有 string 一種形式评雌。
  • 程序邏輯結(jié)構(gòu):

    1. 判斷我是不是在去刷知乎了:當(dāng)一個(gè)標(biāo)簽頁(yè)刷新了 zhihu.com 域名(tab.onUpdate),或者我點(diǎn)到了開(kāi)著 zhihu.com 的標(biāo)簽頁(yè)(tab.onActivated)直焙,就說(shuō)明我開(kāi)始刷知乎了景东,計(jì)時(shí)開(kāi)始。
    2. 判讀我刷了多久:當(dāng)我從 zhihu.com 域名離到別的域名時(shí)(tab.onUpdate)箕般,或者當(dāng)我去到別的 tab(tab.onActivated)耐薯,或者干脆我焦點(diǎn)不在 chrome 上了(windows.onFocusChanged)或者甚至休眠、關(guān)機(jī)(idle.onStateChanged)算作計(jì)時(shí)終止丝里。
    3. 發(fā)送釘釘請(qǐng)求:如果刷超過(guò)一定時(shí)間了,直接讓釘釘機(jī)器人釘你一下体谒”郏或者也可以簡(jiǎn)單的使用 alert 在 chrome 上面彈窗。
  • 代碼實(shí)現(xiàn):全局常量和初始化本地存儲(chǔ)

    // 這里其實(shí)可以增加更多的域名抒痒,比如 youtube.com 幌绍、weibo.com之類(lèi)的,畢竟能刷的又不止知乎。
    var track_sites = ["zhihu.com"]
    
    // 時(shí)間按東八區(qū)的時(shí)間來(lái)算傀广,主要是為了在每天零點(diǎn)清空數(shù)據(jù)用的颁独。
    var GMT = +8
    var MINUTE_PER_DAY = 1440
    
    // 每刷幾分鐘就給出提醒,這里是每 5 分鐘就提醒一次伪冰。
    var TIMEPACE = 5 * 60
    
    //發(fā)送釘釘機(jī)器人的鏈接
    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;
        //以每個(gè)域名為 key 的每個(gè)域名訪問(wèn)了多少時(shí)間
        //雖然邏輯上并不需要用這個(gè)字典誓酒,但將來(lái)可以擴(kuò)展成特定的網(wǎng)站每天或者每周給予特定的訪問(wèn)時(shí)長(zhǎng)。
        for (var i in track_sites){
            localStorage[track_sites[i]] = JSON.stringify({"elapsed_time": 0});
        }
    }
    
    function timeNow(){
        // 返回當(dāng)前時(shí)間戳
        return Math.round(Date.now()/1000) + GMT * 3600;
    }
    
    • track_sites:這里其實(shí)可以增加更多的域名贮聂,比如 youtube.com 靠柑、weibo.com之類(lèi)的,畢竟能刷的又不止知乎吓懈。
    • GMT:時(shí)間按東八區(qū)的時(shí)間來(lái)算歼冰,主要是為了在每天零點(diǎn)清空數(shù)據(jù)用的。
    • MINUTE_PER_DAY:每天有 1440 分鐘耻警,不解釋隔嫡。
    • TIMEPACE:每刷幾分鐘就給出提醒,這里是每 5 分鐘就提醒一次甘穿。
    • NOTIFY_URL腮恩、MOBILE_NUMBER:發(fā)送釘釘機(jī)器人的鏈接,為什么要用釘釘機(jī)器人: http://www.reibang.com/p/418e4ffbb4e3
    • 強(qiáng)迫癥問(wèn)為什么為什么track_sites這個(gè)是小寫(xiě)扒磁,其他都是大寫(xiě)庆揪?因?yàn)椋ㄍ浉牧耍瑒澋簦┻@個(gè)以后做個(gè)接口在前臺(tái)手動(dòng)增加域名的話妨托,會(huì)是個(gè)變量缸榛。
    • initLocalStorage():清空本地存儲(chǔ),然后增加一些變量兰伤,比如 is_idle 電腦是不是在休眠内颗,last_site 上一個(gè)訪問(wèn)的站點(diǎn),total_elapsed_time 總共浪費(fèi)了多少時(shí)間敦腔,next_alarm_time 刷到這個(gè)時(shí)間點(diǎn)就提醒均澳,然后就是以每個(gè)域名為 key 的每個(gè)域名訪問(wèn)了多少時(shí)間的表,雖然邏輯上并不需要用這個(gè)字典符衔,但將來(lái)可以擴(kuò)展成特定的網(wǎng)站每天或者每周給予特定的訪問(wèn)時(shí)長(zhǎng)找前。
    • timeNow():獲取當(dāng)前時(shí)間戳。
  • 代碼實(shí)現(xiàn):事件監(jiān)聽(tīng)

    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(){
        // 獲取當(dāng)前活躍 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("奇怪判族,找到不止一個(gè) 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 狀態(tài)記錄是為了下面每分鐘定時(shí) check 事件躺盛,如果 idle 了,就不再 check
            localStorage["is_idle"] = false;
            getCurrentTabDomin();
        }else{
            localStorage["is_idle"] = true;
            addTimeDelta("null");
        }
    })
    
    • checkCurrentTab()形帮,獲取當(dāng)前活躍的 tab 的域名槽惫,然后去 updateDomin() 去確認(rèn)這個(gè)域名是不是在黑名單里面周叮,然后再去 addTimeDelta()「更新瀏覽時(shí)間」,注:代碼中 addTimeDelta() 會(huì)在下文實(shí)現(xiàn)界斜。
    • tab Update仿耽,Activated,還有 windows focus Changed各薇,把checkCurrentTab() 函數(shù)綁到這三個(gè)事件上项贺。
    • idle.onStateChanged 從電腦休眠中蘇醒和恢復(fù)時(shí),記錄一下 idle 狀態(tài)得糜,同時(shí)如果是從休眠到 active 的狀態(tài)敬扛,等同于 windows focus Changed 事件。
  • 代碼實(shí)現(xiàn):更新瀏覽時(shí)間

    function updateLocalStorageTime(){
        // 更新 localStorage 里的訪問(wèn)時(shí)間
        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){
        // 判斷剛剛過(guò)去的時(shí)間段是不是在刷知乎
        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() 根據(jù)上一次事件時(shí)正在訪問(wèn)的站點(diǎn)域名朝抖,來(lái)判斷上一段時(shí)間是不是在刷知乎啥箭。
    • updateLocalStorageTime() 更新一下 localStorage 的各站點(diǎn)訪問(wèn)時(shí)間。
  • 代碼實(shí)現(xiàn):定時(shí)程序

    function minLeftMidnight(){
        // 距離 0 點(diǎn)還剩下多少分鐘治宣,每日清空定時(shí)任務(wù) init 時(shí)要用
        return MINUTE_PER_DAY - Math.round(timeNow()/60) % MINUTE_PER_DAY
    }
    
    chrome.alarms.create("mignight_clear",
            {delayInMinutes: minLeftMidnight(), periodInMinutes: MINUTE_PER_DAY});
    // 每天零點(diǎn)清空 localStorage
    chrome.alarms.create("minute_check", {periodInMinutes: 1})
    // 每分鐘檢查一下正在瀏覽的網(wǎng)站急侥,超時(shí)發(fā)送提醒
    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();
            }
        }
    })
    
    • 每天零點(diǎn)觸發(fā)清空 localStorage 程序
    • 除了特定事件發(fā)生會(huì)觸發(fā)檢查當(dāng)前 tab domin 以外,每分鐘也觸發(fā)一次侮邀。
  • 代碼實(shí)現(xiàn):發(fā)送提醒

    function notifyDingding(msg){
        // 發(fā)送 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坏怪,彈窗并請(qǐng)求釘釘
        let elapsed_time = parseInt(localStorage["next_alarm_time"]) / 60
        msg = "今天你已經(jīng)刷了" + elapsed_time + "分鐘知乎了。" 
        console.log(msg);
        alert(msg);
        notifyDingding(msg);
        localStorage["next_alarm_time"] = parseInt(localStorage["next_alarm_time"]) + TIMEPACE;
    }
    
    • 釘釘模塊跟 python 的差不多绊茧,不過(guò)是用 ajax 發(fā)送的铝宵。
    • fireNotification 會(huì)直接在瀏覽器上彈窗提醒,不用釘釘一樣能收到华畏。
  • 全部代碼可以去 github 看到:

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

WX20170823-151825.png
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市亡笑,隨后出現(xiàn)的幾起案子侣夷,更是在濱河造成了極大的恐慌,老刑警劉巖仑乌,帶你破解...
    沈念sama閱讀 217,084評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件百拓,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡晰甚,警方通過(guò)查閱死者的電腦和手機(jī)衙传,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,623評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)厕九,“玉大人粪牲,你說(shuō)我怎么就攤上這事≈蛊剩” “怎么了腺阳?”我有些...
    開(kāi)封第一講書(shū)人閱讀 163,450評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)穿香。 經(jīng)常有香客問(wèn)我亭引,道長(zhǎng),這世上最難降的妖魔是什么皮获? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,322評(píng)論 1 293
  • 正文 為了忘掉前任焙蚓,我火速辦了婚禮,結(jié)果婚禮上洒宝,老公的妹妹穿的比我還像新娘购公。我一直安慰自己,他們只是感情好雁歌,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,370評(píng)論 6 390
  • 文/花漫 我一把揭開(kāi)白布宏浩。 她就那樣靜靜地躺著,像睡著了一般靠瞎。 火紅的嫁衣襯著肌膚如雪比庄。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,274評(píng)論 1 300
  • 那天乏盐,我揣著相機(jī)與錄音佳窑,去河邊找鬼。 笑死父能,一個(gè)胖子當(dāng)著我的面吹牛神凑,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播何吝,決...
    沈念sama閱讀 40,126評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼溉委,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了岔霸?” 一聲冷哼從身側(cè)響起薛躬,我...
    開(kāi)封第一講書(shū)人閱讀 38,980評(píng)論 0 275
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎呆细,沒(méi)想到半個(gè)月后型宝,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,414評(píng)論 1 313
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡絮爷,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,599評(píng)論 3 334
  • 正文 我和宋清朗相戀三年趴酣,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片坑夯。...
    茶點(diǎn)故事閱讀 39,773評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡岖寞,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出柜蜈,到底是詐尸還是另有隱情仗谆,我是刑警寧澤指巡,帶...
    沈念sama閱讀 35,470評(píng)論 5 344
  • 正文 年R本政府宣布,位于F島的核電站隶垮,受9級(jí)特大地震影響藻雪,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜狸吞,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,080評(píng)論 3 327
  • 文/蒙蒙 一勉耀、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧蹋偏,春花似錦便斥、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,713評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至字逗,卻和暖如春京郑,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背葫掉。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,852評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工些举, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人俭厚。 一個(gè)月前我還...
    沈念sama閱讀 47,865評(píng)論 2 370
  • 正文 我出身青樓户魏,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親挪挤。 傳聞我的和親對(duì)象是個(gè)殘疾皇子叼丑,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,689評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容

  • 轉(zhuǎn)載自http://blog.csdn.net/qq295445028/article/details/79930...
    WebSSO閱讀 2,926評(píng)論 0 3
  • 架構(gòu) 總括:Manifest:程序清單Background:插件運(yùn)行環(huán)境/主程序Pop up:彈出頁(yè)面Conten...
    程序員小逗逼閱讀 10,350評(píng)論 2 18
  • 大家都知道七巧板用很多圖形(例如:等腰直角三角形,平行四邊形扛门,正方形)做成的鸠信,因?yàn)樯险n時(shí)老師講完了三角形如何...
    top丶浩鍋閱讀 1,799評(píng)論 2 8
  • 雨后初晴,城市的風(fēng)暖暖地拂過(guò)论寨,像極了去年在珠海那個(gè)潮濕海風(fēng)吹過(guò)的夜晚星立。 我和丹丹過(guò)完關(guān),踏入陌生的...
    藍(lán)曦玉閱讀 282評(píng)論 0 0
  • 憑欄倚戶春風(fēng)渡葬凳, 庭院小樓桃花開(kāi)绰垂。 春色滿園春常在, 紅杏出墻山門(mén)外火焰。 微雨細(xì)潤(rùn)庭院處劲装, 百花斗艷爭(zhēng)雨露。 曉看紅...
    星辰溥天閱讀 431評(píng)論 0 2