I’ve just had a nice experience improving and modernizing a large JavaScript codebase in a WordPress plugin. The original code was written in an old-fashioned way with jQuery in a single large file. Using modern EcmaScript and tools like Webpack, I was able to split it into modules and improve the code structure. The new code is much more readable and maintainable, and of course, fewer bugs. In this tutorial, I’ll show you how I did that.
Old-Fashioned Code
Before going into the steps of refactoring, let’s look at a normal JavaScript code in WordPress.
We’re very familiar with this code in a WordPress theme or plugin:
jQuery( function( $ ) { // Many variables go here. var $element = $( '.some-selector' ); // A large code goes here. $element.each( function() { // Do something. } ) // Many functions go here. function myFunc { // Some code. } myFunc(); } );
I see this type of code all the time. I often write it myself. This JavaScript file is just like a group of runtime code in that order. The logic in the file looks like exactly what we have in our mind. It is okay and it works.
But there is a big problem with it: when code grows, the JS file is huge and hard to read and maintain. Sometimes, you even don’t know where to look when a bug happens.
So, what should we do?
Group Logic Into Functions or Objects
There are a few solutions for that. The common way I see (and usually do) is grouping the logic into functions. The code below demonstrates the idea. It has a better structure and readability:
( function( $ ) { function a() { // Some code. } function b { // Some code. } // Run when document is ready. $( function() { a(); b(); } ); } )( jQuery );
Why this code is better? Well, it splits the logic into functions and makes your JavaScript file like a set of functions. Each business logic is stored in a function and they’re separated. When a bug occurs, you know where to find it and fix it.
There are also various similar approaches, like instead of writing functions, you can group similar functions into an object, like this:
( function( $ ) { var Tabs = { open: function () { // Some code. }, close: function() { // Some code. } } function b { // Some code. } // Run when document is ready. $( function() { Tabs.open(); // Or Tabs.close(); b(); } ); } )( jQuery );
Note: you might notice that writing an object like that in JavaScript is very similar to a static class in PHP!
While these methods work and improve readability, it doesn’t solve the problem of a large codebase. Imagine you have a 10k-lines JavaScript file! It’s very hard to fix bugs in that file. And what about if two or more developers working on different bugs? Are they going to edit the same huge file? That will be a nightmare when merging code!
Splitting Into Files
A common way to get rid of a single large JavaScript file is splitting into multiple smaller JavaScript files and enqueuing them in WordPress like this:
wp_enqueue_script( 'one', plugin_dir_url( __FILE__ ) . '/js/one.js', ['jquery'], '1.0', true ); wp_enqueue_script( 'two', plugin_dir_url( __FILE__ ) . '/js/two.js', ['one'], '1.0', true ); wp_enqueue_script( 'three', plugin_dir_url( __FILE__ ) . '/js/three.js', ['one', 'two'], '1.0', true );
That works! And the code in one file is smaller. But it still has a problem: if you enqueue many JavaScript files on the front end, that might hurt the website performance, e.g. the loading speed.
That code will make browsers create 3 requests to your server when a page loads. And all of them are render-blocking. So, your website will be a little bit slower than having only one request.
Now the question is simple: Is there a way to be able to split code, while still creating only one request?
Fortunately, yes! That’s doable with modules in EcmaScript and Webpack. And it’s not hard to do.
Modules
Let’s go back to our large file. The first step now is splitting the code into smaller modules.
In my case, I was working on the MB Views extension for the Meta Box plugin. In the editing screen, there are a few things like:
- Modals
- Tabs
- Inserter panel
- Code editor
- Location rules
This is a screenshot of the editing screen, where you can see a modal, inserter panel (in the background) and code editors:
Previously, I wrote each of these as an object with their methods, like the 2nd approach above. But the code grows faster than I expected. So, I started to move the code of each thing into a separate JavaScript file and structure them as modules.
Here is an example of the Modals module, this code is in a file modals.js
:
const modals = [...document.querySelectorAll( '.mbv-modal' )]; const Modals = { bindEvents: () => document.addEventListener( 'click', Modals.clickHandle ), clickHandle: e => { const el = e.target; if ( el.parentNode.classList.contains( 'mbv-modal-trigger' ) ) { e.preventDefault(); Modals.openTarget( el.parentNode ); } if ( el.classList.contains( 'mbv-modal__overlay' ) ) { e.preventDefault(); Modals.closeCurrent( el ); } if ( el.classList.contains( 'mbv-modal__close' ) ) { e.preventDefault(); Modals.closeCurrent( el ); } }, closeCurrent: el => el.closest( '.mbv-modal' ).classList.add( 'hidden' ), openTarget: el => { Modals.closeAll(); Modals.open( el.dataset.modal ); }, open: name => modals.find( modal => modal.dataset.modal === name ).classList.remove( 'hidden' ), closeAll: () => modals.forEach( modal => modal.classList.add( 'hidden' ) ), } export default Modals;
And in the main index.js
file, I import the module like this:
import Modals from './modals.js'; // Import more modules. Modals.bindEvents(); // Other code.
As you can see, the code of a module is very similar to the normal code when you split it with the normal old-fashioned way. The only difference is the export
keyword, where you export what you need to use in other files.
Modules is a standard in EcmaScript 2015 (ES6). Writing code like this means we’re using modern JavaScript in our plugin. To understand more about ES6 modules, please read this tutorial.
Modules are also called import and export feature in ES6 because import and export is the way modules communicate with each other. The whole idea of ES6 modules is:
- You write everything as modules
- The code for each module is stored in a single file
- In a module, you export only what you need: variables, functions, objects or classes. Anything you need.
- In the main JavaScript file, you import only what you need and use it. You can also change the name of the imported functions, variables (but not the default export), so they don't pollute the global namespace.
The good thing about modules is that in the module file you can write anything you want, not just only what to export. You can declare temporary variables or helper functions and run them in a function that you will export. Those temporary variables and helper functions won’t be exported and won’t be available in the main JavaScript file (where you import).
In the example above, I wrote modals as a module. In that file, I created an object Modals
which I exported. There was also a temporary variable modals
where I used to store all modal elements. This variable wouldn’t be exported and was not available in the main JavaScript file.
Webpack
Modules allow us to split code into smaller pieces, which helps a lot from maintaining them. However, to make modules (export and import) work, we need a bundle tool like Webpack.
Webpack is a powerful tool to bundle, e.g. merge, multiple JavaScript files into one file. So, it does the opposite thing of code splitting.
Assume we have the following modules:
modals.js
tabs.js
panel.js
with similar content as in the previous section. And we also have a main file index.js
with content like this:
import Modals from './modals.js'; import Panels from './panels.js'; import Tabs from './tabs.js'; Modals.bindEvents(); Panels.bindEvents(); Tabs.bindEvents();
To make index.js
file work in browsers, we need Webpack to bundle all modules into the file and compile them into ES5. The result of the job is a file like bundle.js
which contains the content of all files above (while still keeping the logic of import/export, in short, they still work).
Webpack solves the problem of multiple requests sent to your server with multiple wp_enqueue_script
that we talked about above.
To start with Webpack, I highly recommend reading the official getting started guide. Looking for tutorials on the Internet, you might be confused by a lot of complicated things, especially the setup, when it should not be. Things are even more complicated when you read tutorials about both Webpack and Babel! Babel is another tool that transforms your modern JavaScript code to ES5 for browsers to understand.
In my case, I decided to drop support for IE. That meant I could freely use:
let
andconst
- arrow functions
- classes
- spread operator
and many more. Like the sample code above, it works nicely in all modern browsers.
So, I didn’t need Babel. I only needed Webpack to bundle the code. So, my setup was very simple as follows:
First, I installed Webpack with npm install webpack webpack-cli --save-dev
. After doing this, I had a package.json
file with the following content:
{ "devDependencies": { "webpack": "^4.41.6", "webpack-cli": "^3.3.11" } }
Then I created a config file for Webpack webpack.config.js
with the following content:
const path = require('path'); module.exports = { devtool: '', entry: './assets/js/index.js', output: { filename: 'bundle.js', path: path.resolve(__dirname, 'assets'), } };
That’s all. The setup was minimal and standard. There was nothing fancy here and you can understand the Webpack config file easily: it takes the main entry file index.js
and outputs to the bundle.js
file.
To run Webpack, I simply ran webpack
command from the terminal and it merged the files into the bundle.js
file.
And then, all I needed to do was just enqueuing that file in my WordPress plugin:
wp_enqueue_script( 'bundle', plugin_dir_url() . '/js/bundle.js', ['jquery'], '1.0', true );
jQuery?
One of the most common problems when transforming legacy code to the modern one is: how to write jQuery code in modules? Is there a way to detect document ready, and so on.
This question appeared several times when I was coding. At first, I tried to eliminate jQuery code. As you can see in the Modals module above, there is completely no jQuery! Everything was vanilla JavaScript. You can do many things with vanilla JavaScript, such as:
- Event binding with
el.addEventListener
- Querying elements with
el.querySelector
- Adding or removing CSS classes with
el.classList
- Ajax with
fetch
- Traveling the DOM
There is a great guide at You might not need jQuery website. Using these techniques, you can do almost 90% of all you need with jQuery.
But what about if you really need jQuery? Well, it isn’t as hard as you expect. Turns out it very simple: you can just use it!
Assume you already declare jQuery as a dependency when you enqueuing the bundle.js
file as follows:
wp_enqueue_script( 'bundle', plugin_dir_url() . '/js/bundle.js', ['jquery'], '1.0', true );
Then in your JavaScript files (both in modules and the main file), you can use jQuery like this:
import Tabs from './tabs.js'; const $ = jQuery; let cssSettings = $.extend( true, {}, wp.codeEditor.defaultSettings ); cssSettings.codemirror.mode = 'css'; cssSettings.codemirror.theme = 'oceanic-next'; const cssEditor = wp.codeEditor.initialize( 'mbv-post-excerpt', cssSettings ).codemirror; cssEditor.setSize( null, '480px' ); Tabs.bindEvents();
This code is taken from the MB Views where I needed to use CodeMirror for the editors. The CodeMirror library in WordPress is wrapped in the wp.codeEditor
object with various settings and I need to use the extend
function from jQuery to do a deep copy of the settings object. I could use another vanilla JavaScript library, but when jQuery was available, why didn’t I use it? It also reduced the size of the bundle.js
file!
I also assigned jQuery to a local variable $
which was available only in the module. I think it was a good tip. You don’t need to repeat writing jQuery
all the time, just $
is enough.
So, you can use it as usual. No setup required.
Conclusion
With ES6 modules and Webpack, I was able to:
- Split largecode base into smaller files for better structure and easier to read and maintain
- Improve the performance by enqueuing only a single JavaScript file
- Write modern JavaScript code, thanks to ES6
- Work with jQuery when really needed
The result of this work is I had a set of small JavaScript files where I know where to look at if any bug occurs. It also helped our team members to work on the project where each person worked on a feature, whose code was in a separate file.
With the start of the Gutenberg project a few years ago, I feel the fresh air in the WordPress code, both in PHP and JavaScript. Modernizing code is not a yes
or no
question now. It’s only about how
. And I hope this tutorial answers a part of that question.
If you want to have some tips on writing clean and readable PHP code, please read this article.
This article is so helpful, not very much in terms of "how", but in terms of "this-can-be-done" as you mentioned: "Modernizing code is not a yes or no question now. It’s only about how."
i recently upgrading my knowledge of JS to use ES6 and this one is a right on!
Your decision to drop IE is a good one.
Btw, what about `node_modules`? do we also need it after compiling bundle.js?
Anyways, thanks for taking the time to write it. It helped a lot.
Hi Rao, glad that you like the article.
You don't need node_modules folder. It's only for development. When releasing plugins, you can keep only the bundle.js file.
Great primer for adding webpack. Thank you. I just added it to my plugin for some simple modification. I am now looking into Laravel Mix so I can push it out to all of my themes and plugins so my team is on the same page. Looks powerful yet simple for most use cases.
This only works with node.js backend? I'm still doing plugin development with PHP, because old codebase.
Really nice detailed and simplified article I read. I actually recently learned about ES Modules in JavaScript.
I have some questions though.
Is it possible to not use "Webpack" and "bundling files into one" and still manage to split code into multiple files?
And for splitting the code,
I tried exporting a function in modal.js file
and then I "import" the function from modal.js in my index.js file
Now, I tried adding the JS using `wp_enqueue_script` WordPress function but turned out, WordPress don't have an option to add "type='module' in tag and without it, you cannot use import syntax in browser.
So I manually write
and this works.
But then I found myself stuck on another problem that WordPress Autoptimize plugin don't optimize the
Second question:
How do you handle the file cache? Do you manually change the version number of bundle.js file every time you make a change, or does webpack automatically handles that?
Thanks for this great article, though. Definitely going to help in writing Modular JS code in WordPress websites