Babel Tutorial Part 4 - Compatibility with Older Browsers

Published: Thursday, September 3, 2020

Greetings, friends! Welcome to Part 4 of my Babel series! If you haven't already, please check out Part 1 of my series to learn what Babel is. Today, I will talk about transpiling JavaScript down to ES5 syntax, so you can use new JavaScript syntax or methods from ES 2015 and higher in older browsers such as Internet Explorer 11 (IE 11). In this article, I will show how to transpile arrow functions, template literals, and async/await down to ES5-compatible code using Babel and Webpack, so it can be used in IE 11. If you need help testing a website in IE11, please see my VirtualBox for IE tutorial. Let's get started!

Create a directory named babel-tutorial-transpile-to-es5 and navigate to it:

bash
Copied! ⭐️
mkdir babel-tutorial-transpile-to-es5 && cd $_

Next, run the following command to bootstrap a new project with a package.json file:

bash
Copied! ⭐️
npm init -y

We will need to install some Babel and Webpack packages as dev dependencies:

bash
Copied! ⭐️
npm i -D @babel/core @babel/cli @babel/preset-env webpack webpack-cli

If you read Part 2 of my Babel series, then you should already be familiar with the Babel portion of this setup. However, we will use @babel/preset-env instead of @babel/preset-react, since we want to transpile our code down to ES5-compatible code.

What is @babel/preset-env? It's a very powerful group of plugins provided by Babel that uses browserslist together with core-js, regenerator-runtime, and other tools to intelligently inject polyfills whenever you need them. The @babel/preset-env package is extremely useful and common in modern JavaScript projects. It is used with Create React App and Vue CLI internally. This package lets you use new JavaScript syntax sooner and lets you transpile ECMAScript standards supported by modern browsers down to older standards such as ES5.

tip
Remember! You don't have to target IE 11. You can target any browser you like, including older versions of Chrome and Firefox, so you can use new JavaScript features while keeping your customers, who may still use these older versions, happy! 😊

As of Babel 7.4, the @babel/polyfill package has been deprecated in favor of using core-js and regenerator-runtime directly. However, @babel/preset-env is clever enough to add both of them for you depending on the options you provide to it. We'll see an example of this later.

The latest version of Babel supports core-js v2 and core-js v3. By default, preset-env will use core-js v2, but in this project, we will use core-js v3, since it has newer JavaScript features.

bash
Copied! ⭐️
npm i core-js

When you install @babel/preset-env, it comes with a lot of Babel plugins and helper packages. Among these packages, you may find @babel/runtime installed in your node_modules directory. This package installs regenerator-runtime as a dependency. If you don't see regenerator-runtime in your node_modules directory, then you will have to install it manually if you want to use generator functions and async/await in older browsers:

bash
Copied! ⭐️
npm i regenerator-runtime

We also need to install a Webpack loader known as babel-loader, which will serve as the bridge between Babel and Webpack. Babel-loader lets us use Babel to transpile files before Webpack bundles and transforms them.

bash
Copied! ⭐️
npm i -D babel-loader

In this project, we will be making API calls using fetch, so we need a way of using fetch in IE 11. Unfortunately, Babel does not provide a polyfill for the fetch API, since it is related to a browser-specific API and not related to the ECMAScript standard. The fetch API is actually part of a Web platform API defined by the standard bodies, WHATWG and W3C. To use fetch inside IE 11, we will utilize the whatwg-fetch package.

bash
Copied! ⭐️
npm i whatwg-fetch

The last dependency we will install is the serve package used in Part 2 of this Babel series. This will let us serve our html file and listen for changes.

bash
Copied! ⭐️
npm i -D serve

Now that we have all the dependencies out of the way, let's create a simple HTML file named index.html.

html
Copied! ⭐️
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Transpiling to ES5 Demo</title>
</head>
<body>
  <h1>Transpiling to ES5 Demo</h1>
  <hr />
  <div id="user-1"></div>
  <hr />
  <div id="user-2"></div>
  <hr />
  <div id="user-3"></div>
  <script src="src/index.js"></script>
