Webpack from Nothing

Auto-reload Code When You Change It

We've got all the right tools in places to build, manage, test, deploy, and debug our application. But it's all really really slow. We can make this faster by using Webpack's dev server. But first, a word on workflow.

A Reliable Development Workflow

We've been talking about JavaScript and CSS, but at the end of the day, you are building a web application, and that means there is some logic and code that must exist. This logic and code is likely the biggest source of complexity in your application. While CSS and UI is certainly complex, it pales in comparison to what is typically required to build an actual web application.

Because of this, your default workflow should be a test-driven one. You should not run your application to verify that its underlying logic is working correctly—you should run a test of that logic.

Granted, our markdown previewer doesn't have a lot of such logic, but it is important to understand that a “tweak and reload” style of development is the slowest style of development.

Unfortunately, for UI work, there isn't a great substitute, so let's see how Webpack helps.

Auto-reloading On Changes

Webpack is super slow:

> time yarn webpack
yarn run v1.3.2
$ webpack $npm_package_config_webpack_args
Hash: 0b352f59b1ba34cf0be2
Version: webpack 3.8.1
Time: 1923ms
     Asset       Size  Chunks                    Chunk Names
 bundle.js     205 kB       0  [emitted]         main
styles.css     379 kB       0  [emitted]  [big]  main
index.html  833 bytes          [emitted]         
   [0] ./js/index.js 421 bytes {0} [built]
   [2] ./css/styles.css 41 bytes {0} [built]
   [3] ./js/markdownPreviewer.js 381 bytes {0} [built]
   [7] (webpack)/buildin/global.js 488 bytes {0} [built]
    + 8 hidden modules
Child html-webpack-plugin for "index.html":
     1 asset
       [0] ./node_modules/html-webpack-plugin/lib/loader.js!./html/index.html 1.12 kB {0} [built]
       [2] (webpack)/buildin/global.js 488 bytes {0} [built]
       [3] (webpack)/buildin/module.js 495 bytes {0} [built]
        + 1 hidden module
Child extract-text-webpack-plugin node_modules/extract-text-webpack-plugin/dist node_modules/css-loader/index.js??ref--0-1!css/styles.css:
       [0] ./node_modules/css-loader?{"sourceMap":true}!./css/styles.css 507 bytes {0} [built]
        + 1 hidden module
Child extract-text-webpack-plugin node_modules/extract-text-webpack-plugin/dist node_modules/css-loader/index.js??ref--0-1!node_modules/tachyons/css/tachyons.css:
       2 modules
Done in 2.86s.
3.28 real         3.27 user         0.27 sys

When designing a UI, you need to change markup, CSS, and JavaScript. It's often not possible to assure that JavaScript is providing the UI interactions you want without trying it, and that's lots of small changes that you reload in the browser. Having to wait 3+ seconds each time sucks.

We can make this a bit better by using webpack-dev-server, which we can install with yarn:

> yarn add webpack-dev-server -D
yarn add v1.3.2
[1/4] Resolving packages...
[2/4] Fetching packages...
[3/4] Linking dependencies...
[4/4] Building fresh packages...
success Saved lockfile.
success Saved 113 new dependencies.
├─ accepts@1.3.4
├─ ansi-html@0.0.7
├─ array-find-index@1.0.2
├─ array-flatten@1.1.1
├─ array-includes@3.0.3
├─ array-union@1.0.2
├─ array-uniq@1.0.3
├─ batch@0.6.1
├─ bonjour@3.5.0
├─ buffer-indexof@1.1.1
├─ camelcase-keys@2.1.0
├─ camelcase@3.0.0
├─ compressible@2.0.12
├─ compression@1.7.1
├─ connect-history-api-fallback@1.4.0
├─ content-disposition@0.5.2
├─ cookie-signature@1.0.6
├─ currently-unhandled@0.4.1
├─ deep-equal@1.0.1
├─ define-properties@1.1.2
├─ del@3.0.0
├─ destroy@1.0.4
├─ detect-node@2.0.3
├─ dns-equal@1.0.0
├─ dns-packet@1.2.2
├─ dns-txt@2.0.2
├─ es-abstract@1.9.0
├─ es-to-primitive@1.1.1
├─ etag@1.8.1
├─ eventsource@0.1.6
├─ express@4.16.2
├─ faye-websocket@0.10.0
├─ foreach@2.0.5
├─ forwarded@0.1.2
├─ fresh@0.5.2
├─ get-stdin@4.0.1
├─ globby@6.1.0
├─ handle-thing@1.2.5
├─ hpack.js@2.1.6
├─ html-entities@1.2.1
├─ http-deceiver@1.2.7
├─ http-parser-js@0.4.9
├─ http-proxy-middleware@0.17.4
├─ import-local@0.1.1
├─ indent-string@2.1.0
├─ internal-ip@1.2.0
├─ ip@1.1.5
├─ ipaddr.js@1.5.2
├─ is-callable@1.1.3
├─ is-date-object@1.0.1
├─ is-finite@1.0.2
├─ is-path-cwd@1.0.0
├─ is-path-in-cwd@1.0.0
├─ is-path-inside@1.0.0
├─ is-regex@1.0.4
├─ is-symbol@1.0.1
├─ is-utf8@0.2.1
├─ is-wsl@1.1.0
├─ killable@1.0.0
├─ load-json-file@1.1.0
├─ loglevel@1.5.1
├─ loud-rejection@1.6.0
├─ map-obj@1.0.1
├─ meow@3.7.0
├─ merge-descriptors@1.0.1
├─ methods@1.1.2
├─ multicast-dns-service-types@1.1.0
├─ multicast-dns@6.1.1
├─ node-forge@0.6.33
├─ object-keys@1.0.11
├─ obuf@1.1.1
├─ on-headers@1.0.1
├─ opn@5.1.0
├─ original@1.0.0
├─ os-locale@1.4.0
├─ p-map@1.2.0
├─ path-exists@2.1.0
├─ path-is-inside@1.0.2
├─ path-to-regexp@0.1.7
├─ path-type@1.1.0
├─ pkg-dir@2.0.0
├─ portfinder@1.0.13
├─ proxy-addr@2.0.2
├─ querystringify@1.0.0
├─ read-pkg-up@1.0.1
├─ read-pkg@1.1.0
├─ redent@1.0.0
├─ repeating@2.0.1
├─ resolve-cwd@2.0.0
├─ resolve-from@3.0.0
├─ select-hose@2.0.0
├─ selfsigned@1.10.1
├─ send@0.16.1
├─ serve-index@1.9.1
├─ serve-static@1.13.1
├─ setprototypeof@1.1.0
├─ sockjs-client@1.1.4
├─ sockjs@0.3.18
├─ spdy-transport@2.0.20
├─ spdy@3.4.7
├─ strip-bom@2.0.0
├─ strip-indent@1.0.1
├─ thunky@0.1.0
├─ trim-newlines@1.0.0
├─ url-parse@1.2.0
├─ vary@1.1.2
├─ wbuf@1.7.2
├─ webpack-dev-server@2.9.4
├─ websocket-driver@0.7.0
├─ websocket-extensions@0.1.2
├─ which-module@1.0.0
├─ yargs-parser@4.2.1
└─ yargs@6.6.0
Done in 5.17s.
warning " > karma-jasmine@1.1.0" has unmet peer dependency "jasmine-core@*".

