Skip to content

Instantly share code, notes, and snippets.

@zjhiphop
Created August 1, 2014 05:35
Show Gist options
  • Save zjhiphop/773b751b5d89f562dd54 to your computer and use it in GitHub Desktop.
Save zjhiphop/773b751b5d89f562dd54 to your computer and use it in GitHub Desktop.

##Chrome Packaged Apps

Introduction

Introduced at Google I/O 2012, Chrome packaged apps are a new way to develop apps that are running 'natively' within Chrome on the desktop as well as on Chrome mobile in the near future. I'm currently in the middle of a project where I develop a Chrome packaged app and in this article I would like to share my experience with the development of packaged apps.

Please note: This article should give you a basic insight of topics that I think are helpful to know for developing packaged apps. Furthermore I will give links to each topic, so you can dive deeper into that specific topic if you want to. It's not the goal of this article to act as a complete introduction to Chrome packaged apps, for a much more detailed overview of packaged apps development, please look at the official packaged app documentation.

What are Chrome Packaged Apps

Chrome packaged apps are applications that can run 'natively' within the Chrome browser and are developed in HTML5, CSS and JavaScript. It's only necessary to have Chrome installed on the system of choice, to be able to run packaged apps. No other dependencies are necessary. This has the advantage that it's theoretically possible to run packaged apps on Mac, Windows, Linux or Chrome OS in the same way and with no extra work for the developer. Furthermore, Google is planning to extend the platform support for packaged apps, to run also on mobile in the near future.

Basic project structure

It's very easy to start developing a packaged app. The only necessary files in your project are a manifest file, an event page, also called background page, where you usually register listeners for specific app events, like a launch listener that opens the app's window and some kind of icon. A background page file can be a JavaScript or a HTML file.

At the following is an overview of a very sample project with no tests or any MV* framework included. It's just to give you a quick overview, how the start can look like.

Project overview:

Sample Project Overview

manifest.json:

{
  "name": "Sample App",
  "version": "1",
  "manifest_version": 2,
  "minimum_chrome_version": "23",
  "icons": {
    "128": "icon_128.png"
  },
  "app": {
    "background": {
      "scripts": ["background.js"]
    }
  },
  "permissions": [
  ]
}

background.js:

/**
 * Listens for the app launching then creates the window
 *
 * @see http://developer.chrome.com/trunk/apps/app.runtime.html
 * @see http://developer.chrome.com/trunk/apps/app.window.html
 */
chrome.app.runtime.onLaunched.addListener(function() {
  chrome.app.window.create('app.html', {
    bounds: {
      width: 880,
      height: 480
    }
  });
});

app.html:

<!doctype html>
<html>
<head>
  <meta charset="utf-8" />
  <title>Sample App</title>
</head>
<body>
  Hello World!
</body>
</html>

There are some steps to do to load your packaged app into Google Chrome. If you developed a Chrome extension before you may already familiar with the steps to do, if not, here is a quick overview from the Chrome Packaged Apps Codelab guide.

Load Packaged App

  1. Access chrome://extensions
  2. Activate the 'Developer mode' on the right top
  3. With 'Load unpacked extension...' you can load your packaged app from a directory
  4. Click on 'launch' within your packaged app entry in the extensions list to open the app

If you start the packaged app within Chrome, it just opens a new plain window with "Hello World!". Now you can build on that basic project and start developing your packaged app.

You can find a lot of packaged app sample projects at the Google Chrome GitHub account in the chrome-app-samples repository.

Offline first

One of the first sentences you hear in talks from Google engineers or in the documentation about packaged apps is:

"You need to consider offline first: write your app as if it has no internet connection!"

What this mean is that packaged apps should run the same way, whether the user is offline or online. Therefore all HTML, CSS and JavaScript files as well as all further needed data like images, user data etc. needs to be on the local disk.

