Dealing with server systems can be alleviated by systems which allow clear distinction between:
- serving pages; and
- powering applications.
We start the web server.
var camp = require('camp').start();
The start()
function has the following properties:
documentRoot
: the path to the directory containing the static files you serve (and the template files, potentially). If your website is made of HTML pages, this is where they are located. Defaults to./web
.templateReader
: the default template engine used. See below.passphrase
,key
,cert
,ca
: in the case of a secure website (using HTTPS), those are fields you may specify to indicate where to find information about the website's security. Defaults include "https.key", "https.crt", and, as the CA (Certificate Authority, a list of certificates) an empty list.setuid
: once the server has made the connection, set the user id to something else. This is particularly useful if you don't want the server to run as the almighty root user. However, executing this requires to be root (so you will need to usesudo
or the like to run the server).saveRequestChunks
: some requests will have their data processed by Camp before reaching your handlers, in order to fill the Augmented Request'sreq.data
dictionary, but this means that you won't receive any of the processed chunks by doingreq.on('data',function(chunk){})
in your handler. If you need to access these raw chunks (e.g. to pipe complete requests to a different server), you'll find them inreq.chunks
whensaveRequestChunks
is set totrue
.
The result of require('camp')
can also be useful, for instance, to log
warnings from the server. The logging system uses
multilog.
var Camp = require('camp');
Camp.log.unpipe('warn', 'stderr');
// There are three streams: warn, error, and all.
// warn and error are individually piped to stderr by default.
The Camp.js engine targets ease of use of both serving plain html files and ajax
calls. By default, when given a request, it looks for files in the ./web/
directory. However, it also has the concept of Ajax actions.
camp.ajax.on('getinfo', function(json, end, ask) {
console.log(json);
end(json); // Send that back to the client.
});
An action maps a string to the path request /$<string>
. When a client asks
for this resource, sending in information stored in the "json" parameter,
Camp.js will send it back the object literal that the callback function gives.
In the example given, it merely sends back whatever information the client gives, which is a very contrived example.
The purpose of this distinction between normally served html pages and ajax actions is to treat servers more like applications. You first serve the graphical interface, in html and css, and then, you let the user interact with the server's data seemlessly through ajax calls.
Note that the json
parameter given is a single object containing all
parameters from the following sources:
- the query string from GET requests
- POST requests with enctype application/x-www-form-urlencoded
- POST requests with enctype multipart/form-data. This one uses the same API as formidable for file objects.
You also get an Ask object, see below.
Before downloading POST Ajax data, you can hook a function up using the following code:
camp.ajaxReq.on('getinfo', function(ask) { … });
That can be useful to give information about the progress of an upload, for
instance, using ask.form.on('progress', function(bytesReceived, bytesExpected) {})
.
Let's build a path named /path
. When we receive a call on /talk
, we send
the data it gives us to the EventSource path.
// This is actually a full-fledged chat.
var chat = camp.eventSource ( '/all' );
camp.post('/talk', function(req, res) { chat.send(req.data); res.end(); });
This EventSource object we get has two methods:
- The
send
method takes a JSON object and emits themessage
event to the client. It is meant to be used withes.onrecv
. - The
emit
method takes an event name and a textual message and emits this event with that message to the client. It is meant to be used withes.on(event, callback)
.
We also include the raw duplex communication system provided by the WebSocket protocol.
camp.ws('/path', function(socket));
Every time a WebSocket connection is initiated (say, by a Web browser), the
function is run. The socket
is an instance of [ws.WebSocket]
(https://github.com/einaros/ws/blob/master/doc/ws.md#class-wswebsocket).
Usually, you only need to know about socket.on('message', function(data))
,
and socket.send(data)
.
This function returns an instance of a [WebSocket server]
(https://github.com/einaros/ws/blob/master/doc/ws.md#class-wsserver)
for that path.
Most notably, it has a wsServer.clients
list of opened sockets on a path.
A map from paths to WebSocket servers is available at:
camp.wsChannels[path];
For the purpose of broadcasting (ie, sending messages to every connected socket on the path), we provide the following function.
camp.wsBroadcast('/path', function recv(req, res))
The recv
function is run once every time a client sends data.
- Its
req
parameter providesreq.data
(the data that a client sent), andreq.flags
(req.flags.binary
is true if binary data is received;req.flags.masked
if the data was masked). - Its
res
parameter providesres.send(data)
, which sends the same data to each socket on the path.
Client-side, obviously, your browser needs to have a WebSocket API. The client-side code may look like this.
// `socket` is a genuine WebSocket instance.
var socket = new WebSocket('/path');
socket.send(JSON.stringify({ some: "data" }));
Be warned before you read on: the Socket.io interface is deprecated. Use the WebSocket interface provided above instead. Also, do not use both the socket.io interface and the WebSocket interface. That seems to be asking for trouble.
We also include the duplex communication system that socket.io provides. When
you start the server, by default, socket.io is already launched. You can use its
APIs as documented at http://socket.io#how-to-use from the camp.io
object.
camp.io.sockets.on('connection', function (socket) { … });
On the client-side, Scout.js
also provides shortcuts, through its
Scout.socket(namespace)
function. Calling Scout.socket()
returns the
documented Socket.io object that you can use according to their API.
var io = Scout.socket();
io.emit('event name', {data: 'to send'});
io.on('event name', function (jsonObject) { … });
If you want a bit of code to be executed on every request, or if you want to manually manage requests at a low level without all that fluff described above, you can add handlers to the server.
Each request goes through each handler you provided in the order you provided
them. Unless a handler calls next()
, the request gets caught by that handler:
- None of the handlers after that one get called,
- None of the subsequent layers of Camp (such as WebSocket, EventSource, Route…) get called.
Otherwise, all the handlers get called, and the request will get caught by one of the subsequent layers of Camp.
var addOSSHeader = function(req, res, next) {
ask.res.setHeader('X-Open-Source', 'https://github.com/espadrine/sc/');
next();
};
camp.handle(addOSSHeader);
// There's no reason to remove that amazing handler, but if that was what
// floated your boat, here is how you would do that:
camp.removeHandler(addOSSHeader);
An associated possibility, very much linked to the normal use of Camp.js, is to handle templates. Those are server-side preprocessed files.
Mostly, you first decide where to put your template file. Let's say we have
such a file at /first/post.html
(from the root of the web/ or publish/
directory).
var posts = ['This is the f1rst p0st!'];
camp.path( 'first/post.html', function(req, res) {
res.template({
text: posts[0],
comments: ['first comment!', 'second comment…']
});
});
req
is an Augmented Request, and res
an Augmented Response.
Therefore, if the request is /first/post.html?key=value
, then req.data.key
will be "value".
res.template(scope, templates)
responds to the request with a list of
templates (produced with Camp.template()
or camp.template()
), a single
template, or no template:
in the latter case, the URI's path will be treated as a template file on disk
under documentRoot
. This is the case here with "first/post.html".
The file /web/first/post.html
might look like this:
<!doctype html><title></title>
<p>{{= text in html}}</p>
<ul>
{{for comment in comments {{
<li>{{= comment in html}}</li>
}}}}
</ul>
Because it will be preprocessed server-side, the browser will actually receive the following file:
<!doctype html><title></title>
<p>This is the f1rst p0st!</p>
<ul>
<li>first comment!</li>
<li>second comment...</li>
</ul>
If you need to specify a different template, you can do so:
var postsTemplate = Camp.template( './templates/posts.html' );
camp.path('posts', function(req, res) {
res.template({comments: comments}, postsTemplate);
});
Camp.template(paths, options)
takes an Array of String paths to templating
files (or a single path to a templating file), and the following options:
- reader: the template reader function in use, defaulting to
camp.templateReader
, which defaults to Fleau. - asString: boolean; use the string as a template, not as a file path.
- callback: function taking a function(scope) → readableStream.
If you don't want the template creation to be synchronous, use this.
We return nothing from the function if
callback
is set.
This function returns a function(scope) → readableStream, unless callback
is
set.
So this is how to be explicit about the template. On the opposite extreme, you
can be extra implicit: the URL path will them be used as the template path on
disk, and req.data
will be used as the template's scope.
// Supports ?mobile=true
camp.path('blog.html');
camp.notFound( 'blog/*', function(req, res) {
res.file('/templates/404.html');
});
The camp.notFound()
function works in exactly the same way as the
camp.path()
function, with two important differences:
- It only gets used when nothing else matches the path, including paths and
static files on disk under
documentRoot
, - It responds with a 404 (Not Found) status code.
In Camp.js, there is a lot more than meets the eye. Up until now, we have only discussed the default behaviour of ScoutCamp. For most uses, this is actually more than enough. Sometimes, however, you need to dig a little deeper.
Camp.start
is the simple way to launch the server in a single line. You may
not know, however, that it returns an http.Server
(or an https.Server
)
subclass instance. As a result, you can use all node.js' HTTP and HTTPS
methods.
You may provide the start
function with a JSON object defining the server's
settings. It defaults to this:
{
port: 80, // The port to listen to.
hostname: '::', // The hostname to use as a server
security: {
secure: true,
key: 'https.key', // Either the name of a file on disk,
cert: 'https.crt', // or the content as a String.
ca: ['https.ca']
}
}
If you provide the relevant HTTPS files and set the secure
option to true, the
server will be secure.
Camp.createServer()
creates a Camp instance directly, and
Camp.createSecureServer(settings)
creates an HTTPS Camp instance. The latter
takes the same parameters as https.Server
.
Camp.Camp
and Camp.SecureCamp
are the class constructors.
Camp is stack-based. When we receive a request, it goes through all the layers of the stack until it hits the bottom. It should never hit the bottom: each layer can either pass it on to the next, or end the request (by sending a response).
The default stack is defined this way:
campInstance.stack = [wsLayer, ajaxLayer, eventSourceLayer, pathLayer
routeLayer, staticLayer, notfoundLayer];
Each element of the stack function(req, res, next){}
takes two parameters:
- augmented IncomingMessage (
req
) and ServerResponse (res
) (more on that below), - a
next
function, which the layer may call if it will not send an HTTP response itself. The layer that does catch the request and responds fully to it will not callnext()
, the others will callnext()
.
You can add layers to the stack with handle()
, which is described way above.
camp.handle(function(ask, next) {
ask.res.setHeader('X-Open-Source', 'https://github.com/espadrine/sc/');
next();
});
By default, it inserts it before the wsLayer
, but after other inserted
handlers. Its insertion point is at camp.stackInsertion
(an integer).
The Ask class is a way to provide a lot of useful elements associated with a request. It contains the following fields:
- server: the Camp instance,
- req: the http.IncomingMessage object.
- res: the http.ServerResponse object.
- uri: the URI.
- path: the pathname associated with the request.
- query: the query taken from the URI.
- cookies: using the cookies library.
- form: a
formidable.IncomingForm
object as specified by the formidable library API. Noteworthy areform.uploadDir
(where the files are uploaded, this property is settable),form.path
(where the uploaded file resides), andform.on('progress', function(bytesReceived, bytesExpected) {})
. - username, password: in the case of a Basic Authentication HTTP request, parses the contents of the request and places the username and password as strings in those fields.
An Ask
instance is provided as an extra parameter to
camp.route(pattern, function(query, path, end, ask))
(see the start of section "Diving In"),
and as a parameter in each function of the server's stack
function(ask, next)
(see the start of section "The stack").
An Augmented Request is an IncomingMessage which has several additional
fields which you can also find in Ask
: server
, uri
, form
, path
,
data
(which is the same as query
), username
, password
, cookies
.
It also contains form information for multipart/form-data
requests in the
following fields:
- form: a
formidable.IncomingForm
object as specified by the formidable library API. Noteworthy areform.uploadDir
(where the files are uploaded) andform.on('progress', function(bytesReceived, bytesExpected) {})
. - fields: a map from the field name (eg,
fieldname
for<input name=fieldname>
) to the corresponding form values. - files: a map from the field name (eg,
fieldname
for<input name=content type=file>
) to a list of files, each with properties:- path: the location on disk where the the file resides
- name: the name of the file, as asserted by the uploader.
An Augmented Response is a ServerResponse which also has:
template(scope, templates)
: responds to the request with a list of templates (produced withCamp.template()
orcamp.template()
), a single template, or no template (in which case, the URI's path will be treated as a template file on disk underdocumentRoot
). Thescope
is a JS object used to fill in the template.file(path)
: responds to the request with the contents of the file atpath
, on disk underdocumentRoot
.json(data, replacer, space)
: responds to the request with stringified JSON data. Arguments are passed toJSON.stringify()
, so you can use eitherres.json({a: 42})
(minified) orres.json({a: 42}, null, 2)
(human-readable).compressed()
: returns a writable stream. All data sent to that stream gets compressed and sent as a response.redirect(path)
: responds to the request with a 303 redirection to a path or URL.
Note: file(path)
leverages browser caching by comparing If-Modified-Since
request headers against actual file timestamps, and saves time and bandwidth by
replying "304 Not Modified" with no content to requests where the browser
already knows the latest version of a file. However, this header is limited to
second-level precision by specification, so any file changes happening within
the same second, or within a 2-second window in the case of leap seconds, cause
a small risk of browsers fetching and caching a stale version of the file in
between these changes. Such a cached version would remain stale until the next
file change and subsequent browser request updating the cache.
Additionally, you can set the mime type of the response with
req.mime('png')
, for instance.
The default layers provided are generated from what we call units, which are
exported as shown below. Each unit is a function that takes a server instance
and returns a layer (function(ask, next){}
).
Camp.ajaxUnit
(seen previously)Camp.socketUnit
(idem)Camp.wsUnit
(idem)Camp.eventSourceUnit
(idem)Camp.pathUnit
(idem)Camp.routeUnit
(idem)Camp.staticUnit
(idem, relies oncamp.documentRoot
which specifies the location of the root of your static web files. The default is "./web".Camp.notfoundUnit
(idem)
Browsers' built-in Ajax libraries are usually poor. They are not cross-browser (because of Internet Explorer) and they can quickly become a hassle. Scout.js is a javascript library to remove that hassle.
With Scout.js, one can easily target a specific element in the page which must trigger an XHR(XML Http Request) when a specific event is fired. This is what you do, most of the time, anyway. Otherwise, it is also easy to attach an XHR upon a "setTimeout", and so on.
Scout ( '#id-of-element' ).on ( 'click', function (params, evt, xhr) {
params.action = 'getinfo';
var sent = this.parentNode.textContent;
params.data = { ready: true, data: sent };
params.resp = function ( resp, xhr ) {
if (resp.data === sent) {
console.log ('Got exactly what we sent.');
}
};
});
// or...
setTimeout ( Scout.send ( function ( params, xhr ) { … } ), 1000 );
One thing that can bite is the fact that each Scout object only has one XHR object inside. If you do two Ajax roundtrips at the same time, with the same Scout object, one will cancel the other.
This behavior is very easy to spot. On the Web Inspector of your navigator, in
the "Network" tab, if a $action
POST request is red (or cancelled), it means
that it was killed by another XHR call.
The cure is to create another Scout object through the
var newScout = Scout.maker()
call.
All modern browsers support a mechanism for receiving a continuous, event-driven flow of information from the server. This technology is called Server-Sent Events.
The bad news about it is that it is a hassle to set up server-side. The good news is that you are using ScoutCamp, which makes it a breeze. Additionally, ScoutCamp makes it work even in IE7.
var es = Scout.eventSource('/path');
es.on('eventName', function (data) {
// `data` is a string.
});
es.onrecv(function (json) {
// `json` is a JSON object.
});
Thaddee Tyl, author of ScoutCamp.