Once this is done, run it with the --open flag, and your application will pop up in your favorite browser:

> $(yarn bin)/webpack-dev-server --open

This will compile your app and load it, so it will take the same 4+ seconds as before.

Next, open up css/styles.css in your editor, and arrange your windows so that you can see both your editor and your browser. Make a change to the CSS, and viola, your browser refreshes with that change. Repeat with html/index.html and js/index.js. Not too bad.

The refresh isn't as fast as we'd like, but it's still not too bad. You could easily open up your code editor on one side of the screen, and your browser on the other, and get to work.

Part of the reason is that we split our code into dev and production. To re-run Webpack in dev, we aren't minifying, hashing, or generating a slow source map. That helps.

What about tests?

Auto-reloading in Tests

When we learned about Karma, we gave it the --single-run flag. We later saw that omitting this starts up a server you can use to run your tests in any browser. This server also watches for changes in our code and tests and re-runs them automatically.

> $(yarn bin)/karma start spec/karma.conf.js
Hash: e0bdc8cc97632b01d813
Version: webpack 3.0.0
Time: 40ms
webpack: Compiled successfully.
webpack: Compiling...
webpack: wait until bundle finished:
Hash: 45c62a5c6505ba4e17fb
Version: webpack 3.0.0
Time: 176ms
                  Asset     Size  Chunks             Chunk Names
         canary.spec.js   2.6 kB       0  [emitted]  canary.spec.js
markdownPreview.spec.js  78.8 kB       1  [emitted]  markdownPreview.spec.js
   [0] ./spec/canary.spec.js 107 bytes {0} [built]
   [1] ./spec/markdownPreview.spec.js 1.23 kB {1} [built]
   [2] ./js/markdownPreviewer.js 381 bytes {1} [built]
   [3] ./node_modules/markdown/lib/index.js 143 bytes {1} [built]
   [4] ./node_modules/markdown/lib/markdown.js 51 kB {1} [built]
   [5] ./node_modules/util/util.js 15.6 kB {1} [built]
   [6] (webpack)/buildin/global.js 509 bytes {1} [built]
   [7] ./node_modules/process/browser.js 5.42 kB {1} [built]
   [8] ./node_modules/util/support/isBufferBrowser.js 203 bytes {1} [built]
   [9] ./node_modules/util/node_modules/inherits/inherits_browser.js 672 bytes {1} [built]
webpack: Compiled successfully.
25 06 2017 11:32:38.096:WARN [karma]: No captured browser, open http://localhost:9876/
25 06 2017 11:32:38.103:INFO [karma]: Karma v1.7.0 server started at http://0.0.0.0:9876/
25 06 2017 11:32:38.104:INFO [launcher]: Launching browser PhantomJS with unlimited concurrency
25 06 2017 11:32:38.108:INFO [launcher]: Starting browser PhantomJS
25 06 2017 11:32:38.928:INFO [PhantomJS 2.1.1 (Mac OS X 0.0.0)]: Connected on socket l1dBRG0VGQoUpBORAAAA with id 2070764
PhantomJS 2.1.1 (Mac OS X 0.0.0): Executed 2 of 2 SUCCESS (0.012 secs / 0.008 secs)

Your tests might still be broken from the last chapter, so, without stopping Karma, fix the tests. They should automatically re-run without doing anything. Nice!

What this means is that by running Karma all the time, you can work quickly with a TDD flow, and when you need to switch to in-browser design or tweaking, running the Webpack dev server lets you work quickly there, too.

There's one last thing we need to look into, and that's the ability to use a better language than JavaScript Since Webpack is essentially compiling our JavaScript and CSS, it stands to reason that if we wanted to use something like TypeScript or ES2015, it should be able to handle that.