This documentation will introduce you to most of the key concepts in working with Senna.js. Don't worry if you don't understand everything. Each of the concepts presented here is described in detail in the source code of the examples.
Even modest improvements in latency can have measurable impact on usage. The most obvious metric is load time. This is influenced by many factors: DNS lookup, network speed, and how many resources need to be loaded before the page is visible (stylesheets, javascript, images, fonts), etc.
Sites will often optimize and minify stylesheets, combine images into a single sprite, defer loading of javascript, and serve static files from a CDN. All in order to speed up load time. This is great. However, even with cached resources the browser still has to re-parse and execute the CSS and JS on every page load, it still has to lay out the HTML and redraw the UI. This slows down the actual navigation but can also add perceived slowness and often introduces a white flash.
When Tim Berners-Lee invented the web he was looking for a system to publish scientific documents remotely, hyperlinks and static pages only. Creating a webapp can get very slow with all the static web born rules. In order to improve actual and perceived latency many sites are moving to SPA model. That is, once the initial page is loaded all subsequent navigations are handled without full page reload. Additional content is loaded using XMLHttpRequest via History API which is able to update the URL without refreshing the page, therefore your dynamic site can be shared and bookmarked. Having a URL for each state of your page allows the content to be fully crawled by search engines.
Senna.js is a blazing-fast single page application engine that provides several low-level APIs that allows you to build modern web-based applications with only ~8KB of JavaScript without any dependencies.
In order to create a single page application with good perceived latency and good user experience, the SPA engine must handle the browser native behavior in many aspects, for instance:
SEO & Bookmarkability: Sharing or bookmarking a link should always display the same content. Sending a link to a friend should get them where we were. More than that, search engines are able to index that same content.
Hybrid rendering: Ajax + server-side rendering allows disable pushState at any time, allowing progressive enhancement. The way you render the server side doesn't matter, it can be simple HTML fragments or even template views.
State retention: Scrolling, reloading or navigating through the history of the page should get back to where it was.
UI feedback: When some content is requested, it indicates to the user that something is happening.
Pending navigations: Block UI rendering until data is loaded, then displays the content at once. It's important to give some UX feedback during loading.
Timeout detection: Timeout if the request takes too long to load or when navigating to a different link while another request is pending.
History navigation: By using History API you can manipulate the browser history, so you can use browser's back and forward buttons.
Cacheable screens: Once you load a certain surface this content is cached in memory and is retrieved later on without any additional request. This can really speed up your application.
Page resources management: Evaluate scripts and stylesheets from dynamic loaded resources. Additional content loaded using XMLHttpRequest can be appended to the DOM, for security reasons some browsers won't evaluate <script> tags from the new fragment. Therefore, the SPA engine should handle extracting scripts from the content and parsing them, respecting the browser contract for loading scripts.
Not only that the list of features needed for a good SPA is huge, at the moment History API presents many cross-browser inconsistencies that can get pretty complicated to be handled without a SPA engine. Therefore, if you decide to start from scratch your amazing MV* framework that supports SPA, think about using some engine as core.
Senna.js is small, unobtrusive, and flexible enough so that it is easy to start a project with as well as integerate into existing applications. Senna.js makes this easier by providing features like:
AMD Support: Senna.js can be included via AMD loading as well linked with a standard script tag and used via a global variable definiton.
Trackable resources: Track and manage page resources dynamically that are annotated with the data-senna-track html attribute. Scripts and stylesheets can either be temporary or permanent resources. They are then removed after navigation away from the page on which they are needed, or remain on the page permanently.
Lifecycle events: Events are emitted when at certain stages of navigation that can be used for hooking into the Senna.js lifecycle. Such as beforeNavigate, startNavigate, and endNavigate.
To get started, download the project. This project includes all of the Senna.js examples, source code dependencies you'll need to get started.
Unzip the project somewhere on your local drive. The package includes an initial version of the project you'll be working with. While you're working, you'll need a basic HTTP server to serve your pages. Test out the web server by loading the finished version of the project. For example: http://localhost:8000/examples/
In this step, you'll create a example folder for your site called examples/mysite/, located where you unzipped the project under your local drive. Go to this directory and create a file called index.html using your favorite editor. The starting file looks like this:
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Senna - my first blazing fast website</title>
<!-- Senna optional styles -->
<link rel="stylesheet" href="../build/senna.css">
<!-- Senna -->
<script src="../build/senna-debug.js"></script>
</head>
<body>
<script>
// Start here...
</script>
</body>
</html>
Senna also supports AMD. You can use RequireJS to load it like this:
<script>
requirejs(["../build/amd/senna/src/senna"], function(senna) {
// Start here...
});
</script>
Now that you have index.html example file created, it's time to setup Senna.js to speed up your page. Everything starts when you create a var app = new senna.App();, this class will route <a> link elements as well as <form> submissions.
The elements that are routed through Senna.js can be excluded by adding the data-senna-off attribute. This is because by default the linkSelector and formSelector properties are set to a:not([data-senna-off]) and form[enctype="multipart/form-data"]:not([data-senna-off]) respectively. These can easily be configured to meet the needs of your application, e.g., app.setLinkSelector(".senna-links");, app.setFormSelector(".senna-forms");.
Copy the snippets available on this section and paste into your example file, in the end it should look something like:
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Senna - my first blazing fast website</title>
<!-- Senna optional styles -->
<link rel="stylesheet" href="../build/senna.css">
<!-- Senna -->
<script src="../build/senna-debug.js"></script>
</head>
<body>
<a href="/pages/page1.html">Page 1</a>
<a data-senna-off="true" href="/pages/page2.html">Page 2</a>
<!-- Content surface -->
<div id="content">
Default content of my website.
<form id="loginForm" data-senna-off>
<input name="username">
<input name="password">
<button type="submit">
</form>
<form id="feedbackForm">
<input name="content">
<button type="submit">
</form>
</div>
<!-- End of content surface -->
<script>
var app = new senna.App();
</script>
</body>
</html>
Note that two anchors were added to the page, also one surface element to hold the content of the website. And now your website was setup with Senna.js – So, will everything look faster now? Almost, you need to tell senna.App the surfaces of your page that will update dynamically. A surface is just a DOM element with an id, used to identify it later on during navigation. The last step is to register paths to route, in case the path of the clicked link matches with some registered route it will handle the navigation.
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Senna - my first blazing fast website</title>
<!-- Senna optional styles -->
<link rel="stylesheet" href="../build/senna.css">
<!-- Senna -->
<script src="../build/senna-debug.js"></script>
</head>
<body>
<a href="/pages/page1.html">Page 1</a>
<a href="/pages/page2.html">Page 2</a>
<!-- Content surface -->
<div id="content">
Default content of my website.
</div>
<!-- End of content surface -->
<script>
var app = new senna.App();
app.setBasePath('/examples/mysite/pages/');
app.addSurfaces('content');
app.addRoutes([
new senna.Route('page1.html', senna.HtmlScreen),
new senna.Route('page2.html', senna.HtmlScreen)
]);
</script>
</body>
</html>
That's all, your website should be blazing fast now. It is relevant to remember senna.Route supports regular expression, therefore you don't need to map many route if they can be generic, the two routes from the previous example can be reduced to:
app.addRoutes(new senna.Route(/\w+\.html/, senna.HtmlScreen));
Note that page1.html and page2.html was not created yet, therefore the navigation will fail since it cannot find the file. Duplicate the index.html twice and rename the new files file to page1.html and page2.html, respectively.
Get most of the performance benefits of a single-page application without much setup required. All that is needed is to include Senna.js and add the attributes data-senna and data-senna-surface to your body element.
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<title>Turbolinks</title>
<link rel="shortcut icon" href="https://sennajs.com/images/favicon.ico">
<!-- Including Senna -->
<script src="../../build/globals/senna-debug.js"></script>
</head>
<body data-senna data-senna-surface>
<!-- Content surface is the entire body -->
<a href="/pages/page1.html">Page 1</a>
<a href="/pages/page2.html">Page 2</a>
<!-- End of content surface -->
</body>
</html>
The body will then be replaced on each navigation. This alone can provide significant performance increases over a conventional multi-page application.
As seen on the previous example, you can register senna.Route on senna.App. A route is a tuple of path and a handler function:
var route = new senna.Route('/path/file.html', function() {
// Fires when a link with path /path/file.html is clicked.
});
The path also supports string, regular expression or function, e.g.
var route = new senna.Route(/.*/, function() {
// Fires when any link is clicked.
});
var resolvePath = function() {
return '/path/file.html';
}
var route = new senna.Route(resolvePath, function() {
// Fires when a link with path /path/file.html is clicked.
});
From the handler callback you can update parts of your page, though from a simple callback like this is hard to grow your application, for that reason Senna.js provides senna.Screen. A screen is nothing more than a special type of route handler that provides asynchronous lifecycle. The following section will detail what a screen is and how it can help you manage your surfaces.
Senna.js provides a special type of route handler called senna.Screen. Any screen implementation inherits asynchronous lifecycle out of the box. Note that senna.Screen is an abstract class only used to teach how to use the API. The screens can be cacheable and once rendered by a matching path they will be stored in memory, speeding up a lot the next time the app needs it.
// Constructor
senna.Screen = function() {};
// Lifecycle
senna.Screen.prototype.activate = function() {};
senna.Screen.prototype.beforeDeactivate = function() {};
senna.Screen.prototype.deactivate = function() {};
senna.Screen.prototype.disposeInternal = function() {};
senna.Screen.prototype.evaluateScripts = function(surfaces) {};
senna.Screen.prototype.evaluateStyles = function() {};
senna.Screen.prototype.destroy = function() {};
senna.Screen.prototype.flip = function(surfaces) {};
senna.Screen.prototype.load = function() {};
// Provides contents for each registered surface
senna.Screen.prototype.getSurfaceContent = function(surfaceId) {};
// Provides the title of the page when this screen is rendered
senna.Screen.prototype.getTitle = function() {};
To understand the screen lifecycle is good to inspect your browser console when navigating to a link from the user click or using the app.navigate(path[, replaceState]);.
app.navigate('/examples/mysite/pages/page1.html');
Lifecycle logs:
Navigate to [/examples/mysite/pages/page1.html]
Create screen for [/examples/mysite/pages/page1.html]
Screen [screen_1408572719183] load
XHR finished loading: GET "http://localhost:8000/examples/mysite/pages/page1.html".
Screen [screen_1408572719183] add content to surface [header]
Screen [screen_1408572719183] add content to surface [content]
Screen [screen_1408572719183] flip
Screen [screen_1408572719183] activate
Navigation done
The examples shown above you may have noticed the routes are pointing to senna.HtmlScreen. This screen implementation is really powerful since it can transform the navigation of your static HTML files to be handled as a single page application. Checkout the email demo source code with senna.HtmlScreen in practice.
It's easy to create your own screen whenever you need one.
Let's say for example that, for some reason, you need a screen that injects the content Header changed and Body changed on the respective surface elements from your page when navigate to the path /foo.
function FooScreen() {
FooScreen.base(this, 'constructor');
}
senna.inherits(FooScreen, senna.Screen);
FooScreen.prototype.cached = true;
FooScreen.prototype.getSurfaceContent = function(surfaceId) {
switch(surfaceId) {
case 'header':
return 'Header changed';
case 'body':
return 'Body changed';
}
};
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Senna - hello</title>
<!-- Senna optional styles -->
<link rel="stylesheet" href="../build/senna.css">
<!-- Senna -->
<script src="../build/senna-debug.js"></script>
</head>
<body>
<!-- Content surface -->
<div id="header">
Header
</div>
<div id="body">
Body
</div>
<!-- End of content surface -->
<script>
var app = new senna.App();
app.addSurfaces(['header', 'body']);
app.addRoutes(new senna.Route('/foo', HelloScreen));
</script>
</body>
</html>
The initialization of the app, surfaces and routes can be done via data attributes to simplify plugging it into your website. For example, it's really simple to transform your static website to a single page application just adding <body data-senna> and <div id="content" data-senna-surface> to each surface element of your page, check the following example:
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Senna - data attributes</title>
<!-- Senna optional styles -->
<link rel="stylesheet" href="../build/senna.css">
<!-- Senna -->
<script src="../build/senna-debug.js"></script>
</head>
<body data-senna>
<a href="/pages/page1.html">Page 1</a>
<a href="/pages/page2.html">Page 2</a>
<!-- Content surface -->
<div id="content" data-senna-surface>
Default content of my website.
</div>
<!-- End of content surface -->
</body>
</html>
When initialized from data attributes senna.App will route all links to senna.HtmlScreen. If you want to control what to route you can simply add to your page a <link rel="senna-route"> element defining the route, e.g.
<link rel="senna-route" href="page1.html" type="FooScreen">
<link rel="senna-route" href="regex:\w+\.html" type="FooScreen">
Senna.js provides an easy way to manage external style and script resources on the page. When including resources in the <head>, they can be annotated with the data-senna-track attribute. This attribute can be set to temporary or permanent, which determines whether a resource remains on the page permanently or is removed after navigation;
<link data-senna-track="permanent" href="main.css" rel="stylesheet">
<script data-senna-track="temporary" src="main.js"></script>
Senna.js exposes lifecycle events that can be hooked into in order to add features or special behavior.
beforeNavigate: Fires before navigation starts. Event payload: { path: '/pages/page1.html', replaceHistory: false }
startNavigate: Fires when navigation begins. Event payload: { form: '<form name="form"></form>', path: '/pages/page1.html', replaceHistory: false }
endNavigate: Fired after the content has been retrieved and inserted onto the page. Event payload: { form: '<form name="form"></form>', path: '/pages/page1.html' }
<script>
var app = new senna.App();
app.on('beforeNavigate', function(event) {
//do something before navigating
});
app.on('endNavigate', function(event) {
//do something after navigating
});
</script>
It's important to understand how to handle errors when using Senna.js. Three important errors are Invalid Status, Request Error, and Timeout.
Invalid Status: Returns true for an invalid status. Any status code 2xx or 3xx is considered valid.
Request Error: Returns true if there was an error with the request.
Timeout: Returns true if the request timed out.
<script>
var app = new senna.App();
app.on('endNavigate', function(event) {
if (event.error) {
if (event.error.invalidStatus) {
//do something to handle invalid status
}
if (event.error.requestError) {
//do something to handle an error with the request
}
if (event.error.timeout) {
//do something to handle a timeout
}
}
});
</script>
It's recommended to checkout the examples source code in order to visualize all the possibilities Senna.js can provide.