抹桥的博客
Language
Home
Archive
About
GitHub
Language
主题色
250
1957 words
10 minutes
webpack2 Guide (Part 2)
2017-01-19

Caching#

To enable long-term caching of static assets processed by webpack, you need to:

  1. Use [chunkhash] to create a content-based cache identifier for each file.
  2. Retrieve filenames using the compilation stats when including files in HTML.
  3. Generate a chunk-manifest JSON file and embed it into the HTML page before loading resources.
  4. Ensure that the hash of the entry chunk containing the bootstrap code is not modified when its dependencies remain unchanged.

The Problem#

Every time something in our code needs to be updated, it must be deployed to the server and re-downloaded by the client. This is very inefficient, especially when network conditions are poor. This is why browsers cache static resources.

This leads to a pitfall: if we release a new version without updating the filenames, the browser will assume the files haven’t changed, preventing clients from getting the latest resources.

A simple way to solve this is to tell the browser a new filename. Without webpack, we might use a build version to identify the update:

application.js?build=1
application.css?build=1

In webpack, this is also straightforward: every webpack build generates a unique hash that can be used to form filenames. The following configuration will generate two filenames with hashes:

// webpack.config.js
const path = require("path");

module.exports = {
  entry: {
    vendor: "./src/vendor.js",
    main: "./src/index.js",
  },
  output: {
    path: path.join(__dirname, "build"),
    filename: "[name].[hash].js",
  },
};

Running the webpack command will produce the following output:

Hash: 55e783391098c2496a8f
Version: webpack 1.10.1
Time: 58ms
Asset Size Chunks Chunk Names
main.55e783391098c2496a8f.js 1.43 kB 0 [emitted] main
vendor.55e783391098c2496a8f.js 1.43 kB 1 [emitted] vendor
[0] ./src/index.js 46 bytes {0} [built]
[0] ./src/vendor.js 40 bytes {1} [built]

However, the problem is that every time we recompile, all filenames change, causing the client to re-download the entire application code each time. So, how can we ensure that the client only downloads files that have changed?

Generating Unique Hashes for Each File#

How can we ensure that a file’s filename doesn’t change with every compilation if its content hasn’t changed? For example, a code bundle that contains all our dependency libraries.

Webpack allows generating hashes based on file content. Here’s the updated configuration:

// webpack.config.js
module.exports = {
  /*...*/
  output: {
    /*...*/
    filename: "[name].[chunkhash].js",
  },
};

This configuration will also generate two files, but each file will have its own hash:

main.155567618f4367cd1cb8.js 1.43 kB 0 [emitted] main
vendor.c2330c22cd2decb5da5a.js 1.43 kB 1 [emitted] vendor

Do not use [chunkhash] in development environments, as it will increase compilation time. Separate your development and production configurations, using [name].js in development and [name].[chunkhash].js in production.

Retrieving Filenames from Webpack Compilation Stats#

In a development environment, you simply need to include your files in HTML.

<script src="main.js"></srcipt>

However, in a production environment, we get a different filename every time:

<script src="main.12312123t234.js"></srcipt>

To include the correct files in HTML, we need some build information. This can be extracted from webpack compilation stats using the following plugin.

// webpack.config.js
const path = require("path");

module.exports = {
  /*...*/
  plugins: [
    function () {
      this.plugin("done", function (stats) {
        require("fs").writeFileSync(
          path.join(__dirname, "…", "stats.json"),
          JSON.stringify(stats.toJson()),
        );
      });
    },
  ],
};

Alternatively, you can use these plugins to expose JSON files:

A simple file output by webpack-manifest-plugin looks like this:

{
  "main.js": "main.155567618f4367cd1cb8.js",
  "vendor.js": "vendor.c2330c22cd2decb5da5a.js"
}

The next steps depend on your server. Here’s a great example [walk through for Rails-based projects](walk through for Rails-based projects). For Node.js server-side rendering, you can use webpack-isomorphic-tools. If your application doesn’t require server-side rendering, you can directly generate an index.html. This can be done using these two plugins:

Deterministic Hashes#

To compress the size of generated files, webpack uses IDs instead of names to identify modules. During compilation, IDs are generated, mapped to chunk names, and placed into a JavaScript object called the chunk manifest. It is placed in the entry file to ensure the bundled files work correctly.

This leads to the same problem as before: no matter where a file is modified, even if other parts remain unchanged, the updated entry needs to include the manifest file. This generates a new hash, causing our entry filename to change, preventing us from benefiting from long-term caching.

To solve this problem, we should use https://github.com/diurnalist/chunk-manifest-webpack-plugin, which can extract the manifest file into a separate JSON file. Here’s the updated configuration, which will generate chunk-manifest.json and place it in our bundled folder:

// webpack.config.js
var ChunkManifestPlugin = require("chunk-manifest-webpack-plugin");

