loading...

Personalised Development Workspace With Chrome Extension

tanhauhau profile image Tan Li Hau Originally published at lihautan.com on ・17 min read

Abstract

Chrome extension allows us to add features to our browser, personalize our development experience. One good example is React DevTools, which allows React Developers to inspect and debug virtual DOM through the Chrome DevTools.

In this talk, I will be exploring how you can develop your Chrome extension, and how you can use it to improve your development workflow.

Hopefully, at the end of the talk, you would be able to write your Chrome extension for your development workspace.

Overview

A Chrome extension is made up of several components, which can broken down into the background scripts, content scripts, an options page, UI elements.

The background scripts is like a background thread of your Chrome extension. It is where you can observe browser events and modify browser behaviors.

Content scripts run in the context of web pages. It is allowed to access and modify the DOM of the web pages. It is where you provide web pages information to the extension, as well as providing hooks for the web pages to communicate with the extension.

Options page is a standalone page, where users can modify the behavior of your Chrome extension.

The UI elements are what we usually perceived of a chrome extension, which includes:

  • browser action (the small icon in the toolbar beside the address bar)
  • popup (the floating window shown when clicked on the toolbar icon)
  • context menu (yes, you can customise your right-click context menu)
  • devtools panel (a new panel in the devtools)
  • custom pages (Overriding the History, New Tab, or Bookmarks page)

ui elements on extension

Now you know the different components of a Chrome extension, let’s look at the most important file in every extension, the manifest.json.

manifest.json

manifest.json is where you declare everything about the chrome extension:

{
  // information about extension
  "manifest_version": 2,
  "name": "My Chrome extension 😎",
  "version": "1.0.0",
  "description": "My Chrome extension 😎",
  // icons
  "icons": {
    "16": "img_16.png",
    "32": "img_32.png",
    "64": "img_64.png"
  },
  // background script
  "background": {
    "scripts": ["background.js"],
    "persistent": false
  },
  // content script
  "content_scripts": [
    {
      "matches": ["<all_urls>"],
      "js": ["content.js"],
      "run_at": "document_start",
      "all_frames": true
    }
  ],
  // options page
  "options_page": "options.html",
  // ui elements
  "browser_action": {
    "default_title": "My Chrome extension 😎",
    "default_popup": "popup.html",
    "default_icon": {
      "16": "img_16.png",
      "32": "img_32.png",
      "64": "img_64.png"
    }
  },
  "devtools_page": "devtools.html"
}

You can declare all the UI elements inside the manifest.json. You can also programmatically enable them inside the background script.

For example, React Devtools shows a different React logo when the site does not use React, uses development React and uses production React:

// background.js
const iconPath = getIconBasedOnReactVersion();
chrome.browserAction.setIcon({ tabId, path: iconPath });

function getIconBasedOnReactVersion() {
  if (noReactDetected) {
    return 'disabled-react-icon.png';
  }
  if (isReactDevelopment) {
    return 'development-react-icon.png';
  }
  return 'production-react-icon.png';
}

Notice that the options_page, popup of the browser_action, and devtools_page field takes in a path to a HTML page. You can treat the HTML page as any web app, which you can use any framework of your liking to build it. For example, React Devtools is built with React!

<!-- popup.html -->
<html>
  <body>
    <script src="popup.js"></script>
  </body>
</html>

popup.html is just like any web app, that can be build with any web framework

For the devtools page, you need to programmatically add panels to the devtools:

devtools.js

chrome.devtools.panels.create(
  'My Devtools Panel 1',
  'img_16.png',
  'panel.html'
);

Communicating between components

Since only the content script runs in the context of the web page, if your extension requires to interact with the web page, you would need some way to communicate from the content script to the rest of the extension.

For example, the React Devtools uses the content script to detect React version, and notify the background script to update the page action icon appropriately.

The communication between different components of the Chrome extension works by using message passing. There is API for one-time request as well as long-lived connections.

The one-time request API works fine for a simple extension, but it gets messier when you have more communications going on between different parts of your extension.

I studied how React Devtools works in particular, because to show the updated React virtual DOM tree inside the devtools panel, a lot of communication is needed between the devtools and the content script.

