Passion & Opportunity ? continue : break

What is Single Page Application (SPA)?
Written: 2019-05-08 14:11:33 Last update: 2019-08-29 20:52:05
Single-Page-Application (SPA) demo with vanilla JavaScript

Page navigation

SPA-container
Loading ...
Single Page Application (SPA) is a web design which collects all content into 1 page with a single URL, it is different than the traditional way to separate each content into different page with different URL.

To make it easy to understand this article, let's remember that the opposite of SPA is MPA (multi-page application) web design, SPA is the new way and MPA is the traditional way to create a website, please use the 'page navigation' buttons above to try this SPA demo.

An example of an MPA website is using many different URLs such as
  • Home page: https://example.com/home.html
  • Catalog page: https://example.com/product-catalog.html
  • About page: https://example.com/about.html
  • Contact page: https://example.com/contact.html

Using SPA web design we can combine those pages as one page with 1 URL (eg: https://example.com/index.html) and use a method to separate and display the pages inside this single page.

In the most simplest way, we can use '#' (hash) as a pageId (indicator) such as:
  • Home page: https://example.com/index.html#home
  • Catalog page: https://example.com/index.html#product-catalog
  • About page: https://example.com/index.html#about
  • Contact page: https://example.com/index.html#contact

There are many reasons to use SPA web design, one of the main reason is we want visitor to feel an experience similar to application experience instead of web experience, every click or page transition will be faster and smoother like native desktop/smartphone application, the web page transition will be faster because we don't need to load so many similar data (header, footer, navigation, etc.) for each page (because only use 1 URL and browser will not reload the page).
If you have read other SPA articles then maybe you already read about 'Router', I don't know who is the first person to use the word 'Router' for SPA, but 'Router' is the main logic or function behind SPA, 'Router' main job is routing or navigating between 'content' (pages), it should be able to answer the following questions:
  • What is current page?
  • How to get the content for the page? (which server?, what URL?)
  • How to display the content?

In this demo, I've chosen '#' as pageId (indicator) because 2 reasons:
  • With '#' we do not need to handle browser navigation history because '#' inside URL address means to tell browser to go to the specified content in the same page (same url).
  • It is very easy to detect page change, such as
    window.addEventListener('hashchange', function(e) {
      router();
    });
    

Below is the vanilla JavaScript to create simple 'Router' function, every time the page changed then we need to call router() to tell it to do its jobs. Inside the router, there will be a function to get 'content' from remote server, normally by calling an API to get the JSON data, please note that in this demo the 'content' is hardcoded (no connection to any server).

function router() {
  // define pageId and parameters
  var pageId = location.hash.replace('#', '');
  var params = location.search.replace('?', '');

  console.log('router(), pageId: ' + pageId + ', params: ' + params);
  // we got 'pageId', next get the 'content'

  // get content
  var content = getContent(pageId, params);

  // Increase SEO, update page title, NOTE: not all crawlers can read this!
  document.title = content.title;

  // display the content to screen
  display(content);
}
Below is just an example to get the content.
function getContent(pageId, params) {
  // just an example 
  var content = {};
  if(!pageId || pageId == 'home') {
    // this demo use hardcoded predefine value, 
    // normally content is fetched from remote server
    content.title = 'This is home page';
    content.content = 'bla-bla-bla home ...';
  } else if(pageId == 'about') {
    content.title = 'This is about page';
    content.content = 'bla-bla-bla about ...';
  } else if(pageId == 'contact') {
    content.title = 'This is contact page';
    content.content = 'bla-bla-bla contact ...';
  }

  return content;
}
After the 'content' is retrieved/defined then we call display(content) to display it on page. A simple example:
function display(content) {
  // get the 'app' container
  if(!spaContainer) {
    spaContainer = document.getElementById('my-spa-container');
  }

  // 1. parse content and build HTML string (this is only demo, so use simple content)
  var html = '';
  if(content.image) {
    html += '<div style="display: block;">';    
    html += '<img id="random_spa_image" data-src="' + content.image + '" style="display: none; margin: 0 auto; max-width: 100%" />';
    html += '</div>';
  }

  html += '<h3 style="text-align: center; padding: 10px;">' + content.title + '</h3>';
  
  if(content.content) {
    html += '<div>' + content.content + '</div>';
  }

  // 2. change container's content (for display)
  spaContainer.innerHTML = html;
}
First time after our page is fully loaded then we only need to execute router() to make it run and display the first page, such as:
window.addEventListener('load', function() {
  router();
});
In SPA page, there are 2 ways to change page, the first way is the direct link in HTML, like tag <a> (anchor) clicked to change page directly.

<a href="#other-page">Go to other page</a>
The second way is to use JavaScript.
function gotoOtherPage() {
  location.hash = '#other-page';
}
NOTE: if we use JavaScript then we should not manually get the page content inside gotoOtherPage() method, we just change the 'location.hash' value to trigger 'hashchange' event then let router() handle it, this mechanism will maintain the page history (go back and go forward).

Due to the usage of location.hash '#' to move to different page, therefore we can not use '#' to jump to in-page location, to jump to in-page location we need to use element.scrollIntoView(true) such as:
function scrollToElementId(eleId) {
  // jump to element without changing the location.hash
  
  var ele = document.getElementById(eleId);
  if(!ele) {
      return;
  }

  ele.scrollIntoView(true);

  // prevent click bubble
  return false;
}

That's it! this demo is only using a simple 'Router', it is not much but easy to understand the job of 'Router' (get pageId & parameters, get content and display content), some other articles will customize the 'Router' and may add more functions, that customization will depend on your web business logic requirement.
Single Page Application simply means content will be dynamic, it could means the whole page is changed or it means keeping the same for header, footer and navigation and only change the 'content' (normally located in center).

Changing the whole page is possible but very unlikely because it beat the purpose of SPA web, normally we only change some part of the page, in this demo I only change one part of the page, so we need to define an HTML element to be set as 'container' to display dynamic content, such as
<div id='spa-container'></div>
I use the HTML element id as 'spa-container' to put all the dynamic content into it, please see the JavaScript code in 'Router', it explained a simple way to display dynamic content into this 'container'.

In many SPA implementations (not all), there is a need to do some actions after container's content is changed, but the changes will need little time to wait for DOM element(s) to be created (rendered and ready for action), for example in this demo, I've put random image to be displayed, below is only a simple code snippet (not complete, just to understand the logic):
// load the image 
function loadImage() {
  var randomImg = document.getElementById('random_spa_image');
  if(!randomImg) {
    return; // element not found
  }

  randomImg.src = randomImg.dataset.src;
}

function display(content) {
  // get container is not retrieved yet
  if(!this.spaContainer) {
    this.spaContainer = document.getElementById('spa-container');
  }

  // 1. parse content and build HTML string (this is only demo, so use simple content)
  var html = '';
  if(content.image) {
    html += '<div style="display: block;">';    
    html += '<img id="random_spa_image" data-src="' + content.image + '" style="display: none; margin: 0 auto; max-width: 100%" />';
    html += '</div>';
  }

  html += '<h3 style="text-align: center; padding: 10px;">' + content.title + '</h3>';
  
  if(content.content) {
    html += '<div>' + content.content + '</div>';
  }

  // 2. change container's content (for display)
  this.spaContainer.innerHTML = html;

  // 3. do action after change container
  loadImage();
}
see above the call to loadImage() inside display(content), it will NOT work because at that time the container only change the string but the actual HTML DOM elements are not rendered yet, so we must wait until it is finished rendering then we can call loadImage().

We can use timer to delay in loop and keep checking whether the container has been rendered or not but it is not recommended because speed issue (different browsers and different devices have different timing), the proper solution is to use MutationObserver to wait for 'spa-container' to finish rendering, this demo using simple function waitForContainerContentChanged() such as:
function waitForContainerContentChanged() {
  var containerObserver = new MutationObserver(function(mutationsList, observer) {
    loadImage();

    // stop observing to avoid slow page!
    observer.disconnect();
  };);

  // listen to 'spa-container' content changed
  containerObserver.observe(document.getElementById('spa-container'), {
    // only observe this container direct child changes (not grandchildren)
    childList: true
  });
}

We created a MutationObserver to observe if container content is changed, when it changed then we get a callback which we can call loadImage() and then stop observer from keep observing by calling observer.disconnect(), to call this function waitForContainerContentChanged() we need to change the display(content) like
function display(content) {
  // ... other codes

  // 1. parse content and build HTML string 
  // ...

  // 2. register listener to be notified after container is updated.
  waitForContainerContentChanged();

  // 3. change container's content (for display)
  document.getElementById('spa-container').innerHTML = content;
}

Using MutationObserver to detect element changes is very important to build SPA web page, especially if we want to use slide-in and slide-out animation to display content, it is used in some front-end web frameworks such as AngularJS, KnockOutJS, ReactJS, VueJS and others.
PROs:
  • Faster when navigating from one page to another page, we don't need to refresh page or reload all web resources again, we may only require to call one API.
  • Smooth transition between pages, some parts of the page like header, footer, navigation or others may remain the same and don't need to be re-render, this will make user feel like using real 'application' instead of 'web'.
  • Able to create full function offline web, if using 'Service Worker' then we can refresh data (automatically sync) without user interaction.
  • Easier to deploy, because normally only 1 file or less than MPA.
  • If use '#' then SPA may be tested in locally without server.
  • Can create web with only static file resources (eg: *.html, *.css, *.js).
  • Can reduce server high-load problem if attacked by bad-bots or crawlers by only using static files.


CONs:
  • Large initial data, loading the first page require larger network data because may need to load and prepare some library or frameworks which may not be used by the user.
    (This problem may be reduced if we use IndexedDB or LocalStorage to cache some data in client-side)
  • Complexity, the more content added into SPA then development will be more complex.
  • Handling page load or Ajax call failures for each different pages maybe complex.
  • Security issue, because normally SPA uses many Ajax call to fetch the content, so hacker may modify the data in client-side and let SPA transfer the data to the server, also maybe server need to allow CORS (Access-Control-Allow-Origin: *)
  • SEO, most probably SPA web will get less page ranking because
    • Traditional SEO (which unable to read and parse JavaScript) is using 'Robot' crawler that may have problem to read page content rendered by JavaScript (render in client-side).
There are a few ways to increase our web page ranking in search engine:
  • Every time the page content is changed then we also change the page title 'document.title' and probably other meta tags too.
  • SPA website normally has 1 entry URL, search engine will think our web only has 1 URL too, to reduce this problem we can create sitemap.xml to tell 'good search engine' what URLs are available.
  • Use hybrid rendering, in client-side by JavaScript and in server.
SPA web design is not for all websites, SPA is a way to simplify viewing many simple pages as one, I personally think SPA is more suitable for displaying many pages which has simple content such as displaying text and images, like a book or a presentation or a blog site.

A large eCommerce website which has many pages such as front page, category listing, search product, product detail, payment option, etc. may have difficulty to use pure SPA because controlling each pages with Javascript may not be easy.

We can definitely combine MPA (traditional) and SPA, just need to define which pages maybe combined, actually this 'hybrid SPA' is used by many sites.

Once we understand what is SPA and how to implement it with vanilla JavaScript then actually we can see that it is not difficult to create SPA web, we just need to implement the proper 'Router' function for our web content.

There are some other JS frameworks to help to make SPA easier for large website, such as AngularJS (already has built-in 'Router'), ReactJS with React Router, VueJS with Vue Router or other frameworks can help to provide more features for your custom 'Router' function.

Is the router logic is valid? is it good enough? well, it is the most simple, bare bone routing function, get the pageId (location.hash) then get the content based on the pageId then display the content, it could not be more simple and sweet, I am personally recommend to use Preact JS to display/render content to the screen, PreactJS is much more lightweight compare to ReactJS, but if you feel PreactJS is still too big or has more function than you need then you can use HyperScript to render content to screen, HyperScript is good enough to avoid using HTML5 Template and also to make re-useable components.