module.exports = {
  // your config values here
  plugins: [
    new ChunkManifestPlugin({
      filename: "chunk-manifest.json",
      manifestVariable: "webpackManifest",
    }),
  ],
};

When we remove the manifest file from the entry file, we then need to manually provide this file for webpack to use. In the example above, you might notice the manifestVariable option. This is a global variable that webpack uses to find the manifest JSON file, which is why we need to include it in the HTML before our code bundle. Writing the content of the JSON file into HTML is easy, so the <head> section of our HTML file would look like this:

<html>
  <head>
    <script>
      //<![CDATA[
      window.webpackManifest = {
        0: "main.3d038f325b02fdee5724.js",
        1: "1.c4116058de00860e5aa8.js",
      };
      //]]>
    </script>
  </head>
  <body></body>
</html>
 

The final webpack.config.js file is as follows:

var path = require("path");
var webpack = require("webpack");
var ManifestPlugin = require("webpack-manifest-plugin");
var ChunkManifestPlugin = require("chunk-manifest-webpack-plugin");
var WebpackMd5Hash = require("webpack-md5-hash");

module.exports = {
  entry: {
    vendor: "./src/vendor.js",
    main: "./src/index.js",
  },
  output: {
    path: path.join(__dirname, "build"),
    filename: "[name].[chunkhash].js",
    chunkFilename: "[name].[chunkhash].js",
  },
  plugins: [
    new webpack.optimize.CommonsChunkPlugin({
      name: "vendor",
      minChunks: Infinity,
    }),
    new WebpackMd5Hash(),
    new ManifestPlugin(),
    new ChunkManifestPlugin({
      filename: "chunk-manifest.json",
      manifestVariable: "webpackManifest",
    }),
  ],
};

If you are using webpack-html-plugin, you can use inline-manifest-webpack-plugin to do this.

With this configuration, the vendor chunks will not change unless you modify their code or dependencies.

Shimming#

Webpack, as a module bundler, supports module systems including ES2015 modules, CommonJS, and AMD. However, in many cases, when using third-party libraries, we see them relying on a global variable like $ or jquery. They might also create new global variables that need to be exposed. Let’s look at a few different ways to make webpack understand these non-module (broken modules) files.

Prefer unminified CommonJS/AMD files over bundled dist versions. Most modules specify their dist version in the main field of package.json. While this is very useful for most developers, for webpack, it’s often better to set an alias to their src directory to allow webpack to optimize dependencies better. However, in many cases, using the dist version won’t cause major issues.

// webpack.config.js

module.exports = {
  ...
  resolve: {
  alias: {
    jquery: "jquery/src/jquery"
  }
}
};

provide-plugin#

By using provide-plugin, this module becomes available as a variable in all modules referenced by webpack. The corresponding module is only referenced when you actually use this variable. Many older modules use specific global variables, such as jQuery’s $ or jQuery. In this scenario, you can pre-configure webpack to var $=require('jquery') every time it encounters the global $ identifier.

module.exports = {
  plugins: [
    new webpack.ProvidePlugin({
      $: "jquery",
      jQuery: "jquery",
    }),
  ],
};

imports-loader#

imports-loader injects necessary global variables into traditional modules. For example, some traditional modules rely on this pointing to the window object. This causes a problem when the module is executed in a CommonJS context, where this points to module.exports. In this case, you can override this using imports-loader.

webpack.config.js

module.exports = {
  module: {
    rules: [
      {
        test: require.resolve("some-module"),
        use: "imports-loader?this=>window",
      },
    ],
  },
};

It supports different module types, such as AMD, CommonJS, and also traditional modules. However, it often checks for the define variable and then uses some quirky code to expose these properties. In this case, setting define = false to force the CommonJS path might be helpful.

webpack.config.js

module.exports = {
  module: {
    rules: [
      {
        test: require.resolve("some-module"),
        use: "imports-loader?define=>false",
      },
    ],
  },
};

exports-loader#

Suppose a library file creates a global variable and expects its consumers to use it. In this case, we should use exports-loader to expose a CommonJS-style global variable. For example, to expose file as file, and helpers.parse as parse:

webpack.config.js

module.exports = {
  module: {
    rules: [
      {
        test: require.resolve("some-module"),
        use: "exports-loader?file,parse=helpers.parse",
        // adds below code the the file's source:
        //  exports["file"] = file;
        //  exports["parse"] = helpers.parse;
      },
    ],
  },
};

script-loader#

script-loader parses code in the global context, just as if you added a script tag in HTML. In this case, theoretically, all modules should run normally.

This file will be bundled as a string in the code. It will not be compressed by webpack, so please use the compressed version. Also, webpack’s development tools cannot be used in this situation.

Suppose you have a legacy.js file containing:

