15分鐘手摸手教你寫個能夠操控 Chrome 的插件

翁佳瑞,微醫前端技術部前端工程師。javascript

故事背景

事情是這樣的呢css

友人 A: 能不能幫我整一個 chrome 插件?html

我: 啥插件?前端

友人 A: 經過後端服務或者 python 腳本通訊 chrome 插件可以操做瀏覽器java

我: 你小子是想爬數據吧?直接用現成的 python 框架或者 谷歌的 puppeteer 就能操控瀏覽器吧node

友人 A: 你說的路子我早就試過了,對於反爬檢測高的的網站一下就能檢測你的無頭瀏覽器的相應特徵,因此就用平時用的瀏覽器就能以真亂真python

我: 總是整這些花裏胡哨的,有啥用呀jquery

友人 A: 10 斤小龍蝦!git

我:成交!!!github

總體的思路

根據朋友以上的要求,咱們能夠簡單的得出一下的通訊流程:

flow.png

具體有疑問不要緊,咱們只要知道大致的流程是這樣通訊的便可

github 地址 每一個 commit 對應相應的步驟

第一步 建立一個 chrome 插件

咱們首先來建立一個啥功能都沒有的 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",// 對應尺寸的圖標路徑 我這邊所有用一個了
        "128": "icon.png"
    },
    "browser_action": {
        "default_icon": "icon.png", // 圖標
        "default_popup": "popup.html" // 點擊右上角的圖標的 popup 浮層 html 文件
    },
    "background": {
        // 會一直常駐的後臺 JS 或後臺頁面
        // 2 種指定方式,若是指定 JS,那麼會自動生成一個背景頁
        "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", // 標籤
        "notifications", // 通知
        "webRequest", // web 請求
        "webRequestBlocking", // 阻塞式 web 請求
        "storage", // 插件本地存儲
        "http://*/*", // 能夠經過 executeScript 或者 insertCSS 訪問的網站
        "https://*/*" // 能夠經過 executeScript 或者 insertCSS 訪問的網站
    ],
}
複製代碼

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 的擴展程序頁加載咱們的文件目錄 便可

2.png

而後咱們啓用插件 隨手打開一個頁面就發現咱們的插件已經生效了

3.png

4.png

第二步 在本地建立 websocket 的服務

正如上面的通訊流程所示,咱們還須要在本地建立一個可用的 websocket 來發送信息給 chrome 插件

爲了方便起見,我這邊就用 node 的 express 以及 socket.io 這個庫來啓用

目錄結構和代碼都很簡單

5.png

// index.js 用來建立 node 服務
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 --> 
<!-- 點擊事件傳遞的參數後續會用到,這裏能夠不去了解 -->
<!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: { delay: 300 }, element: '#su', operateTabIndex: 0 }); }) document.getElementById('SendInputevent').addEventListener('click', function (e) { const value = document.getElementById('SendInput').value socket.emit('webviewEvent', { event: 'input', params: { inputValue: value }, element: '#kw', operateTabIndex: 0 }); }) document.getElementById('SendGetTextevent').addEventListener('click', function (e) { socket.emit('webviewEvent', { event: 'getElementText', params: {}, element: '.result.c-container.new-pmd .t a', operateTabIndex: 0 }); }) 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"
  }
}

複製代碼

具體的內容也很簡單,就是使用 express 和 socket.io 建立了一個 node 服務支持長連接,對於 socket.io 想有更多的瞭解的能夠參照 官方文檔

運行 npm run dev 便可

好的,這樣咱們的服務就跑起來了

6.png

咱們訪問 http://localhost:9527

並點擊頁面上的按鈕在命令行上有 log 輸出就說明鏈接成功啦!

7.png

第三步 開始使 chrome 插件 與 本地的 node 服務相互通訊

在開始與 node 服務通訊前咱們要了解下 chrome 插件的幾種 js 的使用場景

content-scripts

這個主要功能就是 Chrome 插件中向頁面注入腳本 在第一步的操做中正是該文件在別的頁面控制檯中打印出了咱們指望的 log content-scripts 和 原始頁面共享 DOM,可是不共享 JS 可是這個功能足以讓咱們去操做目標頁面了

background.js

是一個常駐的頁面,它的生命週期是插件中全部類型頁面中最長的,它隨着瀏覽器的打開而打開, 隨着瀏覽器的關閉而關閉,因此一般把須要一直運行的、啓動就運行的、全局的代碼放在 background 裏面

