Weng Jiarui, front-end engineer of Wedoctor Front-end Technology Department.

The story background

Here’s the thing

Friend A: Can you help me complete A Chrome plugin?

Me: What plug-ins?

Friend A: The Chrome plugin allows you to operate the browser through back-end services or python script communication

I: you kid is want to climb data? Use the existing Python framework or Google’s Puppeteer to manipulate the browser

Friend A: the way you say I have already tried, for the anti-climb detection of high website can detect your headless browser corresponding characteristics, so with the usual use of the browser can be true

I: always whole these colorful whistle of, have what use

Friend A: 10 catties crayfish!

Me: deal!!

Overall thinking

According to the above requirements of friends, we can simply draw the following communication process:

It doesn’t matter if there are specific questions, we just need to know that the general process is such communication

Github address Each commit corresponds to the corresponding step

The first step is to create a Chrome plugin

Let’s start by creating a Chrome plugin that does nothing

The directory is shown below

manifest.json

// manifest.json
{
    "manifest_version": 2.// The version of the configuration file
    "name": "SocketEXController".// The name of the plug-in
    "version": "1.0.0".// Version of the plugin
    "description": "Chrome SocketEXController".// Plug-in description
    "author": "wjryours"./ / the author
    "icons": {
        "48": "icon.png".// I'm using one path for all the ICONS corresponding to the size
        "128": "icon.png"
    },
    "browser_action": {
        "default_icon": "icon.png"./ / icon
        "default_popup": "popup.html" // Click the popup icon in the upper right corner of the floating layer HTML file
    },
    "background": {
        // Will always be resident background JS or background page
        If JS is specified, then a background page is automatically generated
        "page": "background.html"
    },
    "content_scripts": [{// Which domain names are allowed to load the injected JS
            // "matches": ["http://*/*", "https://*/*"],
            // "
      
       " indicates that all urls are matched
      
            "matches": [
                "<all_urls>"]."js": [
                "content-script.js"]."run_at": "document_start"}]."permissions": [
        "contextMenus".// Right-click menu
        "tabs"./ / label
        "notifications"./ / notice
        "webRequest"./ / web request
        "webRequestBlocking".// Blocking Web request
        "storage".// Add local storage
        "http://*/*".// An executeScript or insertCSS website
        "https://*/*" // An executeScript or insertCSS website],}Copy the code

js

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

Copy the code
// popup.js
console.log('popup.js')

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

Copy the code

html

<! -- popup -->
<! DOCTYPEhtml>
<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>
Copy the code
<! -- background -->
<! DOCTYPEhtml>
<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>
Copy the code

Then load our file directory on chrome’s extension page

And then we start the plugin and we open a page and we see that our plugin is working

The second step is to create webSocket services locally

As shown in the communication flow above, we also need to create a locally available WebSocket to send information to the Chrome plugin

For convenience, I’ll use node express and the socket. IO library

The directory structure and code are simple

// Index.js is used to create node services
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')})Copy the code
<! -- index.html --> 
<! -- Click to pass parameters that will be used later -->
<! DOCTYPEhtml>
<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>
Copy the code
// 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." "}}Copy the code

I created a node service that supports long links using Express and socket. IO. See the official documentation for more information on socket. IO

Run NPM run dev

Ok, so our service is up and running

Visit http://localhost:9527

And click the button on the page on the command line log output means that the connection is successful!

The third step is to start communicating with your local Node services

Before we start communicating with the Node service we need to look at some of the chrome plugin js usage scenarios

content-scripts

The main feature is to inject scripts into the page in the Chrome plugin. In the first step, it is this file that prints out the desired log content-scripts in the other page console. It shares the DOM with the original page, but does not share the JS, but this feature is sufficient for us to manipulate the target page

background.js

Is a resident page that has the longest lifetime of any type of page in the plug-in. It opens when the browser is opened and closes when the browser is closed, so it is common to put the global code that needs to run all the time, on launch, in the background

popup.js

This is the pop-up shown by clicking the plugin icon in the upper right corner of the browser. It has a short life span and can be used to write temporary interactions here

For our request to stay in the browser background for a long time to communicate with the service, we will write the corresponding background.js

Here we introduce the required JS library and background.js into background.html

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

There are two ways to debug this resident background file

1. Click the corresponding button in Chrome Extension to pop up debugging

2. Enter the corresponding address in the browser

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

Click the button to refresh every time you update the code

For debugging purposes I added the following code to popup.js to open a new background page every time you click on our plugin icon

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

Now all we need to do is write the code in background.js to create the long link

// 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('to try to connectThe ${this.socketRetryMax}Failed to connect to the socket service. Please check whether the service is available)}}}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()

Copy the code

Refresh the plugin and open the plugin background page to see that the link has been established. Then send the MSG from the Node service to the Chrome plugin and we can see that the message has been received successfully

(Tips: Don’t forget to start the previous Node service)

The fourth step is to make the Chrome plugin Background. js communicate with content-script.js

Again, this step is fairly simple, and there’s a lot of information in the Official Chrome documentation that I’m going to write down here

// Change background.js to the following code
static emitMessageToSocketService(socketInstance, params = {}) {
    if(! _.isEmpty(socketInstance) && _.isFunction(socketInstance.emit)) {console.log(params)
        // Send the MSG received from content-script.js to the Node service
        socketInstance.emit('webviewEventCallback', params); }}linstenSocketEvent() {
    if(! _.isEmpty(this.socketInstance) && _.isFunction(this.socketInstance.on)) {
        this.socketInstance.on('webviewEvent'.(msg) = > {
            console.log(`webviewEvent msg`, msg)
            // Send the MSG received from the Node service to 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) = > { // Obtain the corresponding tabs instance and id by obtaining the index
        chrome.tabs.sendMessage(tabs[0].id, message, (response) = > { // Send a message to the corresponding TAB
            console.log(callback)
            if (callback) callback(this.socketInstance, response)
        });
    });
}
Copy the code
// content-script.js

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

Copy the code

Then we will reload the plugin, close the browser, reopen the new browser, place the page that needs to be tested first, and send the message to our localhost:9527 so that we can receive the corresponding parameters on the page that we expect

You might see two logs, which is normal, Because if you are through the opened the chrome – the extension: / / XXX/background. The HTML page directly open the background running a background thread But the real resident and a thread in the background So pretty is two background received socket message so I sent 2 times msg

Step 5 try to manipulate the browser to do the right thing

All right, guys, here we are at the last step

Now that we have established the connection between the three modules, we just need to do some JS operations on the messages sent from the back end by some judgment

We will complete a simple task, open the Baidu page, search keywords, and will be searched to obtain each title

To make the demo easier, I directly introduced jQ to operate the DOM and created operate. Js and jquery.min.js in the JS folder

// Add js to manifest.json
"content_scripts": [{"matches": [
            "<all_urls>"]."js": [
            "lib/js/jquery.min.js"."lib/js/operate.js"."content-script.js"]."run_at": "document_start"}]Copy the code

Operation.js is used to define operations

Based on our small task above, I now add a few simple event definitions to this, which can be extended later

// 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
    }
}
Copy the code

It is then used in 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: 'Operation succeeded'
    }
    sendResponse(res)
});
Copy the code

Ok, let’s try our feature (Tips: reload the plugin to close all tabs and make sure the tabs you want to test are the first)

Yes, it’s perfect

summary

Ok, friends, today’s share here, maybe there are many imperfections in this plug-in, mainly to share with you an idea and ideas, so that did not touch the Chrome plug-in friends can also try

The resources

  • 【 dry 】Chrome add-on (extension) development overview