In my previous step towards a node server implementing a handy minimal WebGL viewer, I started added support for handling a REST API POST request.
Now that we have transferred and received data, let's implement code to display it:
My initial tests were performed by invoking the API from an interactive HTML form posting the values of its input fields.
For that, I just need the bodyParser.urlencoded
middleware.
We will certainly be wanting to handle JSON data as well, so I went ahead and added the middleware for that too, bodyParser.json
.
A couple of examples of adding both are discussed in the Stack Overflow thread about bodyParser deprecated in express 4.
The very first incarnation of the Node WebGL viewer already shows how to serve static content, e.g., an unmodified HTML file.
Serving content that is updated dynamically is called 'rendering' in this context.
A number of Express templating, viewing and rendering engines are available, e.g.:
And there are many more to choose from...
In my simple case, I could also just set up a HTML skeleton myself, populate the handful of variable bits and pieces in it using simple string replacement operations, and return that using response.send
or response.sendFile
.
Just for the heck of it, I'll dig into one of these frameworks and give it a swing.
Which one to choose?
Here is a comparison of Jade vs EJS, and an explanation how to replace Jade with Hogan.
To cut a long story short, I initially tried Express Handlebars:
$ npm install express-handlebars
I added these lines to the node server mainline to install express-handlebars
as a renderer for files with a handlebars
filename extension:
var exphbs = require('express-handlebars'); app.engine('handlebars', exphbs()); app.set('view engine', 'handlebars');
I ran into a number of issues due to my lack of understanding.
The hardest stumbling block was probably the fact that I cannot reference other JavaScript modules using <script src="...">
in the rendered template files, e.g. to pull in the TWGL implementation from twgl-full.min.js
.
Due to that, I ended up switching to Swig, probably making no difference to that specific problem.
I replaced the express-handlebars engine by swig like this:
var swig = require('swig'); app.engine('swig', swig.renderFile); app.set('view engine', 'swig'); app.set('views', __dirname + '/views'); app.set('view cache', false); swig.setDefaults({ cache: false });
More importantly, and probably valid in either case, I finally solved the issue by copying the contents of twgl-full.js
right into my template file.
The data received by the node server from the submitting form after being handled is contained in the request body.
I can pass it on to the template file very easily like this in the API implementation, without touching its internals at all:
res.render('viewer', req.body);
I was not sure that handlebars or swig replace code snippets within the script sections of the template file.
Therefore, I played it safe and transferred the request body data to hidden HTML elements instead:
<head> <style> html, body, canvas { margin: 0px; width: 100%; height: 100%; overflow: hidden; } p { display: none; } </style> </head> ... <body onload="start_render()"> <p id="p">{{ position}}</p> <p id="n">{{normal}}</p> <p id="i">{{indices}}</p> </body>
As you can see, I also added an onload attribute to the body tag to call the start_render
JavaScript function.
That function retrieves the data from the three HTML elements as strings and converts them to arrays of floats and integers like this:
var position = document.getElementById('p').innerHTML; var normal = document.getElementById('n').innerHTML; var indices = document.getElementById('i').innerHTML; position = make_float_array(position); normal = make_float_array(normal); indices = make_int_array(indices);
Here are the two conversion helper functions:
function make_int_array(s) { return s.split(',') .map( function(a){ return parseInt(a, 10); }); } function make_float_array(s) { return s.split(',') .map( function(a){ return parseFloat(a); }); }
The entire node.js server now consists of three main pieces:
Here is the complete mainline implementation in server.js:
var express = require('express'); var app = express(); app.set('port', (process.env.PORT || 5000)); app.use(express.static(__dirname + '/public')); app.get('/', function(req, res) {}); // leads to public/index.html var bodyParser = require('body-parser'); app.use( bodyParser.json() ); app.use( bodyParser.urlencoded({ extended: true }) ); var swig = require('swig'); app.engine('swig', swig.renderFile); app.set('view engine', 'swig'); app.set('views', __dirname + '/views'); app.set('view cache', false); swig.setDefaults({ cache: false }); var api = require('./routes/api'); app.get('/api/v1', api.v1); app.post('/api/v1', api.v1); app.get('/api/v2', api.v2); app.post('/api/v2', api.v2); app.listen(app.get('port'), function() { console.log("Node WebGL app is running at localhost:" + app.get('port')); });
Calling the server with no API route appended is handled and leads to the static index.html provided in the public folder.
The REST API is accessed through the routes api/v1
and api/v2
.
Both respond to both GET and POST requests.
The (trivial) v1 API remains completely unchanged from the initial implementation, so the previous original test client and blog post describing it continue to run unmodified and unaffected.
A GET request to the REST API v2 again just replies with an explanatory message.
The POST request to the v2 API finally brings up the data driven WebGL viewer by rendering the viewer.swig template file as described above.
This is all implemented in a few lines of code in routes/api.js
like this:
exports.v1 = function(req, res) { if (req.method == 'GET') { res.send('API v1 GET: Hello World!'); } else if (req.method == 'POST') { res.send('API v1 POST: ' + JSON.stringify(req.body)); } }; exports.v2 = function(req, res) { if (req.method == 'GET') { res.send('API v2 GET: Here belongs a succinct ' + 'explanation how to use The Building Coder ' + 'WebGL Viewer REST API v2...'); } else if (req.method == 'POST') { console.log('API v2 POST: ' + JSON.stringify(req.body)); res.render('viewer', req.body); } };
Finally, we have the viewer swig template file viewer.swig:
<!DOCTYPE html> <html> <head> <meta charset="utf8"> <title>The Building Coder WebGL Viewer</title> <style> html, body, canvas { margin: 0px; width: 100%; height: 100%; overflow: hidden; } p { display: none; } </style> <!-- sourcing a script does not work like this when rendering the file with swig: --> <!-- <script src="twgl-full.min.js"></script> --> <!-- copy the contents of twgl-full.min.js inline instead: --> <script> // ... contents of twgl-full.min.js </script> <script id="vs" type="notjs"> // ... vertex shader code </script> <script id="fs" type="notjs"> // ... fragment shader code </script> <script> "use strict"; var m4; var gl; var programInfo; function make_int_array(s) { return s.split(',') .map( function(a){ return parseInt(a, 10); }); } function make_float_array(s) { return s.split(',') .map( function(a){ return parseFloat(a); }); } function start_render() { var position = document.getElementById('p').innerHTML; var normal = document.getElementById('n').innerHTML; var indices = document.getElementById('i').innerHTML; position = make_float_array(position); normal = make_float_array(normal); indices = make_int_array(indices); var arrays = { position: position, normal: normal, //texcoord: texcoord, indices: indices }; twgl.setAttributePrefix("a_"); m4 = twgl.m4; gl = twgl.getWebGLContext(document.getElementById("c")); programInfo = twgl.createProgramInfo(gl, ["vs", "fs"]); var bufferInfo = twgl.createBufferInfoFromArrays(gl, arrays); // ... rest of TWGL rendering code } </script> </head> <body onload="start_render()"> <p id="p">{{ position}}</p> <p id="n">{{normal}}</p> <p id="i">{{indices}}</p> <canvas id="c"></canvas> </body> </html>
Just as in previous versions, I implemented an interactive online testing framework for this.
It includes code to support simple toggling between local and global testing.
Below are the two HTML forms for testing the current version v2
of the API.
They use JavaScript helpers to define the base URL and to open a new window to host the server:
// to switch between heroku and localhost, // toggle the Boolean 'local' variable below function get_base_url_0016() { var local = false; return local ? 'http://localhost:5000' : 'https://nameless-harbor-7576.herokuapp.com'; } function get_api_url_0016() { var api_route = '/api/v2'; return get_base_url_0016() + api_route; } function open_node_server_window_0016() { var w = 400; var h = 400; var features = 'width=' + w.toString() + ',height=' + h.toString(); window.open(get_api_url_0016(), 'node_server_0016', features); } window.onload = function(e) { var baseurl = get_base_url_0016(); var apiurl = get_api_url_0016(); document.getElementById('form_0016').action = apiurl; document.getElementById('form2_0016').action = apiurl; document.getElementById('iframe_0016').src = baseurl; }
The open_node_server_window_0016
function is called from the submit button onclick event before the form routes the node server response to it through the form action
– set by the JavaScript above on window load – and target
attributes.
Here is the HTML source of the first form, for testing the POST request:
<form method="post" id="form_0016" target="node_server_0016"> <p style="text-align: center">Form submitting a HTTP POST request:</p> <table> <tr><td>position:</td><td><input type="text" name="position" value="1, 1, -1, 1, 1, 1, 1, -1, 1, 1, -1, -1, -1, 1, 1, -1, 1, -1, -1, -1, -1, -1, -1, 1, -1, 1, 1, 1, 1, 1, 1, 1, -1, -1, 1, -1, -1, -1, -1, 1, -1, -1, 1, -1, 1, -1, -1, 1, 1, 1, 1, -1, 1, 1, -1, -1, 1, 1, -1, 1, -1, 1, -1, 1, 1, -1, 1, -1, -1, -1, -1, -1"></td></tr> <tr><td>normal:</td><td><input type="text" name="normal" value="1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1"></td></tr> <tr><td>indices:</td><td><input type="text" name="indices" value="0, 1, 2, 0, 2, 3, 4, 5, 6, 4, 6, 7, 8, 9, 10, 8, 10, 11, 12, 13, 14, 12, 14, 15, 16, 17, 18, 16, 18, 19, 20, 21, 22, 20, 22, 23"></td></tr> <tr><td></td><td><input type="submit" onclick="open_node_server_window_0016()"></td></tr> </table> </form>
Finally, here it is for you to try out live:
As said, calling the same API with the HTTP GET verb just responds with a simple text message:
Once again, the original non-API-driven version and the API version v1 still work exactly as before; I am enhancing the REST API step by step while preserving complete upward compatibility and keeping the original server running continuously at the same time:
The complete node server implementation is available from the NodeWebGL GitHub repo, and the version discussed here is 1.0.0.4.