抹桥的博客
Language
Home
Archive
About
GitHub
Language
主题色
250
2956 words
15 minutes
webpack2 Guide (Part Two)
2017-01-18

Hot Module Replacement (HMR - React)#

As detailed in the Concepts page, Hot Module Replacement (HMR) dynamically replaces, adds, or removes modules while an application is running, without requiring a full page refresh. HMR is particularly useful when an application has a single state tree.

The methods described below use Babel and React, but these are not strictly necessary tools for using HMR.

Project Configuration#

This section will guide you on how to use HMR with Babel, React, and PostCSS to demonstrate a project. To follow along, you’ll need to add these dependencies to your package.json.

To use HMR, you need the following dependencies:

npm install --save-dev [email protected] [email protected] [email protected] [email protected] [email protected] [email protected] [email protected] [email protected] [email protected] [email protected] [email protected] [email protected]

Additionally, for the purpose of our demonstration, you will also need:

npm install --save [email protected] [email protected]

Babel Config#

Your .babelrc file should look like this:

{
  "presets": [
    ["es2015", { "modules": false }],
    // webpack understands the native import syntax, and uses it for tree shaking

    "stage-2",
    // Specifies what level of language features to activate.
    // Stage 2 is "draft", 4 is finished, 0 is strawman.
    // See https://tc39.github.io/process-document/

    "react"
    // Transpile React components to JavaScript
  ],
  "plugins": [
    "react-hot-loader/babel"
    // Enables React code to work with HMR.
  ]
}

Webpack Config#

const { resolve } = require("path");
const webpack = require("webpack");

module.exports = {
  entry: [
    "react-hot-loader/patch",
    // activate HMR for React

    "webpack-dev-server/client?http://localhost:8080",
    // bundle the client for webpack-dev-server
    // and connect to the provided endpoint

    "webpack/hot/only-dev-server",
    // bundle the client for hot reloading
    // only- means to only hot reload for successful updates

    "./index.js",
    // the entry point of our app
  ],
  output: {
    filename: "bundle.js",
    // the output bundle

    path: resolve(__dirname, "dist"),

    publicPath: "/",
    // necessary for HMR to know where to load the hot update chunks
  },

  context: resolve(__dirname, "src"),

  devtool: "inline-source-map",

  devServer: {
    hot: true,
    // enable HMR on the server

    contentBase: resolve(__dirname, "dist"),
    // match the output path

    publicPath: "/",
    // match the output `publicPath`
  },

  module: {
    rules: [
      {
        test: /\.js$/,
        use: ["babel-loader"],
        exclude: /node_modules/,
      },
      {
        test: /\.css$/,
        use: ["style-loader", "css-loader?modules", "postcss-loader"],
      },
    ],
  },

  plugins: [
    new webpack.HotModuleReplacementPlugin(),
    // enable HMR globally

    new webpack.NamedModulesPlugin(),
    // prints more readable module names in the browser console on HMR updates
  ],
};

There’s a lot of configuration above, but not all of it is related to HMR. You can deepen your understanding by consulting the webpack-dev-server options and concept pages.

Our basic assumption is that your JavaScript entry file is at ./src/index.js and you are using CSS Modules for your stylesheets.

The key configurations to focus on in the config file are the devServer and entry keys. HotModuleReplacementPlugin also needs to be included in the plugins key.

For this purpose, we have introduced two modules:

  • react-hot-loader is added to the entry point to enable React to support HMR.
  • NamedModulePlugin is added to better understand what HMR does with each update.

Code#

// ./src/index.js
import React from "react";
import ReactDOM from "react-dom";
import { AppContainer } from "react-hot-loader";

// AppContainer is a necessary wrapper component for HMR

import App from "./components/App";

const render = (Component) => {
  ReactDOM.render(
    <AppContainer>
      <Component />
    </AppContainer>,
    document.getElementById("root"),
  );
};

render(App);

// Hot Module Replacement API
if (module.hot) {
  module.hot.accept("./components/App", () => {
    const NewApp = require("./components/App").default;
    render(NewApp);
  });
}
// ./src/components/App.js
import React from "react";

import styles from "./App.css";

const App = () => (
  <div className={styles.app}>
    <h2>Hello, </h2>
  </div>
);

export default App;
.app {
  text-size-adjust: none;
  font-family: helvetica, arial, sans-serif;
  line-height: 200%;
  padding: 6px 20px 30px;
}