If you plan to develop an app that don't need any internet connection at all, you will be fine from the start on by just developing your packaged app and submit it to the Google Web Store. You don't have to loose so much thoughts how to run it offline. The reason is that the installation process of packaged apps via the Chrome Web Store downloads all the files bundled within your app to the local disk and the packaged app runs subsequently off of the local disk, in order to speed up performance and provide offline support. So all of your app files like HTML, CSS, JavaScript and fonts, plus other resources it needs (such as images) that are bundled within your application are already downloaded, no internet connection needed.

If you plan to develop a packaged app that needs an internet connection or you have to load external resources before the app is ready for usage, you have to do something more, to provide the best possible user experience, online and offline. There are different ways how to solve this problem.

  • Your app can save and optionally sync small amounts of data using the Chrome Storage API.
  • Furthermore it's possible to store all needed data within IndexedDB
  • or via the HTML5 Filesystem API on the local disk.

The next segments will go deeper into loading external resources like images or user data and persist it for offline usage.

Get External Resources

One of the big challenges to provide the best user experience is to download and cache resources which are necessary for the app to run, but are not bundled within the app. This is not so trivial as it looks like, because packaged apps have strict security restrictions via the CSP (Content Security Policy). It's a lot more effort to e.g. load images as just set the src attribute of an img element to the remote url directly in your HTML or dynamically via JavaScript. Instead you have to use XMLHttpRequest's to load the resource and then either refer to the data with a blob: URL or better, save and then load the data using the Filesystem API. The following code loads an image with a XMLHttpRequest object and refer the data in the img tag via a blob: URL.

// Take care of vendor prefixes
window.URL = window.URL || window.webkitURL;

var xhr = new XMLHttpRequest();
xhr.open('GET', 'http://server/path/to/image.png', true);
xhr.responseType = 'blob';

xhr.onload = function(e) {
  if (this.status == 200) {
    var blob = this.response;

    var img = document.createElement('img');
    img.onload = function(e) {
      // Clean up after yourself
      window.URL.revokeObjectURL(img.src);
    };
    img.src = window.URL.createObjectURL(blob);

    // Do something with the img
    document.body.appendChild(img);
  }
};

xhr.send();

Luckily due to the reason that it's a common use case to load external resources, Google created a library to simplify that and it's called apps-resource-loader. What this library does is to check if the requested ressource is already offline available, if not, it downloads this resources via a XMLHttpRequest from the remote url and saves the data via the HTML5 Filesystem API. If the resource is already locally available, it loads this resource directly from the file system and no request to a server is needed anymore.

To be able to use the apps-resource-loader, you have to download / clone the apps-ressource-files repository and add the apps-resource-loader ral.min.js file into your project. The following code snippet should show you, how to load images with the apps-resource-loader into your app.

var remoteImage,
    container = document.querySelector('.imageContainer'),
    // list of image URLs
    toLoad = { 'images': [
       'http://myserver.com/image1.png',
       'http://myserver.com/image2.png'
      ]
    };

toLoad.images.forEach(function(imageToLoad) {
  remoteImage = new RAL.RemoteImage(imageToLoad);
  container.appendChild(remoteImage.element);
  RAL.Queue.add(remoteImage);
});
RAL.Queue.setMaxConnections(4);
RAL.Queue.start();

Furthermore to get this code running you need permission in the manifest.json to all domains you will be try to get the resource or if you don't know before where those resources will be hosted, you can ask permission for any url.

permissions: ['<all_urls>']

This is just one example where you need to add permissions to the manifest file. To use most chrome.* APIs, your app must declare its intent in the "permissions" field of the manifest but you will find further information what chrome.* API needs which permission in the correspondent chrome.* API documentation.

It's also worth mentioning that you can roll your own library, if the apps-resource-loader library does not suit you. A good starting point is to look at the apps-resource-loader library to get a general idea, how to load external resources via a XMLHttpRequest, refer the data with a blob: URL and save it afterwards on the file system. Another good article is on HTML5Rocks called New Tricks in XMLHttpRequest2 that tells you more details about fetching data via XMLHttpRequest and the 'blob' responseType.

Offline Storage

