Taking A Mixed Approach To Single-Page Applications
A co-worker came up to me with an interesting situation.
The client he was working with would be building hundreds of single-page applications and all would need to be tied into a single shell application. He had first attempted to use an iFrame contained within another Single-Page Application to display the child applications.
While this worked, he came up against another requirement: the child applications may or may not need access to data from the parent shell application.
It was at this point he came to me for suggestions. I had been playing with this exact idea for sometime; how can you manage a collection of Single-Page Applications and still share data between them?
At this point, I decided to create a hybrid solution of mixing Single-Page Applications with a server-rendered shell application. The following is the process I took for creating this solution. I’ll highlight some of the pain points I ran into while building it out along with some suggestions for further enhancements.
Step One: Building the Child Single-Page Application
The first thing I needed to do was to build a child single-page application. The client would be using Angular 1.x, so I found the highly-rated Yeoman generator generator-gulp-angular. This generator was chosen because it uses many best practices for creating an AngularJS 1.x application.
Once the generator runs, you have an application which allows the developer to use Browsersync for live development, the ability to use Gulp to run several types of builds, Bower to manage the front end assets, and has both unit testing and end to end testing built-in.
Running the generator is straightforward and produces a runnable application pretty quickly. I did take the time to clean out many of the default examples items in the application and added in some client-specific features, such as the SASS styles, and a single required feature for example purposes.
I was impressed at how well the generator followed John Papa’s Angular 1.x style guide. I also found that VS Code snippets for Angular 1.x worked well with the generated code right out of the box. We quickly had a child application developed in just a handful of hours.
angular service example
(function () { 'use strict'; angular .module('gulpAngular') .service('apiService', Service) /** @ngInject */ function Service($http) { var getAll = function () { return $http.get('data/users.get.json'); }, getById = function (userId) { return $http.get('data/' + userId + '.get.json'); } return { GetAll: getAll, GetById: getById }; } }());
The single-page application used Gulp for managing all of its build and run tasks. I took advantage of this and was able to build an optimized production version of the application. What happened is that it took all of the SASS files and compiled them down into a single file.
Gulp also condensed all of its vendor-specific files and created one each for the styles and for the scripts. Lastly, Gulp took all of the HTML templates and application-specific scripts and compiled them down also into a single file. The build scripts also created an optimized HTML index file and packaged all of the assets into a distribution folder.
Okay, step one is completed. We now have a working single-page application that has been optimized for production and is ready to placed into a shell application.
Step Two: The Shell
So the initial problem was with the integration of the child SPA and the containing shell.
While it’s easy to have hyperlinks just bring up an HTML page in an iFrame, it’s really pretty difficult to have the parent and the iFrame work well together. My thought was, why not have the SPA just be a page in the shell in the first place and avoid the iFrame all together.
I had worked with NodeJS quite a bit at a former client. There, we had actually used the power of templating to render a single-page application which combined a header and footer from external sources. From this experience I felt that it would be easy to integrate additional single-page applications into a larger parent shell application.
I used the default ExpressJS application generator to get a simple application up and running. I had chosen Handlebars as my default templating engine during the scaffolding phase because of its similarity to the client-side markup used with AngularJS.
The layout for the shell came from startbootstrap.com where I used one of the templates that looked fairly close to what was needed by the client.
A couple of the design decisions that I made was to keep a separation between the single-page applications that will be imported into the shell and any pages which are created with ExpressJS and Handlebars. I did this by breaking out the routes with separate JavaScript files; one for the single-page applications and one for the internal routes which could be used by Express.
I also added a JavaScript file for API calls which might be made. My former client had used Node to marshall quite a bit of the data coming from third-party APIs and I figured this would be a nice example to place in the shell application.
In the view’s folder I broke out the Express partials
(internal page templates using Handlebars) and where the single-page application pages would live at, the apps
folder.
Now here is where it gets a little hairy. Since this was a prototype, I went ahead and left it this way since it worked, but it could definitely be enhanced some with some creative Gulp scripting.
Each page in the apps
folder is essentially the index page of our exported child SPA. But the child SPA wasn’t scripted to live inside of another application, it generated a full HTML page with the head and body tags along with all of the script tags added.
exported index page
<!doctype html><html ng-app=gulpAngular><head><meta charset=utf-8><title>KCS - Angular Reference</title><meta name=description content=""><meta name=viewport content="width=device-width"><link href=https://www.webcodegeeks.com/wp-content/litespeed/localres/aHR0cHM6Ly9tYXhjZG4uYm9vdHN0cmFwY2RuLmNvbS8=font-awesome/4.7.0/css/font-awesome.min.css rel=stylesheet><!-- Place favicon.ico and apple-touch-icon.png in the root directory --><link rel=stylesheet href=styles/vendor-04c8db0ab4.css><link rel=stylesheet href=styles/app-fbcced86b7.css></head><body><!--[if lt IE 10]> You are using an <strong>outdated</strong> browser. Please <a href="http://browsehappy.com/">upgrade your browser</a> to improve your experience. <![endif]--> <div ui-view></div> <img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" data-wp-preserve="%3Cscript%20src%3Dscripts%2Fvendor-5b6b3f5db1.js%3E%3C%2Fscript%3E" data-mce-resize="false" data-mce-placeholder="1" class="mce-object" width="20" height="20" alt="<script>" title="<script>" /><img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" data-wp-preserve="%3Cscript%20src%3Dscripts%2Fapp-9bd529611d.js%3E%3C%2Fscript%3E" data-mce-resize="false" data-mce-placeholder="1" class="mce-object" width="20" height="20" alt="<script>" title="<script>" /></body></html>
Now by default, this won’t “just work” out of the box. Here are the steps I used to correct this:
I copied the entire exported single-page application folder into the shell’s public
folder. From here, all of the assets are accessible when a rendered page is sent to the browser.
Next I created a Handlebars template in the apps
folder and named it the same as the folder found in the public
folder. I opened up the index.html
from the public
folder and I copied only the script tags and the items necessary for Angular to work. Then I pasted those items into the Handlebars template.
Like I said before, we’re making the single-page applications index page here, but we didn’t need the body
tags and head
tags. After adjusting the paths of the scripts and styles, I saved that file.
spa handlebars index page
{{!--<link rel=stylesheet href="../app_puu/styles/vendor-04c8db0ab4.css" />--}} <link rel=stylesheet href="../app_puu/styles/app-81ae878e3a.css" /> <div ng-app="gulpAngular" id="inner-app"> <div ui-view></div> </div> <img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" data-wp-preserve="%3Cscript%20src%3D%22..%2Fapp_puu%2Fscripts%2Fvendor-5b6b3f5db1.js%22%3E%3C%2Fscript%3E" data-mce-resize="false" data-mce-placeholder="1" class="mce-object" width="20" height="20" alt="<script>" title="<script>" /> <img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" data-wp-preserve="%3Cscript%20src%3D%22..%2Fapp_puu%2Fscripts%2Fapp-b586be956c.js%22%3E%3C%2Fscript%3E" data-mce-resize="false" data-mce-placeholder="1" class="mce-object" width="20" height="20" alt="<script>" title="<script>" />
In the routes
folder, I added the single-page application to the application
specific route file. Nothing special here, the route is written just like any other in Express.
What we’re given here by the design is the ability to pass data from Node into our single-page application before it is rendered to the browser. At this point we have all the power of having a server-rendered page available for us to use if needed.
spa route added
module.exports = { register: function (app) { app.get('/puu', function (req, res){ var data = {puu: true, title: 'Pricing User Update'}; res.render('apps/puu', data); }); } };
Last thing done, opened up the shell’s layout template and added a hyperlink to the navigation. At this point we should be done adding a single-page application to the shell.
hyperlink added to layout page
<ul id="demo" class="collapse"> <li class="{{#if puu}}active{{/if}}"> <a href="/puu"><i class="fa fa-user-o fa-fw" aria-hidden="true"></i>Pricing User Update</a> </li> </ul>
Step 3: Does It Work?
Using the ExpressJS NPM scripts, I fired up the shell and navigated to our localhost instance in the browser. The prototype only has a handful of pages because I wanted to keep it lean for the moment. The landing page and Handlebars pages work exactly as expected.
Opening up the single-page application section, we see our hyperlink for the child SPA that we imported and yes, it does work. The page renders, and the single-page application is displaying data and works as it did in its own development environment.
Lessons Learned and Potential Improvements
So I learned a few things from this prototype that, while not painful, were quite annoying. The child application was developed using a completely different stylesheet setup and when the child and the parent were combined together, the shell’s styles and the SPA’s styles clashed and several things/styles on both sides were lost. Taking more time to design a site-wide style would probably alleviate this pain point.
Secondly, while it isn’t difficult to manually add code from the generated index page, it can be very error prone. On a review of this application with a fellow developer, we thought that having Gulp script out a Handlebars page from within the SPA’s build step would be helpful to avoid the copying part. And doing additional research we thought that the hyperlink and routing steps could be somewhat more dynamic if the information came from a database.
And while I said that the application pulled data just fine, that’s only after I went and adjusted where the folder containing my static data was located at. Paths from within the SPA are localized to the public
folder and the layering I had could not be resolved. While this was an issue for the prototype, when real production-ready single-page applications are used, they should be using remote APIs so this would not be as much of an issue.
Summary
So the prototype worked as expected. I successfully took a single-page application built 100% outside of the shell application, imported it into the shell and was able to bring it up within the shell at runtime. We avoided the iFrame issues of not being able to talk to the shell. And we gave the entire application the ability to render data both from the server and across sections, as all items were now rendered together.
We inherited a few advantages of being able to secure the SPAs at the server-level by completely blocking a user’s access at runtime. We avoided having a single SPA with hundreds of views and potentially having large memory leaks happen over time by rendering only the necessary single-page application and dropping it out of memory on a navigation change.
All in all, I feel that this was a successful prototype.
Reference: | Taking A Mixed Approach To Single-Page Applications from our WCG partner Chris Berry at the Keyhole Software blog. |