</body>
</html>

To serve this HTML file, we add "start": "serve" to our package.json file, which should look like the following so far:

json
Copied! ⭐️
{
  "name": "babel-tutorial-transpile-to-es5",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "serve"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@babel/cli": "^7.10.5",
    "@babel/core": "^7.11.4",
    "@babel/preset-env": "^7.11.0",
    "babel-loader": "^8.1.0",
    "serve": "^11.3.2",
    "webpack": "^4.44.1",
    "webpack-cli": "^3.3.12"
  },
  "dependencies": {
    "core-js": "^3.6.5",
    "whatwg-fetch": "^3.4.0"
  }
}

Inside the index.html file, there are three placeholders for user data that we will fetch from an API. Let's create a src directory that contains an index.js file. Inside this file, we will utilize three different ways of making an API call. We will reach out to JSON Placeholder, an awesome fake online REST API, for fetching our user data.

js
Copied! ⭐️
function setHTML(id, data) {
  const user = document.getElementById(`user-${id}`)
  user.innerHTML = `
    <h3>Latin title: ${data.title}</h3>
    <p>User ID: ${data.id}</p>
    <p>Completed Status: ${data.completed}</p>
  `
}

function getUserByXHR(id) {
  const xhr = new XMLHttpRequest()
  xhr.open('GET', `https://jsonplaceholder.typicode.com/todos/${id}`)
  xhr.send()
  xhr.onload = () => {
    setHTML(id, JSON.parse(xhr.response))
  }
}

function getUserByFetch(id) {
  fetch(`https://jsonplaceholder.typicode.com/todos/${id}`)
    .then(response => response.json())
    .then((json) => {
      setHTML(id, json)
    })
}

async function getUserByFetchAsyncAwait(id) {
  const response = await fetch(`https://jsonplaceholder.typicode.com/todos/${id}`)
  const json = await response.json()
  setHTML(id, json)
}

getUserByXHR(1)
getUserByFetch(2)
getUserByFetchAsyncAwait(3)
warning
You may be tempted to use the following code.
js
Copied! ⭐️
xhr.responseType = 'json'
As shown on the browser compatibility table at the bottom of MDN, this will not work in IE!

If you start the server using npm start, then you should be able to see this page work correctly in modern browsers such as Google Chrome and Firefox. However, the page will throw lots of errors if we try to view it in IE 11. 😱

With the power of Babel and Webpack, we can make dreams come true 💫 and use modern JavaScript inside older browsers!

First, we need to make our Babel configuration file and add @babel/preset-env as a preset. As a shortcut, you can use @babel/env instead of @babel/preset-env, since it's so common. Below is the complete babel.config.json file we should include:

json
Copied! ⭐️
{
  "presets": [
    [
      "@babel/preset-env",
      {
        "useBuiltIns": "usage",
        "corejs": 3
      }
    ]
  ]
}

Let me explain what's happening here. We are using the @babel/preset-env and passing in a few options. We are using version 3 of core-js instead of the default, version 2. The useBuiltIns option is a bit more complicated. A detailed explanation can be found on Babel's official website. Generally, it is better to use "useBuiltIns": "usage" instead of "useBuiltIns": "entry".

Using the usage option will tell Babel to automatically import polyfills when they are needed in each file. You may think that this would produce duplicate imports in each file, but modern bundlers such as Webpack are smart enough to load the same polyfill only once.

If we used the entry option, then it will add all polyfills that exist for a target environment. If we choose not to include a target option in our Babel config, then Babel will transpile all ES2015-ES2020 code to be ES5-compatible. This means that the entry option would load a ton of polyfills!!! In my opinion, it seems best to use the entry option for building libraries, where you want the developer to not have to worry about Babel and browser support. Though, one could argue that using peer dependencies or bundling tools such as Webpack may alleviate these concerns.

