Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Setting Up a Dev Environment With Webpack 3

DZone's Guide to

Setting Up a Dev Environment With Webpack 3

Learn how to create your own dev, prod, and legacy browser environments for JavaScript apps using Webpack 3 and Babel.

· Web Dev Zone ·
Free Resource

Learn how error monitoring with Sentry closes the gap between the product team and your customers. With Sentry, you can focus on what you do best: building and scaling software that makes your users’ lives better.

I have observed that some people still write the ancient ES5 syntax of JavaScript, and it’s disheartening. I wonder what keeps them from moving forward. Some must be driven by psychological inertia, but some likely just find it too complex to make their new syntax run in a wide range of browsers.

In fact, nowadays, we don't have to fear it anymore. One can set up a dev environment where tools decide what transformations the code needs and what polyfills to load depending on the selected target (list of user agents to support). The only thing those people need to start “a new life” is a proper setup.

That’s what the article is about.

My Story

In 2012, I was looking for a way to combine my numerous JavaScript modules into a single bundle. I had quite an experience with YUI loader, but I didn’t want to go with asynchronous loading as, in the times of HTTP 1.x, it caused intolerable lag.

I noticed that some projects were simply concatenating files between a header and a footer. With my PHP background, I saw it more like an include, or rather an import statement, so one could have control on bundling. I created a little utility https://github.com/dsheiko/jsic. Shortly, I found out that includes aren’t that handy, as they do not care about the scope.

Then, I thought that if I have a pre-compilation stage, why not to resolve CommonJS notation (like Node.js) into a bundle file? So by the end of the year, I came up with another tool: https://github.com/dsheiko/cjsc.

When publishing it to NPM and seeking proper wording for the description, I ran into Browserfy… Yeah, it’s me and it’s not fixable. Nonetheless, I relied on CJSC for years, and it served me well. It was fast, dead simple to set up, and focused on only one task — bundling CommonJS modules.

Yet after a while, new solutions started to pop up. I recall being pretty impressed by the idea of tree shaking in RollUp. I’m generally very fussy about YAGNI, so it felt like a dream. As for Webpack, honestly, I didn’t get it at first. But I engaged, and I think it is now one of the very best technologies available to a frontend developer.

If you know how to deal with it, of course…

Meet the Webpack

So what is Webpack? It’s a bundler — and an extremely smart one. It reads the specified source, transpiles it if needed (ES.Next, TypeScript, CoffeeScript, etc), resolves JavaScript modules (ES modules, CommonJs, AMD) and other dependencies (HTML, CSS, SASS, images and more), and produces one or more output files.

Yes, you can gracefully split the code base into bundles to achieve a better user experience. You can have a base bundle with code implementing the core UX and other bundles lazy-loading in accordance with their priorities. Webpack does tree-shaking and scope hoisting. What is more, you can pipe in plugins like Uglify to post-process the output code. Impressive, isn’t it? Let’s take an example and see in practice.

Practice Makes Perfect

First, we clone my boilerplate with the example:

git clone https://github.com/dsheiko/boilerplate/
cd boilerplate/webpack-babel


This application doesn’t do much. It simply shows how a few modules resolve statically while a few others resolve dynamically by using ES.Next syntax. Check out the entry script:

/webpack-babel/src/index.js

import { utilFoo } from "./util/foo";


So, here we import the foo module statically. This code is going to be included in the main bundle.

import { utilBar } from "util/bar";


We do the same with bar module, but this time, we specify its location — not relative to the entry script, but to the base directory, which we will set up in our Webpack configuration.

Promise.all([
  import( "./widget/foo" ),
  import( "./widget/bar" )
]).then( ([{ widgetFoo }, { widgetBar }])  => {
  console.log( "Lazy-loaded modules exports ", widgetFoo(), widgetBar() );
}).catch(( e )=> {
  console.error( e );
});


We also resolve two dynamic modules. According to Promise.all, they start loading concurrently as soon as the main bundle executes. When both load, we receive their exports and forward them to the console. As you can see, we use Webpack comments like /* webpackChunkName: "foo" */ to point out what names we want for the bundle files.

The example contains a ready-made manifest (package.json). So, you can obtain all the required dependencies by running:

npm i


What we want from Webpack here is to:

  • Transpile the ES.Next syntax we use in JavaScript suitable for a wider range of browsers
  • Resolve all the encountered (static and dynamic) modules starting from the entry script
  • Minify bundles for production
  • Produce two packages: one thinner for evergreen browsers and one thicker for legacy ones

Configuring Webpack

To avoid code duplication and achieve better readability, we split the Webpack configuration into four files: webpack.common.js, webpack.dev.js, webpack.prod.js, and webpack.common-legacy.js. The first one will be an abstract config extended by the other ones. Webpack.dev.js will serve for development and webpack.dev.js will extend it for code optimization. Eventually, it will be extended by webpack.common-legacy.js to inject polyfills required by legacy browsers.

