Modularization is a popular technique used in modern programming languages that allows programs to be built from a series of smaller programs, or modules. Writing programs that use modules encourages programmers to write code that conforms to the design principle called "Separation of Concerns". In other words, each module focuses on doing one thing, and has a clearly defined interface. If we then consume this module by focusing on the interface, we can easily replace this interface with something else, without breaking our code. We will focus more on "Separation of Concerns" and other object-oriented design patterns in the next chapter.
JavaScript, in itself, does not have a concept of modules, but it is proposed for the upcoming ECMAScript 6 standard. Popular frameworks and libraries such as Node and Require have built module-loading capabilities into their frameworks. These frameworks, however, use slightly different syntax. Node uses the CommonJS syntax for module loading, whereas Require uses the Asynchronous Module Loading (AMD) syntax. The TypeScript compiler has an option to turn on module compilation, and then switch between these two syntax styles.
In this chapter, we will look at the syntax of both module styles, and how the TypeScript compiler implements them. We will take a look at how to use modules when writing code for both Node and Require. We will also have a cursory look at Backbone, and how to write an application using a Model, View and Controller. Each of these Backbone components will be built as loadable modules.
The most prevalent usage of the CommonJs syntax for writing modules is when writing server-side code. It has been argued that browser-based CommonJs syntax simply cannot be done without a lot of overhead, but there are some libraries out there such as Curl (https://github.com/cujojs/curl) that allow this syntax. In this section, we will, however, focus on Node application development.
Using Node within Visual Studio has been made a breeze by the Node tools for Visual Studio plugin (https://nodejstools.codeplex.com). This toolset has also been updated to use TypeScript as a default editor, bringing the full TypeScript development experience to Node. Once the extension has been installed, we can create a new blank Node application, as shown in the following screenshot:
This project template will create a server.ts
TypeScript file, and include the node.d.ts
declaration file automatically for us. If we compile and run this default implementation by simply hitting F5, the project template will automatically start up a new console to run our Node server, start the server instance, and open a browser to connect to this instance. If all goes well at this stage, your browser will simply say Hello World.
Let's take a look at the server.ts
TypeScript file that is creating an instance of our Node server:
import _http = require('http'), var port = process.env.port || 1337 http.createServer(function (req, res) { res.writeHead(200, { 'Content-Type': 'text/plain' }); res.end('Hello World '), }).listen(port);
The first line of this code snippet uses the CommonJs module syntax to tell our Node server that it must import
the library named 'http'
.
This line has two key parts. To explain these key parts, let's start at the right-hand side of the =
sign and work our way towards the left. The require
function takes a single parameter, and is used to tell the application that there is a library named 'http'
out there. The require
function also tells the application that it needs this library to be made available to it, in order to continue functioning. As require
is a key part of the syntax of modules in TypeScript, it has been given the keyword status and will be highlighted in blue, just like other keywords such as var
, string
, and function
. If the application cannot find this 'http'
library, then Node will immediately throw an exception.
The left-hand side of the =
sign uses the import
keyword, which is also a fundamental concept in module syntax. The import
statement tells the application to attach the library that has been loaded via the require
function, require('http')
, into a namespace called _http
. Whatever functions or objects that the 'http'
library has made public will be available to the program via the _http
namespace.
If we jump to the third line very quickly, we can see that we invoke a function called createServer
that is defined in the 'http'
module, and call it via the _http
namespace. hence _http.createServer()
.
The default server.ts
file that is generated by the blank Node project template is very slightly different than our preceding code sample. It names the import http
, which matches the library name 'http',
as follows:
import http = require('http'),
This is a common naming standard for Node. You can, of course, name your import namespaces whatever you like, but it does help to have the namespace match the imported library's name, to help with the readability of the code.
The second line of our code snippet simply sets up the variable named port
to either be the value of the global variable process.env.port
, or a default value of 1337
. This port number is used on the very last line, and uses fluent syntax to call the listen
function on the returned value of the http.createServer
function.
Our createServer
function has two variables named req
and res
. If we use our mouse to hover over the req
variable, we can see that it is of type _http.ServerRequest
. Similarly, the res
variable is of type _http.ServerResponse
. These two variables are our HTTP request and response streams. In the body of the code, we are invoking the writeHead
function on the HTTP response to set the content-type, and then we are invoking the end
function on the HTTP response to write the text 'Hello World
'
to the browser.
With these couple of lines of code, we have created a running node HTTP server that serves up a simple web page with the text "Hello World".
Note that if you have a keen eye for TypeScript syntax, you will notice that this file uses JavaScript syntax and not TypeScript syntax for our createServer
function. This is most probably due to the recent upgrade of the Node toolset from JavaScript to TypeScript. The call to createServer
can also be written using TypeScript fat arrow syntax as follows:
_http.createServer((req, res) => { .. }
To create a Node module, we simply need to create another TypeScript file to house our module code. Let's create a file named ServerMain.ts
, and move the code that writes to the HTTP response into this module as follows:
import http = require('http'), export function processRequest( req: http.ServerRequest, res: http.ServerResponse): void { res.writeHead(200, { 'Content-Type': 'text/plain' }); res.end('Hello World '), }
Our ServerMain
module starts with the import
of the 'http'
module into the http
namespace. This is necessary to allow us to use the ServerRequest
and ServerResponse
types that are part of this library.
The keyword export
is now used to indicate what functions will be made available to users of this module. As we can see, we have exported a function named processRequest
that takes two parameters, req
and res
. This function will be used as a replacement for the anonymous function (req, res) => { … }
that we were using in the server.ts
file previously.
Note that as good TypeScript coders, we have also strongly typed the req
and res
variables to be of type http.ServerRequest
, and of type http.ServerResponse
respectively. This will enable Intellisense within our IDE, and also adheres to two principles of strong typing (S.F.I.A.T and self-describing functions).
Before we modify the server.ts
file to use our new module, let's crack open the generated JavaScript file and take a closer look at the CommonJs syntax in a little more detail:
function processRequest(req, res) { res.writeHead(200, { 'Content-Type': 'text/plain' }); res.end('Hello World '), } exports.processRequest = processRequest;
The first part of this JavaScript is simple enough—we have a function named processRequest
. The last line, however, attaches this function to a property on the exports
global variable. This exports
global variable is how CommonJs publishes modules to the outside world. Any function, class, or property that needs to be exposed to the outside world must be attached to the exports
global variable. The TypeScript compiler will generate this line of code for us whenever we use the exports
keyword in a TypeScript file.
Now that we have our module in place, we can modify our server.ts
file to use this module as follows:
import http = require('http'), import ServerMain = require('./ServerMain'), var port = process.env.port || 1337; http.createServer(ServerMain.processRequest).listen(port);
The first line stays the same, but the second line uses the same import
and require
syntax to now import our './ServerMain'
module into the ServerMain
namespace.
The syntax that we use to name this module points to a local file module, and therefore uses a relative file path to the module file. This relative path will resolve to the ServerMain.js
file that TypeScript has generated. Creating a global Node module with the name 'ServerMain'
, which would be globally available—similar to the 'http'
module—is outside the scope of this discussion.
Our call to the http.createServer
function now passes in our processRequest
function as an argument. We have changed from an anonymous function using the fat arrow syntax, to a named function from the ServerMain
module. We have also started to adhere to our "Separation of Concerns" design pattern. The server.ts
file starts the server on a specific port, and the ServerMain.ts
file now houses the code used to process a single request.
When writing Node code, it is necessary to take a careful note of the asynchronous nature of all Node programming, as well as JavaScript's lexical scoping rules. Luckily, the TypeScript compiler will generate errors if we break any of these rules. As an example of this, let's update our ServerMain
module to read in a file from disk, and serve up the contents of this file, instead of our Hello world
text, as follows:
import fs = require("fs"); export function processRequestReadFromFileAnonymous( req: http.ServerRequest, res: http.ServerResponse) { fs.readFile('server.js', 'utf8', (err, data) => { res.writeHead(200, { 'Content-Type': 'text/plain' }); if (err) res.write("could not open file for reading"); else { res.write(data); res.end(); } }); }
To read files from disk, we will need to use the Node global module named "fs"
, or filesystem, which is imported on the first line of the code. We then expose a new function named processRequestReadFromFileAnonymous
that again uses the req
and res
parameters. Within this function, we then use the fs.readFile
function to read a file from disk using three arguments. The first argument is the name of the file to be read in, the second argument is the file type, and the third argument is a callback function that Node will call, once the file has been read from disk.
The body of this anonymous function is similar to what we have seen previously, but it also checks the err
argument to see whether there was an error while loading the file. If there was no error, the function simply writes the file to the response stream.
In real-world applications, the logic inside of the main processRequestReadFromFileAnonymous
function could become quite complex (besides the name), and may involve more than a single step to read a hardcoded filename from disk. Let's move this anonymous function into a private function, and see what happens. Our first pass at refactoring this code may be something similar to the following:
export function processRequestReadFromFileError( req: http.ServerRequest, res: http.ServerResponse) { fs.readFile('server.js', 'utf8', writeFileToStreamError); } function writeFileToStreamError(err, data) { res.writeHead(200, { 'Content-Type': 'text/plain' }); if (err) res.write("could not open file for reading"); else { res.write(data); res.end(); } }
Here, we have modified the fs.readFile
function call, and replaced the anonymous callback function with a named function—writeFileToStreamError
. This change, however, will immediately generate a compilation error:
Cannot find name 'res'.
This compilation error is caused by the lexical scoping rules of JavaScript. The function writeFileToStreamError
is trying to use the res
parameter of the parent function. However, as soon as we moved this function outside the lexical scope of the parent, the variable res
is no longer in scope – and will therefore be undefined
. To fix this error, we need to ensure that the lexical scope of the res
argument is maintained within our code structure, and we need to pass the value of the res
argument down to our writeFileToStream
function, as follows:
export function processRequestReadFromFileChained( req: http.ServerRequest, res: http.ServerResponse) { fs.readFile('server.js', 'utf8', (err, data) => { writeFileToStream(err, data, res); }); } function writeFileToStream( err: ErrnoException, data: any, res: http.ServerResponse): void { res.writeHead(200, { 'Content-Type': 'text/plain' }); if (err) res.write("could not open file for reading"); else { res.write(data); res.end(); } }
Note that in the call to fs.readFile
on the third line in the preceding code, we have reverted back to our anonymous syntax, and passed on the value of the parent res
argument down to our new function writeFileToStream
. This modification of our code now correctly adheres to the lexical scoping rules of JavaScript. Another side-effect is that we have clearly defined what variables the writeFileToStream
function needs, in order to work. It needs the err
and data
variables from the fs.readFile
callback, but it also needs the res
variable from the original HTTP request.
We can now modify our server.ts
file to use our new chained function:
http.createServer(ServerMain.processRequestReadFromFileChained) .listen(port);
Running the application now will show the world what our server.js
file contains:
Note that because we are using modules, we have been able to write three different versions of the processRequest
function, each with a slight twist. However, our modifications to the server.ts
file that launches the server have been very simple. We have just replaced the function that the server calls, in order to effectively run three different versions of our application. Again, this complies with the "Separation of Concerns" design principle. The server.ts
code is simply used to start the Node server on a specific port, and should not be concerned with how each request is processed. Our code within ServerMain.ts
is responsible simply for processing a request.
This concludes our section on writing Node applications within TypeScript. As we have seen, the TypeScript developer experience brings with it a compilation step, which will quickly trap lexical scoping rules and many other issues within our code. Final score, TypeScript: 1, buggy code: 0!
18.216.216.240