Build a Chrome Extension that Communicates with a Local WebSocket Service
This tutorial walks through creating a Chrome extension from scratch, setting up a Node.js WebSocket server with Express and Socket.IO, and wiring the extension’s background, content‑script, and popup scripts together so they can exchange messages and automate browser actions.
Story Background
A friend asked for a Chrome extension that can communicate with a backend service or Python script to control the browser. The goal is to avoid detection by anti‑scraping measures by using a real browser instead of headless automation.
Overall Idea
The communication flow consists of a Chrome extension, a local WebSocket server, and message passing between them.
Step 1 – Create a Chrome Extension
Start with a minimal extension structure.
{
"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"},
"background": {"page": "background.html"},
"content_scripts": [{
"matches": ["<all_urls>"],
"js": ["content-script.js"],
"run_at": "document_start"
}],
"permissions": [
"contextMenus",
"tabs",
"notifications",
"webRequest",
"webRequestBlocking",
"storage",
"http://*/*",
"https://*/*"
]
}manifest.json
Defines the extension’s name, version, icons, popup, background page, content script injection, and required permissions.
background.js
console.log('background.js')popup.js
console.log('popup.js')content‑script.js
console.log('content-script.js loaded')popup.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.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>bg-container</div>
</body>
</html>Load the extension folder in Chrome’s extensions page.
Step 2 – Create a Local WebSocket Service
Use Node.js with Express and Socket.IO to provide a persistent connection.
// index.js – create node service
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.on('webviewEventCallback', (msg) => {
console.log('webviewEventCallback: ' + msg)
io.emit('webviewEventCallback', msg)
})
})
server.listen(9527, () => {
console.log('listening on 9527')
}) // index.html – simple UI for testing
<!DOCTYPE html>
<html>
<head>
<title>Socket.IO Page</title>
</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>
<script src="/socket.io/socket.io.js"></script>
<script>
var socket = io();
document.getElementById('SendClickevent').addEventListener('click', function () {
socket.emit('webviewEvent', { event: 'click', params: { delay: 300 }, element: '#su', operateTabIndex: 0 })
})
document.getElementById('SendInputevent').addEventListener('click', function () {
const value = document.getElementById('SendInput').value
socket.emit('webviewEvent', { event: 'input', params: { inputValue: value }, element: '#kw', operateTabIndex: 0 })
})
document.getElementById('SendGetTextevent').addEventListener('click', function () {
socket.emit('webviewEvent', { event: 'getElementText', params: {}, element: '.result.c-container.new-pmd .t a', operateTabIndex: 0 })
})
socket.on('webviewEventCallback', (msg) => {
console.log(msg)
})
</script>
</body>
</html> // package.json – dependencies
{
"name": "socket-service",
"version": "1.0.0",
"main": "index.js",
"scripts": { "dev": "nodemon index.js" },
"dependencies": { "express": "^4.17.1", "nodemon": "^2.0.7", "socket.io": "^4.1.2" }
}Run npm run dev and open http://localhost:9527. Clicking the buttons logs messages, confirming the WebSocket connection.
Step 3 – Connect Chrome Extension with the Node Service
Understand the three JavaScript contexts in a Chrome extension:
content‑scripts
Injected into web pages, share the DOM but not the page’s JavaScript.
background.js
A persistent background page that runs as long as the browser is open.
popup.js
The short‑lived script for the extension’s popup UI.
For long‑running communication, place the socket logic in background.js. Load required libraries in background.html:
<script src="./lib/js/lodash.min.js"></script>
<script src="./lib/js/socket.io.min.js"></script>
<script src="./background.js"></script>Debug the background page either via the Chrome extensions UI or by opening chrome-extension://<extensionID>/background.html.
Step 4 – Bridge background.js and content‑script.js
Update background.js to forward messages from the WebSocket to the content script and vice‑versa.
// background.js (excerpt)
class BackgroundService {
constructor() {
this.socketIoURL = 'http://localhost:9527'
this.socketInstance = {}
this.socketRetryMax = 5
this.socketRetry = 0
}
init() {
console.log('background.js')
this.connectSocket()
this.listenSocketEvent()
}
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(`Failed to connect after ${this.socketRetryMax} attempts`)
}
})
}
listenSocketEvent() {
if (!_.isEmpty(this.socketInstance) && _.isFunction(this.socketInstance.on)) {
this.socketInstance.on('webviewEvent', (msg) => {
console.log('webviewEvent msg', msg)
this.sendMessageToContentScript(msg, BackgroundService.emitMessageToSocketService)
})
}
}
static emitMessageToSocketService(socketInstance, params = {}) {
if (!_.isEmpty(socketInstance) && _.isFunction(socketInstance.emit)) {
console.log(params)
socketInstance.emit('webviewEventCallback', params)
}
}
sendMessageToContentScript(message, callback) {
const operateTabIndex = message.operateTabIndex ? message.operateTabIndex : 0
chrome.tabs.query({ index: operateTabIndex }, (tabs) => {
chrome.tabs.sendMessage(tabs[0].id, message, (response) => {
if (callback) callback(this.socketInstance, response)
})
})
}
}
const app = new BackgroundService()
app.init() // content‑script.js (excerpt)
chrome.runtime.onMessage.addListener(function (request, sender, sendResponse) {
console.log(request, sender)
const operateRes = OperateConstant.operateByEventType(request.event, request)
const res = { code: 0, data: operateRes, message: 'Operation successful' }
sendResponse(res)
})Reload the extension, restart the browser, and ensure the Node server is running. Messages sent from the server now appear in the target page.
Step 5 – Automate Browser Actions
Define operation types (click, input, getElementText) in operate.js and use jQuery for DOM manipulation.
// operate.js
const operateTypeMap = { CLICK: 'click', INPUT: 'input', GETELEMENTTEXT: 'getElementText' }
class OperateConstant {
static operateByEventType(type, payload = {}) {
let res
switch (type) {
case operateTypeMap.CLICK:
res = this.handleClickEvent(payload)
break
case operateTypeMap.INPUT:
res = this.handleInputEvent(payload)
break
case operateTypeMap.GETELEMENTTEXT:
res = this.handleGetElementTextEvent(payload)
break
default:
break
}
return res
}
static handleClickEvent(payload) {
if (payload.element) { $(payload.element).click() }
return null
}
static handleInputEvent(payload) {
if (payload.element) { $(payload.element).val(payload.params.inputValue) }
return null
}
static handleGetElementTextEvent(payload) {
let data = []
if (payload.element && $(payload.element)) {
Array.from($(payload.element)).forEach(item => {
data.push({ value: $(item).text() })
})
}
return data
}
}Update manifest.json to load jquery.min.js, operate.js, and content‑script.js as content scripts.
"content_scripts": [{
"matches": ["<all_urls>"],
"js": ["lib/js/jquery.min.js", "lib/js/operate.js", "content-script.js"],
"run_at": "document_start"
}]Now the extension can open Baidu, perform a search, and retrieve result titles via messages from the Node server.
Demo GIF shows the whole workflow working perfectly.
Conclusion
This guide demonstrates a complete pipeline: a Chrome extension, a Node.js WebSocket server, and bidirectional messaging that enables automated browser interactions. While the implementation can be refined, it provides a solid foundation for developers new to Chrome extensions.
References
【干货】Chrome 插件(扩展)开发全攻略 (https://www.cnblogs.com/liuxianan/p/chrome-plugin-develop.html)
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
WeDoctor Frontend Technology
Official WeDoctor Group frontend public account, sharing original tech articles, events, job postings, and occasional daily updates from our tech team.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
