solovyov.net

Showkr - an in-browser application

Some time ago I made quite simple, but (I hope) useful web application called Showkr. And now I decided to tell a story about why and how I did that - I think that could be useful considering growing popularity of in-browser applications.

Why

Flickr has quite heavy pages, and when you’re watching full set of photos, loading time of every next one is quite annoying (I’ve just tried, and it takes up to a second on a fast connection).

And with Showkr you open a page, wait for a reply from Flickr’s API and then relax watching photos, they are all on the same page. Especially given that everything is tailored for such activity - just photos, comments and every meaningful hotkey pair is mapped - j/k, up/down, space/shift+space. Welcome!

How

The first thing, which saved quite a bit of my time, is Twitter Bootstrap. There is nothing to tell, if you haven’t heard about it yet - now you did. Gives pretty look to your pages in almost no time.

Make

Another one is GNU Make. I never properly knew how to write Makefiles

And after doing this for years I felt that time has come. CoffeeScript wants to became JavaScript, templates want to became JavaScript, index.html wants to be different for development and production, and they don’t really want to do it at improper times (like generating JS in runtime).

So pretty and understandable Makefile has risen. And I’m going to cite some moments so that not only I, but others can also be educated by classics.

Basics

So, I have a directory with CoffeeScript files, and I’d like to convert them in JavaScript: nobody wants to include CS on client-side - that just makes your site slower. Let’s start with definition that we have those files:

