(if you have homebrew)
$ brew install node
$ brew install redis
$ brew install mongo
(use sudo if necessary)
$ apt-get install nodejs
$ apt-get install redis-server
$ apt-get install mongodb
$ apt-get install npm
(using chocolatey)
> cinst nodejs.install
> cinst redis
> cinst mongodb
or for manual install instructions, go to:
http://nodejs.org | http://mongodb.org | http://redis.ioAnd clone the workshop repo:
$ git clone https://github.com/cacois/nodejs-three-ways
Constantine Aaron Cois and Tim Palko
Carnegie Mellon University, Software Engineering Institute
Disclaimer: Though we are employees of the Software Engineering Institute at Carnegie Mellon University, this wok was not funded by the SEI and does not reflect the work or opinions of the SEI or its customers.
{
twitter: @aaroncois,
blog: www.codehenge.net,
github: http://github.com/cacois
}
{
twitter: @timpalko,
github: http://github.com/tpalko
}
What's the big deal?
(Front-end devs and back-end devs speak the same language)
This means you have to think a bit differently
fs = require('fs');
fs.readFile('f1.txt','utf8',function(err,data){
if (err) {
// handle error
}
console.log(data);
});
fs = require('fs');
fs.readFile('f1.txt','utf8',
function(err,data) {
if (err) {
// handle error
}
console.log(data);
}
);
Act 1 - Node.js Network Services
$ mkdir node-act1
$ cd node-act1
$ touch worker.js
worker.js will be our asynchronous notification worker
console.log("Hello OSCON!");
...and run it:
$ node worker.js
$ npm install redis
The package will be installed in a local node_modules directory. Check it out:
$ ls node_modules
// import the redis module
var redis = require("redis");
// create a redis client object
var pubSubClient = redis.createClient('6379', 'localhost');
// subscribe to 'notifications' collection
pubSubClient.subscribe("notifications");
// define message handler
pubSubClient.on("message", function(channel, message) {
console.log('Received a message: ' + message);
});
$ redis-server
...and run your app in another...
$ node worker.js
$ redis-cli
redis-cli> publish notifications '{"identifier": 1, "message": "Huzzah!"}'
(integer) 1
$ node worker.js
Received a message: Huzzah!
/act-1-network-services/begin-part-2/
$ touch config.js
.. and put the following content in it:
module.exports = {
host: 'localhost',
port: 6379
}
The 'module.exports' value indicates what will be returned from a require(...) call to this file
var config = require("./config.js");
Now we can replace the Redis client instantiation:
var pubSubClient = redis.createClient('6379', 'localhost');
with:
var pubSubClient = redis.createClient(config.port, config.host);
$ touch package.json
{
"name": "act-1-network-services",
"author": "MY NAME <[email protected]>",
"version": "1.0.0",
"dependencies": {
"redis": "*",
"nodemailer": "*"
},
"engine": { "node" : ">=0.10.0" }
}
This specifies:
$ npm install
NPM will look for a local package.json and use it to set everything up
/act-1-network-services/begin-part-3/
// import the redis module
var redis = require("redis");
// read in the config file as a JavaScript object
var config = require("./config.js");
// create a redis client object
var pubSubClient = redis.createClient(config.port, config.host);
// subscribe to 'notifications' collection
pubSubClient.subscribe("notifications");
// define message handler
pubSubClient.on("message", function(channel, message) {
console.log('Received a message: ' + message);
});
// define message handler
pubSubClient.on("message", handleMessage);
function handleMessage(channel, message) {
console.log('Received a message: ' + message);
}
...
// define message handler
pubSubClient.on("message", handleMessage);
function handleMessage(channel, message) {
console.log('Received a message: ' + message);
var payload = JSON.parse(message);
acquireLock(payload, lockCallback);
}
Notice that we've identified two functions we need to write: acquireLock and the callback lockCallback
...
var client = redis.createClient(config.port, config.host);
...
function acquireLock(payload, callback) {
// create a lock id string
var lockIdentifier = "lock." + payload.identifier;
console.log("Trying to obtain lock: %s", lockIdentifier);
client.setnx(lockIdentifier, "Worker Name", function(error, success) {
if (error) {
console.log("Error acquiring lock for: %s", lockIdentifier);
return callback(error, dataForCallback(false));
}
var data = {
"acquired" : success,
"lockIdentifier" : lockIdentifier,
"payload" : payload };
return callback(data);
});
}
...
function lockCallback(data) {
if(data.acquired == true) {
console.log("I got the lock!");
// send notification!
// TODO: actually notify
console.log('I win! Sending notification: %s',
JSON.stringify(data));
}
else console.log("No lock for me :(");
}
$ node worker.js
(in another terminal)
$ redis-cli
redis-cli> publish notifications '{"identifier": 2, "message": "in a bottle"}'
// this next command will show you the keys currently in
// redis. You should see "lock.2"
redis-cli> keys *
// this command will show the value for the key "lock.2". This
// will be the name of the worker who won the lock
redis-cli> get "lock.2"
/act-1-network-services/begin-part-4/
Everyone connect with a unique worker name
We'll dispatch a notification, and see which worker wins!
function lockCallback(data) {
if(data.acquired == true) {
console.log("I got the lock!");
// send notification!
sendMessage(data);
}
else console.log("No lock for me :(");
}
function sendMessage(payload) {
console.log("Sending email notification...");
var smtpTransport = mailer.createTransport("SMTP",{
service: "Gmail",
auth: {
user: "<Google username>",
pass: "<your Google application-specific password>" }
});
var mailOptions = {
from: "<email>", // sender address
to: "<email>", // list of receivers
subject: "Notification from Node.js", // Subject line
text: "You are hereby notified!", // plaintext body
html: "<b>You are hereby notified!</b>" // html body
};
smtpTransport.sendMail(mailOptions, function(error, response){
if(error) console.log("Error sending mail: " + error);
else console.log("Message sent: " + response.message);
smtpTransport.close(); // shut down the connection pool
});
}
{
"name": "Express-Basic-Tutorial",
"description": "I'm learning nodejs express!",
"version": "0.0.1",
"private": true,
"dependencies": {
"express": "4.4.0"
}
}
$ npm info express version
Or you can simply say
"express": "*"
# Meet My App
App, audience. Audience, app.
var express = require('express');
var app = express();
app.get('/', function(req,res){
res.send("hi there");
});
var server = app.listen(3000, function(){
console.log("Listening on 3000");
});
$ npm install
$ node app.js
Listening on 3000
// install express generator
$ npm install -g express-generator
// create a new web application using ejs as the templating engine
$ express -e ejs myapp
$ cd webapp
$ npm install
$ npm start
> [email protected] start /where/you/cloned/the/repo/nodejs-three-ways/act-2-part-2-advanced-express/begin-part-1/bigwin
> node ./bin/www
checkpoint: act-2-part-2-advanced-express/begin-part-1
var express = require('express');
var path = require('path');
var favicon = require('static-favicon');
var logger = require('morgan');
var cookieParser = require('cookie-parser');
var bodyParser = require('body-parser');
var routes = require('./routes/index');
var users = require('./routes/users');
var app = express();
// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');
app.use(favicon());
app.use(logger('dev'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded());
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));
app.use('/', routes);
app.use('/users', users);
/// catch 404 and forward to error handler
app.use(function(req, res, next) {
var err = new Error('Not Found');
err.status = 404;
next(err);
});
/// error handlers
// development error handler
// will print stacktrace
if (app.get('env') === 'development') {
app.use(function(err, req, res, next) {
res.status(err.status || 500);
res.render('error', {
message: err.message,
error: err
});
});
}
// production error handler
// no stacktraces leaked to user
app.use(function(err, req, res, next) {
res.status(err.status || 500);
res.render('error', {
message: err.message,
error: {}
});
});
module.exports = app;
#!/usr/bin/env node
var debug = require('debug')('newapp');
var app = require('../app');
app.set('port', process.env.PORT || 3000);
var server = app.listen(app.get('port'), function() {
debug('Express server listening on port ' + server.address().port);
});
{
"name": "newapp",
"version": "0.0.1",
"private": true, // disallows npm from publishing
"scripts": {
"start": "node ./bin/www" // you know this guy
},
"dependencies": {
"express": "~4.2.0",
"static-favicon": "~1.0.0",
"morgan": "~1.0.0",
"cookie-parser": "~1.0.1",
"body-parser": "~1.0.0",
"debug": "~0.7.4",
"ejs": "~0.8.5"
}
}
<!DOCTYPE html>
<html>
<head>
<title><%= title %></title>
<link rel='stylesheet' href='/stylesheets/style.css' />
</head>
<body>
<h1><%= title %></h1>
<p>Welcome to <%= title %></p>
</body>
</html>
var express = require('express');
var router = express.Router();
/* GET home page. */
router.get('/', function(req, res) {
res.render('index', { title: 'Express' });
});
module.exports = router;
<!DOCTYPE html>
<html>
<head>
<title>Page Visits</title>
<link rel='stylesheet' href='/stylesheets/style.css' />
</head>
<body>
<h1>Hi, <%= my_name %></h1>
<em>we'll fill this in a minute..</em>
</body>
</html>
Again, note the <%= .. %>. This is data that will be filled in via ejs, and that data will come from our route handler.
var express = require('express');
var router = express.Router();
router.get('/', function(req, res) {
res.render('visits', { my_name: "Tim" }); // -- find our view
});
module.exports = router;
We provide data to the ejs templating engine in our render() call.
...
var routes = require('./routes/index');
var users = require('./routes/users');
var visits = require('./routes/visits'); // -- the route
...
app.use('/', routes);
app.use('/users', users);
app.use('/visits', visits); // -- the request assignment
...
checkpoint: act-2-part-2-advanced-express/begin-part-2
...
"debug": "~0.7.4",
"ejs": "~0.8.5",
"mongoose": "3.8.12"
}
}
$ npm install
var mongoose = require('mongoose')
,Schema = mongoose.Schema
,ObjectId = Schema.ObjectId;
var visitSchema = new Schema({
thread: ObjectId,
date: {type: Date, default: Date.now},
user_agent: {type: String, default: 'none'}
});
module.exports = mongoose.model('Visit', visitSchema);
...
// -- this can go near the top
var mongoose = require('mongoose');
mongoose.connect('mongodb://localhost/myapp');
...
// -- bunch this with the other requires
var Visit = require("./models/visit.js");
...
// -- we run some code inline with each request - a new request decorator
// -- put this before the route handlers
app.use(function(req, res, next){
new Visit({user_agent: req.headers['user-agent']}).save();
next();
});
app.use('/', routes);
app.use('/users', users);
app.use('/visits', visits); // -- the request assignment
...
# in a Mac OS X terminal
$ mongod --config /usr/local/etc/mongod.conf
# in Ubuntu
$ mongod --config /etc/mongodb.conf
checkpoint: act-2-part-2-advanced-express/begin-part-3
...
var Visit = require("../models/visit.js"); // -- new require
router.get('/', function(req, res) {
var query = Visit.find(); // -- only creating a query object
query.sort({date: -1}); // -- no execution here
// -- we still have a render() call
// -- but it is now in the callback to our database query execution
query.exec(function(err, visits){
// -- also note we are passing the results of our query to ejs
res.render('visits', { my_name: "Tim", visits: visits });
});
});
...
..
<h1>Hi, <% my_name %></h1>
<h2>Look who's visiting your site!</h2>
<% for(var v in visits){ %>
<p><%- visits[v].user_agent %></p>
<% } %>
..
checkpoint: act-2-part-2-advanced-express/begin-part-4
...
"debug": "~0.7.4",
"ejs": "~0.8.5",
"mongoose": "3.8.12",
"socket.io": "1.0.3"
}
}
$ npm install
Put this snippet near the top of app.js, after 'app' is defined.
var server = require('http').createServer(app);
var io = require('socket.io').listen(server);
server.listen(3001);
Here, '/chat' is the namespace.
Put this snippet somewhere after 'io' is defined in app.js.
var chat = io.of('/chat').on('connection', function(socket){
socket.on('chat', function(data){
data.color = 'green';
socket.emit('chat', data);
data.color = 'red';
socket.broadcast.emit('chat', data);
});
});
This snippet goes in index.ejs somewhere in the BODY tag..
<div id="chatlog" style="height: 200px;overflow-y:scroll;"></div>
<textarea id="chatwindow" cols="30" rows="10"></textarea>
<input id="send_chat" type="submit" value="Send" />
<script src="http://localhost:3001/socket.io/socket.io.js"></script>
<script src="//code.jquery.com/jquery-1.10.2.min.js"></script>
<script type="text/javascript">
var chat_socket = io.connect('http://localhost:3001/chat');
chat_socket.on('chat', function(data){
$("#chatlog")
.append(
$("<p style='color:" + data.color + ";'></p>")
.text(data.isay)
);
});
$(document).on('click', "#send_chat", function(e){
chat_socket.emit('chat', {isay: $("#chatwindow").val()});
$("#chatwindow").val("");
});
</script>
checkpoint: act-2-part-2-advanced-express/begin-part-5
$ npm install meteor -g
$ mkdir node-act3
$ cd node-act3
$ meteor create mapit
$ cd mapit
$ meteor
[[[[[ ~/Dropbox/Code/test/mapit ]]]]]
=> Started proxy.
=> Started MongoDB.
=> Started your app.
=> App running at: http://localhost:3000/
$ meteor mongo
By default, Meteor publishes all collections to client automatically.
This is basically a security nightmare, and can also bog down your app if you have lots of data. Let's disable it:
$ meteor remove autopublish
autopublish: removed
if (Meteor.isClient) {
Template.hello.greeting = function () {
return "Welcome to mapit.";
};
Template.hello.events({
'click input': function () {
// template data, if any, is available in 'this'
if (typeof console !== 'undefined')
console.log("You pressed the button");
}
});
}
if (Meteor.isServer) {
Meteor.startup(function () {
// code to run on server at startup
});
}
if (Meteor.isClient) {
...
}
if (Meteor.isServer) {
...
}
<head>
mapit
</head>
<body>
{{> hello}}
</body>
Hello World!
{{greeting}}
..
<body>
{{> mytemplate}}
</body>
..
{{someValue}}
Inside the template, you can access data from the server-side
But in Meteor, this data will update reactively
Markers = new Meteor.Collection('markers');
if (Meteor.isClient) {
Meteor.subscribe("markers");
Template.markerlist.markers = function() {
return Markers.find({});
};
}
if (Meteor.isServer) {
// Insert a marker if none exist
if(Markers.find().count() == 0) {
console.log("No markers found in collection - inserting one");
Markers.insert({"coords": [49.25044, -123.137]});
}
// publish collection to client
Meteor.publish("markers", function () {
// you can specify constraints in find() query, if desired
return Markers.find();
});
}
This 'marker' collection will later put points on our map
<head>
mapit
</head>
<body>
{{> markerlist}}
</body>
{{#each markers}}
Marker
-- Coordinates: {{coords}}
{{/each}}
Since the Marker collection is shared between client and server, we can access it from client-side debug tools.
> Markers.insert({"coords": [48, -123]});
You'll see this marker appear in the list of both clients
/act-3-meteor-realtime-webapps/begin-part-2/
We're going to use Leaflet.js. Since Meteor is a highly customized framework, adding a standard JavaScript library may be a bit tricky...
Meteorite is a package manager for Meteor, similar to NPM
Go ahead and install the meteorite client:
$ npm install -g meteorite
You can search Meteorite packages at [PUT LINK HERE]
Since there is already a Meteorite package for Leaflet, this becomes very easy:
$ mrt add leaflet
Add the following to the client section of mapit.js:
Template.map.rendered = function() {
L.Icon.Default.imagePath = 'packages/leaflet/images';
// initialize Leaflet map object
window.map = L.map('map', {
doubleClickZoom: false,
zoomControl:false
}).setView([45.52854352208366,-122.66302943229674], 13);
L.tileLayer.provider('Thunderforest.Outdoors').addTo(map);
var markers = Markers.find(); // db cursor Markers
// Watch the Markers collection for 'add' action
markers.observe({
// When a new marker is added collection, add it to the map
added: function(marker) {
L.marker(marker.coords).addTo(map);
}
});
}
Update mapit.html:
<head>
mapit
</head>
<body>
{{> map}}
{{> markerlist}}
</body>
<template name="map">
Mapit!
</template>
...
mapit.css
#map {
height: 700px;
margin: 0 auto;
}
Next, add a double-click handler to the map template, so we can add markers through the UI.
Edit the Template.map.rendered function in mapit.js to add this feature:
..
// assign click event to add markers
window.map.on('dblclick', function(event, object) {
// We're storing the marker coordinates in an extensibel JSON
// data structure, to leave room to add more info later
console.log("inserting marker: " + event.latlng);
Markers.insert({"coords": [event.latlng.lat,event.latlng.lng]});
});
..
/act-3-meteor-realtime-webapps/begin-part-3/
Let's add one last feature - Users
You've seen Meteorite packages, but Meteor has its own subset of packages included, including packages for built-in user auth
Add user auth funcitonality this way:
$ meteor add accounts-password
$ meteor add accounts-ui
Update mapit.html:
<body>
{{> loginButtons}}
{{> map}}
{{> markerlist}}
</body>
...
You can get the current user with Meteor.user(), or their userid by Meteor.userId()
Make the following changes:
mapit.js -- window.map.on('dblclick'...)
window.map.on('dblclick', function(event, object) {
// We're storing the marker coordinates in an extensibel JSON
// data structure, to leave room to add more info later
console.log("inserting marker: " + event.latlng);
// ADDED
// check for user, override if no one is logged in
var username;
if(Meteor.user()) username = Meteor.user().emails[0].address;
else username = "anonymous";
Markers.insert({"coords": [event.latlng.lat,event.latlng.lng],
"user": username }); // CHANGED
});
mapit.js -- markers.observe()
markers.observe({
// When a new marker is added collection, add it to the map
added: function(marker) {
// CHANGE
L.marker(marker.coords).addTo(map).bindPopup(marker.user);
}
});
mapit.html -- markerlist template
{{#each markers}}
Marker
-- Coordinates: {{coords}}
-- User: {{user}}
{{/each}}
And so, we leave you to boldly go, and
All code and slides from this workshop can be found at:
https://github.com/cacois/nodejs-three-ways