GLOBAL_CONFIG = {};

Using script-loader#

require("script-loader!legacy.js");

Generally, you will get this result:

eval("GLOBAL_CONFIG = {}");

noParse Option#

When there are no AMD/CommonJS style modules, and you need to include them from dist, you can mark this module as noParse. This way, webpack will only include this module but will not process it, which can also reduce build time.

Anything that requires AST support, such as ProvidePlugin, will not work.

module.exports = {
  module: {
    noParse: /jquery|backbone/,
  },
};

Authoring a Library#

Webpack is a tool that can be used to bundle application code, and it can also be used to bundle library code. If you are a JavaScript library author looking to streamline the code bundling process, then this section will be very helpful to you.

Author a Library#

Here’s a simplified wrapper library to convert numbers 1 to 5 to their corresponding words, and vice versa. It might look like this:

src/index.js

import _ from "lodash";

import numRef from "./ref.json";

export function numToWord(num) {
  return _.reduce(
    numRef,
    (accum, ref) => {
      return ref.num === num ? ref.word : accum;
    },
    "",
  );
}

export function wordToNum(word) {
  return _.reduce(
    numRef,
    (accum, ref) => {
      return ref.word === word && word.toLowerCase() ? ref.num : accum;
    },
    -1,
  );
}

The library usage specification is as follows:

//ES2015modules

import*aswebpackNumbersfrom'webpack-numbers';

...
webpackNumbers.wordToNum('Two')//outputis2
...

//CommonJSmodules

varwebpackNumbers=require('webpack-numbers');

...
webpackNumbers.numToWord(3);//outputisThree
...

//Asascripttag

<html>
...
<scriptsrc="https://unpkg.com/webpack-numbers"></script>
<script>
...
/*webpackNumbersisavailableasaglobalvariable*/
webpackNumbers.wordToNum('Five')//outputis5
...
</script>
</html>

The complete library configuration and code are available here: webpack-library-example.

Configuring webpack#

The next step is to bundle this library: • Do not bundle lodash, but expect it to be imported by its consumers. • Name the library webpack-numbers, and the variable webpackNumbers. • The library can be imported via import webpackNumbers from 'webpack-numbers' or require('webpack-numbers'). • When imported via a script tag, it can be accessed through the global variable webpackNumbers. • It can be used in Node.js.

Adding webpack#

Add basic webpack configuration.

webpack.config.js

module.exports = {
  entry: "./src/index.js",
  output: {
    path: "./dist",
    filename: "webpack-numbers.js",
  },
};

This adds a basic configuration to bundle the library.

Adding Loaders#

However, it won’t work without the corresponding loaders to parse the code. Here, we add json-loader to enable parsing of JSON files.

webpack.config.js

module.exports = {
  // ...
  module: {
    rules: [{
      test: /.json$/,
      use: 'json-loader'
    }]
  }
};
 

Adding externals#

Now, if you run the webpack command, you’ll find a significantly larger code bundle generated. If you inspect the code, you’ll find that lodash is bundled into the package. For your library, bundling lodash together is completely unnecessary.

This can be configured via externals:

webpack.config.js

module.exports = {
  ...
    externals: {
  "lodash": {
    commonjs: "lodash",
      commonjs2: "lodash",
      amd: "lodash",
      root: "_"
  }
}
...
};
 

This means your library will expect lodash to be a dependency in the consumer’s environment.

Adding libraryTarget#

For this library to be widely used, we need it to behave consistently across different environments. For example, CommonJS, AMD, Node.js, or as a global variable.

To achieve this, you need to add the library property to the webpack configuration.

webpack.config.js

module.exports = {
  ...
    output: {
...
  library: 'webpackNumbers'
}
...
};
 

This allows your library to be accessed as a global variable when imported. To use it in other scenarios, continue adding libraryTarget values to the configuration:

webpack.config.js

module.exports = {
  ...
    output: {
...
  library: 'webpackNumbers',
    libraryTarget:'umd' // Possible value - amd, commonjs, commonjs2, commonjs-module, this, var
}
...
};
 

If library is set but libraryTarget is not configured, libraryTarget defaults to var as specified in the config reference.

Final Step#

package.json


{
    ...
    "main": "dist/webpack-numbers.js",
    "module": "src/index.js", // To add as standard module as per https://github.com/dherman/defense-of-dot-js/blob/master/proposal.md#typical-usage
    ...
}

Now you can publish it as an npm module and distribute it to your users via unpkg.com.

This article was published on January 19, 2017 and last updated on January 19, 2017, 3182 days ago. The content may be outdated.

webpack2 Guide (Part 2)
https://blog.kisnows.com/en-US/2017/01/19/webpack2-guide-3/
Author
Kisnows
Published at
2017-01-19
License
CC BY-NC-ND 4.0