Stark - Simplified separation of components into decoupled applications

Convoluted solutions

The web industry is a fascinating beast. It is constantly evolving, changing, morphing itself into new shapes and producing fun tools for our pleasure (and disposal). It's like a hyper-active child: it can't sit still.

For all the good things that come from this inherent behaviour (e.g. every few weeks there are new toys and techniques, tricks and tips and those beloved buzz words), there is one problem that seems to consequentially arise… complication.

It seems we've lost the simplicity in solutions we provide. It would seem we're a breed of enthusiasts who (knowingly or unknowingly) love to over-engineer things, and there in lies the problem.

Now I see this in lots of different places, down to the architectures we create, to the code we write to the designs we implement.

Simplicity

"Simplicity" is defined as…

the quality or condition of being easy to understand or do

…but this doesn't always present itself in the projects I see or have worked on in the past.

Perfection?

Now let's be clear here, I'm no white knight in shining armour. I, like most developers, have at some point written (and may continue to write) bad code. That's just the way it is when you either have tight deadlines, or you're unsure of how to solve a problem (or whatever other reason there may be for writing bad code). Sometimes what we do can seem more like a 'feeling out' process than a fine art.

What we do is a creative process. Yes writing 'code' isn't usually thought of as creative but it is, it is an art and a difficult one at that.

Any time I've seen some bad code and thought "WTF?", I'm not trying to disrespect the author, or question their skills as a developer. That's just a gut reaction we all have when looking at something we don't understand.

We need to remember to put ourselves into the shoes of the author and take their perspective, try to understand the circumstances that brought about the code we're looking at now.

It's all so easy to criticise, it's another to "do" (and do well).

Good code design

When I started working at the BBC I had the pleasure of working with a chap called Dan Scotton. Dan, like me, loved to draw his code structure. It was a great exercise in making sure you weren't going to write code that was redundant or useless. Usually he would ask for my thoughts on code he was working on, which was great to be a part of because it helped both Dan and myself better understand the problem and what the right solution should be (you'd be surprised how many developers keep their code or ideas tightly guarded, afraid of conflict or disapproval from others).

One of the main points I always reiterated to Dan was the idea of simplicity: "it needs to be as simple, yet effective, efficient and practical as possible" (luckily I didn't need to preach for too long, as Dan already incorporated those ideas).

But Dan also introduced me to the principles of good 'code design'. Through him I was inspired to read older software engineering books (mostly around object-oriented code and design - lower level, server-side, languages such as Java, C++, Ruby etc).

For years up until this point I was already a big believer in Design Patterns (as prescribed by 'The Gang of Four') but that didn't mean I understood the principle of good code 'design'. Reading these older software engineering books helped nail home the idea of simplicity which really is the heart of the subject and ironically is the hardest and most complicated principle to truly understand.

The front-end landscape

So where am I going with this and what problem am I trying to solve?

Well, for me the front-end developer landscape has become a dumping ground of overly complicated and over-engineered solutions.

There are some seriously good tools out there. For example, the CSS pre-processor Sass is a useful tool which solves a particular problem. Sass is a just one small piece of the overall puzzle (forgive me this next bit), but…

with great power, comes great responsibility

…Sass (like most of the tools we have at our disposal) can, and is, mis-used.

With regards to Sass, I've seen developers writing mixins that try to solve a thousand different problems all at once (scenarios they may never have). I can only assume they must be thinking: "if I include this extra piece of functionality then people will thank me later when they do finally need it".

This isn't just a problem with developers trying to solve too many problems at once. It ultimately comes down to breaching, and probably a lack of understanding, many good code design principles that help us avoid such disastrously unmaintainable and unscalable code**

**if you want a good book to read on the subject then I can whole heartedly recommend you get "Practical Object-Oriented Design in Ruby" (also read my own write-up of that book: "Object-Oriented Design")

Introducing Stark

This brings me to a project I've been working on (and still am working on) called Stark.

Stark is a work in progress. It is by no means a finished product. But the principles it tries to incorporate are things I'll attempt to explain in more detail here in this post, and are definitely worth your time investigating further still.

So what is Stark?

Stark is simply a fancy name for a 'strategy'. It's not another development framework. It's a way of thinking about, and implementing your project's architecture and code design. Primarily based around the loading of JavaScript and JavaScript based components, communication between those components and also the CSS that provides the 'skin' for those components.

It is a mix-mash of different techniques and styles but ultimately is aiming for simplicity and effectiveness.

The API I've used, and some of the code structure is based off of the great work Addy Osmani has done with his Aura.js project.

It's worth noting that what I've done here for Stark is not even in the same league as Aura. The reason I mention this is because I'm aiming for… simplicity :-) I didn't want (or need) the extra bells and whistles. I had my own set of problems I was trying to solve, and reducing the number of dependencies, libraries etc was important for me.

I'm also not suggesting you should use Stark (I'm not suggesting my way is the best way), but just to use it as a basis for learning about other ways of thinking and approaching front-end work.

