Webpack notes

This is a notes based on the official Webpack document and the excellent book SurviveJs Webpack.

1. Introduction

Webpack is a module bundler. It starts from an entry point and build all dependencies into a bundle or, when define split points, a small number of bundles. A module can be specified as ES2015 import, Node.js require(). Through loaders, a module can be specified using css/sass/less @import statement or a html url.

Webpack only understands JavaScript files but treat every source file (.css, .html, .scss, .jpg, etc) as a module that is transformed by a corresponding loader on a per-file basis. Loaders are specified in module.rules. Loaders can be chained in a bottom to top and right to left order.

Plugins intercept runtime events and perform actions and custom functionality on chunks of bundled modules at different stages of the buildling process. Webpack comes with some built-in plugins. A webpack plugin is a JavaScript function whose apply property is called by the webpack compiler.

Webpack is configuration driven and its configuration file is a a standard Node.js module. Webpack resolves module and loader using file type, file path and wheren it was imported/used in a project. Webpack let you control how to treat different assets such as inline separate files or put it into its scope.

As the webpack compiler resolves and builds an application, it keeps module dependency data as the “Manifest”. After every module has been evaluated, webpack creates output that includes a bootstrap script and the manifiest. The manifest can be generated as a separate file.

Hot Module Replacement(HMR) reloads updated modules without a full reload. webpack-dev-server uses in-memory compilation and provides an easy to use development server with fast live reloading.

Webpack supports code spliting and dynamic loading. It can inject a hash to file name.

2. The Basic Development Process

For a basic build, you need to config the entry and output.

The value of the entry could be a folder or a filename. If it is a folder, the default index.js will be used as the entry file.

All output related paths are resolved against the output.path field. By default, the file specified by output.filename is the bundle file generated by putting all JS files together. All JS modules are put into an array of modules in the bundle file. The entry module is exported from the bundle file.

You can use placeholders such as [name], [id], [chunkhash], or [hash] in the output filename.

The html-webpack-plugin helps to create an HTML file to serve the generated JS bundle file. It can sync with the generated bundle file when a hash is used in filename.

Using webpack --watch to rebuild when there is a file change. webpack-dev-server (WDS) goes future in that it stores results in memory. It support Hot Module Replacement (HMR) to patch the browser without a full refresh. HMR requires client-side code to work with WDS interfaces. There are plugins to write emitted files to the file system. WDS can be configured in the devServer section using parameters such as historyApiFallback, stats, host, port and the aggregateTimeout and poll of the watchOptions.

Alternatively, you can use Express server and a middleware such as webpack-dev-middleware to serve the output.

The friendly-errors-webpack-plugin gives better status report. eslint-loader lints JS code.

To manage configuration for different environment, it’s a good idea to have multiple configuraiton files. Files can be shared and merged using webpack-merge plugin.

3. Styling

Webpack use loaders and plugins to handle style files. css-loader goes through @import and url() for the matched files to load internal resources. It skips external resources. css-loader injects styling via a style element. Use ExtractTextPlugin to generate a separate CSS file.

To use the local scope of CSS modules, set options: { modules: true } for css-loader.

To support Sass style, use sass-loader. It depends on node-sass package. stylus-laoder and yeticss for stylus.

To load files from packages in node_modules, use a tilde char ~ prefix. For example, @import "~bootstrap/less/bootstrap";. bootstrap-loader is another option to use Bootstrap.

