The patch on that ticket breaks up the Backbone classes in media-models.js
, media-views.js
, media-audiovideo.js
, and media-grid.js
into modules and injects them via Browserify on build/watch into a built file. Let’s start at the beginning.
Brain overload
Files that are 1000s of lines long are hard to consume. We try to alleviate this by adding copious amounts of docs. Still, it’s a lot to look at. Ideally, we would break our files into smaller modules and then somehow join them together in a build process.
It is common practice to serve (sometimes very large) minified files for JS and CSS that have concatenated many smaller files together and uglify’d (minified/obfuscated) them. It is no longer common or best practice to develop with huge files. We can learn a lot from emerging front end development trends, especially those from the Node/NPM community. In some cases, we can even share code.
We’ll use Media as the main culprit, but this could apply to any “manifest” – a term I use to describe files that contain the entire public API for a feature. Something like media-views.js
, it might be nice to bounce from view to view in the same file, provided you know exactly what you are looking at, what depends on what, etc.
I have found, it is completely overwhelming for almost everyone. It would be great if each discreet piece could be viewed in isolation with its dependencies clearly stated.
There are many ways to accomplish the splitting of large files. I want to focus on 2 of the most common.
Vocabulary
Backbone is one of a growing number of MV* frameworks for JavaScript. A large majority of the code related to media either belongs to a handful of Models or to the increasingly large library of Views and View Templates.
Views are the building blocks for the presentation of Media (you know, “the Media Modal” or 4.0’s “Media Grid”).
The main canvas on which these Views are stitched together are called Frames, which are themselves Views – tilting our use of Backbone more towards MVP, P standing for Presenter.
We have Controllers, which are called States, but they belong to a Frame (Presenter! also a View!), so anyways…. for now….
When we create new UIs, we are more than likely adding new Views/Templates, or updating existing Views.
If we wanted to move from one large file to many files that each contain a class, we would create Modules.
Grunt is a task runner. We use Grunt to build our src
directory into our build
directory.
Require
Require is a great tool for converting AMD modules into built files. Require leans on Dependency Injection in its syntax:
define([ 'models/taco', 'models/burrito', 'controllers/meal' ], function (Taco, Burrito, Meal) { var Dinner = Meal.extend({ // taco-related code }); return Dinner; });
This syntax works great, unless you have way more dependencies. Refactoring code could unwind a module that has a lot of dependencies, but if you are just trying to convert legacy classes into a module, Require starts to get a little weird. Concrete example: Frames have a TON of dependencies.
Require becomes a Grunt task to make one big file by recursing the dependency tree in an initial manifest. Require, by default, loads JS asynchronously, which can cause race conditions with plugins or themes that expect code to be registered on $(document).ready()
or window.onload
.
Require works even if you don’t build via Grunt.
Browserify
Browserify is a tool that allows you to use Node-style modules and run them in a browser without changing from the Node syntax. Browserify requires a build for this to work.
Using our example from above, this is the syntax for Browserify:
var Taco = require( './models/taco.js' ), Burrito = require( './models/burrito.js' ), Meal = require( './controllers/meal.js' ), Dinner; Dinner = Meal.extend({ // taco-related code }); module.exports = Dinner;
Browserify leans more towards the Service Locator pattern.
Browserify scans the abstract syntax tree (AST) of your JS code to compile dependencies. Your modules themselves get wrapped in their own scope like so:
(function (require, module, exports) { .....YOUR_MODULE..... })
After spending a lot of time messing around with both: I think we should use Browserify.
Converting “Legacy” Code
The media JS code is some of the most “modern” code in WordPress, but it still clunkily lives in huge files. To convert the code into modules, we need to make a lot of individual files (one for each Backbone class).
We also need to make sure we maintain the existing wp.media
namespaces for backwards compatibility. We don’t want any existing functionality to change, we just want to build the files differently.
Even though the code is defined differently, wrapped in a new scope, and looks different when “built”, we can still maintain our current API design: what is publicly accessible now will remain publicly accessible.
In the patch
Disclaimer: this patch is for experimentation only. It will go stale probably before this post is published. It works, but it is only a playground for now. If this moves forward, it will be a laborious Subversion process to create a bunch of new files.
I have added a folder to wp-includes/js
, media
, that contains the modules and the built manifests. My patch adjusts script-loader.php
to use these new paths.
media
contains the following files/folders:
controllers/ models/ routers/ utils/ views/ (with another round of subfolders) audio-video.manifest.js grid.manifest.js models.manifest.js views.manifest.js
The build pipeline
If you are following along with that patch and want to see this in action, run in the project root:
npm install
Afterwards, run:
grunt watch
*.manifest.js
files get built into *.js
files when you change a file in media/*
, provided you are running the grunt watch
task. The watcher will automatically call browserify:media
and uglify:media
when those files change. This allows you to run your site from src
or build
, and you will still get Browserify’d files. SCRIPT_DEBUG
will either run *.js
or *.min.js
, just like any other minified JS in core.
This is a proposal
I would like to hear feedback from the overall community and certainly from our fair share of JS-trained ninjas. A common reason to *not* do something like this is the barrier to entry for new developers. I would argue in this case that the code becomes MORE readable and understandable. I was shocked myself to see how much simpler it was to absorb one piece at a time once the code was laid out in modules.