Managing Dev and Production Builds with Webpack

Peter Weber

If you’ve done any javascript application development recently, you’ve probably come across Webpack. It’s a powerful javascript bundler that compiles all of your assets into small, production ready files. One feature I really like is the ability to easily configure separate builds based on environment variables. Webpack makes it easy to deploy multiple environments i.e. development, test and production with only the code and data specifically needed for each.

The Problem

First, unnecessary bloat: libraries, such as Vue and React, have built-in debugging tools that are essential for development but add extraneous code to a live application. Left unmanaged, code bloat can cause significant performance issues in production.

Second, test data: your development environment will likely include mock data that you don’t want in production output.

The Solution

Webpack is an increasingly popular javascript bundler with a suite of features particularly well-suited for working with Vue and React. You may be familiar with “bundling”, which packages your separate javascript files and dependencies into a single file. Webpack also accepts an “environment” variable that can be used to compile distinct application builds from entirely separate config files. This way your production build is light and lean while your development build contains all the tools and local settings you need to work.

Example

In the rest of this post I’ll run through how to configure Webpack to manage builds from separate config files. Webpack expects to see a webpack.config.js file in your app directory.

The module.exports value, in a simple Webpack config file, is usually set to a config object. However, in order to use the env variable, we set module.exports to a function. This function accepts the environment as a parameter and returns a config object.

module.exports =(env) => {
 return require(`./webpack.${env}.js`)
}

Now we can specify the environment when we run Webpack through the command line.

webpack --env dev

Once we have access to the environment variable, we can add logic to our config based on it. In this case, our config function will look for an environment specific config file, such as:

webpack.dev.js or webpack.prod.js.

The prod config can have additional plugins specified, like uglify which will compress and minify your js.

NPM Scripts

In your package.json, you can add scripts that include the correct syntax, so you can quickly type a command without having to remember all the arguments.

"scripts": {
   "build:dev": "webpack --env dev --progress --profile --colors",
   "build:prod": "webpack --env prod --progress --profile --colors",
 },

Now instead of typing

webpack --env dev --progress --profile --colors

You can just type: npm run build:dev

Or if you’re using yarn: yarn build:dev

Externalized Dependencies

If you’re using a library like Vue, you may require it as a dev dependency in your package.json, which means the entire development library will be in node_modules. Vue in particular has excellent debugging tools to help you find problems and step through your application.

However, in your webpack.prod.js you could make this external by adding:

externals: {
   'vue': 'Vue'
 }

And in your application or website include the production library with

<script src=”https://vuejs.org/js/vue.min.js”></script>

The production version is minified and is a much smaller file. Webpack magic will see the Vue dependency in package.json but instead of building it into your production ready file, it will just expect your page to load it separately.

You can verify this by watching the network panel in your browser inspector; your js will be reduced in filesize and you’ll see the Vue library load from a cdn.

Webpack Dev Server

Another advantage of this “split” is when running a test version of your app that focuses on just the component you’re working on.

Webpack has a dev server which can load a simple html page. You only need to add the element your app loads into and a script tag pointing to your dev javascript file.

Require webpack-dev-server with

npm i -D webpack-dev-server Or yarn add webpack-dev-server --dev

Then add to your scripts in package.json

"scripts": {
  "dev": "webpack-dev-server --inline --hot --no-info --env dev",
  ...other scripts...
}

Note the --env dev which tells Webpack to use your dev config.

Now running: npm run dev or yarn dev

...will start up a local server pointing to your index.html file.

In your index.html file make sure to have the element your app will load itself into, and add a script tag for wherever your dev js is being built to- specified in webpack.dev.js in the output property:

output: {
   path: path.resolve(__dirname, 'dist'),
   publicPath: '/dist/',
   filename: 'build-dev.js'
 },

Your script tag should point to that filename.

<script src="dist/build-dev.js"></script>

This file doesn’t even have to exist on your computer. Webpack will build it and load it into memory for the dev server. Any js changes will trigger a hot reload so you can immediately see your progress.

Mocking Development Data

The env variable has other uses, too. Your development environment may not match production, or in this example, depends on data provided by another website. When running the webpack-dev-server you may not have access to this external data and want to mock an example.

For this example application, I wanted to build a list of content from Drupal which I’ll add to drupalSettings, a global Drupal uses to pass data from the server to the client. I’ll use this to then load data into a small menu component that I’ll build with Vue.

While working in Vue I don’t want to have to depend on Drupal, or have to refresh a whole page every time I make a small change to just my javascript.

The production application should use the external dependency though, and should not include any of your mock data.

If all your data is loaded in a file called app-data.js, at the simplest it could be something like

export const data = drupalSettings.yourApp.appData;

This sets a variable to to an object that you’ve loaded into Drupal’s global javascript.

Then in your application load that object with:

import {data} from './../api/app-data';

This of course will break if there is no drupalSettings object.

Just like the config file, any other dependency in your build could be split from a single file into environment specific files.

Webpack can replace any string during a build, you could add a unique string like APP_TARGET and tell it to replace that with the current env variable.

In both your Webpack configs (dev and prod) add the NormalModuleReplacementPlugin to plugins:

plugins: [
   new webpack.NormalModuleReplacementPlugin(/(.*)-APP_TARGET(\.*)/, function(resource) {
     resource.request = resource.request.replace(/-APP_TARGET/, `-${env}`);
   }),
]

This tells Webpack to replace any occurrences of the string “APP_TARGET” with that env value.

Now replace your app-data.js with 2 new files, app-data-dev.js and app-data-prod.js

In the prod file use the string that points to drupalSettings since this will only be used on your site.

export const data = drupalSettings.yourApp.appData;

In the dev file return a stub object with dummy data.

export const data = { ...your stub data here... }

Tip: If you already added some data to drupalSettings, you can load it up from your browser console, stringify it, and then paste that right into your file to ensure you’re testing against the same data structures your site will use.

Type this in your console:

JSON.stringify(drupalSettings.yourApp.appData, null, '\t')

This will format the object and print it to your console. To save even more time, wrap that in a “copy” function to add it directly to your clipboard.

copy(JSON.stringify(drupalSettings.yourApp.appData, null, '\t'));

With your 2 new app-data files, in your app you can change the import statement in your application:

import {data} from './../api/app-data-APP_TARGET';

Now when Webpack builds your dev or prod environment it will replace that string and import the appropriate file.

What’s next?

There is a lot more you can do with this, for example the --env variable can be an object instead, so you could pass more config inside it for additional build settings. You could also split some of your duplicate config into something like a webpack.common.js so that you have more generic boilerplate for your next project.

Webpack gets better every release, but the documentation can be difficult to sort through, especially keeping track of different versions and the changes each one brings. It can be confusing but chances are there is already a solution to any problem you come across!


About the Author

Peter has over 10 years of professional experience as a web developer, and has worked across the field from content creation to programming to graphic design. Originally educated in art and multimedia, he has migrated to development over the course of his career. He is glad to be a part of the overwhelming activity and energy in the front-end developer community.

Read More