To avoid **Flash of Unstyled Content (FOUC), put styles into a separate file. ExtractTextPlugin` is ued to extract styles, usually in production. It doesn’t support HMR.

It’s also possible to bundle sytles using a style entry and a glob file pattern.

Autoprefixing writes vendor prefiexes and can be enabled through the autoprefixer PostCSS plugin.

PurifyCSS and uncss are tools to remove unused styles. Stylelint and CSSLint are tools to lint CSS.

4. Loading Assets

Loader evaluating order is right-to-left, bottom-to-top. Use enforce: 'pre', // or 'post' to change the order.

There are several ways to match files: test, include, exclude, resource: /inline/, resource request issuer: /bar.js/, resourcePath: /inline/ and resourceQuery: /inline/. Boolean based fields can be used to constrain matchers: not, and, or.

loader-runner is used by webpack internally and can be used in solation to understand loader behavior. inspect-loader can be used to inspect what’s being passed between loaders.

url-loader inlines the assets withing JS. It comes with a limit option that switches big assets to file-loader that emits images and returns paths. There are images optimization and size tuning loaders. Set output.publicPath when use sourceMap with image and css loaders.

A simple configuration to load fonts including font awesome is as the following:

{
    test: /\.(ttf|eot|woff|woff2)$/,
    use: {
        loader: 'file-loader',
        options: {
            name: 'fonts/[name].[ext]',
        },
    }
},

Loading fonts should consider the supported browsers.

5. Building

5.1. Source Maps

Source maps provide a mapping between the original source code and the transformed code. Both JS and style code need source maps. Webpack allows inline source maps, separate source maps or hidden source maps that only give stack trace information.

Use devtool property to config source map. eval-source-map for dev and source-map for production are common choices. When use UglifyJsPlugin, enable sourceMap: true for the plugin to create proper source map.

5.2. Bundle Splitting

Bundle splitting enables cache. Use hashed filename to avoid cache invalidation. CommonsChunkPlugin allows extraction of common chunks into separate chunks. Use AggressiveSplittingPlugin and AggressiveMergingPlugin to control the granularity of the chunks.

Webpack has three chunk types: entry chunk, normal chunk and initial chunk. The entry chunks contain webpack runtime and modules loaded. Normal chunks don’t have webpack runtime can can be loaded dynamically. The initial chunks are used in initial loading and are generated by CommonsChunkPlugin.

5.3. Code Splitting

Code splitting allows PRPL pattern: push critical resource for the initial routte, render initial router, pre-cache remaining routes, and lazy-load remaining routes on demand.

Webpack allows dynamic import syntax tha implements the currently under proposal ECMAScript import(). It takes a filename as an argument and returns a promise.

Vue.component('async-component', (resolve) => {
  import('./AsyncComponent.js')
    .then((AsyncComponent) => {
      resolve(AsyncComponent.default);
    });
});

Often use routing as good split points. For example:

const Home = () => import(/* webpackChunkName: "home" */ './Home.vue');
const About = () => import(/* webpackChunkName: "about" */ './About.vue');
const Contact = () => import(/* webpackChunkName: "contact" */ './Contact.vue');

const routes = [
  { path: '/', name: 'home', component: Home },
  { path: '/about', name: 'about', component: About },
  { path: '/contact', name: 'contact', component: Contact }
];

It’s also possible to split within a page for the so-called “below the fold” sections. Conditionally visible components such as modal windows, tabs, dropdown menus etc.

5.4. Tidying Up

Use clean-webpack-plugin to clean the build output.

Use BannerPlugin and git-revision-webpack-plugin to add build revision to build file.

Use copy-webpack-plugin to copy external files to the build.

6. Optimizing

Webpack allows you to set a build size constraint. Config performance field to set maxEntrypointSize and maxAssetSize.

Webpack use UglifyJsPlugin for minification. However, this plugin doesn’t support ES6 syntax. uglifyjs-webpack-plugin suuport ES6.

Webpack also supports tree shaking. webpack.DefinePlugin allows feature flags.

To enable client side cache, webpack can generate build name with hash. Placeholders include [path], [name], [ext], [hash], [chunkhash] and [contenthash]. The [contenthash] is useful for css extracter ExtracTextPlugin.

NamedModulesPlugin replaces module IDs with paths to the modules making it ideal for development. HashedModuleIdsPlugin does the same except it hashes the result and hides the path information.

By default, webpack put the build manifest in the generated bundle or the vendor bundle.

The --profile and --json webpack options generate build information.

7. Output

Output is set by the target field that can be web, node, webworker, node-webkit, electron, and etc. The default value is web.

There are several approaches to generate multiple pages: multi-compiler mode, single configuration, and others. Config html-webpack-plugin options such as title, filename, template and entry etc. for each page,

Written on July 12, 2017