Backbone Models and Marionette Views have some really powerful built in functionality for keeping themselves separated but not crippled.
At the beginning of the year when we started developing Greenhouse Go Carbon Planner, we were met with complex interwoven relationships we needed to abstract. We chose Backbone and Marionette.js because of their flexibility and lack of assumptions, and we haven't looked back.
Intro to Marionette
To quote the website, "[Marionette] is a collection of common design and implementation patterns found in the applications that we have been building with Backbone, and includes pieces inspired by composite application architectures, event-driven architectures, messaging architectures, and more."
Why We Chose Marionette
Carbon Planner is a web-based application for land developers, consultants and public agencies to forecast greenhouse gas emissions from proposed land development, and plan a mitigation strategy to meet local goals. As such, it's more complicated than just retrieving data from an endpoint and displaying it on the screen. We do quite a bit of calculations for display purposes on the front end, and we wanted to have complete control over how that was rendered.
Marionette focuses on the most commonly used patterns found in complex Backbone applications presents them to you as a tool belt. For us and for Carbon Planner, it was the great medium between vanilla Backbone, and a heavily opinionated framework.
A pivotal reason we chose Marionette over another framework, is that the binding of Model to View was more complex in our application than in most. We had a whole set of calculation classes running in the background calculating emissions, land usage, among other things (these were duplicated on the backend if we needed them there). It would have been really computationally expensive to just let a framework with two-way binding do it's thing. We needed finite control over when the computations should run, keeping performance speedy, and we didn't want to hack anything to do that.
Build Process and Folder Structure
Our Dev Environment
Our build process is pretty basic and up to speed with most other digital agencies out there. We have NPM managing our dev dependencies, Grunt for compiling and concatenating SASS and JS, and LiveReload for refreshing the browser.
Our Folder Structure
Our front end folder structure is as follows:
scripts/main.js // This starts up the app
backbone/
apps/
collections/
controllers/
layouts/
screens/
modules/
models/
modules/
routers/
views/
built/
classes/
tests/
vendor/
When building large applications, being able to separate modules and components into logical folder structure is crucial for staying sane.
This is a simplified version of our folder structure, but one thing to note is separating layouts and views based if they were tied to a layout or not. With our naming conventions being repetitive (a property of Carbon Planner), this folder structure helped a lot with knowing where to look for a specific view. Is it meant to be reusable? -> modules, no? -> screens.
Also another thing of note, since we have a companion admin panel for the Greenhouse Go team written in backbone, we have two sets of these folder structures. Separating high level apps within your ecosystem is a must.
Keep Events Simple
Let's get things started with events. I had a conversation with Bryant about what we thought the best events approach would be for Carbon Planner. It is absolutely a valid solution to abstract things into events for multiple modules to talk to each other, but if you aren't careful it can get into a pretty hairy "Who's on first?" scenario. The alternative, getting caught up in callbacks and repeated methods, is just as smelly.
So rather than get tangled in a spaghetti web of willy nilly app wide messaging, we kept our events close to home. What I mean by that, is that it was never very hard to find out where the event was called from, or where the event was being used. We refrained from talking too much between modules and kept things related to each other. This is a huge plus in simplifying a large application with a lot of moving parts. It also makes the debugging process a lot quicker.
As an example, we had our views watching their models, and our models watching for change events (e.g. fetching from the server) to perform some calculations on the front end, but we never broadcasted app-wide "User Model has changed." events. That wouldn't tell us where that event was being used, or what actions were taking place after that event had fired, which could lead to memory leaks and loops.
Keep Models Clean
Marionette doesn't add anything special to the Backbone Model class, but there were two things that really helped clean up our models: utilizing model events and keeping models syncable.
As I mentioned before, we had a set of calculation classes that needed to be used on both the frontend and the backend. The output of these classes were entirely dependent on model they are instantiated with. Backbone events are absolutely crucial in untangling the web of dependencies. But be careful; keep your events close to home and you won't run into any trouble. It's very easy for those events to cascade into a performance nightmare.
So we created the event listener inside the initialization method of our model. Every time the model changes, run these calculations. The great thing about doing it this way, is that the event listener is very clearly coupled with what it is listening to.
I mentioned keeping models syncable. What I mean by that is keeping models that were to be eventually synced with the server prepped and ready to do so. Any extra properties of the model we needed (that came from the calculation classes and not the server) were kept as methods.
model.property() is just as simple model.get('property'), but if the server doesn't need to know about it, it doesn't have to. Try not to set properties that will be thrown away later.
This kept us from having to validate and clean up our models a ton before sending them off to sync. Also, in the instance of properties of a model that were in relation to a collection or other model (think percentage of the whole), properties could remain dynamic as the collection changed.
One gotcha here, is getting into feedback loops. It's easy to fall into models triggering events that change themselves, and continue to trigger the event continuously. Be wary of this.
Marionette Views
Probably the focal point of Marionette's toolset are the various view types it comes with - Layouts, Composite Views, Item Views, and Collection Views. They take a lot of the guess work out of rendering basic and commonly used data structures.
We took a top down approach due to the nature of Carbon Planner which has worked out well. Layouts are made out of sublayouts, modules, smaller views and so on. When a child wants to talk to a sibling, those layouts call up to the closest parent that can relate to all the other subviews, keeping events close to where they were fired.
And since Layouts are nestable, you can easily avoid re-rendering large portions of the app by modularizing the Layouts accordingly. You can nest other view types, which requires some thinking, but is doable and absolutely kosher. Use your own discretion on how granular you get, but it rarely hurts.
In light of keeping our models clean, one of the big advantages of Marionette Views are Template Helpers. Template Helpers are functions on the view that can be used within a template, but don't have to be attached to any model. It's yet another way to keep models ready to be sent off to the server, and it's a really powerful addition for layouts. It solves quite a few problems.
Sidenote related to views, I have run into issues with Marionette's event hash being somewhat unreliable for when switching views in a region, but an easy way to fix that is to call delegateEvents on the render method. Every time the the layout is rendered, make sure events are in place.
Make Views Modular
Halfway through the process of building this app, we made some pretty major design changes that affected just about every view in the application. This was a pretty long process, but it was made much easier by keeping as many things reusable as possible.
Decouple the view from the data it will show as much as possible.
Think about your app being made up of modules that form layouts. Try to allow those modules to be thrown anywhere, and be given a completely different information and subtle view nuances wherever they are put. You can set up your views to be configured from when they are called by accessing the options object in the initialize method, and this helps a ton when creating many views that are similar. This reduces creating duplicate code by quite a bit.
Controllers (not those kind)
Marionette Controllers are note controllers as we see in a typical MVC pattern, they are just easy ways to encapsulate groups of logic, and control when and how those groups of logic are initialized and destroyed.
Use controllers for abstracting logic that doesn't necessarily go with a view or model.
In our case, we were able to use controllers for app wide messaging and alerts, controlling third party scripts like Highcharts and Mapbox, and to minimize how often our client has to check back with the server to make sure our models are in sync. This means if it didn't make sense to be called from a model, and it didn't make sense to be tucked away in a view, it got it's own controller.
Use controllers for making sense of related parts.
We also utilized controllers for creating collections on the fly from multiple types of collections coming from the back end. In our case, Carbon Planner allows the user to add different kinds of land usage to a site. We were able to create collections needed for multiple views based on "model" land use types and the ones that the user has the ability to edit. This way, every time a user added a land use type, we only stored the LUT id, the id of the user, and the id of the site they put it on. We made the connection to what that actually amounted to (in acres or carbon footprint) on the front end.
Modules
I've been using the word modules to refer to encapsulated pieces of functionality or views, but lastly I'm going to touch on Marionette Modules.
Use Modules to decouple high level parts of the app.
This is all about separation of concerns. You don't want a big ole app object controlling multiple unrelated parts. Modules can be started and stopped separately and there are a lot of benefits to this. Separating big chunks of your application make it more navigable and testable. There is even dependency injection to boot.
You can use Modules to lazy load your application.
We didn't have to use modules this way for Carbon Planner, but it is very useful when dealing with robust client heavy applications. Minimizing what the user needs to download and initialize to get pixels on the screen is crucial for perceived performance. Marionette Modules are quite good at solving this problem.
What Marionette does well
Marionette stays out of the way.
Marionette does a great job of abstracting just the high level concepts that most Backbone applications would take advantage of. It's very much a Goldilocks framework in some situations, leaving the developer to make of it what he/she will.
Minimal Magic
We wanted some level of boilerplate abstraction already in place, but nothing we couldn't break apart and understand ourselves. It is very easy, and well documented, to adjust render methods and the like to suite your needs. Because of this, third party code and our own custom classes are implemented without much grief or resorting to poor practices.
What Marionette leaves unsolved
Nesting views can be a pain.
Sometimes individual models might have collections inside of them that you need to render on the template. For example listing all users, and within those user views, listing all their projects. Marionette gives you a really nice way to add regions for nesting views, but this is only built into the app instance itself and in layouts. If you need to do this elsewhere, say an itemView, you have to manually set up and call methods on the RegionManager.
One quick way to solve this, if the data isn't too complex, is to take advantage of looping in your Underscore templates.
Templates don't tell you a damn thing.
One thing that declarative front end frameworks like Angular, Knockout, and now Vue.js have over Backbone and Marionette is clarity when looking at a template. You can see that clicking X button corresponds with Y method on the view class. Looking at a bunch of ids and classes doesn't tell you anything about how that template interacts with its corresponding view. This is absolutely preference, but I'm okay with my templates being a little dirty if it helps me understand what's going on.
<DIV> Hell
One thing I really enjoyed about Angular was directives. They give you precise control over how your view is rendered, how the view is allowed to talk to the model, and cleans up your templates quite a bit, keeping complex view logic out of the DOM and inside your javascript. This is a small thing, but when you have a region and a nested view in Marionette, you can sometimes end up with 2-3 container divs for the same item! Angular directives let's you replace the container with the actual bound view and it still knows what you're talking about.
Coming to a Close
Marionette is a wonderful companion to Backbone for building out large applications quickly. It abstracts away many common patterns found in client heavy MVC applications into a tool belt, and does it in a way that doesn't get in your way if you need to get your hands dirty, by way of making few assumptions.
When dealing with large applications, event logic can get scary. Keeping a declarative approach can really help with understand where events are being utilized within the app and can keep those mystery events subdued.
Backbone Models and Marionette Views have some really powerful built in functionality for keeping themselves separated but not crippled. Try keeping models clean and syncable, and utilizing views as another valid place to put logic that doesn't really belong in the model.
All in all, keep every piece of your application as modular and reusable as possible. What are some things you've learned along the way that keep your large applications manageable?
Subscribe here to get our short and sweet monthly newsletter!