If you don't want to use the apps-resource-loader library mentioned above or you have to store other data then images or plain files, it's good to know what further possibilities HTML5 or Google APIs provides for you to store data. In general, how you save remote data locally is up to you, but the following list should give you quick overview, what you can use to store data for faster access and offline usage.

Chrome Storage API

The Chrome Storage API comes in two flavors. You can use either the chrome.storage.sync or chrome.storage.local API. When using chrome.storage.sync, the stored data will automatically be synced to any Chrome browser that the user is logged into, provided the user has sync enabled. When Chrome is offline, Chrome stores the data locally. The next time the browser is online, Chrome syncs the data. Even if a user disables syncing, storage.sync will still work. In this case, it will behave identically to chrome.storage.local.

// Save data using the Chrome extension storage API.
var theValue = ...;
chrome.storage.sync.set({'value': theValue}, function() {
  // Saved successfully
});

The Chrome Storage API is great for saving user settings, application state or smaller chunks of data. It's not very good for large amount of data, due to quota limitations. For larger data, IndexedDB and the HTML5 Filesystem API is a better choice.

IndexedDB

IndexedDB is a Web Database. Web Databases are hosted and persisted inside an user's browser. IndexedDB is an Object Store that means, that it's is not the same as a Relational Database, which has tables, with collections rows and columns. You don't have any tables within IndexedDB, you create an Object Store for a type of data and simply persist JavaScript Objects to that store. Each Object Store can have a collection of Indexes that make it efficient to query and iterate across. So IndexedDB is best used for storing structured data and it enables fast searches on data via Indexes.

Using IndexedDB is much more complicated than using the Chrome Storage API. The following code is an example from the HTML5Rocks article A Simple TODO list using HTML5 IndexedDB that saves a to do text in an IndexedDB Object Store.

html5rocks.indexedDB.addTodo = function(todoText) {
  var db = html5rocks.indexedDB.db;
  var trans = db.transaction(["todo"], "readwrite");
  var store = trans.objectStore("todo");
  var request = store.put({
    "text": todoText,
    "timeStamp" : new Date().getTime()
  });

  request.onsuccess = function(e) {
    // Re-render all the todo's
    html5rocks.indexedDB.getAllTodoItems();
  };

  request.onerror = function(e) {
    console.log(e.value);
  };
};

A good starting point to get more information about IndexedDB, is the aforementioned HTML5Rocks article and an article on the Mozilla Developer Network: Basic Concepts Behind IndexedDB.

HTML5 Filesystem API

