Cantilever believes that simpler is better when it comes to websites. As such, we have adopted a Vanilla JS approach. Most of our sites have relatively straghtforward JS requirements. When we need to reach for React or Vue we can do that, but in most cases we find those frameworks to be overkill.
For more Vanilla JS tips, check out Chris Fernandini’s writing:
Typical Architecture
One advantage of working in Vanilla JS is that our javascript layer on any given site can morph to the specific requirements of that site.
In the majority of cases, we are working with HTTP/2 and no longer need ES5 support, so we do not compile or bundle or our Javascript. We simply link to scripts in the footer like the good old days.
We use the defer
tag to ensure that the page can load independently from the JS. Most sites, especially those with a backend, should have some kind of asset queue which produces those footer script tags rather than having them literally hard-coded into the footer. That said, we do definitely have sites where we just throw script tags in the footer, and that’s OK. Here is one very nuanced approach we used:
Commonly, we have two kinds of script on a site:
- Global functionality that should run on page load and is not tied 1-1 to a specific DOM element but runs broadly in the global namespace
- Objects which are instantiated with a 1-1 relationship to a specific DOM element or set of elements
Sometimes the distinction is tough to discern. Some tracking or lightbox systems, for instance, might not bind to a specific DOM element but provide a global API for elements with specific data attributes to trigger actions. That would generally fall into #1 but could be argued either way.
Generally, if the file contains a JS constructor function which is then instantiated for some set of DOM elements, see it as #2. Otherwise, #1 is probably better.
The file structure looks like:
app.js
– sets a global namespace and performs any high-level utility functionality like redirecting to a browser unsupported page when neededlib
– a directory with all third-party scripts we need @utilities
– A directory with all files that cover global functionality (#1 above)objects
(formerly calledcomponents
) – files for objects which are tied specifically to some DOM element (#2 above). These usually match a.less
file within thesource/less/05-objects/
directory.
Libraries
We have several projects on Backbone. The same architecture can apply to such projects, it’s just that the objects you create in script style #2 are Backbone views/models rather than plan JS objects.
We also maintain some sites which have Vue or React code. When working within those codebases, please think deeply about the existing coding standard and try to stick to it as well as you can.
Coding Standards
Our linter is configured to complain about use of global variables inside a function, unless they are explicitly included in a small comment at the top of the file:
We appreciate this approach because while the ES5-style non-modular JS does operate in global scope, the linter whines until you’ve explicitly decided what is and is not global. This will never cause code to not work, but it will make it harder to make mistakes having to do with the fact that the code runs globally.Add this kind of comment with all the globals you need to use in the file. Start conservatively – It’s easy to add more later.
Since we are still writing code to be executed in the global scope (as opposed to using any module systems yet), we use IIFE expressions to control leakage of local data to the scope. For consistency, we should maintain a single a style for the IIFEs. There are various possibilities, but please keep them looking like:
(function() {
console.log("In here!")
}();
- Don’t bother passing variables in. It doesn’t have much functional effect and causes bugs. If you have a global which you want to alias within the local scope, for legibility, use a `const` or `let` declaration.
- Use a full function declaration, not arrow syntax. This ensures that `this` is in function scope, and is not the window.
Use constants outside of your actual object declarations. Constructors can also be declared as const, which is nice, because we don’t like for them to be overwritten.
const myConstant = "Hello",
MyObject;
MyObject = function() {
// myConstant is available and unchangeable in here
};
This should be used for all strings and parameters which are global configuration options for ALL instances of a given view. It can also be used to provide defaults for configuration options that can be passed in when the object is initialized:
const myOptionDefault = "Hello",
MyObject;
MyObject = function(options) {
this.myOption = options.myOption || myOptionDefault;
};
myOption can be different between different instances of MyView, but the default is always consistent.
We used to use special namespaced classes on DOM elements to distinguish which should be bound with JS-based functionality and which not. Nowadays we don’t do that. We just write simple, semantic markup for our components, and rely on the JS to read and process that structure to instantiate any related functionality. Any given object within our system might be bound with some JS behavior.
- When extending backbone views, instead of writing "initialize: function() {" and the like, write "initialize() {" as a shortcut
- Use string interpolation via the $ helper
- Use valid aria- properties to handle hiding, showing, and active state selection. Do not write your own classes for these aspects of the functionality.
- As much as possible, JS objects should not require associated CSS in order to function. Their functionality should be adaptable to various CSS configurations. For instance, an accordion object should work well whether the accordion shows its panel to one side, or shows its panel below the trigger. Try to write the JS to be independent.
- Watch the linter for other coding standards problems
- Comment, comment, comment.