popup.js

這個就是點擊瀏覽器右上角的插件圖標展現的彈窗,生命週期很短,能夠將臨時的交互寫在這裏

對於咱們此次要長時間駐存在瀏覽器後臺與服務通訊的要求得出 咱們將相應的寫在 background.js 中便可

咱們這裏將須要的 js 庫 和 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>
複製代碼

咱們可使用兩種方式來調試 這個常駐後臺文件

1.直接在 chrome 拓展點擊對應按鈕便可彈出調試

8.png

9.png

2.直接在瀏覽器上輸入對應的地址 便可

chrome-extension://${extensionID}/background.html
複製代碼

每次更新代碼點擊按鈕刷新便可

爲了調試方便起見我在 popup.js 中加入瞭如下代碼 每次點擊咱們的插件圖標便可新開一個後臺頁面

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

如今咱們只須要在 background.js 中編寫相應代碼,創建長連接就能夠了

// 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}次,沒法鏈接到 socket 服務,請排查服務是否可用`)
            }
        })
    }
    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()

複製代碼

刷新插件,打開插件後臺頁面 就能夠看見連接創建成功,而後從 node 服務發送 msg 給 chrome 插件,咱們就能夠看到信息被成功接收了

(tips:以前的 node 服務別忘記啓動)

10.png

第四步 開始使 chrome 插件 background.js 與 content-script.js 創建通訊

這一步也是至關簡單,chrome 官方的文檔也有不少介紹 我這邊就寫下實現步驟

// 修改 background.js 爲以下代碼
static emitMessageToSocketService(socketInstance, params = {}) {
    if (!_.isEmpty(socketInstance) && _.isFunction(socketInstance.emit)) {
        console.log(params)
        // 將從 content-script.js 接收到的 msg 發送到 node 服務
        socketInstance.emit('webviewEventCallback', params);
    }
}
linstenSocketEvent() {
    if (!_.isEmpty(this.socketInstance) && _.isFunction(this.socketInstance.on)) {
        this.socketInstance.on('webviewEvent', (msg) => {
            console.log(`webviewEvent msg`, msg)
            // 將從 node 服務接收到的 msg 發送到 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) => { // 獲取 索引的方式獲取對應 tabs 實例以及 id
        chrome.tabs.sendMessage(tabs[0].id, message, (response) => { // 發送消息到對應 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)
});

複製代碼

而後咱們這邊將插件從新加載後關閉瀏覽器從新打開新瀏覽器,將須要測試的頁面放置在第一個, 而後在咱們的 localhost:9527 發送信息 這是咱們就能在咱們預期的頁面接收到對應參數了

11.png

這時你可能會看到 2 條 log,其實這個是正常現象, 由於若是你是經過打開了 chrome-extension://xxx/background.html 直接打開後臺頁 運行一個後臺線程 可是真正在後臺常駐的還有一個線程 因此至關是 2 個後臺接收到了 socket 消息因此就發送 2 次 msg

第五步 嘗試操控瀏覽器作對應操做

好的,朋友們,咱們終於來到了最後一步了

咱們如今已經創建起了這 3 個模塊間的聯繫了 如今無非就是要將從後端發送的消息經過一些判斷作一些 js 操做

咱們就來完成一個簡單的任務,打開百度頁面,搜索關鍵字,並將搜索到的各個 title 獲取

我這邊爲了作演示方便點就直接引入了 jq 來操做 dom 在 js 文件夾下建立 operate.js 以及 jquery.min.js

// 在 manifest.json 中加入 相應 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 主要用來定義一些操做

根據咱們上面的小任務,我這邊如今這裏面加幾個簡單的事件定義,後續能夠支持擴展

// 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 = {
        code: 0,
        data: operateRes,
        message: '操做成功'
    }
    sendResponse(res)
});
複製代碼

好的,咱們來試下咱們的功能吧 (tips: 請從新加載插件關閉全部 tab 以及確保你想要測試的 tabs 處於第一個)

demo.gif

能夠,很是完美

小結

好的,朋友們,今天的分享就到這裏了, 也許這個插件有許多不完善的地方,主要仍是給你們分享個想法和思路,讓沒接觸過 chrome 插件的朋友們也能夠嘗試下

參考資料

未命名_自定義px_2021-07-18-0.gif

相關文章
相關標籤/搜索