Background scripts and messaging in webextensions

The timer we built in the previous post of this series was struggling to stay alive. This post will make good on our promise and ensure that the timer keeps running even after the popup is closed. We will do so with the help of background scripts. I recommend that you use Firefox for this post of the series if you intend to follow along.

Background scripts

MDN explains background scripts the best:

  • Use the background key to include one or more background scripts, and optionally a background page in your extension.
  • Background scripts are the place to put code that needs to maintain long-term state, or perform long-term operations, independently of the lifetime of any particular web pages or browser windows.
  • Background scripts are loaded as soon as the extension is loaded and stay loaded until the extension is disabled or uninstalled, unless persistent is specified as false. You can use any of the WebExtension APIs in the script, as long as you have requested the necessary permissions.

The implementation is very simple.

We will have a manifest.json that contains background scripts similar to this:

{
  "manifest_version": 2,
  "name": "Timekeeper",
  "description": "A simple stopwatch.",
  "version": "0.0.1",
  "icons": {
    "48": "icons/icon-48.png",
    "96": "icons/icon-96.png"
  },
  "browser_action": {
    "browser_style": true,
    "default_title": "Timekeeper",
    "default_popup": "popup/index.html"
  },
  "background": {
    "scripts": ["background/index.js"]
  }
}

for a folder structure similar to this:

./
├── background
│   └── index.js
├── icons
│   ├── icon-48.png
│   └── icon-96.png
├── manifest.json
└── popup
    ├── index.html
    ├── main.js
    └── styles.css

The lifetime of this script does not depend on the popup. Our timer will run inside this background script. The webextension will now have 2 components, a background script and a browser_action. These two components will also need a system for passing messages between them.

Runtime API

(I have zero idea how that name makes sense)
The runtime API provides methods for opening communication channels, sending one-off messages with optional replies, both within and outside of the webextension. We will only have to use the simplest of methods of this API for our timer.

To send messages within the same extension:

browser.runtime.sendMessage({ <JSON goes IN> })

And, on the listener’s side:

function handleMessage({ <JSON comes HERE> }) {
  console.log(<that JSON here>)
}

browser.runtime.onMessage.addListener(handleMessage);

Optionally, with direct responses:

// sender
browser.runtime.sendMessage({ message: 'Do not look at the moon!', level: 'critical' })
  .then((response) => { console.log(response) })


// receiver
function handleMessage(request, sender, sendResponse) {
  // look at the moon or heed the robot's advice   
  sendResponse({ ack: true });
}

Rearchitecting the system

Wait! That’s a little too much. Think of this as redecorating a room.
Timer state will be maintained in the background script. The popup will retrieve this state using the messaging system to update the display. We can visualize the activity using the diagram shown below.

A-sort-of-sequene-diagram

1#

  • When the user opens the popup, the script in the popup will ask the background script on the status of the timer.
  • Based on the background script’s response, the popup will show the correct set of buttons.

2#

  • When the user starts the timer, the popup will send the same message to the background script.
  • The background script will set up an interval function

3#

  • The interval function will update the timer count every second and send a message to the popup.
  • The popup will update its display based on the numbers received in the message.

4#

  • The interval will continue to fire inside the background script even after the popup is closed.
  • When the popup gets recreated, it will ask for the status again and update its display.

Code

popup/main.js#

The JS inside the popup will communicate user’s actions and ask for timer’s status. It won’t be running any logical operations anymore.

function startTimer () {
  browser.runtime.sendMessage({ op: 'start' })
}

function stopTimer () {
  browser.runtime.sendMessage({ op: 'stop' })
}

function lap () {
  browser.runtime.sendMessage({ op: 'lap' })
    .then(addLap)
}

function resumeTimer () {
  browser.runtime.sendMessage({ op: 'resume' })
}

function resetTimer () {
  browser.runtime.sendMessage({ op: 'reset' })
    .then(() => {
      lapListDiv.innerHTML = ''
      updateTimerDisplay(0, 0)
      updateButtonDisplay('initial')
    })
}

A listener will for runtime messages will be registered here.

function listener (message) {
  switch (message.op) {
    case 'counter': {
      const { totalTime, lapTime } = message.time
      updateTimerDisplay(totalTime, lapTime)
      break
    }
    case 'state': {
      const { timerState } = message
      updateButtonDisplay(timerState)
      break
    }
  }
}
browser.runtime.onMessage.addListener(listener)

It will also ask for the application’s state whenever it gets loaded and show to right set of buttons, (eg. The “STOP” and “LAP” buttons when the timer is running and the “START” button when the timer is not running.)

function updateButtonDisplay (state) {
  initialButtonsDiv.style.display = 'none'
  runningButtonsDiv.style.display = 'none'
  stoppedButtonsDiv.style.display = 'none'
  switch (state) {
    case 'running': {
      runningButtonsDiv.style.display = 'flex'
      break
    }
    case 'stopped': {
      stoppedButtonsDiv.style.display = 'flex'
      break
    }
    case 'initial': {
      initialButtonsDiv.style.display = 'flex'
      break
    }
  }
}
browser.runtime.sendMessage({ op: 'init' })
  .then((response) => {
    const { totalTime, lapTime, laps, timerState } = response
    updateButtonDisplay(timerState)
    updateTimerDisplay(totalTime, lapTime)
    laps.forEach(addLap)
  })

background/index.js#

This script will contain the app state.

const appState = {
  timerState: timerStates.INITIAL,
  totalTime: 0,
  laps: []
}

let interval = null

It will register a listener for runtime messages and start/stop/reset the timer based on the messages received.

const handleInit = (sendResponse) => {
  sendResponse({
    ...appState,
    lapTime: getCurrentLapTime()
  })
}

const handleStart = () => {
  if (interval === null) interval = setInterval(countSecond, 1000)
  appState.timerState = timerStates.RUNNING
  browser.runtime.sendMessage({ op: 'state', timerState: appState.timerState })
}

const handleStop = () => {
  clearInterval(interval)
  interval = null
  appState.timerState = timerStates.STOPPED
  browser.runtime.sendMessage({ op: 'state', timerState: appState.timerState })
}

const handleResume = handleStart

const handleReset = (sendResponse) => {
  appState.totalTime = 0
  appState.laps = []
  appState.timerState = timerStates.INITIAL

  sendResponse()
}

const handleLap = (sendResponse) => {
  const lap = {
    lapNumber: appState.laps.length + 1,
    totalTime: appState.totalTime,
    lapTime: getCurrentLapTime()
  }
  appState.laps.push(lap)
  sendResponse(lap)
}

function listener (message, _, sendResponse) {
  switch (message.op) {
    case 'init': {
      handleInit(sendResponse)
      break
    }
    case 'start': {
      handleStart()
      break
    }
    case 'stop': {
      handleStop()
      break
    }
    case 'resume': {
      handleResume()
      break
    }
    case 'reset': {
      handleReset(sendResponse)
      break
    }
    case 'lap': {
      handleLap(sendResponse)
    }
  }
}

browser.runtime.onMessage.addListener(listener)

You can find the complete code inside the 02-background folder in the same Github repo . You can load this extension into your browser to verify that it runs in the background.

It will break in Chrome

Browser API incompatibilities rear their ugly head. The webextension will utterly break in Chrome as it does not provide the same API as our good ‘ol Fox. We need to sort this out post-haste. Also, our current approach towards managing webextension projects won’t be efficient in larger projects. In the next post, we will improve our webextension development workflow.