One particular point to note is the reference to module:

  1. Webpack exposes module.hot to our code when we set devServer: { hot: true }.
  2. This allows us to use module.hot to enable HMR for specific resources (here, App.js). A very important API here is module.hot.accept, which determines how to handle these specific dependencies.
  3. It’s important to note that webpack 2 has built-in support for ES2015 module syntax, so you don’t need to re-reference the root component in module.hot.accept. To achieve this, you need to configure Babel’s ES2015 preset in .babelrc:

[“es2015”, { modules: false }];

 As we configured in the Babel Config section earlier. Note that disabling Babel's module features is not only for enabling HMR. If you don't turn off this configuration, you will encounter several issues.
4.  If you are using ES6 modules in your webpack 2 configuration and you have modified `.babelrc` as per #3, then you need to use `require` syntax and create two `.babelrc` files:
 1.  One in the root directory configured with `"presets: ["es2015"]"`.
 1.  One in the folder that webpack compiles, for example, `src/` in this case.
 So in this scenario, `module.hot.accept` will execute the `render` method whenever `src/components/App.js` or any of its dependencies change—this means that even if `App.css` is modified after being imported into `App.js`, the `render` method will still be executed.

### Index.html

The entry page needs to be placed under the `dist` directory, as `webpack-dev-server` requires this file to run.

