forked from developmentseed/bones
-
Notifications
You must be signed in to change notification settings - Fork 1
/
bones.js
166 lines (153 loc) · 6.06 KB
/
bones.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
// Backbone base class overrides for the server-side context. DOM-related
// modules are required *globally* such that Backbone will pick up their
// presence.
$ = require('jquery-1.4.4');
jQuery = require('jquery-1.4.4');
jsdom = require('jsdom').jsdom;
window = jsdom().createWindow();
document = window.document;
var fs = require('fs'),
_ = require('underscore')._,
crypto = require('crypto'),
Backbone = require('backbone'),
Handlebars = require('handlebars'),
clientJS = clientJS || fs.readFileSync(__dirname + '/client.js', 'utf8');
// View (server-side)
// ------------------
// With a server-side DOM present Backbone Views tend to take care of
// themselves. The main override is to clear out `delegateEvents()` - the
// `events` hash is of no use on the server-side with the View being dead
// after initial delivery. `template()` and `html()` serve as
// functions to make client/server-side templating a uniform interface.
//
// The following conventions must be followed in order to ensure that the views
// can be used in both environments:
//
// - Use `render()` only for templating. Any DOM event handlers, other
// js library initialization (e.g. OpenLayers) should be done in the
// `attach()` method.
// - `render()` must `return this` in order to be chainable and any calls to
// `render()` should chain `trigger('attach')`.
// - `template()` should be used to render an object using `template` to
// identify the template to use in the `Bones.templates` hash. Avoid using
// jquery or other doing other DOM element creation if templating could get
// the job done.
Backbone.View = Backbone.View.extend({
delegateEvents: function() {},
template: function(template, data) {
var compiled = Handlebars.compile(Bones.templates[template]);
return compiled(data);
},
html: function() {
if (typeof this.el === 'string') {
return this.el;
} else {
// Consider using the method described here:
// http://stackoverflow.com/questions/652763/jquery-object-to-string
return $(this.el).html();
}
}
});
// Controller/History (server-side)
// --------------------------------
// Expose Backbone's controller/history routing functionality to Connect
// as a middleware. A Connect server can add Backbone controller routing to
// its stack of middlewares by doing:
//
// server.use(Backbone.history.middleware());
// new MyController(); /* Routes will be added at initialize. */
Backbone.Controller = Backbone.Controller.extend({
// Override `.route()` to add a callback handler with an additional
// `response` argument callback for sending back a View as HTML using
// the Connect server.
route: function(route, name, callback) {
Backbone.history || (Backbone.history = new Backbone.History);
if (!_.isRegExp(route)) route = this._routeToRegExp(route);
Backbone.history.route(route, _.bind(function(fragment, res) {
var response = function(view) {
res.send(view.html());
};
var args = this._extractParameters(route, fragment);
var view = callback.apply(this, args.concat([response]));
this.trigger.apply(this, ['route:' + name].concat(args));
}, this));
}
});
// Override `.loadUrl()` to allow `res` response object to be passed through
// to handler callback.
Backbone.History.prototype.loadUrl = function(fragment, res) {
var matched = _.any(this.handlers, function(handler) {
if (handler.route.test(fragment)) {
handler.callback(fragment, res);
return true;
}
});
return matched;
};
// Provide a custom Backbone.History middleware for use with Connect.
Backbone.History.prototype.middleware = function(req, res, next) {
var fragment;
if (req.query && req.query['_escaped_fragment_']) {
fragment = req.query['_escaped_fragment_'];
} else {
fragment = req.url;
}
!Backbone.history.loadUrl(fragment, res) && next();
};
// Clear out unused/unusable methods.
Backbone.History.prototype.start = function() {};
Backbone.History.prototype.getFragment = function() {};
Backbone.History.prototype.saveLocation = function() {};
// Instantiate Backbone.history.
Backbone.history || (Backbone.history = new Backbone.History);
// Bones object.
var Bones = module.exports = {
Bones: function(server, options) {
// Add CSRF protection middleware if `options.secret` is set.
options.secret && server.use(this.middleware.csrf(options));
// Add route rule for bones.js.
server.get('/bones.js', this.middleware.bonesjs);
// Add Backbone routing.
server.use(this.middleware.history);
// Add server reference to Bones.
this.server = server;
return this;
},
middleware: {
history: Backbone.history.middleware,
bonesjs: function(req, res, next) {
res.send(Bones.clientJS(), { 'Content-Type': 'text/javascript' });
},
csrf: function(options) {
return function(req, res, next) {
var cookie;
if (req.cookies['bones.csrf']) {
cookie = req.cookies['bones.csrf'];
} else {
cookie = crypto.createHmac('sha256', options.secret)
.update(req.sessionID)
.digest('hex');
res.cookie('bones.csrf', cookie);
}
if (req.method === 'GET') {
next();
} else if (req.body && req.body['bones.csrf'] === cookie) {
delete req.body['bones.csrf'];
next();
} else {
res.send('Access denied', 403);
}
}
}
},
server: null,
templates: {},
clientJS: function() {
return clientJS + [
'// Bones object (client-side)',
'var Bones = {',
' templates: ' + JSON.stringify(this.templates),
'};'
].join('\n');
}
};