With our Babel config setup, we can run our code through the Babel transpiler to see what we get. Let's add the script, "build:babel": "babel src -d lib", to our package.json file. If you execute the command, npm run build:babel, then your src/index.js file will be transformed into a file located at lib/index.js. If you look at this new file, you should see a few polyfills:

js
Copied! ⭐️
require('core-js/modules/es.array.concat')
require('core-js/modules/es.object.to-string')
require('core-js/modules/es.promise')
require('regenerator-runtime/runtime')

Since we used the "useBuiltIns": "usage" option and left the targets option blank, Babel automatically will inject polyfills related to features we use in our code but are missing in ES5. Promises and template literals do not exist in ES5. Therefore, we see three polyfills from core-js inside our lib/index.js file. Additionally, we use async/await keywords which use generator functions internally. Therefore, we need to include the regenerator-runtime polyfill. You may be wondering how Babel converted our arrow functions into regular functions and the const keywords into var. The @babel/preset-env package comes with a lot of useful Babel plugins for modifying the Abstract Syntax Tree (AST) after Babel's parsing step. One such plugin is the @babel/plugin-transform-arrow-functions plugin.

Are we done yet? Can we use our code in IE 11? Nope! IE 11 does not understand the require statements, which import CommonJS modules. Node understands CommonJS, but IE 11 and modern browsers do not. How do we solve this issue? If you've read the @babel/preset-env documentation, you might think we could use the modules option. By passing "modules": false to the @babel/preset-env options inside our Babel config, we can tell Babel to keep ES Modules untouched and prevent it from transforming modules into CommonJS syntax. As a side note, using ES Modules has the added benefit of Tree Shaking when used with bundlers that support it such as Webpack and Rollup. However, we run into another issue! IE 11 does not understand ES Module syntax either! What do we do? 😳

Webpack to the rescue! 😃

Webpack has the power to transform CommonJS and ES Modules into their own module syntax known as Webpack Modules. You can even mix and match both syntaxes (though it's not recommended), and Webpack will bundle it all up into one big happy family of files. Similar to how Babel needs a babel.config.json file, Webpack needs a configuration file of its own. We will create a webpack.config.js file with the following contents:

js
Copied! ⭐️
module.exports = {
  mode: 'development',
  entry: ['whatwg-fetch', './src/index.js'],
  output: {
    filename: 'bundle.js'
  },
  module: {
    rules: [
      {
        test: /\.m?js$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
        }
      }
    ]
  }
}

In this Webpack config, we are bundling together two entry/input files: whatwg-fetch and ./src/index.js. We need whatwg-fetch because it allows us to use the Fetch API in IE 11. Babel doesn't provide a polyfill for this. We use babel-loader to transpile our files before they are bundled together into an output file named bundle.js inside the dist directory. We will tell Webpack to include all files that end in either .mjs or .js using the /\.m?js$/ regular expression. We will also exclude the node_modules from being transpiled by Babel.

Let's add the script, "build:webpack": "webpack" to our package.json file. When we run npm run build:webpack, then it will trigger a new Webpack build and produce the file, dist/bundle.js.

If we go back to our index.html file and replace src/index.js with dist/bundle.js, then we should see the following content appear correctly in ALL browsers, including IE 11. We did it! 🎉🎉🎉

Image of finished project in IE 11 displaying three users

If you want to download the finished code, you can check out my GitHub repository.

In conclusion, Babel and Webpack go together like peanut butter and jelly 😋. The @babel/preset-env package is a powerful group of plugins that help developers transpile code down to a format suitable for the browser vendors and browser versions they are targeting. By default, this package transforms your code down to ES5-compatible syntax, which is perfect for supporting IE 11. We've only scratched the surface of what @babel/preset-env can do! There are many options you can pass to it and many more things Babel can do! Till next time, happy coding!

Resources