There’s one problem with the way I first set up my build system for Medium: I had no control over how the CoffeeScript files were ordered. In languages with linkers, this isn’t a big deal: within a library, the linker will resolve all the references between object files at once. But without a linker, ordering becomes more of an issue.
Actually, in CoffeeScript or JavaScript, it’s not that much of an issue: in fact, for small projects you can get away with ignoring it entirely. It’s fine for methods in one class to refer to another class that hasn’t been loaded at the time the first class is defined: as long as the second class exists by the time the class has run, you’ll be okay. So that means that the only real issue when starting off is making sure your entry point gets run after everything else is loading; that’s a one-off case that’s easy to deal with manually. (You can just inline the entry point code in the HTML file, for example.)
Having said that, just clobbering everything together like that felt a little distasteful to me; and there also turned out to be two practical issues. The first is that Mocha, the unit test framework I used (which I promise I’ll talk about soon!), didn’t use the browser model of sticking everything in global variables: it used the Node.js concept of modules. I actually spent a couple of weeks ignoring that mismatch, writing code that worked in both realms by checking to see if the Node.js variables were defined, but in retrospect, that was silly: the point of this blog post is that doing things the right way is easier than that workaround.
And the second practical issue is inheritance: if class A inherits from class B, then the browser really does need to have seen the definition for class B before the definition of class A. To get that right, I needed a dependency structure; and doing that by hand would have crossed the line from silly to actively perverse. So I looked around, and found that browserify (in its coffeeify incarnation) was what I wanted.
First, a brief introduction to the Node module system. When you define what looks like a global variable in a Node source file, it doesn’t actually get stuck in the global namespace: the namespace for that file is local to that file. But Node provides a special exports
variable: if you want to export values, attach them to that. For example, if I have a file runner_state.coffee
that defines a RunnerState
class, I’ll end the file with
exports.RunnerState = RunnerState
That last line still doesn’t stick RunnerState
in the global namespace: there’s actually a special global
object you can use for that, but you generally don’t want to do that. Instead, if another file wants to refer to that RunnerState
variable, it puts a line like this at the top:
{RunnerState} = require('./runner_state.coffee')
The return value of the require()
call is the exports
object for that file, and I’m using CoffeeScript structured assignment to get at its RunnerState
member. Once I’ve done that, I can refer to RunnerState
elsewhere in that file. (Incidentally, in some situations you don’t need either the leading ./
or the trailing .coffee
in the argument to require()
, but I found that using both worked best with the collection of tools I was using.)
So, that’s the Node.js module system: a nice way to avoid polluting the global namespace and to express your object graph. It comes for free in the Node ecosystem, and all I wanted was to bring that over to a browser context. And that’s where browserify comes in: it lets you write code like it’s Node modules and then it transforms it into a format that the browser is happy with.
To cut to the chase, here’s how to get it to work. Start with the build system from last time. Then install browserify and coffeeify, plus the grunt plugin:
npm install --save-dev browserify coffeeify grunt-browserify
In your Gruntfile.coffee
, replace the grunt-contrib-coffee
requirement with a grunt-browserify
requirement, and replace the coffee
block with a block that looks like this:
browserify: dist: files: 'js/medium.js': ['coffee/*.coffee'] options: transform: ['coffeeify']
Also, in your default
task, you’ll want to invoke browserify
instead of coffee
.
Here’s the resulting file:
module.exports = (grunt) -> grunt.initConfig { pkg: grunt.file.readJSON('package.json') browserify: dist: files: 'js/medium.js': ['coffee/*.coffee'] options: transform: ['coffeeify'] sass: dist: files: 'css/medium.css': 'scss/medium.scss' watch: coffee: files: 'coffee/*.coffee' tasks: ['coffee'] options: spawn: false sass: files: 'scss/*.scss' tasks: ['sass'] options: spawn: false } grunt.loadNpmTasks('grunt-browserify') grunt.loadNpmTasks('grunt-contrib-sass') grunt.loadNpmTasks('grunt-contrib-watch') grunt.registerTask('default', ['browserify', 'sass'])
Now, if you run grunt
, you’ll build the output JavaScript file (js/medium.js
in this case) like before, but with separate input files treated as separate modules! Which, of course, means that it won’t actually work until you go back through them and add require()
and exports
in appropriate places.
Post Revisions:
- June 12, 2014 @ 20:47:19 [Current Revision] by David Carlton
- June 12, 2014 @ 20:47:19 by David Carlton