For many Drupal developers, JavaScript can sometimes feel like an uncharted wilderness. This is particularly true for those of us who haven’t worked with JavaScript since the first browser war, when cross-browser inconsistencies led to more than a few visits to the swear jar.
Fortunately, JavaScript has entered a new renaissance since the release of open-source projects like V8 and Node.js, and frameworks like Angular, Ember, and React are rapidly gaining steam. The advent of ECMAScript, a codified specification for JavaScript, has ushered in a new era of innovation in the language, culminating in the new modern version of JavaScript: ES6.
In this blog series, I’ll introduce some of the most important features in ES6. In this first installment, we’ll delve into how to actually get started with ES6, the setup process, and issues of variable scope, including the let
and const
keywords. In the second installment, we’ll explore the spread operator, default values, and destructured assignment, a powerful new approach to variable assignment. In the third installment, we’ll look at arrow functions, one of the most distinctive features of ES6. Finally, we’ll cover modules, classes, and promises in the final installment. At the end, we’ll direct our attention to Drupal itself and examining our progress in modernizing our JavaScript using ES6.
Transpiling ES6 with Babel
Browser support for ES6 is still quite spotty. As a result, there are two ways apart from transpilation to ES5 to access the new syntactic features in ES6: either by limiting the extent to which you use ES6 based on available browser support or by using any of the available ES6 shims and polyfills approximating ES6 support.
Nevertheless, transpilation is far and away the most commonly used approach. To accomplish this, we need to provide a development workflow which enables us to write ES6 and seamlessly convert it into browser-supported ES5. First, let's initialize a project using either NPM, the package manager available out of the box with Node.js, or Yarn, an emergent package manager for JavaScript. These package managers are somewhat analogous to PHP’s Composer.
# If using NPM $ npm init -y
# If using Yarn $ yarn init
We need to add some development dependencies to our generated package.json
file, which is our dependency list. Babel is the most common transpiler for ES6. The --save-dev
and --dev
flags in NPM and Yarn, respectively, indicate that the package arguments following are intended for use in a development workflow and not ultimately in production.
# If using NPM $ npm install --save-dev babel-cli babel-core babel-preset-env
# If using Yarn $ yarn add --dev babel-cli babel-core babel-preset-env
To configure Babel, add the following to your package.json
file:
// package.json { // ... "babel": { "presets": ["env"] }, // ... }
Now, you can use Babel CLI, Babel’s built-in command-line interface, to transpile individual files or directories, which may be helpful if you’re doing more targeted development. For instance, the following command transpiles all ES6 files in the src
input directory to a lib
output directory.
$ ./node_modules/.bin/babel src -d lib
It’s a best practice to use NPM scripts within the package.json
file to avoid any discrepancies between local and global package versions. Add the following to your package.json
file:
// package.json { // ... "scripts": { "build": "./node_modules/.bin/babel src -d lib" }, // ... }
Then, you can run the following build command to conduct transpilation:
$ npm run build
Generating a client-side bundle with Webpack
With the Babel CLI, we can arbitrarily transpile any ES6 file into ES5. But what about minified or uglified JavaScript optimized to be browser-ready? To package any ES6 that we write into a client-side bundle out of all of our JavaScript and any dependencies that browsers can consume, we’ll use Webpack 2 and the Webpack plugin babel-loader to generate a client-ready bundle. (If you’re using Browserify, you can use the babelify package.)
# If using NPM $ npm install --save-dev babel-loader webpack webpack-dev-server@2
# If using Yarn $ yarn add --dev babel-loader webpack webpack-dev-server@2
In this example, we’ll be taking an app.js
file in an src
directory and bundling it — and any dependencies required for production that it refers to — into a browser-ready app.bundle.js
file. Place the following code into a webpack.config.js
file in your project root. It iterates over every file in your source directory, processes it according to your configuration, and outputs it into a dist
directory.
// webpack.config.js const path = require('path'); const webpack = require('webpack'); module.exports = { context: path.resolve(__dirname, './src'), entry: { app: './app.js', }, output: { path: path.resolve(__dirname, './dist'), filename: '[name].bundle.js', }, };
Then, by adding Babel into our Webpack configuration, we ensure that all of our ES6 will be transpiled to ES5 during the creation of the client bundle.
// webpack.config.js module.exports = { // ... module: { rules: [ { test: /\.js$/, exclude: [/node_modules/], use: [{ loader: 'babel-loader', options: { presets: ['env'] } }], }, ], }, // ... };
Finally, if we run the following, Webpack will create our dist/app.bundle.js
file.
$ webpack -p
For much more about the newly released Webpack 2, you can consult this helpful blog post by Drew Powers for information far beyond the scope of this blog series.
Scope and let
Variable scope has always been limited to function blocks in JavaScript. But this led to unwieldy ways of creating scope blocks, especially the immediately invoked function expression (IIFE).
// ES5 var foo = 1; (function () { var foo = 2; console.log(foo); // 2 })(); console.log(foo); // 1
Now, in ES6, you can use let to bind variable declarations to an arbitrary block of code. You should put let
statements as early as possible in blocks to prevent them from becoming undetectable in lower code. This let
statement also defines several other variables in the process.
// ES6 var foo = 1; { let foo = 2, bar, baz; console.log(foo); // 2 } console.log(foo); // 1
Functions scoped to blocks also help us keep our code organized and prevent function invocations that can lead to unexpected results due to overreaches in variable scope.
{ multiply(5, 2); // 10 function multiply(x, y) { return x * y; } } multiply(5, 2); // ReferenceError
const
You can now also create constants, which are read-only after their values are set. Bid farewell to variable names in all-caps!
{ const foo = 1; console.log(foo); // 2 foo = 3; // TypeError }
Unlike let
and var
, const
requires a value to be set.
var foo; let bar; const baz = [2, 3, 4];
You can influence the value of the constant if it’s an object or array, since the constant merely refers to the object or array, but you cannot reassign its value directly.
const foo = [2, 3, 4]; foo.push(5); console.log(foo); // [2, 3, 4, 5] foo = [5, 6, 7]; // TypeError
Conclusion
As you can see, while the initial setup for ES6 can be daunting for PHP developers more used to a Composer-driven workflow, the features that emerge from ES6’s capabilities can enrich your JavaScript, whether it is on the server side, via a Node.js server, or on the client side, via Webpack. Now that most web-based approaches utilizing fully decoupled Drupal also require ES6 to work on codebases in Angular, Ember, and React, ES6 has never been more essential to a Drupal developer’s toolkit.
In this installment, we discussed Babel, a flexible transpiler for JavaScript, and Webpack, which integrates Babel with a workflow to prepare your code for browser readiness. We also introduced some of the most fundamental concepts undergirding ES6, namely variable scope and the keywords let
and const
. In the next installment, we’ll cover the spread operator, default values, and destructured assignment, which expand the power that developers can wield with variables — and their values — in JavaScript.
Up next: the second part of the "ES6 for Drupal developers" blog series. This blog series is a heavily updated and improved version of “Introduction to ES6”, a session delivered at SANDCamp 2016. Special thanks to Matt Grill for providing feedback during the writing process.