写一个chrome插件到底有多难?

JavaScript/前端
445
0
0
2023-01-25

灵感来源

这周刚好看到一个大眼的玩具,感觉非常有意思。但是只能放在自己的网页上又感觉缺乏使用场景。因此我想到能把他翻成chrome插件,注入到平常浏览的网页上,这样使用场景就丰富了。

开始翻译

chrome插件使用的还是h5一套,因此改动并不算大。首先将代码全部复制下来,echart的源码也下载下来。然后做一些小改动。

首先考虑到我们要把大眼插入进网页,那么首先我们不需要背景色,也不需要一些通配的样式,避免对网页原本的样式造成破坏。

img

其次是大眼的位置,原本是处于网页居中的位置,在实际浏览中会遮挡住核心的浏览区域,因此我们考虑把大眼移到上方居中。

img

既然把大眼移动到了上方,那么环视四周的动作也应该加上一点俯视的感觉,这边加上了rotateX(-45deg)。

img

相应的监听鼠标相对位置的坐标原点也需要改动一下。

img

最后呢我希望大眼在插入页面后能自动唤醒,不用点击大眼唤醒。改一下也非常的简单。

img

经过这一些细节的改动,大眼本身就改完了。后面我们把他改成chrome插件。

img

chrome插件一共三个文件,manifest.json是插件的配置文件,background.js是chrome全局环境执行的,甚至浏览器关闭了也仍然会在后台执行。content_scripts是每个标签页中会注入执行的。可以看到这边把echarts.min.js也一并注入了。

img

首先是content-script.js,为了避免重复注入,在window里添加了个monitorLoaded的标志位,Init方法中将前面修改完的html和css添加进body中,js部分直接复制即可。

let bigEye;
let eyeball;
let eyeFilter;
let eyeballChart;
let leftRotSize = 0;
let ballSize = 0;
let ballColor = 'transparent'
let rotTimer;
let sleepTimer;
let isSleep = true; // 是否处于休眠状态

if (!window.monitorLoaded) {
    Init();
    window.monitorLoaded = true;
}

