In this article, we’re going to look at creating a build setup for handling modern JavaScript (running in web browsers) using Babel and webpack.
This is needed to ensure that our modern JavaScript code in particular is made compatible with a wider range of browsers than it might otherwise be.
JavaScript, like most web-related technologies, is evolving all the time. In the good old days, we could drop a couple of <script> tags into a page, maybe include jQuery and a couple of plugins, then be good to go.</script>
However, since the introduction of ES6, things have got progressively more complicated. Browser support for newer language features is often patchy, and as JavaScript apps become more ambitious, developers are starting to use modules to organize their code. In turn, this means that if you’re writing modern JavaScript today, you’ll need to introduce a build step into your process.
As you can see from the links beneath, converting down from ES6 to ES5 dramatically increases the number of browsers that we can support.
The purpose of a build system is to automate the workflow needed to get our code ready for browsers and production. This may include steps such as transpiling code to a differing standard, compiling Sass to CSS, bundling files, minifying and compressing code, and many others. To ensure these are consistently repeatable, a build system is needed to initiate the steps in a known sequence from a single command.
In order to follow along, you’ll need to have both Node.js and npm installed (they come packaged together). I would recommend using a version manager such as nvm to manage your Node installation (here’s how), and if you’d like some help getting to grips with npm, then check out SitePoint’s beginner-friendly npm tutorial.
Create a root folder somewhere on your computer and navigate into it from your terminal/command line. This will be your
Create a package.json file with this:
<span>npm init -y </span>
Note: The -y flag creates the file with default settings, and means you don’t need to complete any of the usual details from the command line. They can be changed in your code editor later if you wish.
Within your
To get ourselves going, we’re going to install babel-cli, which provides the ability to transpile ES6 into ES5, and babel-preset-env, which allows us to target specific browser versions with the transpiled code.
<span>npm install babel-cli babel-preset-env --save-dev </span>
You should now see the following in your package.json:
<span>"devDependencies": { </span> <span>"babel-cli": "^6.26.0", </span> <span>"babel-preset-env": "^1.6.1" </span><span>} </span>
Whilst we’re in the package.json file, let’s change the scripts section to read like this:
<span>"scripts": { </span> <span>"build": "babel src -d public" </span><span>}, </span>
This gives us the ability to call Babel via a script, rather than directly from the terminal every time. If you’d like to find out more about npm scripts and what they can do, check out this SitePoint tutorial.
Lastly, before we can test out whether Babel is doing its thing, we need to create a .babelrc configuration file. This is what our babel-preset-env package will refer to for its transpile parameters.
Create a new file in your
<span>{ </span> <span>"presets": [ </span> <span>[ </span> <span>"env", </span> <span>{ </span> <span>"targets": { </span> <span>"browsers": ["last 2 versions", "safari >= 7"] </span> <span>} </span> <span>} </span> <span>] </span> <span>] </span><span>} </span>
This will set up Babel to transpile for the last two versions of each browser, plus Safari at v7 or higher. Other options are available depending on which browsers you need to support.
With that saved, we can now test things out with a sample JavaScript file that uses ES6. For the purposes of this article, I’ve modified a copy of leftpad to use ES6 syntax in a number of places: template literals, arrow functions, const and let.
<span>"use strict"; </span> <span>function leftPad(str<span>, len, ch</span>) { </span> <span>const cache = [ </span> <span>"", </span> <span>" ", </span> <span>" ", </span> <span>" ", </span> <span>" ", </span> <span>" ", </span> <span>" ", </span> <span>" ", </span> <span>" ", </span> <span>" " </span> <span>]; </span> str <span>= str + ""; </span> len <span>= len - str.length; </span> <span>if (len <= 0) return str; </span> <span>if (!ch && ch !== 0) ch = " "; </span> ch <span>= ch + ""; </span> <span>if (ch === " " && len < 10) </span> <span>return () => { </span> cache<span>[len] + str; </span> <span>}; </span> <span>let pad = ""; </span> <span>while (true) { </span> <span>if (len & 1) pad += ch; </span> len <span>>>= 1; </span> <span>if (len) ch += ch; </span> <span>else break; </span> <span>} </span> <span>return <span>`<span>${pad}</span><span>${str}</span>`</span>; </span><span>} </span>
Save this as src/js/leftpad.js and from your terminal run the following:
<span>npm run build </span>
If all is as intended, in your public folder you should now find a new file called js/leftpad.js. If you open that up, you’ll find it no longer contains any ES6 syntax and looks like this:
<span>"use strict"; </span> <span>function leftPad(str<span>, len, ch</span>) { </span> <span>var cache = ["", " ", " ", " ", " ", " ", " ", " ", " ", " "]; </span> str <span>= str + ""; </span> len <span>= len - str.length; </span> <span>if (len <= 0) return str; </span> <span>if (!ch && ch !== 0) ch = " "; </span> ch <span>= ch + ""; </span> <span>if (ch === " " && len < 10) return function () { </span> cache<span>[len] + str; </span> <span>}; </span> <span>var pad = ""; </span> <span>while (true) { </span> <span>if (len & 1) pad += ch; </span> len <span>>>= 1; </span> <span>if (len) ch += ch;else break; </span> <span>} </span> <span>return "" + pad + str; </span><span>} </span>
An ES6 module is a JavaScript file containing functions, objects or primitive values you wish to make available to another JavaScript file. You export from one, and import into the other. Any serious modern JavaScript project should consider using modules. They allow you to break your code into self-contained units and thereby make things easier to maintain; they help you avoid namespace pollution; and they help make your code more portable and reusable.
Whilst the majority of ES6 syntax is widely available in modern browsers, this isn’t yet the case with modules. At the time of writing, they’re available in Chrome, Safari (including the latest iOS version) and Edge; they’re hidden behind a flag in Firefox and Opera; and they’re not available (and likely never will be) in IE11, nor most mobile devices.
In the next section, we’ll look at how we can integrate modules into our build setup.
The export keyword is what allows us to make our ES6 modules available to other files, and it gives us two options for doing so — named and default. With the named export, you can have multiple exports per module, and with a default export you only have one per module. Named exports are particularly useful where you need to export several values. For example, you may have a module containing a number of utility functions that need to be made available in various places within your apps.
So let’s turn our leftPad file into a module, which we can then require in a second file.
To create a named export, add the following to the bottom of the leftPad file:
<span>npm init -y </span>
We can also remove the "use strict"; declaration from the top of the file, as modules run in strict mode by default.
As there’s only a single function to be exported in the leftPad file, it might actually be a good candidate for using export default instead:
<span>npm install babel-cli babel-preset-env --save-dev </span>
Again, you can remove the "use strict"; declaration from the top of the file.
To make use of exported modules, we now need to import them into the file (module) we wish to use them in.
For the export default option, the exported module can be imported under any name you wish to choose. For example, the leftPad module can be imported like so:
<span>"devDependencies": { </span> <span>"babel-cli": "^6.26.0", </span> <span>"babel-preset-env": "^1.6.1" </span><span>} </span>
Or it could be imported as another name, like so:
<span>"scripts": { </span> <span>"build": "babel src -d public" </span><span>}, </span>
Functionally, both will work exactly the same, but it obviously makes sense to use either the same name as it was exported under, or something that makes the import understandable — perhaps where the exported name would clash with another variable name that already exists in the receiving module.
For the named export option, we must import the module using the same name as it was exported under. For our example module, we’d import it in a similar manner to that we used with the export default syntax, but in this case, we must wrap the imported name with curly braces:
<span>{ </span> <span>"presets": [ </span> <span>[ </span> <span>"env", </span> <span>{ </span> <span>"targets": { </span> <span>"browsers": ["last 2 versions", "safari >= 7"] </span> <span>} </span> <span>} </span> <span>] </span> <span>] </span><span>} </span>
The braces are mandatory with a named export, and it will fail if they aren’t used.
It’s possible to change the name of a named export on import if needed, and to do so, we need to modify our syntax a little using an import [module] as [path] syntax. As with export, there’s a variety of ways to do this, all of which are detailed on the MDN import page.
<span>npm init -y </span>
Again, the name change is a little nonsensical, but it illustrates the point that they can be changed to anything. You should keep to good naming practices at all times, unless of course you’re writing routines for preparing fruit-based recipes.
To make use of the exported leftPad module, I’ve created the following index.js file in the src/js folder. Here, I loop through an array of serial numbers, and prefix them with zeros to make them into an eight-character string. Later on, we’ll make use of this and post them out to an ordered list element on an HTML page. Note that this example uses the default export syntax:
<span>npm install babel-cli babel-preset-env --save-dev </span>
As we did earlier, run the build script from the
<span>"devDependencies": { </span> <span>"babel-cli": "^6.26.0", </span> <span>"babel-preset-env": "^1.6.1" </span><span>} </span>
Babel will now create an index.js file in the public/js directory. As with our leftPad.js file, you should see that Babel has replaced all of the ES6 syntax and left behind only ES5 syntax. You might also notice that it has converted the ES6 module syntax to the Node-based module.exports, meaning we can run it from the command line:
<span>"scripts": { </span> <span>"build": "babel src -d public" </span><span>}, </span>
Your terminal should now log out an array of strings prefixed with zeros to make them all eight characters long. With that done, it’s time to take a look at webpack.
As mentioned, ES6 modules allow the JavaScript developer to break their code up into manageable chunks, but the consequence of this is that those chunks have to be served up to the requesting browser, potentially adding dozens of additional HTTP requests back to the server — something we really ought to be looking to avoid. This is where webpack comes in.
webpack is a module bundler. Its primary purpose is to process your application by tracking down all its dependencies, then package them all up into one or more bundles that can be run in the browser. However, it can be far more than that, depending upon how it’s configured.
webpack configuration is based around four key components:
Entry: This holds the start point of your application from where webpack can identify its dependencies.
Output: This specifies where you would like the processed bundle to be saved.
Loaders: These are a way of converting one thing as an input and generating something else as an output. They can be used to extend webpack’s capabilities to handle more than just JavaScript files, and therefore convert those into valid modules as well.
Plugins: These are used to extend webpack’s capabilities into other tasks beyond bundling — such as minification, linting and optimization.
To install webpack, run the following from your
<span>npm init -y </span>
This installs webpack locally to the project, and also gives the ability to run webpack from the command line through the addition of webpack-cli. You should now see webpack listed in your package.json file. Whilst you’re in that file, modify the scripts section as follows, so that it now knows to use webpack instead of Babel directly:
<span>npm install babel-cli babel-preset-env --save-dev </span>
As you can see, this script is calling on a webpack.config.js file, so let’s create that in our
<span>"devDependencies": { </span> <span>"babel-cli": "^6.26.0", </span> <span>"babel-preset-env": "^1.6.1" </span><span>} </span>
This is more or less the simplest config file you need with webpack. You can see that it uses the entry and output sections described earlier (it could function with these alone), but also contains a mode: 'development' setting.
webpack has the option of using either “development” or “production” modes. Setting mode: 'development' optimizes for build speed and debugging, whereas mode: 'production' optimizes for execution speed at runtime and output file size. There’s a good explanation of modes in Tobias Koppers’ article “webpack 4: mode and optimization” should you wish to read more on how they can be configured beyond the default settings.
Next, remove any files from the public/js folder. Then rerun this:
<span>"scripts": { </span> <span>"build": "babel src -d public" </span><span>}, </span>
You’ll see that it now contains a single ./public/bundle.js file. Open up the new file, though, and the two files we started with look rather different. This is the section of the file that contains the index.js code. Even though it’s quite heavily modified from our original, you can still pick out its variable names:
<span>{ </span> <span>"presets": [ </span> <span>[ </span> <span>"env", </span> <span>{ </span> <span>"targets": { </span> <span>"browsers": ["last 2 versions", "safari >= 7"] </span> <span>} </span> <span>} </span> <span>] </span> <span>] </span><span>} </span>
If you run node public/js/bundle.js from the
As mentioned earlier, loaders allow us to convert one thing into something else. In this case, we want ES6 converted into ES5. To do that, we’ll need a couple more packages:
<span>"use strict"; </span> <span>function leftPad(str<span>, len, ch</span>) { </span> <span>const cache = [ </span> <span>"", </span> <span>" ", </span> <span>" ", </span> <span>" ", </span> <span>" ", </span> <span>" ", </span> <span>" ", </span> <span>" ", </span> <span>" ", </span> <span>" " </span> <span>]; </span> str <span>= str + ""; </span> len <span>= len - str.length; </span> <span>if (len <= 0) return str; </span> <span>if (!ch && ch !== 0) ch = " "; </span> ch <span>= ch + ""; </span> <span>if (ch === " " && len < 10) </span> <span>return () => { </span> cache<span>[len] + str; </span> <span>}; </span> <span>let pad = ""; </span> <span>while (true) { </span> <span>if (len & 1) pad += ch; </span> len <span>>>= 1; </span> <span>if (len) ch += ch; </span> <span>else break; </span> <span>} </span> <span>return <span>`<span>${pad}</span><span>${str}</span>`</span>; </span><span>} </span>
To utilize them, the webpack.config.js needs a module section adding to it after the output section, like so:
<span>npm run build </span>
This uses a regex statement to identify the JavaScript files to be transpiled with the babel-loader, whilst excluding anything in the node_modules folder from that. Lastly, the babel-loader is told to use the babel-preset-env package installed earlier, to establish the transpile parameters set in the .babelrc file.
With that done, you can rerun this:
<span>"use strict"; </span> <span>function leftPad(str<span>, len, ch</span>) { </span> <span>var cache = ["", " ", " ", " ", " ", " ", " ", " ", " ", " "]; </span> str <span>= str + ""; </span> len <span>= len - str.length; </span> <span>if (len <= 0) return str; </span> <span>if (!ch && ch !== 0) ch = " "; </span> ch <span>= ch + ""; </span> <span>if (ch === " " && len < 10) return function () { </span> cache<span>[len] + str; </span> <span>}; </span> <span>var pad = ""; </span> <span>while (true) { </span> <span>if (len & 1) pad += ch; </span> len <span>>>= 1; </span> <span>if (len) ch += ch;else break; </span> <span>} </span> <span>return "" + pad + str; </span><span>} </span>
Then check the new public/js/bundle.js and you’ll see that all traces of ES6 syntax have gone, but it still produces the same output as previously.
Having built a functioning webpack and Babel setup, it’s time to bring what we’ve done to the browser. A small HTML file is needed, and this should be created in the
<span>npm init -y </span>
There’s nothing complicated in it. The main points to note are the
The above is the detailed content of Setting up an ES6 Project Using Babel and webpack. For more information, please follow other related articles on the PHP Chinese website!