A framework for ambitious Chrome Extensions

At Envoy, we love Ember.js. We enjoy coming to work every day and not worrying about set-ups, naming things, or doing repetitive tasks. We love convention over configuration (yes, we also use Ruby on Rails). The Ember ecosystem is great, full of amazing add-ons, a wonderful community, a strong testing culture, and one of the […]

At Envoy, we love Ember.js. We enjoy coming to work every day and not worrying about set-ups, naming things, or doing repetitive tasks. We love convention over configuration (yes, we also use Ruby on Rails). The Ember ecosystem is great, full of amazing add-ons, a wonderful community, a strong testing culture, and one of the greatest command line interfaces. That’s why every time we need to build something new on the front-end we ask ourselves, can we use Ember here? Is it the right tool for this problem? Sometimes the answer to that question is no (we also use React); but usually it is yes. Besides, using Ember makes it so easy to transfer knowledge between different projects and we have now nine engineers working almost full-time with Ember.

Source: https://dribbble.com/shots/1996259-Swiss-army-knife

This time we needed to build a Chrome extension, so we asked that question again; the answer ended up being… “maybe”. There are a ton of great starter and boilerplate projects on Github for building extensions, but we found out that with ember-cli and the right mix of add-ons, it would be more than enough to have a solid environment to work with.

So here I’m going to share the path we took to build a Chrome extension using Ember.js and the things we would have wanted to know a couple weeks ago.

Final app

Some context of what wanted to build: we needed a single Chrome extension with multiple components sharing a single Ember code-base. These extension components were:

Browser/Page Action popup: A small window that’s opened with a click on the corresponding UI element.

Content Scripts: A script that runs alongside pages; for compatibility reasons, it runs in an isolated world, but it shares the DOM with the page, and as such can modify it.

One of the coolest parts of the Content Scripts isolated world is that you can inject any library or code into a website and not worry about any conflicts with existing JS running there. You can have different versions of frameworks or trigger different events, plus you can share the DOM 🔝.

Initial setup and configuration

ember-cli-deploy-chrome-app is the initial boilerplate for an Ember Chrome extension and the easiest way to integrate CI and all the deployment processes to the Chrome Web Store.

It generates a chrome folder and a key.pem file in the root of your project. In chrome you'll find background.js and a default manifest.json that you can change according to your needs.

This chrome folder will have mostly configuration files and references to our Ember app. It also includes some handy symlinks pointing to the dist folder to access those files.

⚠️ Because we will need a Content Script extension component (which injects scripts into a page), symlinks do not work in this scenario. So every time the Ember app is built, we need to copy files from dist into chrome.

Obviously there is an add-on for that: ember-cli-post-build-copy

https://medium.com/media/9dbb09352808c593d9724d746494e237/href

You may have noticed we’re only copying two files here, app.js and app.css. But a normal Ember app has also a vendor.js and avendor.css. We used ember-cli-concat to have a single file of JS and CSS so we can manage them more easily.

We won’t have an index.html here either. Therefore, we have to turn off storeConfigInMeta, so that all the config data is included in the app.js.

Finally our ember-cli-build.js will look something like this:

https://medium.com/media/6f67396ee99befc781f5528d0fa724bc/href

Booting up the Ember app

In order to share a single code-base with these extension components, we needed to manually control the booting of the application. Specifically for the Content Script part, where we want to wait until the user performs an action to render the app.

We can easily configure it by setting autoboot: false and calling

require('chrome-extension/app')['default'].create({ rootElement: '#selector' });

later when needed.

https://medium.com/media/5792808378a3eb7e7d68beca1c9d2d72/hrefhttps://medium.com/media/b419d6e61baf5391f9c97de325d05c13/href

⚠️ We need to make sure we’re loading the JS files in the right order, so we can have access to require('chrome-extension/app')['default']

// chrome/manifest.json
"content_scripts": [
{
"matches": ["https://website/where-i-will-run-this-code/*"],
"css": ["assets/app.css"],
"js": [
"assets/app.js",
"content-script.js"
],

"run_at": "document_idle"
}
],

If you want to learn more deeply about how an Ember app boots, I highly recommend:

How does Ember Boot?

Because we set autoboot: false, we also need to manually boot up the Ember app on the pop up side.

// chrome/manifest.json
"browser_action": {
"default_icon": {
"16": "images/logo-icon-16.png"
},
"default_title": "Ember",
"default_popup": "popup.html"
},

https://medium.com/media/d585690d729cff639c33ff2afe0e4aa9/href

