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