Chunliang Lyu

I am a developer and researcher from China. Love to make fun and useful stuff. Co-founded Hyperlink.

Developing Chrome Extension with AngularJS

I love AngularJS. It is a complete framework with built-in AJAX module, template module etc. Besides, they have a very nice testing framework, making developing with AngularJS enjoyable. That’s why we choose AngularJS to develop our Hyperlink browser extension. In this post, I will briefly discuss how to make use of AngularJS in Chrome extension.

Note: I assume you are already familiar with AngularJS as well as Chrome extension development.

Overview

Our product will capture the search query when you search on Google, send to our server to retrieve relevant results from personal knowledge sources, and inject rendered results in Google’s search result page. In the spirit, we are a lot like DuckDuckGo/Likeastore extension, if you are familiar with one of these.

Basically, there are two ways to communicate with our server:

  • from content scripts, after capturing the query, content scripts start XHR request right there. Since the request is sent at www.google.com, the browser need to confirm with your server that you accept CORS from this domain.
  • from background page, after capturing the query, content scripts send it to background page, and the background page send request to our server. You can ask Chrome to allow CORS requests.

If you take a look at likeastore's extension source code, you can find that they take the first approach. In our settings, we prefer the second one. This would give us the following benefits:

  • Better modular design, which makes testing easy.
  • Template rendering can also happen at the background page. This greatly reduce the size of content scripts.
  • No CORS setup on the server-side.

In this settings, the extension would take the following steps:

  1. At the start of Chrome (or right after user installs the extension), an Angular instances will be setup, with templates pre-compiled, and register message listeners.
  2. When user searches on Google, content scripts are injected to get the search query, send it to background page via Chrome messaging.
  3. Background page will send query to our server, render the results with templates, and send the rendered HTML back to content scripts.
  4. The injected scripts receive the HTML and inject DIV to proper place.

This setup is more like the Server-Client architecture, where background page is like our server side, and content scripts injected in Google is the client side. Background page has a permanent object listening messages sending from content scripts. I will mainly discuss how to make use of AngularJS $http/$compile/$templateCache service in the background page.

Manually initialise the AngularJS app

Actually, we do not need a full AngularJS instance with things like DOM-binding in the background page. We are only interested in several modules: $http module to provide server communication, $compile/$templateCache to provide HTML template rendering. To manually get an instance of AngularJS, we use the $injector module. $injector is used to retrieve object instances as defined by provider, instantiate types, invoke methods, and load modules.

angular.module('nemo', [])
  .run(['$http', function ($http) {
    $http.defaults.headers.common.Accept = 'application/json';
  }])

function Background() {
  // load angular module
  var $injector = angular.injector(['ng', 'nemo']);
  // get handle of compile modules
  this.$compile = $injector.get('$compile');
  this.$templateCache = $injector.get('$templateCache');
  // get handle of our services
  this.api = $injector.get('api');
  this.config = $injector.get('config');
}

The api and config module is a regular AngularJS service defined by the factory method. For example, here is a simplified definition of api service.

angular.module('nemo')
  .factory('api', ['$http', function($http) {
    return {
    	search: function(query, successCallback, failCallback) {
    		$http.get('https://hyperlinkapp.com/api/search/'), {params: {query: query}})
        		.success(function(data) {
          		successCallback(data.nodes);
        		})
        		.error(function(data) {
          		if (angular.isFunction(errorCallback)) {
            			errorCallback(data.message);
          		}
        	});
    	}
    }
  }]);

As you can see, you can define Angular modules in the usual way, which makes your code reusable.

Render templates with AngularJS

AngularJS template rendering has two steps, compile and link. The compilation of the templates need only to be done once, preferably at initialisation.

Background.prototype.compileTemplates = function () {
  var $this = this;
  this.templates = {};
  var templateKeys = ['hypercard.html'];
  angular.forEach(templateKeys, function (key) {
    var html = $this.$templateCache.get(key);
    $this.templates[key] = $this.$compile(html);
  });
}

The templates are put in $templateCache using gulp-angular-templatecache.

When we get server response, we can render the templates like:

Background.prototype.renderTemplate = function (template, scope, callback) {
  var $this = this;
  if (!(scope instanceof $this.$rootScope.constructor)) {
    var original = scope;
    scope = $this.$rootScope.$new();
    angular.extend(scope, original);
  }

  // the function parameter ensures that we get a cloned element
  var compiled = $this.templates[template](scope, function (clonedElement, scope) {});

  $this.$timeout(function () {

    // if we include any comments in the template file, it will be compiled as an individual elements
    // we need to find the wanted element, which should always be the first div in template files
    var elem = _.find(compiled, function (el) {
      return el.nodeName.toLowerCase() === 'div';
  });
  var html = elem.outerHTML;
  callback(html);
  }, 0);
};

Here, the scope can be a plain JavaScript object. Notice that at the link step, the second function parameter function (clonedElement, scope) {} is necessary to make a clone of the original templates.

Communication between background page and content scripts

This is the standard API provided by Chrome. To register message listener in background page:

Background.prototype.registerMessageListener = function () {
  chrome.extension.onMessage.addListener(function (request, sender, sendResponse) {

    // request to login
    switch (request.action) {
      case 'search':
        return $this.handleSearchRequest(request.query, sendResponse);
    }
    
    return true;
}
        

To send message from content scripts:

function sendQuery(query) {
  chrome.runtime.sendMessage({action: 'search', query: query}, function (html) {
    // do what you want with the rendered HTML string
  }
}      

CORS

Include your domain in manifest permissions, and Chrome will treat your XHR request from background page equally with requests on your website. That means if user have logged on your website, he/she automatically logged in your Chrome extension.