Lastly, we need to know where we are booting the Ember app from, so we can know which logic to use or which components to render. For this, we add a <meta> tag to the popup.html file, so can get its content from the application controller.

// popup.html
<meta scope=”ember-app-from” content=”popup”>

https://medium.com/media/e649fc6f97213d653e2eaa19b897b117/href

Working with the Chrome API inside an Ember app

Access to the Chrome extension API is available inside the Ember app from any file at any time. You can either use /* global chrome */ on files where you’re using it or just add it to your .eslintrc.js file so ESLint doesn’t yell at you.

// .eslintrc.js
module.exports = {
globals: {
$: true,
chrome: true,
window: true
}
}

One of the APIs we needed was chrome.storage. We use it to save and share the “session” between the popup component and the content script. It provides the same storage capabilities as the normal localStorage API that you would have access to on any web app.

When using storage, the extension’s content scripts can directly access user data without the need for a background page.

To use almost all of these APIs you need request permission when the user is installing the extension. Just specify these permissions in your manifest file.

// chrome/manifest.json
"permissions": ["tabs", "activeTab", "storage", "management"]

One of the must-install add-ons in any Ember app is Ember-Concurrency (EC), as it makes it so easy to write concise, robust, and beautiful asynchronous code. We certainly wanted to use it here. But Chrome extension API uses a callback design structure API. In order to convert all these “callback hell” functions into something we can plug-in inside any EC task we needed: chrome-promise. With it, we canyield any call to the Chrome extension API.

https://medium.com/media/64d0d661d13c19f07f46097bd8cbdb27/href

Installing the Chrome extension locally

  • Open the Extension Management page by navigating to chrome://extensions.
  • Enable Developer Mode by clicking the toggle switch next to Developer mode.
  • Click the LOAD UNPACKED button and select the extension directory (/chrome).

Live reloading

⚠️ Because we’re in an encapsulated environment, the WebSocket connection created by ember-cli-inject-live-reload won’t work here. Instead we need to watch for file changes under thechrome folder. For this we can use https://github.com/xpl/crx-hotreload. When a change is detected, it reloads the extension and refreshes the active tab (to re-trigger the updated scripts).

🚨 Known limitations

  • Chrome apps currently seem unable to handle HTML5 pushState. Make sure locationType is set to hashin your config/environment.js file.
  • Depending on your configuration, you might need to disable fingerprinting. If you have symlinked chrome/window.htmlto dist/index.html, the asset urls in app/index.html will be compiled like normal. However, if you have a different window.html, it will not be compiled and thus will unable to handle fingerprinted assets. In that case you have to disable it in ember-cli-build.js by setting fingerprint to { enabled: false }.

Conclusions

We felt really confident working and publishing this extension, it has a good test coverage, it shares the same patterns we use on our main Ember app and we know that any engineer can jump on it and add new features or fix bugs. We’re basically speaking the same language as if we were working on a normal web app. At the end an Ember app is simply a .js file plus a .css, so with the correct configuration you can basically port it anywhere you want.

Finally, by sharing this, we hope to encourage more people to have Ember.js as an option when creating Chrome apps. The Chrome API and the Ember.js ecosystem is a powerful mix for building ambitious Chrome extensions.

body[data-twttr-rendered=”true”] {background-color: transparent;}.twitter-tweet {margin: auto !important;}

function notifyResize(height) {height = height ? height : document.documentElement.offsetHeight; var resized = false; if (window.donkey && donkey.resize) {donkey.resize(height);resized = true;}if (parent && parent._resizeIframe) {var obj = {iframe: window.frameElement, height: height}; parent._resizeIframe(obj); resized = true;}if (window.location && window.location.hash === “#amp=1” && window.parent && window.parent.postMessage) {window.parent.postMessage({sentinel: “amp”, type: “embed-size”, height: height}, “*”);}if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.resize) {window.webkit.messageHandlers.resize.postMessage(height); resized = true;}return resized;}twttr.events.bind(‘rendered’, function (event) {notifyResize();}); twttr.events.bind(‘resize’, function (event) {notifyResize();});if (parent && parent._resizeIframe) {var maxWidth = parseInt(window.frameElement.getAttribute(“width”)); if ( 500 < maxWidth) {window.frameElement.setAttribute("width", "500");}}


A framework for ambitious Chrome Extensions was originally published in Envoy Engineering on Medium, where people are continuing the conversation by highlighting and responding to this story.

Source: Envoy