function Init() {
    const div = document.createElement("div");
    div.innerHTML = `<div class="eyeSocket eyeSocketSleeping" id='bigEye'>
    <div id="eyeball"></div>
    </div>
    <div class="filter">
    <div class="eyeSocket" id='eyeFilter'>
    </div>
    </div>
    <svg width="0">
    <filter id='filter'>
        <feTurbulence baseFrequency="1">
            <animate id="animate1" attributeName="baseFrequency" dur="1s" from="0.5" to="0.55" begin="0s;animate1.end">
            </animate>
            <animate id="animate2" attributeName="baseFrequency" dur="1s" from="0.55" to="0.5" begin="animate2.end">
            </animate>
        </feTurbulence>
        <feDisplacementMap in="SourceGraphic" scale="50" xChannelSelector="R" yChannelSelector="B" />
    </filter>
    </svg>`;
    document.body.appendChild(div);

    const style = document.createElement('style');
    style.innerHTML = `
        
    body {
        perspective: 1000px;
        --c-eyeSocket: rgb(41, 104, 217);
        --c-eyeSocket-outer: #02ffff;
        --c-eyeSocket-outer-shadow: transparent;
        --c-eyeSocket-inner: rgb(35, 22, 140);
    }
    
    .filter {
        width: 100%;
        height: 100%;
        filter: url('#filter');
    }
    
    .eyeSocket,
    .filter .eyeSocket {
        position: absolute;
        left: calc(50% - 75px);
        top: 17px;
        width: 150px;
        aspect-ratio: 1;
        border-radius: 50%;
        border: 4px solid var(--c-eyeSocket);
        box-shadow: 0px 0px 50px var(--c-eyeSocket-outer-shadow);
        transition: border 0.5s ease-in-out, box-shadow 0.5s ease-in-out;
        z-index: 1000000;
    }
    
    .filter .eyeSocket {
        opacity: 0;
        left: calc(50% - 92px);
        top: 0px;
        transition: all 0.5s ease-in-out;
    }
    
    .eyeSocket::before,
    .eyeSocket::after {
        content: "";
        position: absolute;
        top: 50%;
        left: 50%;
        transform: translate(-50%, -50%);
        border-radius: 50%;
        transition: all 0.5s ease-in-out;
        box-sizing: border-box;
    }
    
    .eyeSocket::before {
        width: calc(100% + 20px);
        height: calc(100% + 20px);
        border: 6px solid var(--c-eyeSocket-outer);
    }
    
    .eyeSocket::after {
        width: 100%;
        height: 100%;
        border: 4px solid var(--c-eyeSocket-inner);
        box-shadow: inset 0px 0px 30px var(--c-eyeSocket-inner);
    }
    
    #eyeball {
        width: 100%;
        height: 100%;
    }
    
    .eyeSocketSleeping {
        animation: sleeping 6s infinite;
    }
    
    .eyeSocketLooking {
        animation: lookAround 2.5s;
    }
    
    @keyframes sleeping {
        0% {
            transform: scale(1);
        }
        50% {
            transform: scale(1.2);
        }
        100% {
            transform: scale(1);
        }
    }
    
    @keyframes lookAround {
        0% {
            transform: translateX(0) rotateY(0);
        }
        10% {
            transform: translateX(0) rotateY(0);
        }
        40% {
            transform: translateX(-70px) rotateX(-45deg) rotateY(-30deg);
        }
        80% {
            transform: translateX(70px) rotateX(-45deg) rotateY(30deg);
        }
        100% {
            transform: translateX(0) rotateY(0);
        }
    }` 
    document.body.appendChild(style);

    bigEye = document.getElementById('bigEye');
    eyeball = document.getElementById('eyeball');
    eyeFilter = document.getElementById('eyeFilter');
    eyeballChart = echarts.init(eyeball);
    setTimeout(() => {
        clickToWeakup();
    }, 2000);
    bigEye.addEventListener('click', () => {
        if (!isSleep) return;
        clickToWeakup();
    })
    bigEye.addEventListener('webkitAnimationEnd', () => {
        new Promise(res => {
            clearInterval(rotTimer);
            rotTimer = setInterval(() => {
                getEyeballChart()
                ballSize > 0 && (ballSize -= 0.5);
                leftRotSize === 360 ? (leftRotSize = 0) : (leftRotSize += 0.1);
                if (ballSize === 0) {
                    clearInterval(rotTimer);

                    res();
                }
            }, 10);
        }).then(() => {
            eyeFilter.style.opacity = '0'
            eyeFilter.className = bigEye.className = 'eyeSocket';
            setNormal();
            document.body.addEventListener('mousemove', focusOnMouse);
            rotTimer = setInterval(() => {
                getEyeballChart()
                ballSize <= 12 && (ballSize += 0.1);
                leftRotSize === 360 ? (leftRotSize = 0) : (leftRotSize += 0.1);
            }, 10);
        })
    })

    chrome.runtime.onMessage.addListener(
        function(request, sender, sendResponse) {
            switch (request.type) {
                case "gethref":
                    sendResponse({ href: window.location.href.toLocaleLowerCase() });
                    break;
                case "setAngry":
                    setAngry();
                    break;
                case "setNormal":
                    setNormal();
                    break;
                default:
                    break;
            }
        });

}

// 画眼球
function getEyeballChart() {
    eyeballChart.setOption({
        series: [{
            type: 'gauge',
            radius: '-20%',
            clockwise: false,
            startAngle: `${0 + leftRotSize * 5}`,
            endAngle: `${270 + leftRotSize * 5}`,
            splitNumber: 3,
            detail: false,
            axisLine: {
                show: false,
            },
            axisTick: false,
            splitLine: {
                show: true,
                length: ballSize,
                lineStyle: {
                    shadowBlur: 20,
                    shadowColor: ballColor,
                    shadowOffsetY: '0',
                    color: ballColor,
                    width: 4,
                }
            },
            axisLabel: false
        }, {
            type: 'gauge',
            radius: '-20%',
            clockwise: false,
            startAngle: `${45 + leftRotSize * 5}`,
            endAngle: `${315 + leftRotSize * 5}`,
            splitNumber: 3,
            detail: false,
            axisLine: {
                show: false,
            },
            axisTick: false,
            splitLine: {
                show: true,
                length: ballSize,
                lineStyle: {
                    shadowBlur: 20,
                    shadowColor: ballColor,
                    shadowOffsetY: '0',
                    color: ballColor,
                    width: 4,
                }
            },
            axisLabel: false
        }]
    })
}

// 休眠
function toSleep() {
    isSleep = true;
    clearInterval(rotTimer);
    rotTimer = setInterval(() => {
        getEyeballChart()
        if (ballSize > 0) {
            ballSize -= 0.1;
        } else {
            bigEye.className = 'eyeSocket eyeSocketSleeping'
        }
        leftRotSize === 360 ? (leftRotSize = 0) : (leftRotSize += 0.1);
    }, 10);
    document.body.removeEventListener('mousemove', focusOnMouse);
    bigEye.style.transform = `rotateY(0deg) rotateX(0deg)`;
    eyeball.style.transform = `translate(0px, 0px)`;
}

