👉 Overview of all Lecture 6 materials
At times we use ☝️ and 👇 to make it clear whether an explanation belongs to the code snippet above or below the text. The
- Learning goals
- Organization and reusability of Node.js code
- Creating and using a (useful) module
- Middleware in Express
- Routing
- Templating with EJS
- Self-check
- Organize Node.js code into modules.
- Understand and employ the concept of middleware.
- Employ routing.
- Employ templating.
So far, we have organized all server-side code in a single file, which is only a feasible solution for small projects. In larger projects, this quickly ends in unmaintainable code, especially when working in a team.
These issues were recognized early on by the creators of Node.js. For this reason, they introduced the concept of modules. A Node.js module is (1) a single file or (2) a directory of files and all code contained in it.
By default, no code in a module is accessible to other modules. Any property or method that should be visible to other modules has to be explicitly marked as such - you will learn shortly how exactly. Node.js modules can be published to npmjs.com, the most important portal to discover and share modules with other developers. This is Express' page on npm:
Screenshot taken on October 15, 2018.
☝️ You see here that modules often depend on a number of other modules (in this case: 30 dependencies). As Express is a very popular module, it is listed as dependency in more than 27,000 other modules.
You already know how to install modules, e.g. npm install winston
installs one of the most popular Node loggers. You can also use the command line to search for modules to install, e.g. npm search winston
.
While it is beyond the scope of this course to dive into the details of the npm registry, it should be mentioned that it is not without issues; the story of how 17 lines of code - a single npm module - nearly broke much of the modern web for half a day can be found here.
In Node.js each file is its own module. This means that the code we write in a file does not pollute the global namespace. In Node.js we get this setup "for free". When we write client-side JavaScript, we have to work hard to achieve the same effect (recall the module pattern covered in Lecture 3).
The module system works as follows: each Node.js file can access its so-called module definition through the module
object. The module object is your entry point to modularize your code. To make something available from a module to the outside world, module.exports
or its alias exports
is used as we will see in a second. The module
object looks as follows 👇:
module {
id: '.',
exports: {},
parent: null,
filename: '/path/to/nodejs-file.js',
loaded: false,
children: [],
paths:
[ '/Users/path/path2/node_modules',
'/Users/path/node_modules',
'/Users/node_modules' ]
}
To see this for yourself, create a Node.js script containing only the line console.log(module);
and run it. We see that currently nothing is exported (exports
is empty) from this module (which makes sense, it just prints a single line).
Once you have defined your own module, the globally available require
function is used to import a module. At this stage, you should recognize that you have been using Node.js modules since your first attempts with Node.js.
Here is a graphical overview of the connection between require
and module.exports
:
An application uses the require
function to import module code. The module itself populates the module.exports
variable to make certain parts of the code base in the module available to the outside world. Whatever was assigned to module.exports
(or its alias exports
) is then returned to the application when the application calls require()
.
Let's consider files foo.js
👇:
var fooA = 1; //LINE 1
module.exports = "Hello!"; //LINE 2
module.exports = function() { //LINE 3
console.log("Hi from foo!"); //LINE 4
}; //LINE 5
and bar.js
👇:
var foo = require('./foo');
foo(); //CASE 1: Hi from foo!
require('./foo')(); //CASE 2: Hi from foo!
console.log(foo); //CASE 3: [Function]
console.log(foo.toString());//CASE 4: function () {console.log("Hi from foo!");}
console.log(fooA); //CASE 5: ReferenceError
console.log(module.exports);//CASE 6: {}
☝️ Here, foo.js
is our foo
module and bar.js
is our application that imports the module to make use of the module's functionality (you can run the script as usual with node bar.js
).
- In the
foo
module, we define a variablefooA
in line 1. In lines 2 and 3, you can see how a module usesmodule.exports
to make parts of its code available: in line 2, we assign a string tomodule.exports
, in line 3 we make a new assignment tomodule.exports
and define a function that prints out Hi from foo! on the console. Which of the two assignments will our applicationbar
end up with? - In
bar.js
the first line calls therequire()
function and assigns the returned value to the variablefoo
. In line 1 we used as argument./foo
instead of./foo.js
- you can use both variants. The dot-slash indicates thatfoo.js
resides in the current directory.
Node.js runs the referenced JavaScript file ☝️ (here: foo.js
) in a new scope and returns the final value of module.exports
. What then is the final value after executing foo.js
? It is the function we defined in line 3.
As you can see in lines 2 and beyond of bar.js
there are several ways to access whatever require
returned:
- CASE 1️⃣ We can call the returned function and this results in Hi from foo! as you would expect.
- CASE 2️⃣ We can also combine lines 1 and 2 into a single line with the same result.
- CASE 3️⃣ If we print out the variable
foo
, we learn that it is a function. - CASE 4️⃣ Using the
toString()
function prints out the content of the function. - CASE 5️⃣ Next, we try to access
fooA
- a variable defined infoo.js
. Remember that Node.js runs each file in a new scope and only what is assigned tomodule.exports
is available. Accordingly,fooA
is not available inbar.js
and we end up with a reference error. - CASE 6️⃣ Finally, we can also look at the
module.exports
variable ofbar.js
- this is always available to a file in Node.js. Inbar.js
we have not assigned anything tomodule.exports
and thus it is an empty object.
This module setup also explains why require
is blocking: once a call to require()
is made, the referenced file's code is executed and only once that is done, does require()
return. This is in contrast to the usual asynchronous nature of Node.js functions.
Let's now consider what happens if a module is imported more than once 👇:
var t1 = new Date().getTime();
var foo1 = require('./foo');
console.log(new Date().getTime() - t1); // > 0
var t2 = new Date().getTime();
var foo2 = require('./foo');
console.log(new Date().getTime() - t2); // approx 0
☝️ Here, we execute require('./foo')
twice and log both times the time it takes for require
to return. The first time the line require(foo.js)
is executed, the file foo.js
is read from disk (this takes some time, at least a few milliseconds). In subsequent calls to require(foo.js)
, however, the in-memory object is returned (takes less than a millisecond). Thus, module.exports
is cached.
Every Node.js file has access to module.exports
. If a file does not assign anything to it, it will be an empty object, but it is always present. Instead of module.exports
we can use exports
as exports
is an alias of module.exports
. This means that the following two code snippets are equivalent 👇:
//SNIPPET 1
module.exports.foo = function () {
console.log('foo called');
};
module.exports.bar = function () {
console.log('bar called');
};
//SNIPPET 2
exports.foo = function () {
console.log('foo called');
};
exports.bar = function () {
console.log('bar called');
};
☝️ In the first snippet, we use module.exports
to make two functions (foo
and bar
) accessible to the outside world. In the second snippet, we use exports
to do exactly the same. Note that in these two examples, we do not assign something to exports
directly, i.e. we do not write exports = function ....
. This is in fact not possible: if you directly assign a function or object to exports
, then its reference to module.exports
will be broken. You can only assign directly to module.exports
, for instance, if you only want to make a single function accessible.
In the example above ☝️, foo.js
is a module we created. Not a very sensible one, but still, it is a module. Modules can be either:
- a single file, or,
- a directory of files, one of which is
index.js
.
A module can contain other modules (that's what require
is for) and should have a specific purpose. For instance, we can create a grade rounding module whose functionality is the rounding of grades in the Dutch grading system 👇:
/* not exposed */
function roundGrade(grade) {
return Math.round(grade);
}
/* not exposed */
function roundGradeUp(grade) {
return Math.round(0.5+parseFloat(grade));
}
/* exposed */
exports.maxGrade = 10;
exports.roundGradeUp = roundGradeUp;
exports.roundGradeDown = function(grade) {
return Math.round(grade-0.5);
}
We can use the grading module in an Express application as follows 👇:
var express = require("express");
var url = require("url");
var http = require("http");
var grading = require("./grades"); // our module file resides in the current directory
var app;
var port = process.argv[2];
app = express();
http.createServer(app).listen(port);
app.get("/round", function (req, res) {
var query = url.parse(req.url, true).query;
var grade = ( query["grade"]!=undefined) ? query["grade"] : "0";
//accessing module functions
res.send("UP: " + grading.roundGradeUp(grade) +", DOWN: "+ grading.roundGradeDown(grade));
});
Middleware components are small, self-contained and reusable code pieces across applications. Imagine you have written an Express application with tens of different routes and now decide to log every single HTTP request coming in. You could add 2-3 lines of code to every route to achieve this logging OR you write a middleware logging component that gets called before any other route is called. How exactly this works in Express is discussed here.
Middleware components have three parameters:
- an HTTP request object,
- an HTTP response object, and,
- an optional callback function (
next()
) to indicate that the component is finished and the dispatcher (which orchestrates the order of middleware components) can move on to the next component.
Middleware components have a number of abilities:
- Execute code.
- Change the request and response objects.
- End the request-response cycle.
- Call the next middleware function in the middleware stack.
As a concrete example, imagine an Express application with a POST route /user/addHabit
👇:
☝️ The first middleware to be called is the logging component, followed by the bodyParser component which parses the HTTP request body; next, the static component is probed (is there a static resource that should be served to the user?) and if no static resource exists, a final custom component is called. When an HTTP response is sent (res.end
), the middleware call chain is complete.
Our goal is to create a logger that records every single HTTP request made to our application as well as the URL of the request. We need to write a function that accepts the HTTP request and response objects as arguments and next
as callback function. Here, we write two functions to showcase the use of several middleware components 👇:
var express = require('express');
//a middleware logger component
function logger(request, response, next) {
console.log('%s\t%s\t%s', new Date(), request.method, request.url);
next(); //control shifts to next middleware function
}
//a middleware delimiter component
function delimiter(request, response, next){
console.log("-----------------");
next();
}
var app = express();
app.use(logger); //register middleware component
app.use(delimiter);
app.listen(3001);
The server's console output will look something like this:
Mon Oct 22 2018 21:37:13 GMT+0200 (CEST) GET /
-----------------
Mon Oct 22 2018 21:37:23 GMT+0200 (CEST) GET /greetme
-----------------
Mon Oct 22 2018 21:37:25 GMT+0200 (CEST) GET /hello
-----------------
☝️ Importantly, next()
enables us to move on to the next middleware component while app.use(...)
registers the middleware component with the dispatcher. Try out this code for yourself and see what happens if:
app.use
is removed;- the order of the middleware components is switched, i.e. we first add
app.use(delimiter)
and thenapp.use(logger)
; - in one or both of the middleware components the
next()
call is removed.
You will observe different behaviours of the application that make clear how the middleware components interact with each other and how they should be used in an Express application.
In the example above we did not actually sent an HTTP response back, but you know how to write such a code snippet yourself. So far, none of our routes have contained next
for a simple reason: all our routes ended with an HTTP response being sent and this completes the request-response cycle; in this case there is no need for a next()
call.
In the node-component-ex example application, we add an authorisation component to a simple Todo application back-end: only clients with the correct username and password (i.e. authorised users) should be able to receive the list of todos when requesting them. We achieve this by adding a middleware component that is activated for every single HTTP request and determines:
- whether the HTTP request contains an authorization header (if not, access is denied);
- and whether the provided username and password combination is the correct one.
Before we dive into the code details, install and start the server as explained here. Take a look at app.js
before proceeding.
Once the server is started, open another terminal and use curl
(a command line tool that provides us with a convenient way to include username and password as you will see in a second):
- Request the list of todos without authorisation, i.e.
curl http://localhost:3000/todos
- you should see anUnauthorized access
error. - Request the list of todos with the correct username and password (as hardcoded in our demonstration code):
curl --user user:password http://localhost:3000/todos
. The option--user
allows us to specify the username and password to use for authentication in the[USER]:[PASSWORD]
format. This request should work and you should receive the list of todos. - Request the list of todos with an incorrect username/password combination:
curl --user test:test http://localhost:3000/todos
. You should receive aWrong username/password combination
error.
Having found out how the code works, let us look at the authorization component. We here define it as an anynymous function as argument to app.use
👇:
app.use(function (req, res, next) {
var auth = req.headers.authorization;
if (!auth) {
return next(new Error("Unauthorized access!"));
}
//extract username and password
var parts = auth.split(' ');
var buf = new Buffer(parts[1], 'base64');
var login = buf.toString().split(':');
var user = login[0];
var password = login[1];
//compare to 'correct' username/password combination
//hardcoded for demonstration purposes
if (user === "user" && password === "password") {
next();
}
else {
return next(new Error("Wrong username/password combination!"));
}
});
☝️ This code snippet first determines whether an authorization header was included in the HTTP request (accessible at req.headers.authorization
). If no header was sent, we pass an error to the next()
function, for Express to catch and process, i.e. sending the appropriate HTTP response. If an authorization header is present, we now extract the username and password and determine whether they match user
and password
respectively. If they match, next()
is called and the next middleware component processes the request, which in our app.js
file is app.get("/todos",...)
.
One of the design goals of middleware is reusability across applications: once we define a logger or an authorization component, we should be able to use it in a wide range of applications without additional engineering effort. Reusable code usually has parameters that can be set. To make this happen, we can wrap the original middleware function in a setup function which takes the function parameters as input 👇:
function setup(options) {
// setup logic
return function(req, res, next) {
// middleware logic
}
}
app.use( setup({ param1 : 'value1' }) );
Routing is the mechanism by which requests are routed to the code that handles them. The routes are specified by a URL and HTTP method (most often GET
or POST
). You have employed routes already - every time you wrote app.get()
you specified a so-called route handler and wrote code that should be executed when that route (or URL) is called.
This routing paradigm is a significant departure from the past, where file-based routing was commonly employed. In file-based routing, we access files on the server by their actual name, e.g. if you have a web application with your contact details, you typically would write those details in a file contact.html
and a client would access that information through a URL that ends in contact.html
. Modern web applications are not based on file-based routing, as is evident by the fact URLs these days do not contain file endings (such as .html
or .asp
) anymore.
In terms of routes, we distinguish between request types (GET /user
differs from POST /user
) and request routes (GET /user
differs from GET /users
).
Route handlers are middleware. So far, we have not introduced routes that include next
as third argument, but since they are middleware, we can indeed add next
as third argument.
Let's look at an example where this makes sense 👇:
//clients requests todos
app.get("/todos", function (req, res, next) {
//hardcoded “A-B” testing
if (Math.random() < 0.5) {
return next();
}
console.log("Todos in schema A returned");
res.json(todosA);
});
app.get("/todos", function (req, res,next) {
console.log("Todos in schema B returned");
res.json(todosB);
});
☝️ We define two route handlers for the same route /todos
. Both anonymous functions passed as arguments to app.get()
include the next
argument. The first route handler generates a random number between 0 and 1 and if that generated number is below 0.5, it calls next()
in the return statement. If the generated number is >=0.5, next()
is not called, and instead a response is sent to the client making the request.
If next
was used, the dispatcher will move on to the second route handler and here, we do not call next
, but instead send a response to the client.
What we have done here is to hardcode so-called A/B testing. Imagine you have an application and two data schemas and you aim to learn which schema your users prefer. Half of the clients making requests will receive schema A and half will receive schema B.
We can also provide multiple handlers in a single app.get()
call 👇:
//A-B-C testing
app.get('/todos',
function(req,res, next){
if (Math.random() < 0.33) {
return next();
}
console.log("Todos in schema A returned");
res.json(todosA);
},
function(req,res, next){
if (Math.random() < 0.66) {
return next();
}
console.log("Todos in schema B returned");
res.json(todosB);
},
function(req,res){
console.log("Todos in schema C returned");
res.json(todosC);
}
);
☝️ This code snippet contains three handlers - and each handler will be used for about one third of all clients requesting /todos
. While this may not seem particularly useful at first, it allows you to create generic functions that can be used in any of your routes, by dropping them into the list of functions passed into app.get()
. What is important to understand when to call next
and why in this setting we have to use a return
statement - without it, the function's code would be continued to be executed.
When we specify a path (like /todos
) in a route, the path is eventually converted into a regular expression (short: regex) by Express. Regular expressions are patterns to match character combinations in strings. They are very powerful and allow us to specify matching patterns instead of hard-coding all potential routes. For example, we may want to allow users to access todos via a number of similar looking routes (such as /todos
, /toodos
, /todo
). Instead of duplicating code three times for three routes, we can employ a regular expression to capture all of those similarly looking routes in one expression.
Express distinguishes three different types of route paths: strings, string patterns and regular expressions. So far, we have employed just strings to set route paths. String patterns are routes defined with strings and a subset of the standard regex meta-characters, namely: + ? * ( ) []
. Regular expressions contain the full range of common regex functionalities (routes defined through regular expressions are enclosed in / /
, not ' '
), allowing you to create arbitrarily complex patterns. If you are curious how complex regular expressions can become, take a look at the size of regular expressions to validate email addresses.
Express' string pattern meta-characters have the following interpretations:
Character | Description | Regex | Matched expressions |
---|---|---|---|
+ | one or more occurrences | ab+cd | abcd, abbcd, … |
? | zero or one occurrence | ab?cd | acd, abcd |
* | zero or more occurrences of any char (wildcard) | ab*cd | abcd, ab1234cd, … |
[…] | match anything inside for one character position | ab[cd]?e | abe, abce, abde |
(…) | boundaries | ab(cd)?e | abe, abcde |
It is important to realize that the use of *
in Express' string patterns is somewhat unique. In most other languages/frameworks, whenever *
is mentioned in relation to regular expressions, it refers to zero or more occurrences of the preceding element. In Express' string patterns, *
is a wildcard.
These meta-characters can be combined as seen here 👇
app.get('/user(name)?s+', function(req,res){
res.send(…)
});
In order to test your own string patterns, you can set up a simple server-side script such as the following 👇
var express = require("express");
var app = express();
app.get('/user(name)?s+',function(req, res){
res.send("Yes!");
});
app.get('*', function(req,res){
res.send("No!");
});
app.listen(3001);
Once a string pattern is coded, you can simply open the browser, and test different routes, receiving a Yes! for a matching route and a No! for a non-matching route.
Apart from regular expressions, routing parameters can be employed to enable variable input as part of the route. Consider the following code snippet 👇:
var todoTypes = {
important: ["TI1506","OOP","Calculus"],
urgent: ["Dentist","Hotel booking"],
unimportant: ["Groceries"],
};
app.get('/todos/:type', function (req, res, next) {
var todos = todoTypes[req.params.type];
if (!todos) {
return next(); // will eventually fall through to 404
}
res.send(todos);
});
☝️ We have defined an object todoTypes
which contains important
, urgent
and unimportant
todos. We can hardcode routes, for example /todos/important
to return only the important todos, /todos/urgent
to return the urgent todos only and /todos/unimportant
to return the unimportant todos. This is not a maintainable solution though (just think about objects with hundreds of properties). Instead, we create a single route that, dependent on a routing parameter, serves different todos. This is achieved in the code snippet shown here. The routing parameter type (indicated with a starting colon :
) will match any string that does not contain a slash. The routing parameter is available to us in the req.params
object. Since the route parameter is called type
, we access it as req.params.type
. The code snippet checks whether the route parameter matches a proprty of the todoTypes
object and if it does, the correct todo list is returned in an HTTP response. If the parameter does not match any property of the todoTypes
object, we make a call to next and move on the next route handler - e.g. a 404 page.
Routing parameters can have various levels of nesting 👇:
var todoTypes = {
important: {
today: ["TI1506"],
tomorrow: ["OOP", "Calculus"]
},
urgent: {
today: ["Dentist", "Hotel booking"],
tomorrow: []
},
unimportant: {
today: ["Groceries"],
tomorrow: []
}
};
app.get('/todos/:type/:level', function (req, res, next) {
var todos = todoTypes[req.params.type][req.params.level];
if (!todos) {return next();}
res.send(todos);
});
☝️ We do not only use the importance type for our todos, but also partition them according to their due date. The route handler now contains two routing parameters, :type
and :level
. Both are accessible through the HTTP request object. We now use the two parameters to access the contents of the todoTypes
object. If the two parameters do not match properties of todoTypes
, we call next()
and otherwise, send the requested response.
Lastly, a word on how to organize your routes. Adding routes to the main application file becomes unwieldy as the codebase grows. Based on the knowledge of this lecture, you can move routes into a separate module. All you need to do is to pass the app
instance into the module (here: routes.js
) as an argument 👇:
/* routes.js */
module.exports = function(app){
/* Route 1 */
app.get('/', function(req,res){
res.send(...);
}))
/* Route 2, ... */
};
/* app.js */
//...
require('./routes.js')(app);
//...
☝️ routes.js
is a route module in which we assign a function to module.exports
which contains the routes. In app.js
we add the routes to our application through the require
function and passing app
in as an argument.
When we started our journey with Node.js and Express, we discussed that writing HTML in this manner 👇:
is a poor choice, as the code quickly becomes unmaintainable, hard to debug and generally a pain to work with.
One approach to solve this problem is the use of Ajax: the HTML code is blank in the sense that it does not contain any user-specific data. The HTML and JavaScript (and other resources) are sent to the client and the client makes an Ajax request to retrieve the user-specific data from the server-side.
With templating, we are able to directly send HTML with user-specific data to the client and thus remove the extra request-response cycle that Ajax requires.
With templates, our goal is to write as little HTML by hand as possible. Instead,
- we create a HTML template void of any data,
- add data, and
- from template+data generate a rendered HTML view.
This approach keeps the code clean and separates the coding logic from the presentation markup. Templates fit naturally into the Model-View-Controller paradigm which is designed to keep logic, data and presentation separate.
This concept exists in several languages and even for Node.js alone, several template engines exist. In this course, we teach the basics of EJS version 2 - Embedded JavaScript - a relatively straightforward template engine and language.
Let's take a first look at EJS. For this exercise, we will use Node's REPL (Read-Eval-Print Loop). It is the Node.js shell; any valid JavaScript which can be written in a script can be passed to the REPL as well. It useful for experimenting with Node.js, and figuring out some of JavaScript's more eccentric behaviors.
To start the REPL, simply type node
in the terminal and the Node shell becomes available, indicated by >
. Start your REPL and type each of the JavaScript code lines below 👇 into the shell, ending each line with <ENTER>
.
If you receive an Error: Cannot find module 'ejs'
error after the var ejs = require('ejs');
line, exit the shell (to do so, type .exit
) and install the ejs
module. To do this, run npm install ejs
and then go back to the REPL.
If you want to avoid the constant Undefined
messages on the REPL (which are simply the return values of the commands entered), start the REPL with node -e "require('repl').start({ignoreUndefined:true})"
.
var ejs = require('ejs');
var template = '<%= message %>'; //<%= outputs the value into the template (HTML escaped)
var context = {message: 'Hello template!'};
console.log(ejs.render(template, context));
☝️ Here, we first make the EJS object available via require()
. Next, we define our template string. In this template we aim to replace the message with the actual data. Our context
variable holds an object with a property message
and value Hello template!
. Lastly, we have to bring the template and the data together by calling ejs.render()
. The output will be the rendered view. The template contains <%=
, a so-called scriptlet tag to indicate the start of an element to be replaced with data as well as an ending tag %>
.
Ther are two types of scriptlet tags that output values:
<%= ... %>
outputs the value into the template in HTML escaped form.<%- ... %>
outputs the value into the template in unescaped form. This enables cross-site scripting attacks, which we will discuss in Lecture 8.
In order to see the difference between the two types of tags, go back to Node's REPL and try out the following code snippet twice, each time with a different variant of the template
string 👇:
var ejs = require('ejs');
var template = '<%- message %>'; //UNESCAPED
//var template = ‘<%= message %>'; //ESCAPED
var context = {message: "<script>alert('hi!');</script>"};
console.log(ejs.render(template, context));
☝️ The HTML-escaped variant produces the output <script>alert('hi');</script>
while the un-escaped template
variant produces <script>alert('hi');</script>
. In the latter case, this is code that the browser will execute.
In order to make templates maintainable, it is possible to provide user-defined functions to a template as follows 👇:
var ejs = require('ejs');
var people = ['wolverine', 'paul', 'picard'];
var transformUpper = function (inputString) { return inputString.toUpperCase();}
var template = '<%= helperFunc(input.join(", ")); %>';
var context = {
input: people,
helperFunc: transformUpper //user-defined function
};
console.log(ejs.render(template, context));
☝️ transformUpper
is a user-defined function that expects a string as input and transforms it to uppercase. The context
object has a property helperFunc
which is assigned a user-defined function as value. In the template, we use the properties of the context
object; ejs.render
brings template and data together.
To make templates even more flexible, we can incorporate JavaScript in the template, using the <%
scriptlet tag 👇:
var ejs = require('ejs');
var template = "<% if(movies.length>2){movies.forEach(function(m){console.log(m.title)})} %>";
var context = {'movies': [
{title:'The Hobbit', release:2014},
{title:'X-Men', release:2016},
{title:'Superman V', release:2014}
]};
ejs.render(template, context);
☝️ The context is an array of objects, each movie with a title and release date. In the template, we use Array.prototype.forEach
(it executes a provided function once per array element) to iterate over the array and print out the title and release date if our array has more than two elements (admittedly, not a very sensible example but it shows off the main principle). The <%
scriptlet tags are used for control-flow purposes. If you replace the opening scriptlet tag with <%-
or <%=
(try it!), you will end up with an error.
How do templates tie in with the Express framework? So far, we have used the REPL to show off some of EJS' capabilities. It turns out that so-called views can be easily configured with Express. Not only that, an application can also make use of several template engines at the same time.
Three steps are involved:
1️⃣ We set up the views directory - the directory containing all templates. Templates are essentially HTML files with EJS scriptlet tags embedded and file ending .ejs
:
app.set('views', __dirname + '/views');
2️⃣ We define the template engine of our choosing:
app.set('view engine', 'ejs');
3️⃣ We create template files.
An functioning Express/EJS demo can be found at demo-code/node-ejs-ex. Try it out (i.e. install and run it). Let's consider the application's app.js
👇:
var express = require("express");
var url = require("url");
var http = require("http");
var app;
var port = process.argv[2];
app = express();
http.createServer(app).listen(port, function () {
console.log("Ready on port " + port);
});
var todos = [];
todos.push({ message: 'Final exam', dueDate: 'January 2016' });
todos.push({ message: 'Prepare for assignment 6', dueDate: '05/01/2016' });
todos.push({ message: 'Sign up for final exam', dueDate: '06/01/2016' });
app.set('views', __dirname + '/views');
app.set('view engine', 'ejs');
app.get("/todos", function (req, res) {
res.render('todos', { title: 'My list of TODOs', todo_array: todos });
});
☝️ We first set up the views directory, then the view engine and finally we use Express' res.render
in order to render a view and send the rendered HTML to the client. Important to realize in this example is, that the first argument of res.render
is a view stored in views/todos.ejs
which the Express framework retrieves for us. The second argument is an object that holds the variables of the template, here title
and todo_array
. To confirm this, let's look at the template file itself, todos.ejs
which contains the corresponding variable names 👇:
<!DOCTYPE html>
<html>
<head>
<title><%= title %></title>
</head>
<body>
<h1>TODOs</h1>
<div>
<% todo_array.forEach(function(todo) { %>
<div>
<h3><%=todo.dueDate%></h3>
<p><%=todo.message%></p>
</div>
<% }) %>
</div>
</body>
</html>
Here are a few questions you should be able to answer after having followed the lecture and having worked through the required readings:
- Does
require()
use synchronous or asynchronous access? - Consider these two files,
constants.js
andbar.js
. What is the console output ofnode bar.js
?
//constants.js
module.exports.pi = 3.1415;
module.exports.password = "root";
//bar.js
var constants1 = require('./constants');
constants1.password = "admin";
var constants2 = require('./constants');
console.log(constants2.password);
var constants3 = require('./constants');
constants2.pi = 3;
console.log(constants3.pi);
- Consider these two files,
constants.js
andbar.js
. What is the console output ofnode bar.js
?
//constants.js
module.exports = function() {
return {
pi: 3.1415,
one: 1,
login: "root",
password: "root"
}
}(); //pay attention to the final bracket pair!
//bar.js
var constants1 = require('./constants');
constants1["password"] = "admin";
var constants2 = require('./constants');
console.log(constants2["password"]);
var constants3 = require('./constants');
constants2["pi"] = 3;
console.log(constants3["pi"]);
- Name three different routes that this handler matches.
app.get('/user(name)?s+', function(req,res){
res.send(…)
});
- Name three different routes that this handler matches.
app.get('/whaa+[dt]s+upp*', function(req,res){
res.send(…)
});
- What is the console output after executing this code snippet?
var ejs = require('ejs');
var people = ['Wolverine', 'paul', 'picard'];
var X = function (input) {
if(input){
return input[0];
}
return "";
}
var template = '<%= helperFunc(input); %>';
var context = {
input: people,
helperFunc: X
};
console.log(ejs.render(template, context));