After some study and experimentation, I came up with the following architecture for my Chrome extension:

  • All the extension components (eg: popup, content script, devtools) maintained a long-lived connection with the background script
  • The background script act as a central controller that receives messages from each component and dispatches them out to the relevant components
  • Each component is like an actor in the actor model, where it acts on the messages received and sends out messages to the other actors where needed.

communication between components

Communicating between the Content script and the web page

The content script runs in the context of the web page, which means it can interact with the DOM, such as manipulating the DOM structure and adding event listeners to the DOM elements.

Besides, the content script can access the page history, cookies, local storage and other browsers’ APIs in the context of the web page.

However, the content script lives in a different global scope of the web page. Meaning, if your web application declared a global variable foo, it is not possible for the content script to access it.

<!-- http://any.page -->
<script>
  var foo = 1;
</script>
// content script
console.log(typeof foo, typeof window.foo); // undefined undefined

and the converse is true too:

// content script
var bar = 1;
<!-- http://any.page -->
<script>
  console.log(typeof bar, typeof window.bar); // undefined undefined
</script>

However, it is still possible for the content script to declare a variable into the web application, since it has access to the same DOM, it can do so by adding a script tag:

// content script
const script = document.createElement('script');
script.textContent = 'var baz = 1;';
document.documentElement.appendChild(script);
<!-- http://any.page -->
<script>
  console.log(baz, window.baz); // 1 1
</script>

Note: Depending on when you start running your content script, the DOM may or may not have constructed when your content script is executed.

Still, you can’t declare a variable from a web application into the content script scope.

I stumbleed upon an idea where your web application can “declare a variable” through the dom by creating a special DOM element for content script consumption only:

<!-- http://any.page -->
<div style="display:none;" id="for-content-script-only">
  baz = 1;
</div>
// content script
const result = document.querySelector('#for-content-script-only');
let baz;
eval(result.textContent);
console.log('baz =', baz); // baz = 1

It is technically possible, though I wouldn’t recommend it.

Instead, you should use window.postMessage to communicate between the web page and the content script.

<!-- http://any.page -->
<script>
  // listen to content script
  window.addEventListener('message', listenFromContentScript);
  function listenFromContentScript(event) {
    if (
      event.source === window &&
      event.data &&
      event.data.source === 'my-chrome-extension-content-script'
    ) {
      // handle the event
      console.log(event.data.text); // hello from content script
    }
  }
  // send to content script
  window.postMessage({
    source: 'my-chrome-extension-web-page',
    text: 'hello from web page',
  });
</script>
// content script
// listen to web page
window.addEventListener('message', listenFromWebPage);
function listenFromWebPage(event) {
  if (event.data && event.data.source === 'my-chrome-extension-web-page') {
    // handle the event
    console.log(event.data.text); // hello from web page
  }
}
// send to web page
window.postMessage({
  source: 'my-chrome-extension-content-script',
  text: 'hello from content script',
});

Note: Be sure to add an identifier field, eg: "source", to the event data for filtering, you will be amazed by how much data is communicated through window.postMessage if you don’t filter out events that are sent from your use.

Providing a hook to your extension

If you installed React Devtools, try type __REACT_DEVTOOLS_GLOBAL_HOOK__ in your console. This is a global object that was injected by React Devtools content script, to provide a simple interface for your web page to communicate with the content script.

You can do so too:

<!-- http://any.page -->
<script>
  // if ` __MY_EXTENSION_HOOK__ ` is not defined, 
  // meaning the user did not install the extension.
  if (typeof __MY_EXTENSION_HOOK__!== 'undefined') {
    __MY_EXTENSION_HOOK__.subscribe('event_a', event => {
      console.log(event);
      __MY_EXTENSION_HOOK__.sendMessage({ data: 'foo' });
    });
  } else {
    console.log('Please install my awesome chrome extension 🙏');
  }
