1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        15分鐘手摸手教你寫個(gè)可以操控 Chrome 的插件

        共 22935字,需瀏覽 46分鐘

         ·

        2021-07-21 11:56


        故事背景

        事情是這樣的呢

        友人 A: 能不能幫我整一個(gè) chrome 插件?

        我: 啥插件?

        友人 A: 通過(guò)后端服務(wù)或者 python 腳本通信 chrome 插件能夠操作瀏覽器

        我: 你小子是想爬數(shù)據(jù)吧?直接用現(xiàn)成的 python 框架或者 谷歌的 puppeteer 就能操控瀏覽器吧

        友人 A: 你說(shuō)的路子我早就試過(guò)了,對(duì)于反爬檢測(cè)高的網(wǎng)站一下就能檢測(cè)你的無(wú)頭瀏覽器的相應(yīng)特征,所以就用平時(shí)用的瀏覽器就能以真亂真

        我: 老是整這些花里胡哨的,有啥用呀

        友人 A: 10 斤小龍蝦!

        我:成交!!!

        整體的思路

        根據(jù)朋友以上的要求,我們可以簡(jiǎn)單的得出一下的通信流程:

        flow.png

        具體有疑問(wèn)沒(méi)關(guān)系,我們只要知道大體的流程是這樣通信的即可

        github 地址 每個(gè) commit 對(duì)應(yīng)相應(yīng)的步驟

        第一步 創(chuàng)建一個(gè) chrome 插件

        我們首先來(lái)創(chuàng)建一個(gè)啥功能都沒(méi)有的 chrome 插件

        目錄如下所示

        1.png

        manifest.json

        // manifest.json
        {
            "manifest_version"2// 配置文件的版本
            "name""SocketEXController"// 插件的名稱
            "version""1.0.0"// 插件的版本
            "description""Chrome SocketEXController",// 插件描述
            "author""wjryours"// 作者
            "icons": {
                "48""icon.png",// 對(duì)應(yīng)尺寸的圖標(biāo)路徑 我這邊全部用一個(gè)了
                "128""icon.png"
            },
            "browser_action": {
                "default_icon""icon.png"// 圖標(biāo)
                "default_popup""popup.html" // 點(diǎn)擊右上角的圖標(biāo)的 popup 浮層 html 文件
            },
            "background": {
                // 會(huì)一直常駐的后臺(tái) JS 或后臺(tái)頁(yè)面
                // 2 種指定方式,如果指定 JS,那么會(huì)自動(dòng)生成一個(gè)背景頁(yè)
                "page""background.html"
            },
            "content_scripts": [
                {
                    // 允許哪些域名下加載 注入的 JS
                    // "matches": ["http://*/*", "https://*/*"],
                    // "<all_urls>" 表示匹配所有地址
                    "matches": [
                        "<all_urls>"
                    ],
                    "js": [
                        "content-script.js"
                    ],
                    "run_at""document_start"
                }
            ],
            "permissions": [
                "contextMenus"// 右鍵菜單
                "tabs"// 標(biāo)簽
                "notifications"// 通知
                "webRequest"// web 請(qǐng)求
                "webRequestBlocking"// 阻塞式 web 請(qǐng)求
                "storage"// 插件本地存儲(chǔ)
                "http://*/*"// 可以通過(guò) executeScript 或者 insertCSS 訪問(wèn)的網(wǎng)站
                "https://*/*" // 可以通過(guò) executeScript 或者 insertCSS 訪問(wèn)的網(wǎng)站
            ],
        }

        js

        // background.js
        console.log('background.js')

        // popup.js
        console.log('popup.js')

        // content-script.js
        console.log('content-script.js loaded')

        html

        <!-- popup -->
        <!DOCTYPE html>
        <html lang="en">
        <head>
            <meta charset="UTF-8">
            <meta http-equiv="X-UA-Compatible" content="IE=edge">
            <meta name="viewport" content="width=device-width, initial-scale=1.0">
            <title>SocketController Popup</title>
            <link rel="stylesheet" href="./lib/css/popup.css">
            <script src="./popup.js"></script>
        </head>
        <body>
            popup
        </body>
        </html>
        <!-- background -->
        <!DOCTYPE html>
        <html lang="en">

        <head>
            <meta charset="UTF-8">
            <meta http-equiv="X-UA-Compatible" content="IE=edge">
            <meta name="viewport" content="width=device-width, initial-scale=1.0">
            <title>SocketController</title>
        </head>

        <body>
            <div class="bg-container">
                bg-container
            </div>
        </body>

        </html>

        然后在 chrome 的擴(kuò)展程序頁(yè)加載我們的文件目錄 即可

        2.png

        然后我們啟用插件 隨手打開(kāi)一個(gè)頁(yè)面就發(fā)現(xiàn)我們的插件已經(jīng)生效了

        3.png
        4.png

        第二步 在本地創(chuàng)建 websocket 的服務(wù)

        正如上面的通信流程所示,我們還需要在本地創(chuàng)建一個(gè)可用的 websocket 來(lái)發(fā)送信息給 chrome 插件

        為了方便起見(jiàn),我這邊就用 node 的 express 以及 socket.io 這個(gè)庫(kù)來(lái)啟用

        目錄結(jié)構(gòu)和代碼都很簡(jiǎn)單

        5.png
        // index.js  用來(lái)創(chuàng)建 node 服務(wù)
        const express = require('express')
        const app = express()
        const http = require('http')
        const server = http.createServer(app)
        const { Server } = require("socket.io")
        const io = new Server(server)

        app.get('/', (req, res) => {
            res.sendFile(__dirname + '/index.html')
        })

        io.on('connection', (socket) => {
            console.log('a user connected')
            socket.on('disconnect', () => {
                console.log('user disconnected');
            });
            socket.on('webviewEvent', (msg) => {
                console.log('webviewEvent: ' + msg);
                io.emit('webviewEvent', msg);
                // socket.broadcast.emit('chat message', msg);
            });
            socket.on('webviewEventCallback', (msg) => {
                console.log('webviewEventCallback: ' + msg);
                io.emit('webviewEventCallback', msg);
            });
        })


        server.listen(9527, () => {
            console.log('listening on 9527')
        })
        <!-- index.html --> 
        <!-- 點(diǎn)擊事件傳遞的參數(shù)后續(xù)會(huì)用到,這里可以不去了解 -->
        <!DOCTYPE html>
        <html>

        <head>
          <title>Socket.IO Page</title>
          <style>
        </head>

        <body>
          <input id="SendInput" autocomplete="off" />
          <button id="SendInputevent">Send input event</button>
          <button id="SendClickevent">Send click event</button>
          <button id="SendGetTextevent">Send getText event</button>
        </body>
        <script src="/socket.io/socket.io.js"></script>
        <script>
          var socket = io();

          var form = document.getElementById('form');
          var input = document.getElementById('input');

          document.getElementById('SendClickevent').addEventListener('click'function (e{
            socket.emit('webviewEvent', { event'click'params: { delay300 }, element'#su'operateTabIndex0 });
          })
          document.getElementById('SendInputevent').addEventListener('click'function (e{
            const value = document.getElementById('SendInput').value
            socket.emit('webviewEvent', { event'input'params: { inputValue: value }, element'#kw'operateTabIndex0 });
          })
          document.getElementById('SendGetTextevent').addEventListener('click'function (e{
            socket.emit('webviewEvent', { event'getElementText'params: {}, element'.result.c-container.new-pmd .t a'operateTabIndex0 });
          })

          socket.on('webviewEventCallback', (msg) => {
            console.log(msg)
          })
        </script>

        </html>
        // package.json
        {
          "name""socket-service",
          "version""1.0.0",
          "description""",
          "main""index.js",
          "scripts": {
            "test""echo \"Error: no test specified\" && exit 1",
            "dev""nodemon index.js"
          },
          "author""",
          "license""ISC",
          "dependencies": {
            "express""^4.17.1",
            "nodemon""^2.0.7",
            "socket.io""^4.1.2"
          }
        }

        具體的內(nèi)容也很簡(jiǎn)單,就是使用 express 和 socket.io 創(chuàng)建了一個(gè) node 服務(wù)支持長(zhǎng)鏈接,對(duì)于 socket.io 想有更多的了解的可以參照 官方文檔

        運(yùn)行 npm run dev 即可

        好的,這樣我們的服務(wù)就跑起來(lái)了

        6.png

        我們?cè)L問(wèn) http://localhost:9527

        并點(diǎn)擊頁(yè)面上的按鈕在命令行上有 log 輸出就說(shuō)明連接成功啦!

        7.png

        第三步 開(kāi)始使 chrome 插件 與 本地的 node 服務(wù)相互通信

        在開(kāi)始與 node 服務(wù)通信前我們要了解下 chrome 插件的幾種 js 的使用場(chǎng)景

        content-scripts

        這個(gè)主要功能就是 Chrome 插件中向頁(yè)面注入腳本 在第一步的操作中正是該文件在別的頁(yè)面控制臺(tái)中打印出了我們期望的 log content-scripts 和 原始頁(yè)面共享 DOM,但是不共享 JS 但是這個(gè)功能足以讓我們?nèi)ゲ僮髂繕?biāo)頁(yè)面了

        background.js

        是一個(gè)常駐的頁(yè)面,它的生命周期是插件中所有類型頁(yè)面中最長(zhǎng)的,它隨著瀏覽器的打開(kāi)而打開(kāi), 隨著瀏覽器的關(guān)閉而關(guān)閉,所以通常把需要一直運(yùn)行的、啟動(dòng)就運(yùn)行的、全局的代碼放在 background 里面

        popup.js

        這個(gè)就是點(diǎn)擊瀏覽器右上角的插件圖標(biāo)展示的彈窗,生命周期很短,可以將臨時(shí)的交互寫在這里

        對(duì)于我們這次要長(zhǎng)時(shí)間駐存在瀏覽器后臺(tái)與服務(wù)通信的要求得出 我們將相應(yīng)的寫在 background.js 中即可

        我們這里將需要的 js 庫(kù) 和 background.js 引入到 background.html 中

        <script src="./lib/js/lodash.min.js"></script>
        <script src="./lib/js/socket.io.min.js"></script>
        <script src="./background.js"></script>

        我們可以使用兩種方式來(lái)調(diào)試 這個(gè)常駐后臺(tái)文件

        1.直接在 chrome 拓展點(diǎn)擊對(duì)應(yīng)按鈕即可彈出調(diào)試

        8.png
        9.png

        2.直接在瀏覽器上輸入對(duì)應(yīng)的地址 即可

        chrome-extension://${extensionID}/background.html

        每次更新代碼點(diǎn)擊按鈕刷新即可

        為了調(diào)試方便起見(jiàn)我在 popup.js 中加入了以下代碼 每次點(diǎn)擊我們的插件圖標(biāo)即可新開(kāi)一個(gè)后臺(tái)頁(yè)面

        const extensionId = chrome.runtime.id
        const backgroundURL = `chrome-extension://${extensionId}/background.html`
        window.open(backgroundURL)

        現(xiàn)在我們只需要在 background.js 中編寫相應(yīng)代碼,建立長(zhǎng)鏈接就可以了

        // background.js
        class BackgroundService {
            constructor() {
                this.socketIoURL = 'http://localhost:9527'
                this.socketInstance = {}
                this.socketRetryMax = 5
                this.socketRetry = 0
            }
            init() {
                console.log('background.js')   
                this.connectSocket()
                this.linstenSocketEvent()
            }
            setSocketURL(url) {
                this.socketIoURL = url
            }
            connectSocket() {
                if (!_.isEmpty(this.socketInstance) && _.isFunction(this.socketInstance.disconnect)) {
                    this.socketInstance.disconnect()
                }
                this.socketInstance = io(this.socketIoURL);
                this.socketRetry = 0
                this.socketInstance.on('connect_error', (e) => {
                    console.log('connect_error', e)
                    this.socketRetry++
                    if (this.socketRetryMax < this.socketRetry) {
                        this.socketInstance.close()
                        alert(`以嘗試連接${this.socketRetryMax}次,無(wú)法連接到 socket 服務(wù),請(qǐng)排查服務(wù)是否可用`)
                    }
                })
            }
            linstenSocketEvent() {
                if (!_.isEmpty(this.socketInstance) && _.isFunction(this.socketInstance.on)) {
                    this.socketInstance.on('webviewEvent', (msg) => {
                        console.log(`webviewEvent msg`, msg)
                    });
                }
            }
        }
        const app = new BackgroundService()
        app.init()

        刷新插件,打開(kāi)插件后臺(tái)頁(yè)面 就可以看見(jiàn)鏈接建立成功,然后從 node 服務(wù)發(fā)送 msg 給 chrome 插件,我們就可以看到信息被成功接收了

        (tips:之前的 node 服務(wù)別忘記啟動(dòng))

        10.png

        第四步 開(kāi)始使 chrome 插件 background.js 與 content-script.js 建立通信

        這一步也是相當(dāng)簡(jiǎn)單,chrome 官方的文檔也有很多介紹 我這邊就寫下實(shí)現(xiàn)步驟

        // 修改 background.js 為如下代碼
        static emitMessageToSocketService(socketInstance, params = {}) {
            if (!_.isEmpty(socketInstance) && _.isFunction(socketInstance.emit)) {
                console.log(params)
                // 將從 content-script.js 接收到的 msg 發(fā)送到 node 服務(wù)
                socketInstance.emit('webviewEventCallback', params);
            }
        }
        linstenSocketEvent() {
            if (!_.isEmpty(this.socketInstance) && _.isFunction(this.socketInstance.on)) {
                this.socketInstance.on('webviewEvent', (msg) => {
                    console.log(`webviewEvent msg`, msg)
                    // 將從 node 服務(wù)接收到的 msg 發(fā)送到 content-script.js
                    this.sendMessageToContentScript(msg, BackgroundService.emitMessageToSocketService)
                });
            }
        }
        sendMessageToContentScript(message, callback) {
            const operateTabIndex = message.operateTabIndex ? message.operateTabIndex : 0
            console.log(message)
            chrome.tabs.query({ index: operateTabIndex }, (tabs) => { // 獲取 索引的方式獲取對(duì)應(yīng) tabs 實(shí)例以及 id
                chrome.tabs.sendMessage(tabs[0].id, message, (response) => { // 發(fā)送消息到對(duì)應(yīng) tab
                    console.log(callback)
                    if (callback) callback(this.socketInstance, response)
                });
            });
        }
        // content-script.js

        chrome.runtime.onMessage.addListener(function (request, sender, sendResponse{
            console.log(request, sender, sendResponse)
            sendResponse(res)
        });

        然后我們這邊將插件重新加載后關(guān)閉瀏覽器重新打開(kāi)新瀏覽器,將需要測(cè)試的頁(yè)面放置在第一個(gè), 然后在我們的 localhost:9527 發(fā)送信息 這是我們就能在我們預(yù)期的頁(yè)面接收到對(duì)應(yīng)參數(shù)了

        11.png

        這時(shí)你可能會(huì)看到 2 條 log,其實(shí)這個(gè)是正?,F(xiàn)象, 因?yàn)槿绻闶峭ㄟ^(guò)打開(kāi)了 chrome-extension://xxx/background.html 直接打開(kāi)后臺(tái)頁(yè) 運(yùn)行一個(gè)后臺(tái)線程 但是真正在后臺(tái)常駐的還有一個(gè)線程 所以相當(dāng)是 2 個(gè)后臺(tái)接收到了 socket 消息所以就發(fā)送 2 次 msg

        第五步 嘗試操控瀏覽器做對(duì)應(yīng)操作

        好的,朋友們,我們終于來(lái)到了最后一步了

        我們現(xiàn)在已經(jīng)建立起了這 3 個(gè)模塊間的聯(lián)系了 現(xiàn)在無(wú)非就是要將從后端發(fā)送的消息通過(guò)一些判斷做一些 js 操作

        我們就來(lái)完成一個(gè)簡(jiǎn)單的任務(wù),打開(kāi)百度頁(yè)面,搜索關(guān)鍵字,并將搜索到的各個(gè) title 獲取

        我這邊為了做演示方便點(diǎn)就直接引入了 jq 來(lái)操作 dom 在 js 文件夾下創(chuàng)建 operate.js 以及 jquery.min.js

        // 在 manifest.json 中加入 相應(yīng) js
        "content_scripts": [
            {
                "matches": [
                    "<all_urls>"
                ],
                "js": [
                    "lib/js/jquery.min.js",
                    "lib/js/operate.js",
                    "content-script.js"
                ],
                "run_at""document_start"
            }
        ]

        operate.js 主要用來(lái)定義一些操作

        根據(jù)我們上面的小任務(wù),我這邊現(xiàn)在這里面加幾個(gè)簡(jiǎn)單的事件定義,后續(xù)可以支持?jǐn)U展

        // operate.js
        const operateTypeMap = {
            CLICK'click',
            INPUT'input',
            GETELEMENTTEXT'getElementText'
        }

        class OperateConstant {
            static operateByEventType(type, payload = {}) {
                let res
                switch (type) {
                    case operateTypeMap.CLICK:
                        res = OperateConstant.handleClickEvent(payload)
                        break;
                    case operateTypeMap.INPUT:
                        res = OperateConstant.handleInputEvent(payload)
                        break;
                    case operateTypeMap.GETELEMENTTEXT:
                        res = OperateConstant.handleGetElementTextEvent(payload)
                        break;
                    default:
                        break;
                }
                return res
            }
            static handleClickEvent(payload) {
                let data = null
                if (payload.element) {
                    $(payload.element).click()
                }
                return data
            }
            static handleInputEvent(payload) {
                let data = null
                if (payload.element) {
                    $(payload.element).val(payload.params.inputValue)
                }
                return data
            }
            static handleGetElementTextEvent(payload) {
                let data = []
                if (payload.element && $(payload.element)) {
                    Array.from($(payload.element)).forEach((item) => {
                        const resItem = {
                            value: $(item).text()
                        }
                        data.push(resItem)
                    })
                }
                return data
            }
        }

        然后在 conent-script.js 使用

        chrome.runtime.onMessage.addListener(function (request, sender, sendResponse{
            const operateRes =  OperateConstant.operateByEventType(request.event, request)
            console.log(operateRes)
            const res = {
                code0,
                data: operateRes,
                message'操作成功'
            }
            sendResponse(res)
        });

        好的,我們來(lái)試下我們的功能吧 (tips: 請(qǐng)重新加載插件關(guān)閉所有 tab 以及確保你想要測(cè)試的 tabs 處于第一個(gè))

        可以,非常完美

        小結(jié)

        好的,朋友們,今天的分享就到這里了, 也許這個(gè)插件有許多不完善的地方,主要還是給大家分享個(gè)想法和思路,讓沒(méi)接觸過(guò) chrome 插件的朋友們也可以嘗試下

        參考資料

        【干貨】Chrome 插件(擴(kuò)展)開(kāi)發(fā)全攻略(https://www.cnblogs.com/liuxianan/p/chrome-plugin-develop.html)
          瀏覽 84
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          1. <strong id="7actg"></strong>
          2. <table id="7actg"></table>

          3. <address id="7actg"></address>
            <address id="7actg"></address>
            1. <object id="7actg"><tt id="7actg"></tt></object>
              中文字幕无码人妻二三区免费 | 操逼的毛片 | 性生活免费在线观看 | 国产91探花在线观看 | 久久国产黑丝熟女的诱惑 | 肏小嫩骚逼 | 99re在线观看 | 国产主播一区 | 欧美成人三级网站 | 天天插夜夜操 |