How does it work

Stark is based around the idea of components (pages being made up of uniquely individual components).

Components are written into the page using plain HTML. Our JavaScript application when initialised will find all components and instantiate them for you automatically. The components can have fallback content which won't be visible as long as JavaScript is enabled (this is because in the <head> of the page we have a single inline <script> tag which adds the class js to the <html> tag. With this class added our CSS only applies styling to elements if there is a .js class available - similar to how Modernizr works).

On the JavaScript side of things we utilise AMD (Asynchronous Module Definition) which is an API for loading individual JavaScript modules. Nothing new here. But we also use RequireJS for our script loader. Lastly, because we're lazy-loading component JavaScript modules at run time (e.g. in development) we don't want to be doing that when we go live (e.g. into production) because we want to be good web citizens and reduce the number of HTTP requests we make, so we lean very heavily on a custom build script using the associated r.js tool which handles that for us.

On the CSS side of things we want to be as maintainable, flexible and scalable (i.e. all the great things Harry Roberts talks about). We use the BEM naming conventions for making sure our CSS isn't overly specific and we use Sass for helping to keep our implementation details of a component encapsulated within the component's own style sheet. We also use a trick that Jake Archibald came up with for generating an IE based style sheet (we'll cover that shortly).

Later on when we look at some example code this will start making a bit more sense.

Components and Extensions

I made reference above, and I do so regularly in Stark to 'components' and 'extensions'.

I think components are pretty self explanatory, but just to be clear: they are self-contained pieces of functionality.

Components know nothing about each other (i.e. they lack 'context'). Components communicate through the 'mediator' design pattern, meaning they can publish an event when they do something interesting and they can also be notified of events outside them, but they don't need to know anything about the objects that publish those outside events. Total separation means components can more easily be updated and migrated if need be.

Extensions on the other hand are just small pieces of extra functionality that you can mix into your code. The mediator pattern itself is an extension that I include (i.e. 'mix') into my application.

I'll give examples of these shortly.

Convention over configuration

Oh boy, "convention over configuration" is usually an alarm bell for me when looking at a new project. It screams "hey, this is my opinionated view on how things should be done". But what I didn't realise before (which I do realise now) is that, it's not until you try and solve a particular problem that you soon discover yourself becoming very opinionated about the route you've taken to implement that solution.

We developers are a sensitive bunch and don't normally take criticisms well.

But the difference between you and an overly opinionated developer is that those opinionated developers generally aren't willing to hear alternatives or to take advice or alternative ways of thinking about a problem.

I like to think I'm very open to suggestions, change and better ways of doing things. If someone opens up an issue (or better yet a pull request) that suggests a more effective way of solving a problem, then I'm happy to hear it.

That being said, I've implemented a small set of conventions (explained in the repo, but I'll cover briefly here)…

…see that wasn't too bad now was it.

Examples

Finally we come to some code!

API

As mentioned earlier, we use RequireJS and AMD to wrap the above API calls, for example…

// the only part of this code that needs to change
// is the list of extensions you want to use
// the rest is standard (read: required to be run)
require(['app', 'configuration'], function(app) {
    app.use('extension-a', 'extension-b');
    app.start();
});

