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.

WeDoctor Frontend Technology
WeDoctor Frontend Technology
WeDoctor Frontend Technology
Build a Chrome Extension that Communicates with a Local WebSocket Service

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)

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

FrontendJavaScriptNode.jsChrome extensionSocket.IO
WeDoctor Frontend Technology
Written by

WeDoctor Frontend Technology

Official WeDoctor Group frontend public account, sharing original tech articles, events, job postings, and occasional daily updates from our tech team.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.