So let’s start with the basics.

webpack.common.js

module.exports = {
    
    entry: {
      
      index : join( SRC_FULL_PATH, "index.js" )
    },
    
    output: {
path: PUBLIC_FULL_PATH,
filename: `[name].js`,
      chunkFilename: `[name].v${pkg.version}.widget.js`,
      publicPath: PUBLIC_PATH
    },
…


Here we specify the entry script location and give it an alias (index). Next, we set the output configuration. For the main bundle name, we use a placeholder, [name], which receives the alias we already set (index). For lazy-loaded modules, we define a name template and base public path. Note that I add the manifest version to the chunk names. Thus, we bust the CDN cache with every new published version of the project.

...
resolve: {
   modules: [
    "node_modules",
    SRC_FULL_PATH
  ],
  extensions: [ ".js" ]javascript:void(0)
},
...


With the field resolve, we state that while resolving modules, Webpack has to try searching for a file relative to the respective NPM package and our base directory (given in the constant SRC_FULL_PATH).

plugins: [
      new CleanWebpackPlugin([ PUBLIC_PATH ])
    ]
};


At last, we call clean-webpack-plugin to clean up the output directory every time Webpack is about to write the build assets there.

webpack.dev.js

const merge = require( "webpack-merge" ),
      baseConfig = require( "./webpack.common" );
      
module.exports = merge( baseConfig, {
...


We start our dev configuration by extending webpack.common.js. For that, we use webpack-merge.

module: {
  rules: [
    {
      test: /.js$/,
      exclude: /node_modules/,
      use: [{
        loader: "babel-loader",
        options: {
          presets: [ [ "env", {
            "targets": {
              "browsers": [
                "Chrome >= 60",
                "Safari >= 10.1",
                "iOS >= 10.3",
                "Firefox >= 54",
                "Edge >= 15"
              ]
            },
            "modules": false,
            "useBuiltIns": true,
            "debug": false
          }] ],
          plugins: [
            "transform-class-properties",
            "transform-object-rest-spread",
            "babel-plugin-syntax-dynamic-import",
            "transform-runtime"
          ]
        }
      }]
    }
  ]
}


This one is all about Babel configuration. In plain English, we state that Webpack will give every encountered .js module to Babel loader. Babel will load the env plugin preset, which makes our ES.Next syntax suitable for browser matching patterns enlisted in the targets.browsers field. With useBuiltIns, we allow Babel to include polyfills if necessary. On top of it, we apply the following plugins:

Actually, that’s already enough to make a dev build:

npm run build:dev


For a production build, we pipe in uglifyjs-webpack-plugin to minify the bundles:

webpack.prod.js

plugins: [
      new UglifyJSPlugin()
]


Note that to optimize ES.Next syntax, we need at least the 1.0 version of the plugin:

npm i uglifyjs-webpack-plugin@^1.0.0


As for webpack.prod-legacy.js, we apply customizeArray and customizeObject of webpack-merge to override output and Babel config. So with this configuration, we make Webpack output in ./build/legacy, which differs from default one: ./build. We also widened the range of target browsers.

Now we can build for production:

npm run build:prod


This runs Webpack firstly on webpack.prod.js, secondly webpack.prod-legacy.js, and results in the following output:

You can see from the log that both util/foo.js and foo.v0.0.1.widget.js have a slightly bigger size in the legacy bundle. This example has very little code, but in a real application, this difference can be significant.

Now, as we have two sets of bundles, you may ask how we are going to switch between them depending on the running browser? Here is the trick:

<script type="module" src="./build/index.js"></script>

<script nomodule src="./build/legacy/index.js"></script>


The target browsers we specified in webpack.dev.js all support ES modules and therefore load the source from script type=module. They also ignore the source of script nomodule. On the contrary, legacy browsers skip the first script and load from the second.

If we start the application, we are going to see our console.log output redirected in HTML by the [console-log-html | https://github.com/Alorel/console-log-html} module:

npm start


Final Words

So, what have we achieved? We set up a dev environment for a generic JavaScript application. We were diligent about progressive enhancement and split the application code into three bundles, where one represented the core functionality and loaded first. As it completeds, it initializes concurrent loading of other two bundles. We used ES.Next (the latest EcmaScript syntax) wrapped in ES modules. We made Webpack to compile the code with Babel. We came up with three runnable configurations for Webpack to build for dev, for production, and for legacy browsers.

What’s the best way to boost the efficiency of your product team and ship with confidence? Check out this ebook to learn how Sentry's real-time error monitoring helps developers stay in their workflow to fix bugs before the user even knows there’s a problem.

Topics:
web dev ,javascript ,webpack 3 ,dev environment ,tutorial ,babel

Published at DZone with permission of

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}