SOURCE = $(wildcard app/*.coffee)

And a rule to compile them:

build/%.js: app/%.coffee
    @mkdir -p $(@D)
    coffee -pc $< > $@

This can look quite nasty, but I’ll explain syntax and it’s not impossible to live with it - it’s useful and dense DSL (though you probably could have something prettier). Makefile has:

Everything else doesn’t bother use. Here we have variable SOURCE, which contains a result of execution of function wildcard. Both variables and functions are retrieved (or executed) with $(...) construction (excluding single-letter variables, then you can simply write $x). Functions, naturally, desire some arguments.

Thing with a colon and indented body is a rule. Tells us that file with .js extension placed in build/ directory, depends on a file with exactly some name, but in directory app/ and extension .coffee.

This rule has two instructions or, in make-speak, recipes. Those recipes are calls to usual shell commands, though every is executed in its own shell instance (if you set variables here, they won’t be saved). Every recipe is printed to a standard output unless it starts with an @ - in this case it’s hidden.

Also, make provides you with a number of variables with strange-looking names. $@ - file-target (which we want to get), $< - first (and the only here) dependency. $(@D) - directory part of target file name. I decided to stop worrying about directories being created before they are necessary, and jsut started to create them everywhere I write something to a file.

And a final (intermediate final) chord: rule which will make this work:

all: $(patsubst app/%.coffee, build/%.js, $(SOURCE))

This rule is the first so that running just make will start it, it tells us that rule all depends on some files and we have already defined a rule for building those files. Which exactly files - everything in $(SOURCE), but with app/ replaced with build/, and .coffee with .js. That’s understandable, our compilation depends on having JS files in build directory. And every file depends on corresponding CoffeeScript file, which we defined earlier.

Running make in directory will compile every source file to JS. More than that, if you will run make again, it will only compile files which were changed - make looks for a file change date and makes no unnecessary moves.

It seems that nobody will need this, given that coffee -bco build/ app/ makes the same. Well, first of all, it’s not the same - it doesn’t track file modification time, but compiles everything (and you could have a lot of everything), and after that, it’s not only about CoffeeScript! But let’s not get ahead of ourselves.

Clean up

We have first incarnation of make file:

SOURCE = $(wildcard app/*.coffee)

all: $(patsubst app/%.coffee, build/%.js, $(SOURCE))

build/%.js: app/%.coffee
    @mkdir -p $(@D)
    coffee -pc $< > $@

What can we clean up? Well, we don’t need a list of source files, only results, so let’s change beginning of file this way:

SOURCE = $(patsubst app/%.coffee, build/%.js, $(wildcard app/*.coffee))

all: $(SOURCE)

More

It’s easier to understand now what main rule needs - targets. What do we need now? We have to put all our generated stuff to index.html.

Small digression: I don’t use `require.js`_ here, since I’m too lazy to make it work together with ender, so modules want to be loaded serially directly from the index page. In the other case, I wouldn’t have this part and index.html would be a static page, but I believe right now it’s a good excuse to write more rules.

So, our main rule wants index.html:

all: $(SOURCE) build/index.html

How do we generate it? I decided to do the right thing and take awk to process it. Now index look like that:

...
<head>
...
<!-- js-deps -->
</head>
...

And then I have an awk script, which takes DEPS environment variable and puts it into html:

/<!-- js-deps -->/ {
    split(ENVIRON["DEPS"], DEPS)
    # this way it goes from 1 to 9 instead of random ordering
    for (i = 1; DEPS[i]; i++)
        printf("<script type=\"text/javascript\" src=\"%s\"></script>\n", DEPS[i])
    next
}

1 # print everything else

I don’t want to make this article long with an explanation of make, but you can read some manual or google for more documentation.

Rule for building index looks herewith:

build/index.html: index.html $(SOURCE)
    @mkdir -p $(@D)
    DEPS="$(SOURCE:build/%=%)" awk -f build.awk $< > $@

What’s new here? We remove directory name, referring to a variable with substitution (similarly to $(patsubst ...), which we used earlier). It seems that’s all: mkdir created a directory, awk read a script, changed a file, redirected result to our target ($@ == build/index.html). Quite elegant.

Now make will compile (if necessary) CoffeeScript and then index.html.

Production version

Now we have to compile version for deployment - single JavaScript file. Okay:

prod: all prod/app.js prod/index.html

prod/index.html: index.html
    @mkdir -p $(@D)
    DEPS="app.js" awk -f build.awk $< > $@

prod/app.js: $(SOURCE:build/%=prod/%)
    @mkdir -p $(@D)
    cat $^ | uglifyjs > $@

Here make prod will take all dependencies of prod/app.js ($^ is another automatic variable and contains all dependencies of the rule) and minify them in target location. And will compile index.html.

I should say that all those directory name replacements in variable names annoy me a bit, so I will clean them up. That’s what I got in the end:

SOURCE = $(patsubst app/%.coffee, %.js, $(wildcard app/*.coffee))

all: $(addprefix build/, $(SOURCE) index.html)

build/%.js: app/%.coffee
    @mkdir -p $(@D)
    coffee -pc $< > $@

build/index.html: index.html $(addprefix build/, $(SOURCE))
    @mkdir -p $(@D)
    DEPS="$(SOURCE:build/%=%)" awk -f build.awk $< > $@

prod: all $(addprefix prod/, app.js index.html)

prod/index.html: index.html
    @mkdir -p $(@D)
    DEPS="app.js" awk -f build.awk $< > $@

prod/app.js: $(addprefix prod/, $(SOURCE))
    @mkdir -p $(@D)
    cat $^ | uglifyjs > $@

Another else?

We have a nice Makefile already. Let’s add templates! I have some in app/templates directory with .eco extension and I’d like to see them with extension .eco.js (to be differentiable from just .js).

TEMPLATES = $(patsubst app/%, %.js, $(wildcard app/templates/*.eco))

all: $(addprefix build/, $(TEMPLATES) $(SOURCE) index.html)

build/templates/%.js: app/templates/%
    @mkdir -p $(@D)
    ./eco.js $< $(<:app/%=%) > $@

prod/app.js: $(addprefix prod/, $(TEMPLATES) $(SOURCE))
    @mkdir -p $(@D)
    cat $^ | uglifyjs > $@

Here ./eco.js is a custom script to run Eco templates compiler, which applies necessary wrapper to results (ender module boilerplate). It takes two parameters - path to a file and module name (templates/something.eco). Templates will be minified in a single file with the application.

Important details

JS file order is important to me (because Ender modules are synchronous), so I just set them directly:

SOURCE = $(patsubst %,%.js,util api models viewing browsing showkr)

Article describes a situation when it’s ok to have default sorting (f.e. for AMD).

Function wildcard can’t find files recursively, so when I have subdirectories in structure, I use $(shell find ...) - your usual find.

That’s all, I hope, that I covered basics well enough. You can find full contents of Makefile (this link points to a version which existed in time when article was written) in the repository.

Architecture

Let’s get back to actual application. It’s built on top of `Backbone.js`_, which is probably most popular library for writing MVC applications on JavaScript. That’s not by randomness; Backbone doesn’t try to hide implementation details (as opposed to Ember, which I’ve tried and was unhappy), but organizes everything very well.

Core

Core part of application is a Router Showkr. Application is started by constructing this router’s object.

Main function, besides routing (calling functions by address in location hash), is view management. Router can create View by an unique identifier and switch between existing ones. Once created, views are not destroyed, so that there is no need to wait for same data for Flickr again.

The rest

The rest is simple - views initialize the models and inner views, models fetch data from Flickr (using redefined sync and parse methods).

Most of the models have some nested collection, so I’ve got a hierarchy User -> SetList -> Set -> PhotoList -> Photo -> CommentList -> Comment. Nested collections are initialized when model is initialized, but fetch is started where it makes sense - photos are fetched right after fetching set information, but comments only when photo is rendered.

To be honest, I have no desire to write detailed tutorial for Backbone - there is a lot of them already. But if you’re interested it’s worth it to look at source.

Epilogue

I had some thoughts to add Picasa support, but I’m a bit reluctant - I don’t use it myself, but API is vastly different, so it’s a lot of work. I would gladly accept patches though.

This article was written to guide those who need that to employ better techniques and tools for build in-browser applications. I hope that reading this article was interesting to you. And if you have any questions which were not covered, just send a mail to me.

If you like what you read — subscribe to my Twitter, I always post links to new posts there. Or, in case you're an old school person longing for an ancient technology, put a link to my RSS feed in your feed reader (it's actually Atom feed, but who cares).

Other recent posts

Server-Sent Events, but with POST
ngrok for the wicked, or expose your ports comfortably
PostgreSQL collation
History Snapshotting in TwinSpark
Code streaming: hundred ounces of nuances