But now let's look at each section individually…

HTML…

<div data-component="hello" id="js-component-hello">hello</div>
<div data-component="world" id="js-component-world">world</div>
<script src="libs/require.js" data-main="bootstrap-about" async></script>

As you can see we have a data-component attribute which is used by the JavaScript application when it is booting up (initialised).

But we also have an id attribute which I use to reference the component from within the component's JavaScript module code. So for example, based on the 'conventions' I mentioned earlier, the JavaScript application will find a HTML div with a data-component name of hello and will try to load components/hello/component.js - inside of that JavaScript file I might want to replace the div in the DOM with a totally different chunk of HTML code, or I might want to inject more HTML inside of that div element, the choice is up to you as the developer to decide what to do at that point.

You don't really need to include the id attribute if you don't want to but my build script will try and use it as a reference to the DOM node. You can reference the DOM node any way you see fit, I just personally prefer the speed of native getElementById to grab DOM nodes.

Originally I had thought about not using an empty div container with a custom data- attribute but just a simple HTML comment <!-- #component hello --> but then I realised that there wasn't an easy hook into the page without parsing the entire DOM, and also there was no fallback content that could be provided.

Bootstrap...

require(['app', 'configuration'], function(app) {
    app.use('mediator', 'extension-a', 'extension-b');
    app.start();
});

I had considered just having a single bootstrap script but the list of extensions used is very likely going to be different per page, hence we have a different bootstrap for each page.

The bootstrap loads in our JavaScript application along with a configuration object (which is just the RequireJS config)…

require.config({
    baseUrl: './',
    paths: {
        jquery: 'libs/jquery'
    }
});

Once our application is loaded we tell the app what extensions we want to use and then we tell the application to start.

Component...

define({
    init: function() {
        console.log('an example component');
    }
});

This is an example component. As mentioned earlier: components need to have a public init method, otherwise they can do whatever you want them to.

Extension...

// Silly example
define(function(){
    if (!String.prototype.blah) {
        String.prototype.blah = function() {
            return this + ' BLAH!';
        };
    }
});

This is an example extension. Extensions can do whatever you want them to do.

The only thing worth noting about extensions is that they can't take advantage of dependency injection (e.g. can't be passed directly into your components).

To work around this Stark uses the idea of a single global namespace called app (e.g. window.app = {};) which you can attach any extensions or other data you need to pass around.

So for example, our Mediator extension uses window.app.mediator = mediator; like so…

define(function(){
    var mediator = (function(){
        // code

        return {
            // public api
        };

    }());

    window.app.mediator = mediator;

    return mediator;
});

Don't mistake the phrase: "Don't pollute the global namespace" with "Don't ever use the global namespace ever ever ever" - that's just dogmatic nonsense.

CSS

So our CSS uses Sass to help us keep our code more easily maintainable. Each page should load a page specific style sheet that contains the code relevant for that page.

So our example page loads project.css, the Sass file looks like this…

/* =============================================================================
  Base Utilities
  ========================================================================== */

  @import 'partials/variables';
  @import 'partials/utils';
  @import 'partials/baseline';
  @import 'partials/grid';

/* =============================================================================
  Patterns
  These are common design patterns abstracted into reusable chunks of code
  ========================================================================== */

  @import 'patterns/media';

/* =============================================================================
  Core
  This is the experience for either really old devices, or non-js devices
  ========================================================================== */

  @import 'partials/core';

/* =============================================================================
  Groups
  Any design enhancements that aren't a separate component and aren't part 
  of the core experience should be placed within one of the following groups.

  Note: we're using inheritance for our CSS loading strategy which means each
  stylesheet inherits styles from the group that came before it
  ========================================================================== */

  @import 'groups/group1';
  @import 'groups/group2';
  @import 'groups/group3';
  @import 'groups/group4';

/* =============================================================================
  Components
  You only need to import components into the top-level project/project-ie files
  Components have been built to be self contained.
  This means they encapsulate any logic about viewport dimensions.
  This allows them to control their own visual display.
  ========================================================================== */

  @import 'components/hello';
  @import 'components/world';
  @import 'components/features-and-analysis';
  @import 'components/index';

