As ES6 is really hyped this year and the specification has eventually been finished, I wanted to start using it. I have been (okay I still am) the guy at work who took every chance talking about ES6 and all the glory it provides. Which might not have been the best idea with a lot of back-end developers joining the discussion:
Sounds cool Moritz, but when can we use it? :trollface:
Sad to say that browser support is still under heavy development, and using it in a production environment was beyond considering. So I accepted our fate, having to wait for a couple of years. And what about all the browser who won't have any ES6 features but still need to be supported? Sigh, let's add another few years.
Fortunately ES6 turns JavaScript into the language for compilers. And there are already plenty of ES6 to ES5 transpilers, which make it possible to write ES6 for production-ready projects today. A dream comes true! By now I have turned almost all my private projects and a couple of client projects into a solid ES6 setup.
In this article I want to share my experience on how to create a good production-ready ES6 setup for front-end projects.
Getting started: Choosing a transpiler
Compiler or transpiler? Let me explain this here because I was also confused when I first stumbled upon this. A compiler turns a high-level programming language into a low-level programming language. Whereas a transpiler, or source-to-source compiler, remains on the same level of complexity translating from one high-level programming language to another.
The three major transpiler out there are Babel.js with currently 71% of feature compatibility, Traceur supporting 59% and TypeScript with 52%. This is already quite good and the majority of features are supported (a lot of the unsupported features refer to Subclassing and Proxying). I recommend using Babel as it provides the best support and you probably want to feel as free as possible writing ES6.
The features I tend to use the most are =>
arrow functions, classes, const
and let
, template strings and modules. And I am really trying to find a good use case for generator functions. This can quickly change based on my project requirements though, but I guess this is what most developers will want to use at the beginning.
Adding the final ES6 feeling
Babel is great, but lacks one huge feature: ES6 modules. Browserify to the rescue! With its CommonJS support that is very similiar to modules, we can require('modules')
in the browser and properly put all dependencies into bundles. For me it added the final ES6 feel.
Combine and automate
Browserify + Babel is a common setup today and together they cover a lot of features which front-end developers are most interested about. Even though both have their own Command Line Interface, it is quite annoying and time consuming to type $ browserify
everytime a file has been modified.
So I suggest to use a task runner, as this is part of a default front-end setup nowadays.
For the rest of this article I will use Grunt as an example, but everything is easily portable to a Gulp setup.
The project's structure
cool-es6-project/
├── dist/
│ ├── assets/...
│ ├── styles.min.css
│ ├── app.js
│ ├── index.html
├── src/
│ ├── scripts/
│ │ ├── module.js
│ │ ├── index.js
│ ├── styles/...
│ ├── assets/...
│ ├── index.html
├── Gruntfile.js
├── package.json
└── .eslintrc
This is very simplified for this article and would be more advanced in a bigger project. Feel free to have a look at my example boilerplate FrontBook, where I also showcase views/
, assets/
and styles/
.
Create the package.json
A package.json
usually contains relevant meta data for the project. Here are the ES6 devDependencies
:
{
"name": "cool-es6-project",
"devDependencies": {
"grunt": "0.4.x",
"babelify": "^6.1.2",
"grunt-browserify": "^3.8.0",
"grunt-eslint": "^16.0.0",
"grunt-contrib-watch": "0.6.x"
}
}
The modules used for this are:
grunt
: The JavaScript task runnerbabelify
: Babel.js transformer for Browserifygrunt-browserify
: Browserify Grunt taskgrunt-eslint
: ESLint Grunt taskgrunt-contrib-watch
: Grunt task to watch over changed files
Run npm install
from the projects directory to make sure all dependencies are installed and you won't run into any errors.
Define tasks in Gruntfile.js
module.exports = function (grunt) {
'use strict';
grunt.loadNpmTasks('grunt-eslint');
grunt.loadNpmTasks('grunt-browserify');
grunt.loadNpmTasks('grunt-contrib-watch');
grunt.registerTask('dev', ['browserify', 'eslint', 'watch']);
grunt.registerTask('build', ['browserify', 'eslint']);
grunt.initConfig({
/**
_ Write ES6 today, compile it to ES5.
_/
browserify: {
dist: {
options: {
transform: [
['babelify', { loose: 'all' }]
],
browserifyOptions: { debug: true }
},
files: {
'dist/app.js': ['src/scripts/**/_.js']
}
}
},
/\*\*
_ Validates ES6 files via ESLint.
_/
eslint: {
options: {
configFile: '.eslintrc'
},
target: 'src/scripts/\*\*/_.js'
},
/**
_ Run predefined tasks whenever watched files are added,
_ modified or deleted.
\*/
watch: {
scripts: {
files: ['src/scripts/**/\*.js'],
tasks: ['browserify', 'eslint']
}
}
});
};
We defined two tasks at the beginning:
grunt build
: Compiles your ES6 code to proper ES5 code and tests it via ESLint.grunt dev
: This is basically the same asgrunt build
, just with an additionalwatch
task. So when anything changes in your code, everything will be compiled and linted automatically.
In the browserify
task we define to use babelify
with the loose: 'all'
option. This tells babelify to keep the ES5 code as close as possible to the ES6 code. In browserifyOptions
we add debug: true
, which enables source maps for better debugging. There are a couple of more Babel and Browserify options available, but in the beginning this should be fine. The task then simply runs through all files in src/scripts/
and compiles them to dist/
.
Linting
Code needs to be tested. I recently moved from JSHint to ESLint just because I like the possibility of creating my own rules and all the other available options. For me it also felt slightly easier to get it to work with ES6 code.
My ES6 specific configurations are:
.eslintrc
{
"env": {
"browser": true,
"es6": true
},
"ecmaFeatures": {
"arrowFunctions": true,
"binaryLiterals": true,
"blockBindings": true,
"classes": true,
"defaultParams": true,
"destructuring": true,
"forOf": true,
"generators": true,
"modules": true,
"objectLiteralComputedProperties": true,
"objectLiteralDuplicateProperties": true,
"objectLiteralShorthandMethods": true,
"objectLiteralShorthandProperties": true,
"octalLiterals": true,
"regexUFlag": true,
"regexYFlag": true,
"spread": true,
"superInFunctions": false,
"templateStrings": true,
"unicodeCodePointEscapes": true,
"globalReturn": true,
"jsx": true
}
}
Take a look at the rest of my .eslintrc
configuration here.
Let's be honest: I just turned everything on because I want all ES6 features. If I don't want specific features, I would just disable them.
Coding in ECMAScript 6
Our setup is ready and idly waiting to compile some ES6 code for us. Great! Let's start then. I assume that you at least have some knowledge of the ES6 features and how they work. I will only briefly showcase a few of these, including modules.
We will create a simple module using the new class
syntax, export
it and then import
in our index.js
to use it.
module.js
'use strict';
const INTERVAL = 1000;
class Timer {
constructor(element) {
this.element = element;
}
getTime() {
let date = new Date();
let hours = date.getHours();
let minutes = date.getMinutes();
return hours + ':' + minutes;
}
update() {
this.element.textContent = this.getTime();
setInterval(() => {
this.element.textContent = this.getTime();
}, INTERVAL);
}
}
export default Timer;
We could also use a template string in getTime()
, but unfortunately the syntax highlighter I use don't support them yet.
The module.js
contains a class called Timer
, which takes an HTML element as argument in its constructor function. The const
variable INTERVAL
is not accessible from outside of the file and neither is part of the global scope, even though it's used inside of the class. The class has a simple getTime()
function to return the current time and an update()
function to apply the current time to the HTML element passed in the constructor.
index.js
import Timer from './module.js';
let timeElement = new Timer( document.querySelector('time') );
timeElement.update();
This is pretty straight-forward. We imported the class, assigned it to the timeElement
variable and called update()
to initialise the module.
Pretty cool, isn't it? We don't have to worry much about the global scope anymore and can eventually think in a more modularised way.
The ES5 output
Since all this will be compiled to proper ES5 code, let's take a quick look at how the output looks like:
(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
'use strict';
function \_interopRequireDefault(obj) { return obj && obj.\_\_esModule ? obj : { 'default': obj }; }
var \_moduleJs = require('./module.js');
var \_moduleJs2 = \_interopRequireDefault(\_moduleJs);
var timeElement = new \_moduleJs2['default'](<document.querySelector('time')>);
timeElement.update();
},{"./module.js":2}],2:[function(require,module,exports){
'use strict';
exports.\_\_esModule = true;
function \_classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } }
var INTERVAL = 1000;
var Timer = (function () {
function Timer(element) {
\_classCallCheck(this, Timer);
this.element = element;
}
Timer.prototype.getTime = function getTime() {
var date = new Date();
var hours = date.getHours();
var minutes = date.getMinutes();
return hours + ':' + minutes;
};
Timer.prototype.update = function update() {
var \_this = this;
this.element.textContent = this.getTime();
setInterval(function () {
_this.element.textContent = _this.getTime();
}, INTERVAL);
};
return Timer;
})();
exports['default'] = Timer;
module.exports = exports['default'];
},{}]},{},[1,2])
As you can see Browserify use a helper function at the beginning to bundle all modules in one single file. But what I think is most important here, is that the code of module.js
and index.js
pretty much look the same. You can easily recognise your code and still understand what is going on.
This file can be found in dist/app.js
and should be included in your HTML views:
<script type="text/javascript" src="app.js"></script>
What to keep in mind
As mentioned earlier, some features not supported yet. Subclassing Date
, Array
and DOM
doesn't work because of the limitations of ES5. It also depends a little bit on what your production environment will be like. Do you need to support Internet Explorer 8? Then keep in mind that Object.defineProperty
(which is used to polyfill getters and setters) doesn't work there.
Babel has a short table of its caveats.
Babel and Object.assign
This is another thing. Object.assign
is only supported with Chrome 45 and Firefox 34. If you use it though, Babel won't polyfill it and just keep it as is. In order to add the polyfill, we have to modify our transform
option in the Gruntfile.js
to use the runtime
transformer.
There is also a babel-plugin-object-assign
plugin which replaces all occurences of Object.assign
with an extend helper.
Conclusion
So, that's it. We could now move on and extend this setup with a good view handling or add a CSS preprocessor such as Sass. Whatever fits your project. Thanks to Babel and Browserify it's already possible to write ES6 code, but still use ES5 code on the production environment. By that we can have a lot of fun and keep support for browser without complete feature support.
I have made own ES6 boilerplate called FrontBook open source and like to share it here. It also covers some more topics such as views, styles and assets. Feel free to check it out for your own projects and customise to your needs.
TL;DR
A good ES6 setup requires three main dependencies: Babel.js, Browserify and a task runner such as Grunt or gulp. Babel covers 71% of the ES6 feature set and Browserify adds the great module system.
Running $ browserify
in the command line every time you modify something is quite annoying and time consuming. Let's use a task runner for that. You will need:
grunt-browserify
using thetransform: ['babelify']
option,- ideally a linter such as
grunt-eslint
, grunt-watch
to check for any modified files.
Take care using features such as Object.assign
, as this still needs an additional plugin to work in all browser.