Loading
Ryan McVeigh
This is a live streamed presentation. You will automatically follow the presenter and see the slide they're currently on.
Watch as we go at:
Design Patterns
JavaScript
Scalable Application Architecture
- Friedrich Nietzsche
Solving problems
Solving scalability
Structuring solutions
Inconsistent solutions
Inconsistent architecture
Difficult refactoring
“We search for some kind of harmony between two intangibles: a form we have not yet designed and a context we cannot properly describe’
- Christopher Alexander, the father of design patterns.
Solid
Reflect experience
Reliable approaches
Represent insights
Incredibly flexible
Out-of-the-box
solutions
Easily adapted
By Krdan [Public domain], via Wikimedia Commons
Easier than describing syntax and semantics
Common vocabulary
for expressing solutions elegantly.
Problem agnostic
Major problems that can cause later down the line
Prevent minor issues that can cause
“Anything can be defined as a reusable module”
- Nicholas Zakas, author ‘Professional JavaScript For Web Developers’
From a historical perspective, the Module pattern was originally developed by a number of people including Richard Cornford in 2003. It was later popularized by Douglas Crockford in his lectures. Another piece of trivia is that if you've ever played with Yahoo's YUI library, some of its features may appear quite familiar and the reason for this is that the Module pattern was a strong influence for YUI when creating their components.
If you've ever played with Yahoo's YUI library, some of its features may appear quite familiar and the reason for this is that the Module pattern was a strong influence for YUI when creating their components.
(function() {
// code to be immediately invoked
}()); // Crockford recommend this way
(function() {
// code to be immediately invoked
})(); // This is just as valid
(function( window, document, undefined ){
//code to be immediately invoked
})( this, this.document);
(function( global, undefined ){
//code to be immediately invoked
})( this );
Defined by Ben Alman in his blog.
Variables & Methods can’t be ‘private’
No Access Modifiers
Variables & Methods can’t be ‘public’
The typical module pattern is where immediately invoked function expressions (IIFEs) use execution context to create ‘privacy’. Here, objects are returned instead of functions.
// Object literal pattern
var basketModule = (function() {
var basket = []; //private
return { //exposed to public
addItem: function(values) {
basket.push(values);
},
getItemCount: function() {
return basket.length;
},
getTotal: function(){
var q = this.getItemCount(),p=0;
while(q--){
p+= basket[q].price;
}
return p;
}
}
}());
In the pattern, variables declared are only available inside the module.
Variables defined within the returning object are available to everyone
This allows us to simulate privacy
Notice the () around the anonymous function. This is required by the language, since statements that begin with the token function are always considered to be function declarations. Including () creates a function expression instead.
Inside the module, you'll notice we return an object. This gets automatically assigned to basketModule so that you can interact with it as follows:
//basketModule is an object with properties which can also be methods
basketModule.addItem({item:'bread',price:0.5});
basketModule.addItem({item:'butter',price:0.3});
console.log(basketModule.getItemCount());
console.log(basketModule.getTotal());
//however, the following will not work:
// (undefined as not inside the returned object)
console.log(basketModule.basket);
//(only exists within the module scope)
console.log(basket);
Whenever a name is used, the interpreter walks the scope chain backwards looking for a var statement for that name. If none is found, that variable is assumed to be global. If it’s used in an assignment, the global is created if it doesn’t already exist. This means that using or creating global variables in an anonymous closure is easy. Unfortunately, this leads to hard-to-manage code, as it’s not obvious (to humans) which variables are global in a given file.
(function ($, YAHOO) {
// now have access to globals jQuery (as $) and YAHOO in this code
}(jQuery, YAHOO));
By passing globals as parameters to our anonymous function, we import them into our code, which is both clearer and faster than implied globals.
Dojo attempts to provide 'class'-like functionality through dojo.declare, which can be used for amongst other things, creating implementations of the module pattern. Powerful when used with dojo.provide.
// traditional way
var store = window.store || {};
store.basket = store.basket || {};
// another alternative..
// using dojo.setObject (with basket as a module of the store namespace)
dojo.setObject("store.basket.object", (function() {
var basket = [];
function privateMethod() {
console.log(basket);
}
return {
publicMethod: function(){
}
};
}()));
In the following example, a library function is defined which declares a new library and automatically binds up the init function to document.ready when new libraries (ie. modules) are created.
function library(module) {
$(function() {
if (module.init) {
module.init();
}
});
return module;
}
var myLibrary = library(function() {
return {
init: function() {
/*implementation*/
}
};
}());
A YUI module pattern implementation that follows the same general concept.
YAHOO.store.basket = function () {
//"private" variables:
var myPrivateVar = "I can be accessed only within YAHOO.store.basket .";
//"private" method:
var myPrivateMethod = function () {
YAHOO.log("I can be accessed only from within YAHOO.store.basket");
}
return {
myPublicProperty: "I'm a public property.",
myPublicMethod: function () {
YAHOO.log("I'm a public method.");
//Within basket, I can access "private" vars and methods:
YAHOO.log(myPrivateVar);
YAHOO.log(myPrivateMethod());
//The native scope of myPublicMethod is store so we can
//access public members using "this":
YAHOO.log(this.myPublicProperty);
}
};
}();
Take the concept of reusable JavaScript modules further with the Asynchronous Module Definition.
Non-blocking, parallel loading and well defined.
Mechanism for defining asynchronously loadable modules & dependencies
Stepping-stone to the module system proposed for ES Harmony
define allows the definition of modules with a signature of define(id /*optional*/, [dependencies], factory /*module instantiation fn*/);
// A module_id (myModule) is used here for demonstration purposes only
define('myModule',
['foo', 'bar'],
// module definition function
// dependencies (foo and bar) are mapped to function parameters
function ( foo, bar ) {
// return a value that defines the module export
// (i.e the functionality we want to expose for consumption)
// create your module here
var myModule = {
doStuff:function(){
console.log('Yay! Stuff');
}
}
return myModule;
});
require.js is used to load code for top-level JS files or inside modules for dynamically fetching dependencies
/* top-level: the module exports (one, two) are passed as
function args to the callback.*/
require(['one', 'two'], function (one, two) {
});
/* inside: complete example */
define('three', ['one', 'two'], function (one, two) {
/*require('string') can be used inside the function
to get the module export of a module that has
already been fetched and evaluated.*/
var temp = require('one');
/*This next line would fail*/
var bad = require('four');
/* Return a value to define the module export */
return function () {};
});
AMD loaders need to take into account multiple versions of the library being loaded into the same page as you ideally don't want several different versions loading at the same time.
// Account for the existence of more than one global
// instances of jQuery in the document, cater for testing
// .noConflict()
var jQuery = this.jQuery || "jQuery",
$ = this.$ || "$",
originaljQuery = jQuery,
original$ = $,
amdDefined;
define(['jquery'] , function ($) {
$('.items').css('background','green');
return function () {};
});
// The very easy to implement flag stating support which
// would be used by the AMD loader
define.amd = {
//supports multiple jQuery versions
jQuery: true
};
A promise is an object which is used for deferred and asynchronous computations. A promise represents an operation that hasn’t completed yet, but is expected in the future. Promises are a way of organizing asynchronous operations in such a way that they appear synchronous.
Async functions are built on top of ECMAScript 6 features like generators. The introduction of promises and generators in ECMAScript presents an opportunity to dramatically improve the language-level model for writing asynchronous code in ECMAScript. Indeed, generators can be used jointly with promises to produce the same results but with much more user code.
Note: The async function is available in Edge.
Format widely accepted
on a number of server-side platforms (Node)
CommonJS
Working group designing, prototyping, standardizing JS APIs
Competing standard. Tries to solve a few things AMD doesn’t.
They basically contain two parts: an exports object that contains the objects a module wishes to expose and a require function that modules can use to import the exports of other modules
/* here we achieve compatibility with AMD and CommonJS
using some boilerplate around the CommonJS module format*/
(function(define){
define(function(require,exports){
/*module contents*/
var dep1 = require("foo");
var dep2 = require("bar");
exports.hello = function(){...};
exports.world = function(){...};
});
})(typeof define=="function"? define:function(factory){factory
(require,exports)});
Defining modules that can work anywhere. Credit: @KitCambridge
(function (root, Library) {
// The square bracket notation is used to avoid property munging by the Closure Compiler.
if (typeof define == "function" && typeof define["amd"] =="object" && define["amd"]) {
// Export for asynchronous module loaders (e.g., RequireJS, `curl.js`).
define(["exports"], Library);
}
else {
// Export for CommonJS environments, web browsers, and JavaScript engines.
Library = Library(typeof exports == "object" && exports|| (root["Library"] = {
"noConflict": (function (original) {
function noConflict() {
root["Library"] = original;
// `noConflict` can't be invoked more than once.
delete Library.noConflict;
return Library;
}
return noConflict;
})(root["Library"])
}));
}
})(this, function (exports) {
// module code here
return exports;
});
// JavaScript should be made compatible with libraries other than jQuery by
// wrapping it with an "anonymous closure". See:
// - https://drupal.org/node/1446420
// - http://www.adequatelygood.com/2010/3/JavaScript-Module-Pattern-In-Depth
(function ($, Drupal, window, document, undefined) {
'use strict';
// To understand behaviors, see https://drupal.org/node/756722#behaviors
Drupal.behaviors.myCustomBehavior = {
attach: function (context, settings) { // jshint ignore:line
// Place your code here.
}
};
})(jQuery, Drupal, this, this.document);
Drupal 7 uses immediately invoked function expressions (IIFE's)
(function ($, Drupal, window, document, undefined) {
'use strict';
Drupal.behaviors.equalizeHeights = {
attach: function (context, settings) { // jshint ignore:line
// Build our equalize function.
function equalize() {
$('footer .region').not('.region.region-footer-fourth').matchHeight();
$('.view-id-grid .views-row').matchHeight();
$('.trip-idea-trip_idea_related .field-name-title-field h2').matchHeight();
$('.field-group--mentioned > div').not('.field-group--mentioned .field-name-field-related-listings-title').matchHeight();
$('.group-find-nearby > form, .group-find-nearby > div').matchHeight();
}
// Run equalize on page load.
$(window).load(function() {
equalize();
});
// Rerun equalize function after ajax runs.
$(document).ajaxComplete(function () {
equalize();
});
}
};
})(jQuery, Drupal, this, this.document);
var equalize = { // object literal pattern (var).
elements: [$('.view-id-grid .views-row'), $('.group-find-nearby > div')],
init: function() {
this.cacheDom();
this.bindEvents();
},
cacheDom: function() {
this.$doc = $(document);
this.$win = $(window);
},
bindEvents: function() {
this.$win.on('load', this.matchEl.bind(this));
this.$doc.ajaxComplete(this.matchEl.bind(this));
},
matchEl: function() {
$.each(this.elements, function() {
$(this).matchHeight();
});
}
};
equalize.init();
// JavaScript should be made compatible with libraries other than jQuery by
// wrapping it with an "anonymous closure". See:
// - https://drupal.org/node/1446420
// - http://www.adequatelygood.com/2010/3/JavaScript-Module-Pattern-In-Depth
(function ($, Drupal) {
'use strict';
// To understand behaviors, see https://drupal.org/node/756722#behaviors
Drupal.behaviors.myCustomBehavior = {
attach: function (context, settings) {
// Place code here.
}
};
})(jQuery, Drupal);
drupal.nav-tabs:
version: VERSION
js:
js/nav-tabs.js: {}
dependencies:
- core/matchmedia
- core/jquery
- core/drupal
- core/jquery.once
- core/drupal.debounce
From D8's seven.libraries.yml file
If you reviewed your architecture right now, could a decision to switch libraries be made without rewriting your entire application?
- Justin Meyer
- Rebecca Murphey
- Nicholas Zakas
“The only difference between a problem and a solution is that people understand the solution.”
- Charles F. Kettering
Loosely coupled
architecture
Functionality broken down into smaller independent modules
Framework or library agnostic. Flexibility to change in future.
Theme interprets requests. Modules don’t access the core or libraries directly.
Single modules speak
to the app when something interesting happens
Prevent apps from falling over due to errors with specific modules.
Basic Files (.info.yml, template.php, etc)
Assets Direcotries (js, scss, css, images, etc)
Template Files
Extras
templates/page/page.tpl.php
templates/page/page-front.tpl.php
templates/node/node.tpl.php
templates/node/node-blog.tpl.php
templates/views/views-view.tpl.php
templates/views/views-view-fields--directory.tpl.php
templates/views/views-view-field--user-list.tpl.php
templates/block/block.tpl.php
templates/block/block-footer.tpl.php
templates/block/block-login.tpl.php
templates/blocks/block--system-branding-block.html.twig
templates/blocks/block--system-menu-block.html.twig
templates/blocks/block.html.twig
templates/content/node.html.twig
templates/content/page-title.html.twig
templates/content/search-result.html.twig
templates/layout/maintenance-page.html.twig
templates/layout/html.html.twig
templates/layout/page.html.twig
templates/layout/region.html.twig
templates/page/page.tpl.php
templates/page/page-front.tpl.php
templates/node/node.tpl.php
templates/node/node-blog.tpl.php
templates/views/views-view.tpl.php
templates/views/views-view-fields--directory.tpl.php
templates/views/views-view-field--user-list.tpl.php
templates/block/block.tpl.php
templates/block/block-footer.tpl.php
templates/block/block-login.tpl.php
Keep assets in separate folders like so:
theme-name/css/
theme-name/images/
theme-name/js/
theme-name/scss/
theme-name/assets/css/
theme-name/assets/images/
theme-name/assets/js/
theme-name/assets/scss/
or you may want to organize them into an assets folder like this:
theme-name/template.php
theme-name/includes/pager.inc
theme-name/includes/field.inc
//////////////////////////////
// Includes
//////////////////////////////
require_once dirname(__FILE__) . '/includes/field.inc';
require_once dirname(__FILE__) . '/includes/pager.inc';
template.php
theme-name/app/
theme-name/bower_components/
theme-name/fonts/
theme-name/grunt/
theme-name/images-min/
theme-name/node_modules/
(SMACSS for JS)
The Mediator Pattern
Just like with our scss partials our js needs a logical structure.
Site
Performance
Developer Performance
Concat whenever possible
Optimize
all things
“Nearly half of web users expect a site to load in 2 seconds or less, and they tend to abandon a site that isn’t loaded within 3 seconds.”
- surveys done by Akamai and Gomez.com
Automize
all things
Test & Lint
Don't make them think
'use strict';
module.exports = function(grunt) {
require('load-grunt-config')(grunt);
};
Gruntfile.js
Lives in the theme directory
Repo size constraints
Keeps dependencies in one place
Version control
npm install [package] --ignore-scripts --save-dev
Using --ignore-scripts helps prevent malicious scripts from running during install.
If we have to commit dependencies maintain your team's directory patern
// Revealing last slide group using a reveal pattern
var slides = (function (slide) {
// Next Slide
function nextSlide(slide) {
// Some dorky code that really is just saying almost done.
}
}(slide));
- Addy Osmani
- Ben Cherry
Stephane Bellity, Addy Osmani, Ates GoralJuan, Pablo Buritica, Joel Hooks, Dan Lynch, Robert Djurasaj, Peter Rudolfsen, Sindre Sorhus, Romain Dardour, Tony Narlock, Dustin Boston
A framework-agnostic, extensible architecture for decoupled and reusable components.
- Addy Osmani
- Ben Alman
- Khan Academy