Intro
This article is a continuation of the Stages of Learning Webpack series. The repository containing the source code has been updated since the last article to include the latest versions of all dependencies as well as some configuration improvements (read: I might've made a lot of goofs the first time around).
Step 2, Cont.
At some point, you'll need to debug your application. Most modern browsers provide intuitive developer tools which can assist in debugging and optimizing your application.
In your text editor, change the first line in src/app/sayHello.js
to the following:
export default name => alet(`Hello ${name}`);
We've placed an intentional error in our application by misspelling alert
as alet
. From the command line, run npm run build
/yarn build
. The build should still succeed; Webpack is not responsible for maintaining the accuracy of our JavaScript code. Open the index.html
file in your browser and open your browser's developer tools. There will be an error message along the lines of ReferenceError: alet is not defined
.
Clicking on the filename to the right of the error message will navigate us to the line & column of the file in which the error occurred. Since our index.html
file is using the generated Webpack bundle to load our JavaScript code, we'll be pointed to the line in the generated Webpack bundle at which the error occurred. From there, it's up to us to work our way backwards from the point of error in the bundle to the point of error in our actual source code.
If that's what's involved in debugging, then I'm not debugging any more. There must be an easier way. We can do better. We have the technology.
Step 3
Branch: sourceMaps
Let's start by looking at the differences between this branch (sourceMaps) and the previous (init):
Changes to webpack.config.js
:
We've added a new key to the Webpack configuration object; the devtool
key. The value associated with this key depends on the value of the argument env
.
module.exports = env => ({
devtool: (env && env.production) ? "source-map" : "cheap-eval-source-map",
entry: "./src/index.js",
output: { filename: "./build/bundle.js" },
resolve: { extensions: [".js"] }
});
We can rewrite the file in ES5 as:
module.exports = function(env) {
var devtool;
if (env !== undefined && env.production === true) {
devtool = "source-map";
} else {
devtool = "cheap-eval-source-map";
};
return {
devtool: devtool,
entry: "./src/index.js",
output: { filename: "./build/bundle.js" },
resolve: { extensions: [".js"] }
};
};
First, we declare a variable devtool
. Then, if the env
argument isn't undefined
and env
is an object with a key/value pair { production: true }
, then define the value of devtool
as a string "source-map"
. Otherwise, define devtool
as "cheap-eval-source-map"
. The meaning associated with these values will be explained later; for now, I want to be clear that all we've done is create a variable, and define that variable as a string. The value of that string is dependent on a conditional statement (the if/else block).
Finally, we return an object with a set of key/value pairs that Webpack can use to create our bundle. The entry
, output
, and resolve
key/value pairs have been carried over from the init
branch.
Changes to package.json
:
We've updated the scripts
section of the package.json
file.
Before:
/*...*/
"scripts": {
"build": "webpack"
},
/*...*/
After:
/*...*/
"scripts": {
"dev": "webpack",
"prod": "webpack --env.production"
},
/*...*/
The name of the command which calls Webpack has been changed from build
to dev
. The naming convention implies that this will create a development version of the bundle, and this is true. We're not having Webpack run any sort of optimization when it creates the bundle. Our configuration just says 'take this entry file (src/index.js
) and every file it imports, bundle them all together, and output that bundle as a file (./build/bundle.js
).
There is also a new key, prod
. Again, the naming convention implies that this will create a production version of the bundle. It doesn't. Yet. But it will! Right now, the only difference between the prod
script and the dev
script is that we're now passing an argument to the exported function in webpack.config.js
as the env
argument, which the function then uses to create and return the Webpack configuration object. To see this in action, you can place a console.log(env)
statement inside the function exported from webpack.config.js
.
// webpack.config.js
module.exports = env => {
console.log(env);
return {
devtool: env && env.production ? "source-map" : "cheap-eval-source-map",
entry: "./src/index.js",
output: { filename: "./build/bundle.js" },
resolve: { extensions: [".js"] }
}
};
From the command line, run the command npm run dev
/yarn dev
.
> webpack
undefined
Hash: 9d81a1b766e4629aec0c
Version: webpack 2.6.1
Time: 82ms
Asset Size Chunks Chunk Names
./build/bundle.js 5.75 kB 0 [emitted] main
[0] ./src/app/sayHello.js 233 bytes {0} [built]
[1] ./src/index.js 453 bytes {0} [built]
That undefined
right after > webpack
is our console.log(env)
statement. It's undefined because we didn't pass any additional arguments to Webpack in our dev
command. Now, let's run the npm run prod
/yarn prod
command from the command line.
> webpack --env.production
{ production: true }
Hash: cbc8e27e9f167ab0bc36
Version: webpack 2.6.1
Time: 90ms
Asset Size Chunks Chunk Names
./build/bundle.js 3.79 kB 0 [emitted] main
./build/bundle.js.map 3.81 kB 0 [emitted] main
[0] ./src/app/sayHello.js 233 bytes {0} [built]
[1] ./src/index.js 453 bytes {0} [built]
Instead of seeing undefined
, we're seeing an object with one key/value pair { production: true }
. These values match up with the conditional statement in our Webpack configuration; our conditional statement ensures that the argument env
isn't undefined, and that it is an object with a key/value pair { production: true }
. You might have noticed that the generated bundles from the commands are different as well. The bundle generated with the dev
command is larger than bundle generated by prod
, however the prod
command generated an additional file bundle.js.map
.
Open the file src/app/sayHello.js
. Since this is a different branch of the Git repository, the error we previously placed in this file might not carry over if the changes were made in the init
branch. If that's the case, change the first line so that the alert
call is misspelled as alet
. Save your changes, then run npm run dev/yarn dev
from the command line again. Open index.html
in your browser, then open the browser's devtools. You should have an error in the console stating alet is not defined
.
If the console claims that this error is being generated in the index.html
file, refresh the page. You should see something along the lines of:
ReferenceError: alet is not defined sayHello.js?7eb0:1
Clicking on this error should take you to the line & file in which the error occurred, but you'll notice that the entire line is highlighted as an error. In this case, that's not entirely inaccurate. But let's say we change the src/app/sayHello.js
file around again. This time, we'll change the reference to name
inside of the alert
call to be namen
:
export default name => alert(`Hello ${namen}`);
export const donut = "I WANT YOUR DONUTS";
/**
* Same code, ES5 style:
*
* function sayName(name){
* return alert('Hello ' + name);
* }
*
* export default sayName;
*
*/
Run npm run dev/yarn dev
from the command line again, and refresh the index.html
file that's open in your browser. The console in your devtools should display a similar error message; namen is not defined
. Clicking on the error message will, again, take us to the line in which the error occurred.
Now, run npm run prod
/yarn prod
from the command line, and refresh the index.html
file in your browser. Open your devtools and look at the error in your console, the filename is now just sayHello.js
. Clicking on the error navigates us not only to the file & line in which the error occurred, but also the column in which it occurred. The error underline is more specific as well; it begins at namen
as opposed to underlining the whole first line.
And that's the difference between the two commands; the accuracy of the source maps they generate. The reason we use a less accurate version of source maps for development purposes is because they are faster to generate than to have Webpack generate full source map files each time we create a build. You can learn about the different options for source mapping with Webpack here: Webpack Devtool Configuration.
Step 4
Branch: loader
Notice that the generated bundles maintain all ES2015 syntax used in the source files; let
& const
, arrow functions, newer object literal syntax, etc. If we tried to run our application in an older browser which didn't have support for these features, the application would fail. This is where we'd usually take advantage of a transpiler such as Babel, TypeScript, CoffeeScript, etc. to run through our code and translate it to a version with better cross-browser support. The loader branch covers how to integrate TypeScript into our Webpack build process in order to transpile our application code down to ES3 syntax. Note that we don't introduce any TypeScript-specific features; I even leave the files as .js files. We'll be using TypeScript as an ESNext --> ES3 transpiler.
Strap in folks; this one's gonna be bumpy.
Dependencies
Looking at the package.json
file, we've added two new developer dependencies.
- TypeScript: As stated earlier, we'll use TypeScript as our transpiler.
- TS-Loader: Loaders allow Webpack to understand more than JavaScript. In this case, TS-Loader allows Webpack to use TypeScript to load TypeScript (and JavaScript) files and transpile them based on your TypeScript configuration before generating a browser-friendly bundle.
To install these dependencies, run npm install
from the command line. NPM should read the package.json
file and install the dependencies as listed. In general, to install additional developer dependencies, you can run npm i -D <package-name>
, where <package-name>
is the package you want to install, ie: npm i -D typescript
. The -D flag tells NPM to save the installed package as a developer dependency.
The prod
command has been updated as well; it now includes the flag -p
. The -p
flag is an option that can be provided to the Webpack CLI (command line interface, the tool that NPM calls on when a script
in the package.json
file uses webpack
) which provides optimizations for a production environment. We'll take a deeper look at this shortly.
TypeScript Configuration
The tsconfig.json
file provides information for TypeScript to utilize when transpiling our code.
{
"compilerOptions": {
"allowJs": true,
"module": "es2015",
"target": "es3",
"sourceMap": true,
"strict": true
},
"include": [
"./src/"
],
"exclude": [
"node_modules/",
"./build/"
]
}
This configuration object tells TypeScript a few things:
- TypeScript is generally used to transpile TypeScript files (
.ts
) into JavaScript. By settingallowJs
totrue
, we're allowing TypeScript to transpile .js files. - TypeScript is capable of transpiling JavaScript to work with a variety of module systems. We're telling TypeScript to use the ES2015 module system because Webpack is able to apply some pretty nifty optimizations when applications are created using this variation.
- We can target most JavaScript versions from ES3 to ESNext. Given that we're aiming for BETTER browser support, not horrendously worse, we go with ES3.
- Generate source maps for each transpiled file.
- Use all the
strict
type-checking features that TypeScript offers.
Webpack Configuration Updates
module: {
devtool: env && env.production ? "source-map" : "inline-source-map",
/* ... */
rules: [
{
test: /\.js(x)?/,
loader: "ts-loader",
options: {
transpileOnly: true,
entryFileIsJs: true
}
}
]
}
We've introduced a new key to the Webpack configuration object; module
. The module
section provides information to Webpack regarding how to work with certain files that are utilized throughout the application. We've provided one rule, which can be read as such:
When Webpack comes across a file ending in
.js
or.jsx
, it will first pass the files to TS-Loader before generating a the requested bundle. TS-Loader is responsible only for transpiling the application; we are not using any of the type-checking features provided by TypeScript.
The type of source map used for development environments has been changed from "cheap-eval-source-map" to "inline-source-map". The differences between these two options are covered in the Webpack documentation: here: Webpack Devtool Configuration.
Run npm run dev
/yarn dev
from the command line and open the index.html
file in your browser. Everything should work as expected. Look at lines 73-105 in the generated bundle:
"use strict";
/* unused harmony export donut */
/* harmony default export */ __webpack_exports__["a"] = (function (name) { return alert("Hello " + name); });;
var donut = "I WANT YOUR DONUTS";
/**
* Same code, ES5 style:
*
* function sayName(name){
* return alert('Hello ' + name);
* }
*
* export default sayName;
*
*/
/***/ }),
/* 1 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__app_sayHello__ = __webpack_require__(0);
// Import whatever the default export is from /app/sayHello
// and refer to it in this file as 'Hello'
var name = "Nitish";
// Reference to the <div id="root"> element in
var root = document.getElementById("root");
// Call the function that was imported from /app/sayHello, passing in
// `const name` that was created on line 5.
__webpack_require__.i(__WEBPACK_IMPORTED_MODULE_0__app_sayHello__["a" /* default */])(name);
root.textContent = "Helllloooo " + name + "!";
All const
and let
declarations have been converted to var
. The template strings used in the alert message and for root.textContent
have been replaced with string concatenation. Our bundle was created using the transpiled code generated by TypeScript.
If we remember from earlier, src/app/sayHello.js
exports two items; a function as a default export, and a constant donut
as a named export.
export default name => alert(`Hello ${name}`);
export const donut = "I WANT YOUR DONUTS";
The second export isn't used anywhere in the application, but it's still included in the bundle. However, if we run npm run prod
/yarn prod
and take a look at our bundle then...
It's a hot mess! Here's a (nicer, formatted) look at the bundle:
!(function(t) {
function e(r) {
if (n[r]) return n[r].exports;
var o = (n[r] = { i: r, l: !1, exports: {} });
return t[r].call(o.exports, o, o.exports, e), (o.l = !0), o.exports;
}
var n = {};
(e.m = t), (e.c = n), (e.i = function(t) {
return t;
}), (e.d = function(t, n, r) {
e.o(t, n) ||
Object.defineProperty(t, n, { configurable: !1, enumerable: !0, get: r });
}), (e.n = function(t) {
var n = t && t.__esModule
? function() {
return t.default;
}
: function() {
return t;
};
return e.d(n, "a", n), n;
}), (e.o = function(t, e) {
return Object.prototype.hasOwnProperty.call(t, e);
}), (e.p = ""), e((e.s = 1));
})([
function(t, e, n) {
"use strict";
e.a = function(t) {
return alert("Hello " + t);
};
},
function(t, e, n) {
"use strict";
Object.defineProperty(e, "__esModule", { value: !0 });
var r = n(0), o = document.getElementById("root");
n.i(r.a)("Nitish"), (o.textContent = "Helllloooo Nitish!");
}
]);
//# sourceMappingURL=bundle.js.map
It's still a hot mess! There isn't much of a need to manually parse through this; it's 38 lines of IIFE goodness, so it's doable, but there's no obligation and it won't assist with the rest of this guide. What I'm trying to show here is that the generated production bundle has no reference to the line const donut = "I WANT YOUR DONUTS!";
. It's completely dropped from the bundle. Along with the minification, uglification, and handful of other out-of-the-box production optimizations Webpack is capable of implementing when provided the -p
flag, tree-shaking is part of that list. I didn't have to do anything to enable tree-shaking; it Just Works™.
Excellent! We're transpiling our ES2015+ code down to ES3, removing any unused code along the way, and generating a production(ish)-quality bundle that can be loaded by most modern browsers with errors and warnings pointing back to our source code for simplified debugging.
Step 5
Branch: plugin
Plugins do exactly what they say on the tin; they plug into the build process to introduce extra functionality. In this example, we'll get introduced to HTMLWebpackPlugin, a plugin for generating HTML documents which can serve our Webpack bundles.
As it stands, we created an HTML file which points to the expected bundle. In simple situations, a setup like this would work fine. As the application grows, the bundle could get split into more than one file, The filenames might be randomly generated, etc. If we were to try and manually maintain the list of files that need to be loaded into our HTML file...well, we're kind of back to square A, right? We'll use HTMLWebpackPlugin to automate the process of loading our bundles into our HTML document.
File Changes
- Introduced a new developer dependency to the
package.json
file;HTMLWebpackPlugin
. Make sure to runnpm install
/yarn
when you've switched to this branch to get the necessary dependencies.
"devDependencies": {
"html-webpack-plugin": "^2.28.0",
"ts-loader": "^2.1.0",
"typescript": "^2.3.4",
"webpack": "^2.6.1"
}
The
index.html
file no longer loads thebuild/bundle.js
file.webpack.config.js
has been updated to include a CommonJS style import statement (const HTMLWebpackPlugin = require("html-webpack-plugin");
) at the top of the file, and has a new section,plugins
:
//webpack.config.js
const HTMLWebpackPlugin = require("html-webpack-plugin");
module.exports = env => {
/*...*/
plugins: [
new HTMLWebpackPlugin({
filename: "./build/index.html",
inject: "body",
template: "./index.html"
})
]
}
We're telling Webpack that we'll be using HTMLWebpackPlugin to generate an HTML file named index.html
inside of the build
folder. HTMLWebpackPlugin is to take any generated bundles and inject
them into the body of the HTML file in script tags. It will use the existing index.html
found in our application root as a template.
If we call on npm run dev
/yarn dev
or npm run prod
/yard prod
, we should see something similar to:
$ npm run dev
> webpack -p --env.production
ts-loader: Using typescript@2.3.4 and /Projects/dev_to/webpack_configs/example/tsconfig.json
Hash: 693b4a366ee89bdb9cde
Version: webpack 2.6.1
Time: 2233ms
Asset Size Chunks Chunk Names
./build/bundle.js 8.96 kB 0 [emitted] main
./build/index.html 352 bytes [emitted]
Based on the configuration provided, Webpack generated the requested bundle along with an index.html
file. The generated index.html
file looks very similar to our existing template, but carries a reference to the generated Webpack bundle inside of the document body.
Open the new index.html file (./build/index.html
) in your browser to make sure everything works as expected.
Now stand back, rejoice in your work, and soak it all in. You're on your way to Webpacking the world, amigos.
Top comments (0)