As Aaron mention last week, we recently developed a Chrome App for wikiHow.com (Chrome Web Store or directly). In reality though (with one small exception) we built a modern web app that leveraged many features of HTML5 and CSS3; the exception was offline mode, but more on that shortly. As Engadget put it, Chrome apps have a lot in common with iPad apps. While many blogs have questioned the need for such a store, what is clear is that modern browsers allow developers to brake the mold of traditional websites both in terms of design and client-side implementation.
Designing Outside the Box(es)
Websites are mostly, for all intents and purposes, collections of rectangles. For the wikiHow app, our design took cues from the real world, combining decks of playing cards with newspaper-esque broadsheets. To give the cards the appearance of being dealt, we used CSS transitions. In fact, the first prototype we built used no JavaScript at all, but instead relied on :target
selectors to trigger the animations and state changes. Unfortunately, however, the complexity of the application became a bit more than that implementation could handle.
The visual indication for changing from deck-to-deck was a card flip, much like the one Apple demonstrates. It didn’t take long, however, to figure out that the simple markup used there wasn’t going to work for us.
In the demo the card is made of a single “card” element containing two “face” elements: one for the front, one for the back. With a bit of CSS to style the rotation and a JavaScript handler to trigger a class
change, the card flips over to reveal its “back”. For us, this markup requirement was not going to work: the front face of the card was going to be in one deck and the back face would be in another.
The solution was to place both elements at the same point on the screen, with the back face pre-rotated, then rotate them both at the same time. Well, at least that’s the solution in browsers that support 3D transforms; at the time, and still at the time of this writing, Chrome wasn’t in that list. In 2D-only browsers, the -webkit-backface-visibility
property used in the demo has no effect, leaving the rotated card showing it’s supposed-to-be-hidden face visible but mirrored.
Borrowing some inspiration from modernizr.js, we detected if the browser supported 3D transforms or not and added a class
to the body
so we could have two sets of CSS (grab the code over at Github). In 2D browsers, instead of rotating both elements 180°, we rotate the front face 90° then rotate the back face from -90° to 0°. This meant adding a -webkit-transition-delay
property on the animation for -webkit-transform
, but only when the face is about to be revealed, and not when it needs to be hidden. It got really complex quickly (this is just the relevant CSS for flipping and zooming the cards).
Besides all of the cool animations that set the design apart, one of the requirements from the client was that all the fonts used by the app needed to be embedded fonts (using @font-face
). We used a collection from the Google Font Directory which made it easy to implement. However, we ran into a bug in Chrome on Windows where (yes, this is very specific) a web font with ligatures that has text-shadow
applied to it causes a block of text to disappear.
You don’t see any drop shadow on our type, you say? That’s because it is set to rgba(255,255,255,0.01)
in order to trigger anti-aliasing on the type. Without it, Chrome rendered these fonts with no anti-aliasing at all on Windows. Surely we could just use -webkit-font-smoothing
like Tim suggests, right? Unfortunately, it isn’t supported by any production browser or we would have. So we were stuck with the text-shadow
hack and the accompanying bug. We ended up working around the issue by detecting the OS and the browser (obviously running counter to our inclination toward feature-detection) to specify a different font on Chrome for Windows.
Those were just the two biggest design challenges we faced; there were dozens of smaller ones for which we found simple workarounds.
The biggest thing I learned in building this app is that transform
as a property was never thought through to handle complex scenarios. It works great when you want to do one thing to an object at a time; it falls apart when you want to do multiple things. As I outlined in more detail for .net Magazine, I would love to see transform
become a shorthand property (like background
and font
are now) for a family of properties that could be independently manipulated: transform-translate
, transform-rotate
, transform-scale
, etc. I’d also like to see an @media
query for 3D support so that 2D and 3D transforms could be specified without any scripting involved.
Client-side Databases
This app does not talk to the main wikiHow site to get its content; all 120+ articles, dozens of quiz questions and quiz responses are stored in an HTML5-driven client-side SQLite database. When the app loads, it grabs a SQL file, parses it and loads the data in. In theory, you can give your database a version number then check against that to see if it needs updating. In practice, that only worked sporadically. What we settled on doing was loading the data at launch time, setting a session cookie to note that the database was loaded, and then reloading it if that cookie wasn’t present. The only table we don’t reload is the one containing that user’s quiz results; that ensures their scores persist across database upgrades. Aside from the versioning issue, HTML5 databases work as the spec calls for, which was a welcome relief.
We did find it odd, however, that there was no way to provide a pre-created SQLite database as part of the Chrome installer. It would have been really nice to have that be part of the install rather than having to trigger it when the application is loaded like it does for a normal web app.
Navigating Through a One Page Site
There’s only one page of HTML for the app (plus a few small snippets for the About and Feedback pages). To track the user’s state as she moves through the app, we followed the convention of updating window.location.hash
. All of our app’s logic for handling URLs is contained in about 400 lines of code. This single function, triggered by the onHashChange
event, determines what content is currently showing and what content needs to be shown according to the current hash
.
For our app, that meant five different possible types of content to be shown that had to transition in from one of six possible current states. While it’s all fairly straightforward, it took a lot of testing to get right; I can’t thank everyone on our team enough for the hours of clicking through the app and submitting bug reports that exhaustively listed every step they’d taken from launch to the broken state they’d found themselves in. All of this work resulted in an app that can be refreshed at any point and will get you right back to where you were, has shareable links to every piece of content, and fully supports the browser’s back and forward buttons.
It took a lot of effort to make something that “just works.”
Going Offline
HTML5 has a specification for offline manifest files that allows a web app to specify how it can work when no network connection is present. However, the amount of static media that we needed to cache to provide the right experience in offline mode was larger than any current browsers allow. This was where we benefited from the Chrome Web Store: Chrome apps can ask for unlimited storage (this goes beyond just the offline cache). For users that have the app installed from the store, they can access most of the app even when not connected to the Internet. The exceptions were obvious things like social network sharing and sending feedback to wikiHow.
What proved difficult here was detecting when the app was offline. The specification for HTML5 says that JavaScript can query window.navigator.onLine
but that always returns true in current versions of Webkit. Instead we turned to AJAX to load a page on the server and see what it contained. When the app is online, it has the normal content we expect to get back. When it’s offline, however, we used the manifest to point the same URL to an empty HTML file. If the app doesn’t detect any content from the request, it hides all the online-only features.
Another issue we ran into was the way that the spec parses URLs in the FALLBACK
section (this is where you map online URLs to their offline counterparts). These URLs are greedy; that is to say they are a pattern match with no way to limit the match. In our case, our base URL is just “/”. That meant that any URL we tried to load while offline loaded the content of our index.html file. As you’d expect, this caused some major issues. Any URL that was called (and which hadn’t been specifically dealt with in the manifest) would reload index.html and run all of its scripts, resulting in an infinite loop of loading content. To address this issue, we had to add resources to FALLBACK
for every URL we had locally and move things like our Google Analytics code into the core JavaScript file, only executing them if the app was determined to be online when it was launched.
Final Thoughts
It’s easy to understand many of the critics of the Web Store when they don’t see the point of having these apps be installable when many just point to a website. I’m sure Google has its reasons (not the least of which is Chrome OS), but what I’ve taken away from this project is that the store is a way to give developers permission to really go beyond what is normally considered a website and deliver true applications in a browser. It makes it OK to ignore legacy browsers and focus on the future. Like Tom Merrit said in his coverage of the store announcement, many of the best iPad apps today are just wrappers around great web apps. It’s the same with the Chrome Web Store: it is essentially a wrapper—or better yet, a gatekeeper—around great web apps and enables the developer to know exactly what platform they’ll be running on.
In a few years, when HTML5 is more widely supported in the browsers that people are actually using, it might not be necessary to have these restrictions (or the Chrome Web Store), but for now, it lets us move forward, pushing the boundaries of new technology, and that’s a good thing for the web.