</script>
// content script
function installHook() {
  const listeners = new Map();
  const hook = {
    subscribe(eventName, listener) {
      if (!listeners.has(eventName)) listeners.set(eventName, []);
      listeners.get(eventName).push(listener);
    },
    sendMessage(data) {
      window.postMessage({
        source: 'my-chrome-extension-web-page',
        data,
      });
    },
  };
  // listen for events
  window.addEventListener('message', listenFromContentScript);
  function listenFromContentScript(event) {
    if (
      event.source === window &&
      event.data &&
      event.data.source === 'my-chrome-extension-content-script'
    ) {
      if (listeners.has(event.data.type)) {
        listeners
          .get(event.data.type)
          .forEach(listener => listener(event.data));
      }
    }
  }
  // define a read only, non-overridable and couldn't be found on globalThis property keys
  Object.defineProperty(globalThis, ' __MY_EXTENSION_HOOK__', {
    configurable: false,
    enumerable: false,
    get() {
      return hook;
    },
  });
}
// execute the install hook in web page context
const script = document.createElement('script');
script.textContent = `;(${installHook.toString()})();`;
document.documentElement.appendChild(script);

You can check out my repo for a basic Chrome extension setup that includes all the code above.

Congrats, we’ve cleared through the arguably hardest part developing Chrome extension!

Now, let’s see what kind of Chrome extension we can develop that can help us with our daily development.

What you can do with Chrome Extension

I don’t know about you, but React DevTools and Redux DevTools have been extremely helpful for my daily React development. Besides that, I’ve been using EditThisCookie for cookie management, JSON Formatter has been helping me with inspecting .json files in Chrome, and there are a lot more extensions that made my development work easier, which I listed at the end of this article.

As you can see, these extensions are specialised and helpful in a certain aspect of my development:

  • React Devtools for debugging React Virtual DOM
  • Redux Devtools for debugging Redux store and time travel
  • EditThisCookie for debugging cookie

They are specialised for a generic React or Redux project, yet not specialised enough for your personal or your teams’ development workspace.

In the following, I will show you a few examples, along with source code for each example, and hopefully, these examples will inspire you to create your Chrome extension.

Switching environments and feature toggles

A web application is usually served in different environments (eg: test, staging, live), different languages (eg: english, chinese), and may have different feature toggles to enable / disable features on the web app.

Depending on your web app setup, switching environments, language or feature toggles may require you to mock it, or manually editing cookie / local storage (if your flags are persisted there).

Think about how you would need to educate every new developer / QA / PM on how to manually switching environments, language, or feature toggles.

What if instead, you have a Chrome extension that provides an intuitive UI that allows you to do that?

switching feature toggles with chrome extension

You can have the extension write into cookie / local storage. You can subscribe to events from extension and make changes appropriately in your web app.

Do it however you like, it’s your Chrome extension.

Code for demo

Reporting bugs with screen recording

Maybe you are using a screen recording tool to record bugs, or you are using some paid service, like LogRocket to record down every user interaction, but how well are they integrated with your bug tracking system?

You can have a Chrome extension that uses chrome.tabCapture API to record video recordings of the tab, getting essential information of your application, such as the state of your web app, console errors, network requests, and send them to your bug tracking system.

You can pass along information that is unique to your development setup, such as Redux store / Vuex store / Svelte store state and actions history, feature toggles, request ids…

And you can integrate with your bug tracking system, be it Jira, Trello, Google Sheets, email or some in-house systems.

The idea is that your extension can be personalised to your development workspace setup.

bug-reporter

Code for demo

Debugging events and analytics

Debugging and testing log and analytic events is usually a hassle.

Usually, especially production build, events are not logged out in the console. Hence, the only way to inspect and debug those events is to use the network inspector, inspecting the request body when those events are being sent out to a backend server.

What if we log those events out only when the extension is installed?

Just like Google Analytics Debuger, the extension provides a switch to turn on the debug mode of the google analytics client.

<!-- http://any.page -->
<script>
  function sendEvent(event) {
    if (typeof __MY_EXTENSION_HOOK__!== "undefined") {
      __MY_EXTENSION_HOOK__.recordEvent(event);
    }
    // proceed with sending the event to backend server
  }
</script>

events analytics

Code for demo

Closing Note

I’ve shown you how you can create your Chrome extension, and also provided some extension ideas you can have. It’s your turn to write your Chrome extension and create your personalised development workspace.

Share with me what your Chrome extension can do, looking forward to seeing them!

Extensions that has helped my daily development


If you wish to know more, follow me on Twitter.

Discussion

pic
Editor guide