Frameworks like Rendr, Derby, and Meteor try to create this magical environment where you can stop thinking about whether your code is running on the client or server. This seems to lead to learning a bunch of concepts unique to the framework that define this "shared" environment. Sometimes that can feel like a leaky abstraction (what is a server-side Backbone view?), or lock you in to the full stack to make it work (Derby/Meteor backed by a REST API instead of mongo?).
Instead of tackling the massive problem of trying to abstract away the server and client, lets acknowledge they're completely different and embrace using modules of code that we can reasonably expect to work on both sides.
- Plain ol' Javascript Logic
- Domain logic
- Certain Libraries (e.g. moment, accounting, underscore)
- HTTP(AJAX) requests
- Javascript Templates
- Given you only use & pass in things that can be run on both sides
We use Backbone a lot, so that's an easy choice for wrapping domain logic and HTTP requests into models and collections. Node uses common.js modules so it would be simplest to write code in this manner for the client too. Browserify pacakges common.js, and with plugins like Jadeify we can easily package javascript templates to the client as well.
The tools:
- Browserify
- Jadeify
- Jade
- Backbone
- Backbone server-sync
That's it! With these tools you can easily share a good portion of your code on the client and server. We're purposefully avoiding some of the messier glue code like routing/controllers/views that vary quite a bit depending on which environment you're in.
app.coffee
/models
/collections
/client
/lib
/client
/server
/shared
/templates
/client
/server
/shared
app.coffee
app = require 'express'
Artwork = require './models/artwork'
Backbone = require 'backbone'
backboneServerSync = require 'backbone-server-sync'
app.set 'views', __dirname + '/templates/server'
app.set 'view engine', 'jade'
# Middleware that compiles assets on refresh & override Backbone.sync
app.use require 'compile-assets-with-browserify' if process.env is 'development'
Backbone.sync = backboneServerSync
# Render artwork page
app.get '/artwork/:id', (req, res) ->
Artwork = new Artwork id: req.params.id
Artwork.fetch success: (artwork) ->
res.render 'artwork_page', artwork: artwork
app.listen 3000
/templates/shared/artwork.jade
img( src=artwork.imageUrl() )
h1= artwork.get('artist').name
h2= artwork.get('title')
/templates/server/artwork_page.jade
doctype 5
html
body
#content
include ../shared/artwork
script.
BOOTSTRAP = {};
BOOTSTRAP.ARTWORK = #{artwork.toJSON()};
/models/artwork.coffee
Backbone = require 'backbone'
module.exports = class Artwork extends Backbone.Model
urlRoot: "http://artsy.net/api/v1/artwork"
/client/artwork/view.coffee
require 'zepto'
Backbone = require 'backbone'
Artwork = require '../../models/artwork.coffee'
artworkTemplate = require('../../templates/shared/artwork.jade') # This works b/c of Jadeify
class ArtworkView extends Backbone.View
initialize: ->
@model.on 'change', @render
render: =>
@$el.html artworkTemplate(artwork: @model)
$ ->
new ArtworkView model: new Artwork(BOOTSTRAP.ARTWORK), el: $('content')