I won't go into detail for each part of this file because the code comments should be self explanatory. But we'll look at bit more at the 'groups' and also the 'components'.

Groups

We break-down our layouts into 'groups', and each group inherits styles from the group that came before it.

Here is what our group 1 looks like…

/* =============================================================================
   Group 1
   All devices sizes
   ========================================================================== */

.js {
    body {
        background-color: red;
    }
}

…so group 1 is applied for all device sizes and we're saying all device sizes should see a red background colour.

/* =============================================================================
   Group 2
   Viewport >= 400px
   ========================================================================== */

.js {
    @include respond-min($mq-group2) {
        body {
            background-color: yellow;
        }
    }
}

All our other groups constrain their stylings using media queries (which we've abstracted into a mixin) and use a variable to store a reference to the relevant break-point size.

You can see that the styles are constrained further by the .js class prefix and nesting the rest of the CSS/Sass inside of it.

In our group 2 we're overriding our previous groups styling by saying we want the background colour to be yellow for group 2 sized devices.

Components

Our components work in a similar way to our groups…

Here is an example component…

/* =============================================================================
   Component: Features & Analysis
   Distinguishes featured content from standard content
   ========================================================================== */

[data-component="features-and-analysis"] {
    border: 1px solid blue;

    @include respond-min($mq-group2) {
        & {
            border-color: pink;

            .media {
                float: left;
                width: (1 / 2) * 100%;
            }
        }
    }

    @include respond-min($mq-group3) {
        & {
            border-color: yellow;

            .media {
                width: (1 / 3) * 100%;
            }
        }
    }

    @include respond-min($mq-group4) {
        & {
            border-color: red;

            .media {
                width: (1 / 4) * 100%;
            }
        }
    }
}

Internet Explorer

So how do we encapsulate logic (read: bug fixes) for Internet Explorer? We use Sass to help us incorporate these IE specific fixes…

[data-component="hello"] {
    border: 1px solid blue;

    @include respond-min($mq-group2) {
        & {
            border-color: green;

            @include old-ie {
                border-width: 2px;
            }
        }
    }

    @include respond-min($mq-group3) {
        & {
            border-color: yellow;

            @include old-ie {
                border-width: 3px;
            }
        }
    }

    @include respond-min($mq-group4) {
        & {
            border-color: red;

            @include old-ie {
                border-width: 4px;
            }
        }
    }
}

…in the above example you can see we have a old-ie mixin that applies a different style for Internet Explorer versions <= 8. It's nested inside and the way the mixin works is when you compile this Sass code, if a variable ($old-ie) is set to true then the mixin will generate the relevant code.

So in our HTML page we have the following code…

<link rel="stylesheet" href="styles/project.css">
<!--[if lte IE 8]>
<link rel="stylesheet" href="styles/project-ie.css">
<![endif]-->

…as you can see we use a Microsoft Conditional Comment to load an additional style sheet just for IE versions less than or equal to 8.

The contents of the associated Sass file is…

@import 'partials/variables';

$old-ie: true;
$fix-mqs: $mq-group4;

@import 'project';

…this Sass file will generate a project-ie.css file. It simply sets some variables and then imports our main projects.scss file. But when we import that file, the decision the Sass code makes (e.g. the old-ie mixin) will change as it relies on variables which we've overwritten within project-ie.css.

Also, our media query mixin internally uses the variable fix-mqs and because IE <=8 doesn't support media queries, we overwrite that variable value with a fix width (in this case we want old IE to load up our group 4 width, which is our desktop width).

Conclusion

OK, so this was a quick run-through of my Stark project and its purpose might still not be totally clear to a lot of you at this point. But as I said from the start, I'm not selling an all encompassing framework or set of tools that work 'out of the box'. I'm simply demonstrating a 'strategy', a way of working, thinking, and writing/structuring your front-end code.

I'd love to hear your feedback and get your thoughts so contact me on twitter, open up an discussion on GitHub (or better yet a Pull Request).


Links