// 唤醒
function clickToWeakup() {
    isSleep = false;
    eyeFilter.style.opacity = '1'
    eyeFilter.className = bigEye.className = 'eyeSocket eyeSocketLooking' 
    setAngry();
    clearInterval(rotTimer);
    rotTimer = setInterval(() => {
        getEyeballChart();
        ballSize <= 50 && (ballSize += 1);
        leftRotSize === 360 ? (leftRotSize = 0) : (leftRotSize += 0.5);
    }, 10);
}
// 生气模式
function setAngry() {
    document.body.style.setProperty('--c-eyeSocket', 'rgb(255,187,255)')
    document.body.style.setProperty('--c-eyeSocket-outer', 'rgb(238,85,135)')
    document.body.style.setProperty('--c-eyeSocket-outer-shadow', 'rgb(255, 60, 86)')
    document.body.style.setProperty('--c-eyeSocket-inner', 'rgb(208,14,74)')
    ballColor = 'rgb(208,14,74)';
}
// 常态模式
function setNormal() {
    document.body.style.setProperty('--c-eyeSocket', 'rgb(41, 104, 217)')
    document.body.style.setProperty('--c-eyeSocket-outer', '#02ffff')
    document.body.style.setProperty('--c-eyeSocket-outer-shadow', 'transparent')
    document.body.style.setProperty('--c-eyeSocket-inner', 'rgb(35, 22, 140)')
    ballColor = 'rgb(0,238,255)';
}

// 关注鼠标
function focusOnMouse(e) {
    // 视口尺寸 
    let clientWidth = document.body.clientWidth;
    let clientHeight = document.body.clientHeight;
    // 原点,即bigEye中心位置,页面中心 
    let origin = [clientWidth / 2, 0];
    // 鼠标坐标 
    let mouseCoords = [e.clientX - origin[0], origin[1] - e.clientY];
    let eyeXDeg = mouseCoords[1] / clientHeight * 80;
    let eyeYDeg = mouseCoords[0] / clientWidth * 60;
    bigEye.style.transform = `rotateY(${eyeYDeg}deg) rotateX(${eyeXDeg}deg)`;
    eyeball.style.transform = `translate(${eyeYDeg / 1.5}px, ${-eyeXDeg / 1.5}px)`;
    // 设置休眠 
    if (sleepTimer) clearTimeout(sleepTimer);
    sleepTimer = setTimeout(() => {
        toSleep();
    }, 30000);
}

加入chrome插件间消息的处理,后面会在background中对浏览的页面进行统计,这边先加上了消息处理。可以根据消息返回当前页面url,设置大眼状态。

img

然后是background.js,chrome.runtime.onInstalled是安装插件的时候,chrome.runtime.onStartup是启动浏览器的时候,chrome.action.onClicked是点击扩展里本插件的图标的时候。 chrome.tabs.sendMessage是给标签页发送消息。第一个参数tab.id即标签页的id,来选择发送消息的目标tab页,第二个参数是消息体,第三个参数是回调方法。 chrome.notifications.create是发送桌面通知。

在background中我监听了当前正在浏览的页面,当开始监听时,会弹出问候语。 之后每隔60秒会检测一次正在访问的页面。如果是正在浏览工作的页面,那么情绪值会上升。如果是在看娱乐页面,那么情绪值会下降。当情绪值>80时,会将大眼设置成angry的状态,并且会弹出一些安慰的话语。同样当情绪值恢复到60以下时,会将大眼设置回normal的状态。当关闭浏览器时,会弹出一个总结的通知。

let logintime;
let emotion;
let worknotice;
let Interval;
let openSign = false;

let type = [
    { href: "baidu", type: "work" },
    { href: "juejin", type: "work" },
    { href: "cloud.tencent", type: "work" },
    { href: "192.168", type: "work" },
    { href: "localhost", type: "work" },
    { href: "csdn", type: "work" },
    { href: "bilibili", type: "fun" },
]

chrome.runtime.onInstalled.addListener(() => {
    start();
    monitor();
});

chrome.runtime.onStartup.addListener(() => {
    start();
    monitor();
});

chrome.action.onClicked.addListener(() => {
    start();
    //     chrome.tabs.executeScript({ 
    //         file: 'content-script.js', 
    //     }); 
    //     chrome.tabs.executeScript({ 
    //         file: 'echarts/4.3.0/echarts.min.js', 
    //     });
});

function checkWindow() {
    chrome.windows.getAll({}, function(windows) {
        let windowCount = windows.length;
        if (windowCount == 0) {
            close();
        }
    });
}

