Appearance
Using Nunjucks
What is Nunjucks? It's a template engine.
So, what is a template engine?
A template engine is a component that constructs string outputs based on templates combined with data. For example, the following function is a template engine:
javascript
function examResult(data) {
return `${data.name} scored ${data.chinese} in Chinese, ${data.math} in Math, and ranked ${data.ranking} in the first grade final exam.`;
}
If we input data like this:
javascript
examResult({
name: 'Xiao Ming',
chinese: 78,
math: 87,
ranking: 999
});
This template engine replaces the corresponding variables in the template string and produces the following output:
Xiao Ming scored 78 in Chinese, 87 in Math, and ranked 999 in the first grade final exam.
The most common output of a template engine is web pages, specifically HTML text. Of course, it can also output text in any format, such as Text, XML, Markdown, etc.
Some might ask: since JavaScript's template strings can achieve template functionality, why do we need another template engine?
Because JavaScript's template strings must be written in JavaScript code, creating a complex page like the Sina homepage is very difficult.
There are several important issues to consider when outputting HTML:
Escaping
Special characters must be escaped to avoid XSS attacks. For instance, if the value of the variable name
is not Xiao Ming but <script>...</script>
, the HTML output by the template engine will execute malicious JavaScript code in the browser.
Formatting
Different types of variables need to be formatted, for example, currency should be formatted like $12,345.00, and dates should look like 2016-01-01.
Simple Logic
Templates also need to execute some simple logic. For instance, to output content conditionally, we need to implement the following using if
:
{{ name }} student,
{% if score >= 90 %}
Excellent performance, should be rewarded
{% elif score >= 60 %}
Good performance, keep it up
{% else %}
Failed, should consider a punishment at home
{% endif %}
Thus, we need a powerful template engine to handle page output.
Nunjucks
We choose Nunjucks as our template engine. Nunjucks is a pure JavaScript template engine developed by Mozilla, which can run in both Node environments and browsers. However, it mainly runs in Node environments, as there are better templating solutions in the browser, such as MVVM frameworks.
If you have used Python's template engine Jinja2, using Nunjucks will be very simple, as their syntax is almost identical because Nunjucks is a JavaScript implementation of Jinja2.
From the example above, we can see that while the internal workings of a template engine may be complex, using one is quite simple because essentially we just need to construct such a function:
javascript
function render(view, model) {
// TODO:...
}
Here, view
is the name of the template (also known as the view), as there may be multiple templates from which to choose. model
is the data, which in JavaScript is simply an Object. The render
function returns a string, which is the output of the template.
Now let's use the Nunjucks template engine to write a few HTML templates, render them with actual data, and get the final HTML output.
We create a VS Code project structure named use-nunjucks
as follows:
use-nunjucks/
├── app.mjs
├── package-lock.json
├── package.json
└── view
├── base.html <-- HTML template file
├── extend.html <-- HTML template file
└── hello.html <-- HTML template file
The template files are stored in the view
directory.
First, we install the dependencies using npm install nunjucks
and add the Nunjucks dependency to package.json
:
json
"nunjucks": "^3.2.4"
Note that the template engine can be used independently and does not require Koa.
Next, we need to write a function render
that uses Nunjucks. How do we do that? We can refer to the Nunjucks official documentation, read it carefully, and write the following code in app.js
:
javascript
import nunjucks from 'nunjucks';
function createEnv(path, { autoescape = true, noCache = false, watch = false, throwOnUndefined = false }, filters = {}) {
const loader = new nunjucks.FileSystemLoader(path, {
noCache: noCache,
watch: watch
});
const env = new nunjucks.Environment(loader, {
autoescape: autoescape,
throwOnUndefined: throwOnUndefined
});
for (let name in filters) {
env.addFilter(name, filters[name]);
}
return env;
}
const env = createEnv('view', {
noCache: true
}, {
hex: function(n) {
return '0x' + n.toString(16);
}
});
The variable env
represents the Nunjucks template engine object, which has a render(view, model)
method that takes the view
and model
parameters and returns a string.
The parameters needed to create env
can be found in the documentation. We use keyword parameters as default values, and finally create a file system loader with new nunjucks.FileSystemLoader('view')
to read templates from the view
directory.
Next, we write a hello.html
template file and place it in the view
directory with the following content:
html
<h1>Hello {{ name }}</h1>
Then, we can render this template with the following code:
javascript
const s = env.render('hello.html', { name: 'Xiao Ming' });
console.log(s);
The output will be:
html
<h1>Hello Xiao Ming</h1>
At first glance, this doesn't seem much different from using JavaScript template strings. However, let's try:
javascript
const s = env.render('hello.html', { name: '<script>alert("Xiao Ming")</script>' });
console.log(s);
The output will be:
html
<h1>Hello <script>alert("Xiao Ming")</script></h1>
This effectively prevents the output of malicious scripts.
Additionally, Nunjucks provides powerful tags for conditional statements, loops, and other functionalities. For example:
html
<!-- Loop through the list of names -->
<body>
<h3>Fruits List</h3>
{% for f in fruits %}
<p>{{ f }}</p>
{% endfor %}
</body>
The strongest feature of the Nunjucks template engine is template inheritance. By observing various websites, we can find that the structure is actually similar, with fixed header and footer formats, while only the content of the middle page differs. If each template repeats the header and footer, modifying the header or footer would require changing all templates.
A better approach is to use inheritance. First, we define a basic web page framework in base.html
:
html
<html><body>
{% block header %} <h3>Unnamed</h3> {% endblock %}
{% block body %} <div>No body</div> {% endblock %}
{% block footer %} <div>copyright</div> {% endblock %}
</body>
base.html
defines three editable blocks named header
, body
, and footer
. Child templates can selectively redefine these blocks:
{% extends 'base.html' %}
{% block header %}<h1>{{ header }}</h1>{% endblock %}
{% block body %}<p>{{ body }}</p>{% endblock %}
Then we can render the child template:
javascript
console.log(env.render('extend.html', {
header: 'Hello',
body: 'bla bla bla...'
}));
The output HTML will be:
html
<html><body>
<h1>Hello</h1>
<p>bla bla bla...</p>
<div>copyright</div> <!-- footer remains unchanged as it is not redefined -->
</body>
Performance
Finally, we need to consider the performance of Nunjucks.
For template rendering itself, the speed is extremely fast, as it is just string concatenation, which is purely a CPU operation.
Performance issues mainly arise from reading template content from files. This is an I/O operation, and in a Node.js environment, we know that synchronous I/O is the least tolerable in single-threaded JavaScript. However, Nunjucks defaults to using synchronous I/O to read template files.
The good news is that Nunjucks caches the contents of files that have been read. This means that each template file is read at most once and stored in memory; subsequent requests will not read the file again as long as we specify the noCache: false
parameter.
In development environments, cache can be turned off, allowing for real-time modifications of templates. In production environments, cache should be enabled to avoid performance issues.
Nunjucks also provides an asynchronous reading method, but this can be cumbersome to write. We prefer simpler methods whenever possible, as keeping code simple is key to maintainability.