The HTML5 Filesystem API gives web applications controlled access to a private local filesystem 'sandbox' in which they can read / write files, create and list directories, and so on. You can find the File API: Directories and System working draft on the W3C website. With the Filesystem API you can basically save all kinds of data to the local disk. The following code sample from the excellent article on HTML5Rocks called [Exploring the FileSystem APIs](http://www.html5rocks provides a first insight in what the Filesystem API looks like. The code creates an empty file called "log.txt" (if it doesn't exist) and fills it with the text 'Lorem Ipsum'.

function onInitFs(fs) {

  fs.root.getFile('log.txt', {create: true}, function(fileEntry) {

    // Create a FileWriter object for our FileEntry (log.txt).
    fileEntry.createWriter(function(fileWriter) {

      fileWriter.onwriteend = function(e) {
        console.log('Write completed.');
      };

      fileWriter.onerror = function(e) {
        console.log('Write failed: ' + e.toString());
      };

      // Create a new Blob and write it to log.txt.
      var blob = new Blob(['Lorem Ipsum'], {type: 'text/plain'});

      fileWriter.write(blob);

    }, errorHandler);

  }, errorHandler);
}

window.requestFileSystem(window.TEMPORARY, 1024*1024, onInitFs, errorHandler);

One library I would like to mention within this context is filer.js. Filer.js is a wrapper library for the HTML5 Filesystem API, what reuses UNIX commands (cp, mv, ls) for its API and is a huge help, if you want to interact with the HTML5 Filesystem API. For comparison, the following code does the same as the code from above, but uses filer.js for that.

// Write string data.
filer.write('log.txt', {data: 'Lorem Ipsum', type: 'text/plain'},
  function(fileEntry, fileWriter) {
    ...
  },
  onError
);

Embedding external content

With the tight restrictions of packaged apps via the CSP, it's not possible to run iframes within packaged apps. Maybe you will ask yourself now: but how can I embed external content like YouTube / Vimeo videos or social widgets in my packaged app then? For this functionality Google provides the webview tag.

You can use the webview tag to embed any kind of 'guest' content (such as web pages, videos, social widgets) in your packaged app. The guest content is contained within the webview container; an embedder page within your packaged app controls how the guest content is laid out and rendered. One of the big differences between iframes and the webview tag is that the each webview runs in a separate process than your app, it doesn't have the same permissions as your app and all interactions between your app and embedded content will be asynchronous. This keeps your app safe from the embedded content.

The following code loads an external page into the packaged app via the webview tag ...

...
<webview src="http://google.com" style="width:640px; height:480px"></webview>
...

... and you can use the following code to embed a YouTube video within your app.

...
<webview src="http://www.youtube.com/embed/a1Y73sPHKxw" style="width:640px; height:480px"></webview>
...

The webview tag was developed in a way so that you can have a lot of webviews at the same time within your app and it should not have a big influence on performance. So it's no problem for you, to embed multiple videos or guest sites into your app. You will find this information and a lot of further tips about the webview tag in the Chrome Apps Office Hours - the WebView Control video as well as in the documentation for the webview tag.

Web Application Frameworks

For bigger packaged apps it may be clever to use a web framework. Within packaged apps, it's no problem at all, to use a web framework. Just to mention a few in this paragraph: AngularJS is a web framework developed directly within Google. Unlike many other JavaScript MV* frameworks, AngularJS starting with version 1.1.0 and up requires no tweaks to work within a strict CSP like packaged apps have. Further frameworks are Ember.js or Backbone.js.

I will not go any further into web frameworks in this article as this article is not about web frameworks, I just wanted to mention that you should have no problem to use a web framework within a packaged app. Whatever, if you want a further overview of web frameworks, I would like to point you to TodoMVC. TodoMVC compares and provides further information of the many many web frameworks that are out there at the moment.

Restrictions

If you start developing a packaged app or try to convert a existing web app to a packaged app, you will stumble upon some things that will work differently in packaged apps as expected. Furthermore you will have restrictions that have something to do with the CSP that is very strict within the packaged app infrastructure. At the following are a couple of these restrictions and some short tips what you can use instead of. You will find a full list of all Disabled Web Features in packaged apps over at the corresponding documentation.

Access remote resources

As often mentioned in this article, it's not allowed to access remote resources directly. You have to use cross-origin XMLHttpRequests to fetch these resources and then serve them via blob: URLs as described within the Get External Resources segment in this article.

No localStorage

The localStorage API is disabled by default. The main reason is that it's synchronously and this means, it can be slow in some circumstances. So if you want to save data that you would normally store into localStorage, you should use the Chrome Storage API and if you want to persists larger amount of data use IndexedDB or the HTML5 Filesystem API.

No inline scripts

Another restriction from the CSP is no inline scripts. I think it's not so a big deal that it's not allowed to use inline scripts, because it's not so good practice anyway. Just extract all inline scripts from your app and port it to an external script.

No string-to-JavaScript methods like eval() and function()

This is especially a problem, if you want to use certain templating languages that make use of eval() or function(). Maybe the best way to deal with this, is to use a library that offers precompiled templates and you should be all set.
You can still use a library that doesn't offer precompilation, but it will require some extra work on your part and there are further restrictions. You will need to use sandboxing to isolate any content that you want to do 'eval' things to. Sandboxing lifts CSP on the content that you specify. If you want to use the very powerful Chrome APIs in your packaged app, your sandboxed content can't directly interact with these APIs.

Embed web content

It's not allowed to use iframes within packaged apps. In this article, I already describe a way how to embed external content via the webview tag.
Another way is to sandbox local content. Sandboxing allows specified pages to be served in a sandboxed, unique origin. These pages are then exempt from their Content Security Policy. Sandboxed pages can use iframes, inline scripting, and eval().

No alert() method

I think it's not so good to use the alert() method at all to provide any feedback to the user, but if you plan to do so, you have to find a better way. Within packaged apps it's not allowed to use alert(). A better way would be to have some kind of general notification component that can show different kind of notifications like errors, general informations etc. If it's a slider view from the top or any kind of popup that's totally up to you, you should know what's the best is for your app, but with such a component in place, you have a lot of opportunities to show the user notifications and information in a general way throughout the app.

No HTML5 History API

The HTML 5 History API is not accessible within packaged apps. You have to find your own way how to handle the routing within your app as well as handle the application state.

Scrolling

If you work on a packaged app maybe you will come to a point where you will ask yourself, why you cannot scroll within the packaged app window, although the content is longer then the window. The reason is that you have make the content scrollable. For most of you this it will be obvious to resolve this problem but if not, just add the following css to your stylesheet and you should be good to go on scrolling of your site.

html {
  overflow-y:scroll;
}

Another restriction or problem if you are currently thinking of porting your web app to a packaged app is that you have to develop a packaged app in HTML, CSS and JavaScript. It's not possible to run any PHP, Python, Ruby etc. code within a packaged app. If you developed your web app with web frameworks like AngularJS or Ember.js or you used plain HTML, CSS and JavaScript without any framework, you are good to go else you need to to do some extra work to get your web app running as a packaged app. If you are currently starting or in the middle of developing your web app and plan to port it later to a packaged app, it's good to have some points in mind:

Develop against your public API from the start

If you use some kind of backend component in your web app, don't use any PHP code to connect to your backend with your frontend. Develop your web app as you would develop a third party app that is connecting to your public API and use plain HTML, CSS and JavaScript or develop it with the help of a web framework.

Try to develop your web app component based

That's especially good if you want to share code between your web and your packaged app. In your web app most of the time it's not so necessary to save any external resource to the hard drive or in a database. If you have a good app architecture it's very easy to share most of the code between the web and packaged app and just replace or add components that are specific for either one, like the syncing and offline storage component for the packaged app that maybe you will not need in your web app.

Suggestions for Improvements

There is everytime and everywhere room for improvements. The following list includes a couple of thoughts from my side for packaged apps improvements.

Installing a Packaged App + Extension with one click

If you plan or in the middle of developping a packaged app, it's very likely that you have some experience with developping of Chrome extensions before. As packaged apps as well as the Chrome extensions can be installed from the Chrome Web Store, a really good move from Google would be to provide a way for the user the decide, if the user wants to install not only the packaged app or extension separately but to combine both and with one click extension and packaged app gets installed on the users system.

Native window frame

If you open a packaged app you will notice the white frame around the packaged app window.

White Toolbar

To let packaged app feels more like a native running app on a system, it would be good to have a native looking window frame for packaged apps on each of the operating systems.

Menu Entries in the Mac Menu Bar should be the packaged app specific

If you on a Mac, you know that within OS X, if you switch to an app, the menu bar entries at the top changes to app specific entries. If you switch to a packaged app at the moment, it's still the Chrome menu bar entries.

OSX Menubar

One great possibilities for developers would be, to be able define packaged app specific menu bar entries. I'm sure on Windows or Linux there is some similar as app specific menu entries.

Conclusion

Google gives us developer a new possibility to create apps on top of Chrome. If you are familiar with development for the web, you are good to go to within minutes. The goal of this article was to give a quick overview of the architecture of packaged apps and provide some further notes about my experiences with packaged app development, Google's offer to write apps for the desktop. Chrome packaged apps are a new direction from Google to entering the desktop and mobile market and we will see what Google and the we developer will make with it in the future.

Further ressources

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment