Now that we learned the basics of using webpack, we're going to learn to leverage babel 6 in order to write ES6, the new specification of javascript.
If you've ever written ES6, it's hard to want to go back to writing ES5. If you haven't had the chance to write ES6 yet, a big reason is probably because setting up a dev environment, understanding all the configuration options, and deciding which ones to use can be discouraging.
I hope that this tutorial can make that process of getting started much easier.
I will gladly accept any and all contributions/corrections. If you have any questions, feel free to leave them as issues. If I made mistakes, please point them out. Finally, if you feel that I left anything out or could have explained something better, make sure to leave an issue or make a pull request.
If you want a more in-depth explanation, and finer grained control over babel then please look at their handbook. I'm paraphrasing the basics here.
Simply stated, babel lets you take advantage of a much more fully featured specification of javascript, which isn't supported by most browsers and environments, and turns it into ES5, which has much more widespread support.
With babel, this code, which is just getting support now in the latest browsers
const square = n => n * n;
is transformed into something like
var square = function square(n) {
return n * n;
};
which you could run anywhere that supports javascript.
Another tool, another config file. This time around we'll have a file called
.babelrc
Thankfully, the .babelrc
file will only be a single line long.
{
"presets": ["es2015", "stage-2"]
}
The only option you need to specify is presets
, which are described in the excerpt below:
JavaScript also has some proposals that are making their way into the standard through the TC39's (the technical committee behind the ECMAScript standard) process.
This process is broken through a 5 stage (0-4) process. As proposals gain more traction and are more likely to be accepted into the standard they proceed through the various stages, finally being accepted into the standard at stage 4.
Note that there is no stage-4 preset as it is simply the
es2015
preset above.
To sum it up, presets are bundles of plugins that add features to the code you're writing. es2015
adds features that definitely are going to be in the official release of ES6, and the presets
that are stages 0-3 are proposals for future specifications of Javascript that are still being drafted.
The lower you go, the higher the risk that the features you're using are going to have support dropped.
From my experience, the lowest I've needed to go is stage-2
to be able to use something called object spread. You can see the rest of the proposals
here, and decide how low you want to go.
Anyways to use these presets, we need to install them
npm install --save-dev babel-preset-es2015 babel-preset-stage-2
and that's actually all you need to do.
We're going to use the same exact configs from example 7 from part 1, but add the functionality needed to use ES6.
Current configs:
// webpack.config.dev.js
var path = require('path')
var webpack = require('webpack')
var HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
devtool: 'cheap-eval-source-map',
entry: [
'webpack-dev-server/client?http://localhost:8080',
'webpack/hot/dev-server',
'./src/index'
],
output: {
path: path.join(__dirname, 'dist'),
filename: 'bundle.js'
},
plugins: [
new webpack.HotModuleReplacementPlugin(),
new HtmlWebpackPlugin({
template: './src/index.html'
})
],
module: {
loaders: [{
test: /\.css$/,
loaders: ['style', 'css']
}]
},
devServer: {
contentBase: './dist',
hot: true
}
}
and
// webpack.config.prod.js
var path = require('path')
var webpack = require('webpack')
var HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
devtool: 'source-map',
entry: ['./src/index'],
output: {
path: path.join(__dirname, 'dist'),
filename: 'bundle.js'
},
plugins: [
new webpack.optimize.UglifyJsPlugin({
compressor: {
warnings: false,
},
}),
new webpack.optimize.OccurrenceOrderPlugin(),
new HtmlWebpackPlugin({
template: './src/index.html'
})
],
module: {
loaders: [{
test: /\.css$/,
loaders: ['style', 'css']
}]
}
}
To transpile our code to ES5 we need to run it through a new loader called babel-loader
, which has
a dependency of babel-core
. This loader will use our .babelrc
config to understand and transform
our code into its new form.
npm install --save-dev babel-loader babel-core
We add that to both our dev and prod configs:
// To save space I'll just show the "loaders" part
// Both webpack.config.dev.js and webpack.config.prod.js
module: {
loaders: [{
test: /\.css$/,
loaders: ['style', 'css']
}, {
test: /\.js$/,
loaders: ['babel'],
include: path.join(__dirname, 'src')
}]
}
An extremely important thing to note is the usage of the include
property. When we run
webpack
, because we have set our test
to /.js$/
, webpack will try to run the babel loader on
every single js
file in your dependency tree.
Can you spot the problem with this? What if I require('bluebird')
, or any other large npm
package?
It'll try to run node_modules through your babel-loader
, which will extend your build process
by an extreme amount.
include
prevents this by specifying that this loader only applies to .js
files in your src
directory.
Alternatively you could change include: path.join(__dirname, 'src')
to exclude: /node_modules/
which will then
include everything but the node_modules
folder. More information can be found here.
Honestly I thought this tutorial would be longer, but it seems I forgot that adding babel is
actually pretty trivial. We can now update our earlier code in index.js
to use the ES6 syntax:
// index.js
// Accept hot module reloading
if (module.hot) {
module.hot.accept()
}
require('./styles.css') // The page is now styled
const Please = require('pleasejs')
const div = document.getElementById('color')
const button = document.getElementById('button')
const changeColor = () => div.style.backgroundColor = Please.make_color()
button.addEventListener('click', changeColor)
Another thing to note is that now we can use the es6 module system. For example, instead of
const Please = require('pleasejs')
we can now do
import Please from 'pleasejs'
Since that actually didn't take so long I'm going to cover two more topics that are pretty important and useful.
If we don't want to execute a portion of code in production we can use the handy dandy DefinePlugin.
The plugin lets us create a global constant for our entire bundle, which we could name anything,
such as DONT_USE_IN_PRODUCTION: true
, but more practically, it's a much better choice to use
process.env.NODE_ENV: JSON.stringify('production')
. This is because many programs recognize and
use process.env.NODE_ENV
for additional features and optimization of your code.
Why JSON.stringify
? Because according to the docs:
If the value is a string it will be used as a code fragment.
This means a value of 'production'
would just be an error. If you think JSON.stringify
is weird, a valid alternative is '"production"'
.
Your plugins array should now look like
plugins: [
new webpack.optimize.UglifyJsPlugin({
compressor: {
warnings: false,
},
}),
new webpack.optimize.OccurrenceOrderPlugin(),
new HtmlWebpackPlugin({
template: './src/index.html'
}),
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify('production')
})
]
Now, if we don't want to execute some code in production, we can put it in an if statement:
if (process.env.NODE_ENV !== 'production') {
// not for production
}
In our current project we could say to exclude the hot reloading if it's in production:
// Accept hot module reloading during development
if (process.env.NODE_ENV !== 'production') {
if (module.hot) {
module.hot.accept()
}
}
Defining our production variable as process.env.NODE_ENV
has another added benefit.
The current environment will use process.env.BABEL_ENV. When BABEL_ENV is not available, it will fallback to NODE_ENV, and if that is not available it will default to "development".
This means that the babel environment will match our webpack environment.
We can take advantage of this, tweaking our .babelrc
to have development only config by adding env
:
{
"presets": ["es2015", "stage-2"],
"env": {
// only happens if NODE_ENV is undefined or set to 'development'
"development": {
// ignored when NODE_ENV is production!
}
}
We'll be using this in part 3 with react when we introduce the React Transform HMR
If you've looked at any project seeds/starters for Webpack/React, you've probably seen a file called
.eslintrc
. If you aren't using an IDE, but instead are using a text editor like Atom, Sublime,
Emacs, Vim, etc., eslint provides syntax and style checks, pointing out your mistakes. Moreover,
even if you're using an IDE, it can provide more features, and ensure uniformity in coding style
for all contributors to the project.
Do take note that if you want it integrated into your editor, you need to install a package. For example, I use linter-eslint for Atom.
To reduce the amount of our config we write manually, we're going to take advantage of the fact that eslint lets you inherit from other people's configs. I always like to use a config based upon airbnb's style guide.
To get started, we need to install eslint as well as airbnb's config
npm install eslint
npm install -g eslint-cli
npm install --save-dev eslint eslint-config-airbnb-base
Our starting config will look like:
// .eslintrc
{
"extends": "airbnb-base" // 'airbnb-base' because 'airbnb' assumes usage of react
}
However because linting is highly opinionated, I like to tweak this a bit. If you want to know what all these rules mean, or tweak them to fit your preferences look here:
// .eslintrc
{
"extends": "airbnb-base",
"rules": {
"comma-dangle": 0,
"no-console": 0,
"semi": [2, "never"],
"func-names": 0,
"space-before-function-paren": 0,
"no-multi-spaces": 0
}
}
Additionally, out of the box, eslint does not support/recognize babel syntax so we be installing two new packages:
npm install --save-dev babel-eslint eslint-plugin-babel
and tweaking our config once more to add babel specific rules:
// .eslintrc
{
"extends": "airbnb-base",
"parser": "babel-eslint",
"rules": {
"comma-dangle": 0,
"no-console": 0,
"semi": [2, "never"],
"func-names": 0,
"space-before-function-paren": 0,
"no-multi-spaces": 0,
"babel/generator-star-spacing": 1,
"babel/new-cap": 1,
"babel/object-shorthand": 1,
"babel/arrow-parens": 1,
"babel/no-await-in-loop": 1
},
"plugins": [
"babel"
]
}
Finally it's also a good idea to add an npm script for linting to our existing package.json file
// package.json
"scripts": {
"build": "webpack --config webpack.config.prod.js",
"dev": "webpack-dev-server --config webpack.config.dev.js",
"lint": "eslint src"
}
which you can run with npm run lint
just to make sure none of your code violates the rules you specified.
I've put the end result of all of this into example 1 just in case I wasn't clear on anything. If you still have trouble understanding, feel free to leave a question in issues.
So now we can easily write ES6 code, and additionally, understand the config that enables us to write it 🎉!
However, just because you can write it from scratch, it doesn't mean you have to. For your convenience, I have made a separate repository you can clone to get started, which is minimal and based on this tutorial series.
Looking towards the future:
- Part 3 will address adding React into the picture
- Part 4 will cover more advanced webpack features
Thank you for reading!