Earlier this week, I realized that every organizing-a-jQuery-application blog, article, and conference talk misses the most important lesson on how to organize a jQuery app.
They talk about how to organize an individual widget or piece of functionality, but not how you can break up an application into logically separate and testable components.
Separation of concerns is the bedrock of software engineering. It is the best way to achieve a quality, error free, and maintainable project.
Think about it ... If your code's logic is isolated, how much damage does making an incorrect decision do? Very little!
The secret to building large apps is NEVER build large apps. Break up your applications into small pieces. Then, assemble those testable, bite-sized pieces into your big application.
This article shows how to do this elegantly with JavaScriptMVC 3.0 (which was created with this pattern in mind). We'll use Rebecca Murphey's Srchr app as an example of this pattern in practice.
Srchr
The Srchr app makes searches using multiple services and saves the searches between page loads.
Install Srchr
To install our srchr app:
-
Download And Unzip JavaScriptMVC
-
Install Srchr
./js steal/getjs srchr
Note: window's users do js steal\getjs srchr
Open srchr/srchr.html to see something like:
Note: this won't work in Chrome on the filesystem b/c of it's insane cross domain policy.
Srchr's Sweetness
Srchr was built the 'JavaScriptMVC' way (i.e. competently). It has a folder/file structure where:
- Code is logically separated and tested
- Code is easily assembled into higher-order functionality.
- Higher order functionality is tested.
- We are able to regression test.
Logically Separate and Test
We've broken up Srchr into the following components:
- Disabler - Listens for search messages and disables tab buttons. demo test
- History - A cookie saved list of items. demo test
- Search - Creates a search message when a search happens. demo test
- Search Result - Seaches for results and displays them. demo test
- Tabs - A Basic Tabs widget. demo test
Note: For the test pages, make sure you have popup blocker off!
The following shows the srchr folder's contents:
Each of Srchr's sub components has its own folder, demo page, tests, and test page. For example, srchr/search looks like:
This makes it extremely easy to develop a component in isolation. Lets look at the Srchr.History and Srchr.Search widgets a little more in depth:
Srchr.History
Srchr.History maintains a list of items in a cookie. You can add items to the list like:
$("#history").srchr_history("add", search);
You can also listen to when items in the list are selected like:
$("#history").bind("selected", function(ev, item){});
The srchr/history
folder has the following files to make developing and testing the history widget independently easy:
- history.js - Loads Srchr.History's dependencies then defines its functionality.
- history.html
- A demo page for Srchr.History.
- funcunit/history_test.js
- Srchr.History's tests.
- funcunit.html
- Runs Srchr.History's tests.
Srchr.Search
Search maintains a form that creates search events. You can listen to search events like:
$("#searchArea").bind("search", function(ev, item){});
You can also set the search form by passing it a 'search' object like:
$("#searchArea").srchr_search("val", search);
The srchr/search
folder has the following files to make developing and testing independently easy:
- search.js - Loads Srchr.Search's dependencies then defines it's functionality.
- search.html - A demo page for Srchr.Search.
- funcunit/search_test.js
- Srchr.Search's tests.
- funcunit.html
- Runs Srchr.Search's tests.
Assemble Higher-Order Functionality
Now that we've built and tested each of our widgets, it's time to assemble them into the final application. We do this in srchr/srchr.js
This file pulls in all the widgets and models we will need with steal:
steal.plugins('srchr/search',
'srchr/history',
'srchr/search_result',
'srchr/tabs',
'srchr/disabler')
.models('flickr','yahoo','upcoming','twitter')
.then(function($){
And then assembles them.
The following code makes Srchr.History and Srchr.Search work together:
// when a search happens, add to history
$("#searchArea").bind("search", function(ev, search){
$("#history").srchr_history("add", search);
});
// when a history item is selected, update search
$("#history").bind("selected", function(ev, search){
$("#searchArea").srchr_search("val", search);
});
Pretty nifty. It's like we're hooking up big legos. It's almost like it was engineered that way!
Now lets test the app as a whole.
Higher Order Testing
Srchr has the same file structure as our widgets for testing:
- test/funcunit/srchr_test.js
- Srchr's tests.
- funcunit.html - Runs Srchr's tests.
When you run the test page (funcunit.html), you'll notice that it runs all the widget's tests before running the Srchr tests. This is regression testing! You just have to open up Srchr's funcunit page and it will test all the other widgets before testing the application itself. This is a great way to find low-level bugs. And, the lower you can find a bug, the more easy you can solve it.
P.S. Unit Testing!
Srchr also tests connecting to the various search services. The test page is at srchr/qunit.html and the tests are at srchr/test/qunit/srchr_test.js
Conclusion
We've pretty easily accomplished our goal of splitting the application up into reusable components that are individually testable and testable as a whole.
This type of development isn't really possible without solid dependency management. Srchr.js just has to include its submodule, view, and model files. It doesn't have to worry about their individual dependencies.
If we need to make a change, we can work in the submodule folder, test it, make changes, then regression test the whole application.
This is why JavaScriptMVC is simply the best way to develop large applications - it makes the process straightforward and repeatable.
There are multiple (and probably better) ways to break up Srchr's components. How you divide up your app is up to you. But hopefully we've shown that you CAN break up your applications easily and it's a damn good idea.