README
Jessica: The JS ES6 Template Engine
"Jessica," drawn from JESS as an anagram of "JS ES6," is a simple, super fast, and extendable Template Engine for Node and Express applications using pure ES6 JavaScript syntax, as a JavaScript module. It works by scanning files in a working directory, then reading the contents of the files and converting them from plain strings to ES6 template literals. Once conversion is complete, it is then compiled to plain text by the V8 engine. Being less than 1kb, Jessica offloads a lot of the processing directly to the V8 interpreter, which compiles the code and runs as fast as the rest of the Express App. In fact, Jessica will add very little, if any, overhead to your project at all! It should also allow you to implement any functionality you'd like within the bounds of JavaScript.
Template literals, enclosed by back ticks, feature String Interpolation, Embedded Expressions, Multiline strings and String Tagging for safe HTML escaping, localisation, and more.
Minimum requirements Node.js v13.0.0
, for native ES6 modules support.
Installation
$ npm i @nonsensecodes/jessica --save
Features
- No Dependencies
- Fully Configurable
- Compiled and interpreted by V8 (Super Fast)
- Learning new syntax is not required
- Partials Support
- Conditional Support
- Iterators Support
- Native Javascript Support
Usage
Prerequisites
List of used html files in the views
folder:
index.html
<!DOCTYPE html>
<html>
<body>
<h1>${title}</h1>
</body>
</html>
template.html
<!DOCTYPE html>
<html>
<body>
<h1>${title}</h1>
<main>${partial}</main>
</body>
</html>
partial.html
<p>The fastest javascript template string engine!</p>
partial-conditional.html
Jessica is ${maintainedBy ? `a template engine maintained by ${maintainedBy}` : 'not maintained anymore'}.
partial-iteration.html
<dl>
${features.map(f => `
<dt>${f.dt}</dt>
<dd>${f.dd}</dd>
`).join('')}
</dl>
Setup with Express
The basics required to integrate Jessica in your app are pretty simple and easy to implement:
import express from 'express';
import jessica from 'jessica';
const app = express();
app.engine('html', jessica);
app.set('views', 'views');
app.set('view engine', 'html');
app.get('/', function(req, res) {
res.render('index', {locals: {title: 'Welcome!'}});
});
app.listen(3000);
Before Express can render template files, the following application settings must be set:
- views, the directory where the template files are located. Eg: app.set('views', './views')
- view engine, the template engine to use. Eg: app.set('view engine', 'html')
HTML template file named index.html
in the views directory is needed (the content of the file can be found in the prerequisites section). Route to render the html file is expected. If the view engine property is not set, we must specify the extension of the view file. Otherwise, it can be safely omitted.
app.render('index', {locals: {title: 'jessica'}});
Express-compliant template engines such as Jessica export a function named __express(filePath, options, callback), which is called by the res.render() function to render the template code. When a request is made to the home page, the index.html file will be rendered as HTML.
Setup without Express
To get up and running without having to worry about managing extra libraries one only needs the following:
import jessica from 'jessica';
jessica(
__dirname + '/views/index.html',
{ locals: { title: 'jessica' } },
(err, content) => err || content
);
The content below will be rendered on the client side as a response from both setups:
<!DOCTYPE html>
<html>
<body>
<h1>JESSica</h1>
</body>
</html>
Rendering a template
Within your app route callback, call res.render
, passing any partials and local variables required by your template. For example:
app.get('/', function(req, res) {
res.render('template', {
locals: {
title: 'jessica'
},
partials: {
partial: __dirname + '/views/partial'
}
});
});
Partial with a file name partial.html
(see the content of the file in the prerequisites section above) will be injected into template.html
:
<!DOCTYPE html>
<html>
<body>
<h1>Jessica</h1>
<main><p>The fastest javascript template string engine!</p></main>
</body>
</html>
All templates files paths are defined as absolute to the root directory of the project.
Compiling a string
Jessica's rendering functionality has separate scanning, parsing, string generation and response sending phases. Compilation is pretty much the same but without the response sending phase. This feature can be useful for pre-processing templates on the server. Compiling has the following syntax:
const titleTpl = '${engineName} - The fastest javascript template string engine!';
const cb = (err, content) => err || content;
// sync - second parameter is a string representation of an array of variable names.
// The returned function is called with a string representation of an array of variable values.
const compiled = jessica(titleTpl, 'engineName')('jessica');
// async - second parameter is an object and third parameter is a callback function
jessica(titleTpl,{ template: true, locals:{ engineName: 'jessica' } }, cb);
Both methods will result in the following output:
jessica - The fastest javascript template string engine!
The template engine allows both synchronous and asynchronous method invocations. If string is rendered as in the examples provided above a 'template' option needs to be set to true. The preceding synchronous invocation returns an output immediately in response to the function execution. Alternatively, you can specify partials and omit template parameter to force file lookup and content reading and invoke the function asynchronously.
Compiling a template
The two functions app.render
and jessica
are almost identical, but they require slightly different parameters to be passed. While app.render
uses an absolute path, or a path relative to the views setting, jessica
expects a path relative to root folder.
They both return the rendered content of a view via the callback function. The callback function which is provided as a third parameter is called once the asynchronous activity is completed. The output in the two examples provided below is the same:
app.render('template', {
locals: {
title: 'jessica'
},
partials: {
template: __dirname + '/views/partial'
}
}, (err, content) => err || content);
jessica(__dirname + '/views/template.html', {
locals: {
title: 'jessica'
},
partials: {
template: __dirname + '/views/partial.html'
}
}, (err, content) => err || content);
On average jessica
yields slightly better performance than app.render
. Async function invocation of jessica
also returns a promise, which enables us to chain method calls:
const compile = jessica(__dirname + '/views/template.html', {
locals: {
title: 'jessica'
},
partials: {
template: __dirname + '/views/partial.html'
}
}, (err, content) => err || content);
compile.then(output => res.send(output))
Compiling a nested template
Template nesting is currently not supported by the engine. A simple workaround to this issue would be to perform multiple template compilations:
const renderPage = (err, content) => res.render('template', {
locals: {
partial: content
}
});
jessica(__dirname + '/views/partial-conditional.html', {
locals: {
maintainedBy: 'Good Samaritans'
}
}, renderPage);
Precompiling
Jessica allows us bypassing Express view rendering for speed and modularity. Compiling a template is much slower than rendering it, so when it comes to speed, we should precompile our templates as part of the optimisation process. The result of precompilation can be stored to an object:
const text = '${engineName} - The fastest javascript template string engine in the whole ${place}!';
const precompiled = jessica(text, 'engineName, place');
and then invoked whenever needed:
console.log(precompiled('jessica', 'multiverse'));
To make use of this precompilation, templates should be compiled with names that the compiler would expect and the result function called with an argument list that consists of values relative to the names. If no property name is defined a default one is created with a value of '