Continuing Frameworkless.js
Part 2: Templating, Dynamic content and routes
Recap of Last Time
- We built a Node.js app utilising the built-in http.createServer
- Starts an HTTP server and serves static files out of the ./public directory
Important note!
Thanks to Github user @joshuaeliers (Joshua Eliers) for his PR to fix a directory traversal vulnerability:
A very good question...
Source: Makerlog Telegram channel
I'd love to hear from you! Join the Frameworkless.js Telegram group and take part in discussions:
👉 https://t.me/frameworkless
Packages – packages everywhere...
After the tutorial, minimalist versions of the implementations of some of the code will be released as NPM packages.
Such as:
- Static file responder
- A function to easily parse form data
- Requests...?
error handling
- Not covered in the last lesson but this is really needed.
- Honestly one of the most difficult things to do without overwriting, over-engineering
- Node gives us the tools we need with the Error object, and utilising try/catch blocks
- Writing good error handling is like operating on various layers of an onion without pulling it apart
Define some potential errors
// config/errors.js
const errors = {
not_found: {
code: 404
},
__fallback: {
code: 500
}
}
module.exports = error => errors[error.message] || errors.__fallback
Errors can be made easy to handle by putting them into a central place and standardising them.
Implement our error handler
// initialisers/http.js
// Near the top...
const errors = require('../config/errors')
try {
// code here to attempt to serve content
} catch (error) {
console.error(error)
const errorData = errors(error)
return await serveStaticFile({
file: '/error.html',
extension: 'html',
statusCode: errorData.code
}, response)
}
Errors can be made easy to handle by putting them into a central place and standardising them.
Templating – what is it really?
- Separation of logic and presentation
- Reusable pieces
- Dynamic content
Handlebars.js
- More than 7 million weekly downloads (via NPM) 😱
- Simple to learn with easy syntax
- Can be dropped into plain HTML
- Supports partials and helpers
Read the docs on handlebarsjs.com
$ npm install handlebars --save
<p>
{{#if user_logged_in}}
Hello, {{user.username}}!
{{else}}
<a href="/login">Please log in</a>
{{/if}}
</p>
Install glob
Read the docs on Github
$ npm install glob --save
Let's make some folders...
...never enough folders
$ mkdir ./routes
$ mkdir ./templates
In the "routes" folder, we will have the business logic for our routes.
In the "templates" folder, we will have the HTML/Handlebars markup (the view).
The "base" template
{{!-- templates/template.hbs --}}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{app_name}}</title>
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="manifest" href="/site.webmanifest">
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5">
<meta name="msapplication-TileColor" content="#da532c">
<meta name="theme-color" content="#ffffff">
<style>
body {
font-family: sans-serif;
line-height: 1.625;
font-weight: 300;
font-size: 1em;
}
.image {
text-align: center;
margin-top: 5%;
}
h1 {
text-align: center;
}
</style>
</head>
<body>
{{>content}}
</body>
</html>
Copy over most of public/index.html to templates/template.hbs:
Note the {{>content}} partial!
And now our first view...
{{!-- templates/root.hbs --}}
<div class="image"><img src="/favicon-32x32.png"></div>
<h1>
{{app_name}} is here
</h1>
We will write the code to insert this little bit where {{>content}} is in templates/template.hbs.
And now time for srs bsns logic
// routes/index.js
const { readFileSync } = require('fs')
const glob = require('glob')
module.exports = glob.sync(
'./routes/**/*.js',
{ ignore: [ './routes/index.js' ] }
).reduce((routeMap, filename) => {
const route = require(`.${filename}`)
if (route.template) {
route.body = readFileSync(`./templates/${route.template}.hbs`, { encoding: 'utf8' })
}
routeMap[`${route.method || 'GET'}:${route.uri}`] = route
return routeMap
}, {})
// routes/root.js
exports.uri = '/'
exports.template = 'root'
Time to wire it into the responder
// some additions to lib/responder.js
const { readFileSync } = require('fs')
const Handlebars = require('handlebars')
const routes = require('../routes')
const basePage = readFileSync(`./templates/template.hbs`, { encoding: 'utf8' })
const serveRoute = async ({ request, context }, response) => {
const key = `${request.method}:${request.url}`
if (!routes[key]) throw new Error('not_found')
Handlebars.registerPartial('content', routes[key].body)
const hbs = Handlebars.compile(basePage)
return response.end(hbs(context))
}
// Don't forget this! we now export 2 different responder types instead of 1!
module.exports = { serveStaticFile, serveRoute }
HTTP server additions
// initialisers/http.js
const { serveStaticFile, serveRoute } = require('../lib/responder')
// And, this is our new response function:
const urlTokens = request.url.split('.')
const extension = urlTokens.length > 1 ? `${urlTokens[urlTokens.length - 1].toLowerCase().trim()}` : false
const serveResponse = extension ? serveStaticFile : serveRoute
const responseParams = { path: request.url }
if (extension) {
responseParams.extension = extension
} else {
responseParams.request = request
responseParams.context = {
app_name: APP_NAME
}
}
try {
return await serveResponse(responseParams, response)
} catch (error) {
console.error(error)
const errorData = errors(error)
return await serveStaticFile({
path: '/error.html',
extension: 'html',
statusCode: errorData.code
}, response)
}
Pat yourself on the back!
As of today, you have built a Node.js web server serving static files as well as routes!
Well done!!!
Well done, you made it! 👏👏👏
See you next time where we look at Working with user-submitted content by adding in forms.
Please don't hesitate to contact me or leave feedback on the course:
Telegram channel: https://t.me/frameworkless
Twitter: @mtimofiiv
See today's code: https://github.com/frameworkless-js/remind.ist/tree/stage/2
See today's lesson running live: https://part2.remind.ist
frameworkless.js -> Part 2
By Mike Timofiiv
frameworkless.js -> Part 2
Check out https://frameworkless.js.org/course/2 for the whole presentation
- 1,414