magnify
magnify
Home Angular AngularJS – server-side rendering
category

AngularJS – server-side rendering

Published on June 26, 2016 by in Angular, JavaScript

node
Node allows do various things with an unlimited set of libraries, …

I didn’t want to have a PhantomJS crawler to generate a huge set of html files, so, I dug around jsdom.

In this small article, I’m gonna to share the way I found to render my angular templates on server side.

First step is to add jsdom and angularjs in your node project if not yet done.

npm install --save jsdom
npm install --save angular

Here is a simple test snippet to see a server-side rendering with AngularJS:

var jsdom = require("jsdom");
var fs = require("fs");
var angular = fs.readFileSync('./node_modules/angular/angular.min.js', 'utf-8');

jsdom.env({
  html: '<div ng-app="app" ng-controller="AppCtrl"></div>',
  src: [angular],
  done: function (err, window) {
    var app = window.angular.module('app', ['ng']);

    app.factory("testfact", function ($rootScope, $compile) {

      return {
        render: function () {
          var scope = $rootScope.$new();
          scope.title = 'ABCD';
          var element = $compile('<div><span>{{title}}</span></div>')(scope);
          scope.$digest();
          return element.html();
        }
      };
    });

    window.angular.injector(["app"]).invoke(function (testfact) {
      console.log(testfact.render());
    });

  }
});

When executing this snippet, you get the “<span class=”ng-binding”>ABCD</span>”, I didn’t waste time on why the main node of the template is lost (div), so, I just keep in mind to wrap the template in a div.
Another point to be aware of, is the voluntary use of sync function (readFileSync), because, my target is to have an express API to render public user profile, I don’t wan’t the API to waste time on reading x times the same files, so I will load them in memory and reuse always the same.

In the API I’m targeting, I don’t wan’t the whole angular app to be run, I just want to consume a foreign service to get the user data and populate a scope to render the template.

Here is the main part of my API to provide this html user profile:

require('dotenv').load({silent: true});
var config = require('./config');
var express = require('express');
var request = require('request');
var bodyParser = require('body-parser');
var jsdom = require("jsdom");
var fs = require("fs");

var app = express();
app.use(express.static(__dirname + '/public'));
app.use(bodyParser.json());

var cache = {
  files: {
    index: '' + fs.readFileSync('./www/index.html', 'utf-8'),
    publicProfile: '' + fs.readFileSync('./www/modules/modals/templates/publicProfile.html', 'utf-8'),
    angular: '' + fs.readFileSync('./node_modules/angular/angular.min.js', 'utf-8'),
    avatar: '' + fs.readFileSync('./www/modules/app/directives/avatar.js', 'utf-8'),
    tools: '' + fs.readFileSync('./www/modules/tools/index.js', 'utf-8')
  }
};

app.get('/:urn([0-9a-zA-Z]{5,30})', function (req, res) {
  request.get(config.api.host + '/people/urn/' + req.params.urn, function (error, response, body) {
    generateProfile(body, res);
  });
});

function generateProfile(body, res) {
  var user;
  try {
    if (body) {
      user = typeof body === 'string' ? JSON.parse(body) : body;
    }
  } catch (err) {
    console.log(err);
  }
  if (!user) {
    return res.redirect('/');
  }

  jsdom.env({
    html: '<div ng-app="app" ng-controller="AppCtrl"></div>',
    src: [
      cache.files.angular,
      'angular.module("Main", ["ng", "Main.tools"])',
      cache.files.avatar
    ],
    done: function (err, window) {
      var app = window.angular.module('Main');

      app.factory("generator", function ($rootScope, $compile) {
        function produce() {
          var scope = $rootScope.$new();
          scope.input = {};
          scope.input.profileShown = user;
          var element = $compile('<div translate-namespace="tabs.profile">' + cache.files.publicProfile + '</div>')(scope);
          scope.$digest();
          var html = element.html();
          html = html.replace(/\sng-[a-z\-]+\s*=\s*"[^"]+"/gi, '');
          html = html.replace(/\sng-[a-z\-]+\s*=\s*'[^']+'/gi, '');
          html = html.replace(/\sng-(scope|binding)/gi, '');
          html = html.replace(/<!--\s[^-]+\s-->/g, '');

          return  '<script>' +
                    'var style = document.createElement("style");' +
                    'style.type = "text/css";' +
                    'style.innerHTML = "#no-js-profile {display: none}";' +
                    'document.getElementsByTagName("head")[0].appendChild(style);' +
                  '</script>' +
                  '<div id="no-js-profile">' +
                    '<div id="profile-web">'  + html + '</div>' +
                  '</div>' +
                  '<script>' +
                    'try {' +
                      'var element = document.getElementById("no-js-profile");' +
                      'if (element.parentNode) {' +
                        'element.parentNode.removeChild(element);' +
                      '}' +
                      'element.remove();' +
                    '} catch(err) {} ' +
                  '</script>';
        }
        return {
          produce: produce
        };
      });

      window.angular.injector(["Main"]).invoke(function (generator) {
        var content = cache.files.index.replace(/(<body[^>]*>)/, function (match, body) {
          return body + generator.produce();
        });
        res.send(content);
      });
    }
  });
}

As you can see, I do several things in the API:

  • Request REST API to get user data
  • Prepare a small html app in jsdom adding a few js sources from my memory cache (angular + some directives & services used in the template)
  • Generate and populate a scope
  • Compile the template and get the HTML
  • Remove the angular tags & comments which are useless there
  • Return the HTML with a small JS used to hide and remove it when the browser handle the JS
  • Inject the HTMl in the top of the body part of the original index.html

thats all folks!

 
 Share on Facebook Share on Twitter Share on Reddit Share on LinkedIn
No Comments  comments 

You must be logged in to post a comment.