function monitor() {
    if (!Interval) {
        Interval = setInterval(async() => {
            let tab = await getActiveTab();
            if (tab != null) {
                chrome.tabs.sendMessage(
                    tab.id, { type: "gethref" },
                    (response) => {
                        if (!response) return;
                        let href = response.href;
                        type.forEach(o => {
                            if (href.indexOf(o.href) >= 0) {
                                if (o.type == "work") {
                                    emotion = emotion + 1;
                                }
                                if (o.type == "fun") {
                                    emotion = emotion - 1;
                                }
                                if (emotion > 100) emotion = 100;
                                if (emotion < 0) emotion = 0;
                                if (emotion >= 80) {
                                    chrome.tabs.sendMessage(tab.id, { type: "setAngry" }, () => {});
                                    if (!worknotice) {
                                        let week = logintime.getDay();
                                        if (week == "0" || week == "6") {
                                            chrome.notifications.create(null, {
                                                type: 'basic',
                                                iconUrl: 'icon.png',
                                                title: "太累了!",
                                                message: "周末还加班!你老板真不是人!",
                                            });
                                        } else {
                                            chrome.notifications.create(null, {
                                                type: 'basic',
                                                iconUrl: 'icon.png',
                                                title: "太累了!",
                                                message: "加班辛苦了!休息一下吧!",
                                            });
                                        }
                                        worknotice = true;
                                    }
                                }
                                if (emotion < 60) {
                                    chrome.tabs.sendMessage(tab.id, { type: "setNormal" }, () => {});
                                }
                            }
                        });
                    });
            } else {
                checkWindow();
            }
        }, 60000)
    }
}

function start() {
    logintime = new Date();
    emotion = 50;
    worknotice = false;
    openSign = true;
    chrome.notifications.create(null, {
        type: 'basic',
        iconUrl: 'icon.png',
        title: gettitle(logintime),
        message: getmessage(logintime),
    });
}

function close() {
    if (openSign) {
        if (emotion < 20) {
            chrome.notifications.create(null, {
                type: 'basic',
                iconUrl: 'icon.png',
                title: "这么早?",
                message: "这么早就不玩啦?不多整两把?",
            });
        }
        if (emotion > 80) {
            chrome.notifications.create(null, {
                type: 'basic',
                iconUrl: 'icon.png',
                title: "太累了!",
                message: "活终于干完了!休息一下吧!",
            });
        }
    }
    openSign = false;
}

function gettitle(date) {
    let hour = date.getHours();
    if (hour < 12 && hour > 5) {
        return "早上好!";
    } else if (hour >= 12 && hour < 14) {
        return "中午好!";
    } else if (hour >= 14 && hour < 18) {
        return "下午好";
    } else if (hour >= 18 && hour < 24) {
        return "下午好";
    } else {
        return "滚去睡觉!";
    }
}

function getmessage(date) {
    let fmtlogintime = dateFormat(date, 'yyyy-MM-dd hh:mm:ss');
    //let week = data.getDay(); 
    let message = `当前时间:${fmtlogintime}`;
    return message;
}

function dateFormat(date, fmt) { // author: meizz 
    const o = {
        'M+': date.getMonth() + 1, // 月份 
        'd+': date.getDate(), // 日 
        'h+': date.getHours(), // 小时 
        'm+': date.getMinutes(), // 分 
        's+': date.getSeconds(), // 秒 
        'q+': Math.floor((date.getMonth() + 3) / 3), // 季度 
        'S': date.getMilliseconds() // 毫秒
    };
    if (/(y+)/.test(fmt)) {
        fmt = fmt.replace(RegExp.$1, (date.getFullYear() + '').substr(4 - RegExp.$1.length));
    }
    for (const k in o) {
        if (new RegExp('(' + k + ')').test(fmt)) {
            fmt = fmt.replace(RegExp.$1, (RegExp.$1.length === 1) ? (o[k]) : (('00' + o[k]).substr(('' + o[k]).length)));
        }
    }
    return fmt;
}


// 获取活跃的 tab,通常是用户正在浏览的页面
async function getActiveTab() {
    return new Promise((resolve) => {
        chrome.tabs.query({
                active: true,
                currentWindow: true,
            },
            (tabs) => {
                if (tabs.length > 0) {
                    resolve(tabs[0]);
                } else {
                    resolve(null);
                }
            }
        );
    });
}

这样一个简单的chrome插件就完成了。还是非常的简单的!

这是我的github地址,需要源码的小伙伴可以直接获取。

https://github.com/4cos90/MonitorChromeEx