```html
<!doctype html>
<html lang="en">
<head>
 <meta charset="UTF-8" />
 <title>Example Index</title>
</head>
<body>
 <div id="root"></div>
 <script src="bundle.js"></script>
</body>
</html>

Package.json#

Finally, we need to start webpack-dev-server to bundle our code and see how HMR works. We can use the following package.json entry:

{
  "scripts": {
    "start": "webpack-dev-server"
  }
}

Execute npm start, open your browser and navigate to http://localhost:8080. You should see the following items displayed in the console.log:

dev-server.js:49[HMR] Waiting for update signal from WDS…
only-dev-server.js:74[HMR] Waiting for update signal from WDS…
client?c7c8:24 [WDS] Hot Module Replacement enabled.

Then, edit and modify the App.js file, and you will see logs similar to the following in the console.log:

[WDS] App updated. Recompiling…
client?c7c8:91 [WDS] App hot update…
dev-server.js:45 [HMR] Checking for updates on the server…
log-apply-result.js:20 [HMR] Updated modules:
log-apply-result.js:22 [HMR]  - ./components/App.js
dev-server.js:27 [HMR] App is up to date.

Notice that HMR indicates the path of the updated module. This is because we used NamedModulesPlugin.

Development Environment#

This section introduces some tools that can be used during development.

Note: Do not use in production.

Source Map#

When a JS error occurs, we need to know which file and line number caused the error. However, when files are bundled by webpack, finding the problem can become very difficult. Source Maps are designed to solve this problem. There are many different options, each with its own advantages and disadvantages. To start, we use:

devtool: "cheap-eval-source-map"

Choosing a Tool#

Webpack can be used in watch mode. In this mode, webpack monitors your files and recompiles them when changes are detected. Webpack-dev-server provides a convenient development server with automatic refresh functionality. If you already have a development server and want more flexibility, webpack-dev-middleware can be used as a middleware to achieve this.

Webpack-dev-server and webpack-dev-middleware compile in memory, which means the bundled code is not saved to local disk. This makes bundling very fast and avoids creating many temporary files that clutter your local file system.

In most cases, you will want to use webpack-dev-server because it is easy to use and provides many out-of-the-box features.

Webpack Watch Mode#

Webpack’s watch mode detects file changes. As soon as a change is detected, it recompiles. We want its compilation process to have a good progress display. So, execute the following command:

webpack --progress --watch

Modify any file and save it, and you will see the recompilation process.

Watch mode does not consider any service-related issues, so you need to provide your own server. A simple server is [server](https://github.com/tj/serve). Once installed (npm i server -g), run it in your bundled file directory:

server

You will need to manually refresh the browser after each recompilation.

webpack-dev-server#

webpack-dev-server provides a server that supports automatic refresh.

First, ensure that your index.html page already references your bundled code. Let’s assume output.filename is set to bundle.js:

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

Install webpack-dev-server from npm:

npm install webpack-dev-server --save-dev

Then you can execute the webpack-dev-server command:

webpack-dev-server --open

The command above will automatically open your browser and navigate to http://localhost:8080.

Modify one of your files and save it. You will find that the code is re-bundled, and when bundling is complete, the page will automatically refresh. If it doesn’t work as expected, you may need to adjust watchOptions(https://webpack.js.org/configuration/dev-server#devserver-watchoptions-).

Now that you have a server with automatic refresh, let’s look at how to enable Hot Module Replacement. This provides an interface to replace modules without refreshing the page. See here for more information.

webpack-dev-server can do many things, such as proxying requests to your backend service. To learn more about configuration options, check out the devServer documentation.

webpack-dev-middleware#

webpack-dev-middleware is suitable for middleware-based connection stacks. It is useful when you already have a Node.js server or want complete control over the service.

This middleware performs file compilation in memory. While a compilation is in progress, it will delay a file request until compilation is complete.

First, install from npm:

npm install express webpack-dev-server --save-dev

As an example, we can use the middleware like this:

var express = require("express");
var webpackDevMiddleware = require("webpack-dev-middleware");
var webpack = require("webpack");
var webpackConfig = require("./webpack.config");

var app = express();
var compiler = webpack(webpackConfig);

app.use(
  webpackDevMiddleware(compiler, {
    publicPath: "/", // Same as `output.publicPath` in most cases.
  }),
);

app.listen(3000, function () {
  console.log("Listening on port 3000!");
});

Depending on your configuration in output.publicPath and output.filename, your bundled code should be accessible via http://localhost:3000/bundle.js.

By default, it uses watch mode. It also supports lazy mode, where it only recompiles when a request comes in.

The configuration is as follows:

app.use(
  webpackDevMiddleware(compiler, {
    lazy: true,
    filename: "bundle.js", // Same as `output.filename` in most cases.
  }),
);

There are many other useful options; for details, please refer to the documentation.

Building for Production#

This chapter introduces how to use webpack for production builds.

An Automated Approach#

Execute webpack -p (equivalent to webpack --optimize-minimize --define process.env.NODE_ENV="production"). This will perform the following steps:

  • Compress files using UglifyJsPlugin.
  • Execute LoaderOptionsPlugin, see documentation.
  • Set Node’s environment variables.

Source Code Compression#

Webpack uses UglifyJsPlugin to compress source code, achieving the goal of compressing output code by executing UglifyJs. This plugin supports all UglifyJs features. Entering --optimize-minimize on the command line is equivalent to adding the following configuration in the config file:

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

module.exports = {
  /*...*/
  plugins: [
    new webpack.optimize.UglifyJsPlugin({
      sourceMap:
        options.devtool &&
        (options.devtool.indexOf("sourcemap") >= 0 ||
          options.devtool.indexOf("source-map") >= 0),
    }),
  ],
};

This will generate Source Maps during bundling, based on the devtools option.

Source Map#

We recommend enabling Source Maps in the development environment, as they are very useful for debugging and testing. Webpack can generate inline Source Maps included in the bundle or as separate files.

In the configuration file, you can set the Source Map type by modifying the devtools configuration. Currently, we support seven different types of Source Maps. More detailed information can be found in the specific documentation.

A good choice is to use cheap-module-source-map, which simplifies the Source Maps to a single mapping per line.

Node Environment Variables#

Executing webpack -p (--define process.env.NODE_ENV="production") will invoke DefinePlugin with the following configuration:

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

module.exports = {
  /*...*/
  plugins: [
    new webpack.DefinePlugin({
      "process.env.NODE_ENV": JSON.stringify("production"),
    }),
  ],
};

DefinePlugin performs a find-and-replace operation in the source code. All instances of process.env.NODE_ENV found will be replaced with "production". This way, code like if(process.env.NODE_ENV !=='production') console.log(...) will be considered by UglifyJs to be equivalent to if(false) console.log(...).

A Manual Approach: Configuring Webpack with Different Environment-Specific Files#

The simplest way to configure webpack with different environment-specific files is to create multiple configuration files. For example:

dev.js

// 此处官网文档有语法错误,我改了一下
module.exports = function (env) {
  return {
    devtool: "cheap-module-source-map",
    output: {
      path: path.join(__dirname, "/../dist/assets"),
      filename: "[name].bundle.js",
      publicPath: publicPath,
      sourceMapFilename: "[name].map",
    },
    devServer: {
      port: 7777,
      host: "localhost",
      historyApiFallback: true,
      noInfo: false,
      stats: "minimal",
      publicPath: publicPath,
    },
  };
};

prod.js

module.exports = function (env) {
  return {
    output: {
      path: path.join(__dirname, "/../dist/assets"),
      filename: "[name].bundle.js",
      publicPath: publicPath,
      sourceMapFilename: "[name].map",
    },
    plugins: [
      new webpack.LoaderOptionsPlugin({
        minimize: true,
        debug: false,
      }),
      new UglifyJsPlugin({
        beautify: false,
        mangle: {
          screw_ie8: true,
          keep_fnames: true,
        },
        compress: {
          screw_ie8: true,
        },
        comments: false,
      }),
    ],
  };
};

Then, change the content of our webpack.config.js to the following:

function buildConfig(env) {
  return require("./config/" + env + ".js")({ env: env });
}

module.exports = buildConfig(env);

Finally, add the following commands to package.json:

"build:dev": "webpack --env=dev --progress --profile --colors",
 "build:dist": "webpack --env=prod --progress --profile --colors",

As you can see, we passed environment variables to the webpack.config.js file. From here, we use a simple method to determine which configuration file to use by passing environment variables.

A more advanced approach is to have a base configuration file containing all common functionalities, and then define different functionalities for different environments in specific files, using webpack-merge to combine them into a complete configuration. This avoids writing a lot of repetitive code. For example, common functionalities like parsing js, ts, png, jpeg, etc., should be placed in the base configuration file:

base.js

module.exports = function () {
  return {
    entry: {
      polyfills: "./src/polyfills.ts",
      vendor: "./src/vendor.ts",
      main: "./src/main.ts",
    },
    output: {
      path: path.join(__dirname, "/../dist/assets"),
      filename: "[name].bundle.js",
      publicPath: publicPath,
      sourceMapFilename: "[name].map",
    },
    resolve: {
      extensions: ["", ".ts", ".js", ".json"],
      modules: [path.join(__dirname, "src"), "node_modules"],
    },
    module: {
      loaders: [
        {
          test: /\.ts$/,
          loaders: ["awesome-typescript-loader", "angular2-template-loader"],
          exclude: [/\.(spec|e2e)\.ts$/],
        },
        {
          test: /\.css$/,
          loaders: ["to-string-loader", "css-loader"],
        },
        {
          test: /\.(jpg|png|gif)$/,
          loader: "file",
        },
        {
          test: /\.(woff|woff2|eot|ttf|svg)$/,
          loader: "url-loader?limit=100000",
        },
      ],
    },
    plugins: [
      new ForkCheckerPlugin(),

      new webpack.optimize.CommonsChunkPlugin({
        name: ["polyfills", "vendor"].reverse(),
      }),
      new HtmlWebpackPlugin({
        template: "src/index.html",
        chunksSortMode: "dependency",
      }),
    ],
  };
};

Then, use webpack-merge to combine environment-specific configuration files. Let’s look at an example of merging production-specific configurations (compare with prod.js above):

prod.js (updated)

const webpackMerge = require("webpack-merge");

const commonConfig = require("./base.js");

module.exports = function (env) {
  return webpackMerge(commonConfig(), {
    plugins: [
      new webpack.LoaderOptionsPlugin({
        minimize: true,
        debug: false,
      }),
      new webpack.DefinePlugin({
        "process.env": {
          NODE_ENV: JSON.stringify("prod"),
        },
      }),
      new webpack.optimize.UglifyJsPlugin({
        beautify: false,
        mangle: {
          screw_ie8: true,
          keep_fnames: true,
        },
        compress: {
          screw_ie8: true,
        },
        comments: false,
      }),
    ],
  });
};

You can notice three main updates in prod.js:

  • base.js is merged via webpack-merge.
  • The output property has been moved to base.js. We only need to concern ourselves with configurations that differ from base.js.
  • process.env.NODE_ENV is set to prod via DefinePlugin. This ensures that process.env.NODE_ENV has a value of prod throughout the entire application code.

What needs to remain consistent across different environments is up to you. This is just a demo to typically illustrate how to maintain partial configuration consistency across different environments.

As you can see, webpack-merge is very powerful, allowing us to avoid writing a lot of repetitive code.

Lazy Loading - React#

By using higher-order functions, a component can lazy-load its dependencies without its consumers knowing, or by using a component that accepts a function or module, a consumer can lazy-load its child components without its child components knowing.

Component Lazy Loading#

Let’s first look at a consumer choosing to lazy-load some components. importLazy is a function that returns the default property, which is for interoperability with Babel/ES2015. If you don’t need it, you can ignore the importLazy method. importLazy simply returns the module exposed via export default.

<LazilyLoad
  modules={{
    TodoHandler: () => importLazy(import("./components/TodoHandler")),
    TodoMenuHandler: () => importLazy(import("./components/TodoMenuHandler")),
    TodoMenu: () => importLazy(import("./components/TodoMenu")),
  }}
>
  {({ TodoHandler, TodoMenuHandler, TodoMenu }) => (
    <TodoHandler>
      <TodoMenuHandler>
        <TodoMenu />
      </TodoMenuHandler>
    </TodoHandler>
  )}
</LazilyLoad>

Higher Order Component#

As a component, you can ensure that the component’s own dependencies are lazy-loaded. This is very useful when a component depends on a very large library file. Suppose we want to write a Todo component that supports code highlighting:

class Todo extends React.Component {
  render() {
    return (
      <div>
        {this.props.isCode ? <Highlight>{content}</Highlight> : content}
      </div>
    );
  }
}

We can ensure that this expensive library file is only loaded when we actually need the code highlighting feature:

// Highlight.js
class Highlight extends React.Component {
  render() {
    const {Highlight} = this.props.highlight;
    // highlight js is now on our props for use
  }
}
export LazilyLoadFactory(Highlight, {
  highlight: () => import('highlight'),
});

Notice how the consumer of this Highlight component is lazy-loaded without their knowledge.

Complete Code#

The source code for the LazilyLoad component exposes both the component interface and the higher-order component interface.

import React from "react";

class LazilyLoad extends React.Component {
  constructor() {
    super(...arguments);
    this.state = {
      isLoaded: false,
    };
  }

  componentWillMount() {
    this.load(this.props);
  }

  componentDidMount() {
    this._isMounted = true;
  }

  componentWillReceiveProps(next) {
    if (next.modules === this.props.modules) return null;
    this.load(next);
  }

  componentWillUnmount() {
    this._isMounted = false;
  }

  load(props) {
    this.setState({
      isLoaded: false,
    });

    const { modules } = props;
    const keys = Object.keys(modules);

    Promise.all(keys.map((key) => modules[key]()))
      .then((values) =>
        keys.reduce((agg, key, index) => {
          agg[key] = values[index];
          return agg;
        }, {}),
      )
      .then((result) => {
        if (!this._isMounted) return null;
        this.setState({ modules: result, isLoaded: true });
      });
  }

  render() {
    if (!this.state.isLoaded) return null;
    return React.Children.only(this.props.children(this.state.modules));
  }
}

LazilyLoad.propTypes = {
  children: React.PropTypes.func.isRequired,
};

export const LazilyLoadFactory = (Component, modules) => {
  return (props) => (
    <LazilyLoad modules={modules}>
      {(mods) => <Component {...mods} {...props} />}
    </LazilyLoad>
  );
};

export const importLazy = (promise) => promise.then((result) => result.default);

export default LazilyLoad;

Tips

  • By using bundle loader, you can semantically name code blocks to intelligently load a group of code.
  • Ensure you are using babel-preset-2015 and have modules set to false, which allows webpack to handle modules.

Public Path#

Webpack provides a very useful feature that allows you to set the base path for all resource references in your application. It is called publicPath.

Use Case#

Here are some real-world application scenarios where this feature can be used.

Setting the Value at Build Time#

In development mode, we usually place the assets/ directory at the same level as the entry page. This is fine, but what if your static resources are stored on a CDN in a production environment?

This can be easily solved with environment variables. Suppose we have a variable ASSET_PATH:

// 这里看起来好像有问题
import webpack from "webpack";

// Whatever comes as an environment variable, otherwise use root
const ASSET_PATH = process.env.ASSET_PATH || "/";

export default {
  output: {
    publicPath: ASSET_PATH,
  },

  plugins: [
    // This makes it possible for us to safely use env vars on our code
    new webpack.DefinePlugin({
      "process.env.ASSET_PATH": JSON.stringify(ASSET_PATH),
    }),
  ],
};

Setting Value on the Fly#

Another way is to set the public path during development. Webpack exposes a global variable __webpack_public_path__ to allow us to achieve this. So in your entry file, you can do this:

__webpack_publick_path__ = process.en.ASSET_PATH;

How you do it is up to you. Once we configure it via DefinePlugin, process.env.ASSET_PATH can be used directly anywhere.

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

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