DEV Community

loading...
Cover image for Browser extension - Manage your state in the Background script

Browser extension - Manage your state in the Background script

Quentin Ménoret
Engineering Manager @Doctolib – Mostly writing about TypeScript / JavaScript
・4 min read

I just published a new extension on Chrome and Firefox that allows anyone to run Code Tours from the Github UI. More information about Code Tours and the extension in this blog post.

I thought it would be nice to write a series about how you could do exactly the same, step by step.

This fourth blog post will focus on keeping your state in a Background Script.

Our next feature

If you have followed the series until today, we have created an extension capable of requesting the content of the Code Tours. Now, we need to act on them.

What we'll build today is the ability to, from the list of tours, jump to the first step of any of them. And once we're there, we'll need to prepare to display the Code Tour.

Adding the link

Just so we're up to date, here is the state of the content script:

function forwardRequest(message) {
  return new Promise((resolve, reject) => {
    chrome.runtime.sendMessage(message, (response) => {
      if (!response) return reject(chrome.runtime.lastError)
      return resolve(response)
    })
  })
}

document.addEventListener("DOMContentLoaded", function(){
  Array.from(
      document.querySelectorAll('div[role=row] > div[role="rowheader"] > span > a').values(),
    ).map(
      async (parentElement) => {
        const title = parentElement.getAttribute('title')
        const href = parentElement.getAttribute('href')


        // Now we want to query the file content as a raw string.
        // In github, this means fetching the file using “raw” instead of “blob”
        const codeTourUrl = href.replace('blob', 'raw')

        // A Code Tour is a json object, we can use the fetch API to receive an object
        const content = await forwardRequest({ url: codeTourUrl })
        console.log(title, content)
  })
})
Enter fullscreen mode Exit fullscreen mode

What we need to do now is, instead of logging the Code Tour content, to add a link to the right page. A basic Code Tour object looks like this:

{
  title: "The tour name",
  steps: [
    { 
      file: "manifest.json",
      line: 1,
      description: "The text that describes the step"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

We can definitely use it to generate the right link. For instance for this step, we want to go to:

https://github.com/<repository_owner>/<repository_name>/blob/main/manifest.json
Enter fullscreen mode Exit fullscreen mode

For the sake of the exercise, let's consider that the base branch of a repository is always main.

Generating this URL is fairly easy. We take the URL we're at up to the repository name, then add blob/main/ and the filename.

In order to have the line focused on Github we can even add #L1 to the URL to focus the line 1.

function getLink(codeTour) {
  const currentRepo = /^\/([^/]+\/[^/]+)\//.exec(window.location.pathname)[1]
  return `/${currentRepo}/blob/main/${codeTour.steps[0].file}#L${codeTour.steps[0].line}`
}
Enter fullscreen mode Exit fullscreen mode

Now let's display our link. Just replace the log in the code to add a link to the page:

const link = document.createElement("a");
link.setAttribute("href", getLink(content));
link.setAttribute("style", "padding: 5px;");
link.text = "Go!";
parentElement.parentNode.prepend(link);
Enter fullscreen mode Exit fullscreen mode

The links are now displayed

It doesn't look pretty but it will work for now.

Content Scripts have the memories of goldfish

Now we're able to navigate to the first step of the tour. That's great. Unfortunately, as the page loads, the Content Script comes to life again... With no memory of why it's here!

We need it to know what to do.

The first thing we're going to do is to add a few query parameters to pass some information.

Let's add the current step and the name of the Code Tour.

function getLink(codeTour) {
  const currentRepo = /^\/([^/]+\/[^/]+)\//.exec(window.location.pathname)[1]
  return `/${currentRepo}/blob/main/${codeTour.steps[0].file}?step=0&code-tour-title=${codeTour.title}#L${codeTour.steps[0].line}`
}
Enter fullscreen mode Exit fullscreen mode

This way, when the content script gets loaded, we know that we are currently playing a Code Tour, which one, and at which step we're at. For now we always return 0 as the step number but it'll be easy to make it generic later.

Keep the Code Tours in memory, in the Background Script

Now that we know which Code Tour we have to play, and which step we're at, let's save all the Code Tours in memory in the Background Script. Since the Background Script is already the one querying the data for the Content Script, we can just keep an index:


const tours = {}

chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
    fetch(`https://github.com/${request.url}`)
         .then((response) => response.json())
         .then((codeTourContent) => {
           // Let's save the content of the code tour
           tours[codeTourContent.title] = codeTourContent
           sendResponse(codeTourContent)
         })
    return true
})
Enter fullscreen mode Exit fullscreen mode

Retrieving the Code Tour from the Content Script

Now that we store the Code Tours, we need a way to be able to retrieve them from the Content Script. Let's do it this way:

  • if the message received by the Background Script contains a URL, it fetches the Code Tour
  • if it contains a title, it returns the cached version

We just need to add this line at the beginning of the function:

if (request.title) return sendResponse(tours[request.title])
Enter fullscreen mode Exit fullscreen mode

Now, let query it from the content script:

function forwardRequest(message) {
  return new Promise((resolve, reject) => {
    chrome.runtime.sendMessage(message, (response) => {
      if (!response) return reject(chrome.runtime.lastError)
      return resolve(response)
    })
  })
}

document.addEventListener("DOMContentLoaded", async () => {
  const urlParams = new URLSearchParams(window.location.search)
  const title = urlParams.get('code-tour-title')
  if (!title) return

  const tour = await forwardRequest({ title })

  const step = urlParams.get('step')
  // Here we can retrieve the current step to be displayed
  console.log(tour.steps[step])
})
Enter fullscreen mode Exit fullscreen mode

And voilà. We can now display the Code Tour, and continue linking to the next and previous steps.

Conclusion

We've just learned how to store the state of our extension in the Background Script so we can keep the Content Script aware of what it should do. That's a great milestone! With what we know now, you can definitely build a basic version of the Code Tour extension.

In the next post, we will have a look at how to make our Code Tour feature appear as though it was native to Github and how to securely inject the html needed. Feel free to follow me here if you want to check the next one when it's out:

qmenoret image

Photo by Ricardo Gomez Angel on Unsplash

Discussion (0)