使用 React-hot-loader 3、React-router 4 和 Webpack-hot-middleware 进行 React Hot Reload

React Hot Reload with React-hot-loader 3, React-router 4, and Webpack-hot-middleware

我正在尝试让 React-hot-loader 3 与 React-hot-loader 3、React-router 4 和 Webpack-hot-middleware(最新版本,2.18.2)一起工作。

这是我的 server.js:

const express = require('express');
const bodyParser = require('body-parser');
const cookiesMiddleware = require('universal-cookie-express');
/* eslint-disable import/no-extraneous-dependencies */
const webpack = require('webpack');
const webpackDevMiddleware = require('webpack-dev-middleware');
const webpackHotMiddleware = require('webpack-hot-middleware');
const webpackHotServerMiddleware = require('webpack-hot-server-middleware');
/* eslint-enable import/no-extraneous-dependencies */
const clientConfig = require('./webpack.config.dev.client');
const serverConfig = require('./webpack.config.dev.server');

const PORT_NUMBER = process.env.PORT || 3000;
const app = express();

app.use(bodyParser.urlencoded({ extended: true }));
app.use(cookiesMiddleware());
app.use(express.static('public'));

const multiCompiler = webpack([clientConfig, serverConfig]);
const clientCompiler = multiCompiler.compilers[0];

app.use(webpackDevMiddleware(multiCompiler, {
  publicPath: clientConfig.output.publicPath,
  noInfo: true,
  stats: { children: false },
}));
app.use(webpackHotMiddleware(clientCompiler));
app.use(webpackHotServerMiddleware(multiCompiler, {
  serverRendererOptions: { outputPath: clientConfig.output.path },
}));

app.listen(PORT_NUMBER, () => {
  // eslint-disable-next-line no-console
  console.log(`Server listening at port ${PORT_NUMBER}`);
});

我的client entry point:

import React from 'react';
import { render } from 'react-dom';
import { AppContainer } from 'react-hot-loader';

import * as Bundles from './components/Bundles';
import App from './App';

const doRender = () => {
  render(
    <AppContainer>
      <App type="client" />
    </AppContainer>,
    document.getElementById('content'),
  );
};

const splitPoints = window.splitPoints || [];
Promise.all(splitPoints.map(chunk => Bundles[chunk].loadComponent()))
  .then(doRender);

if (module.hot) {
  module.hot.accept('./App', doRender);
}

.babelrc:

{
  "plugins": [
    "transform-decorators-legacy",
    "transform-object-rest-spread"
  ],
  "presets": [
    ["es2015", { "modules": false }],
    "react",
    "stage-0"
  ],
  "env": {
    "development": {
      "plugins": ["react-hot-loader/babel"]
    },
    "test": {
      "plugins": ["transform-es2015-modules-commonjs"]
    }
  }
}

看起来我遵循了 react-hot-loader's README 的每一步,但每次我更改组件中的一些代码时,我都会在控制台中收到此消息:

[HMR] bundle rebuilding
client.js:207 [HMR] bundle rebuilt in 8218ms
process-update.js:27 [HMR] Checking for updates on the server...
process-update.js:81 [HMR] The following modules couldn't be hot updated: (Full reload needed)
This is usually because the modules which have changed (and their parents) do not know how to hot reload themselves. See http://webpack.github.io/docs/hot-module-replacement-with-webpack.html for more details.

有人无意中发现了这个吗?提前致谢!

编辑:这是我的客户端 webpack 配置:

const path = require('path');
const webpack = require('webpack');
const autoprefixer = require('autoprefixer');
const StyleLintPlugin = require('stylelint-webpack-plugin');
const notifier = require('node-notifier');

const configFileName = './.env.development.json';
let envConfig;

try {
  // eslint-disable-next-line import/no-dynamic-require, global-require
  envConfig = require(configFileName);
} catch (e) {
  envConfig = {};
}

const eslintSettings = {
  extends: path.join(__dirname, '.eslintrc.js'),
  configFile: path.join(__dirname, '.eslintrc.js'),
  emitWarning: true,
  cache: true,
};
const babelSettings = {
  extends: path.join(__dirname, '.babelrc'),
  cacheDirectory: true,
};

const excludes = [
  /node_modules(?![/\]@local-package[/\])/,
];
const roots = [
  path.join(__dirname, '../../node_modules'),
  path.join(__dirname, 'node_modules'),
  path.join(__dirname, 'client'),
];

const getCommonCSSLoaders = enableCSSModules => [
  {
    loader: 'style-loader',
  },
  {
    loader: 'css-loader',
    options: {
      modules: enableCSSModules,
      importLoaders: 1,
      localIdentName: '[name]_[local]_[hash:base64:3]',
    },
  },
  {
    loader: 'postcss-loader',
    options: {
      sourceMap: true,
      ident: 'postcss',
      plugins: () => [
        // eslint-disable-next-line global-require, import/no-extraneous-dependencies
        require('postcss-flexbugs-fixes'),
        autoprefixer({
          env: 'development',
          flexbox: 'no-2009',
        }),
      ],
    },
  },
];

const rules = [
  {
    enforce: 'pre',
    test: /\.js$/,
    exclude: /node_modules/,
    loader: 'eslint-loader',
    options: eslintSettings,
  },
  {
    test: /\.js$/,
    exclude: excludes,
    loader: 'babel-loader',
    options: babelSettings,
  },
  {
    test: /\.css$/,
    exclude: excludes,
    use: [
      ...getCommonCSSLoaders(true),
    ],
  },
  {
    test: /\.css$/,
    include: excludes,
    use: [
      ...getCommonCSSLoaders(false),
    ],
  },
  {
    test: /\.scss$/,
    exclude: excludes,
    use: [
      ...getCommonCSSLoaders(true),
      {
        loader: 'resolve-url-loader',
      },
      {
        loader: 'sass-loader',
        options: {
          sourceMap: true,
        },
      },
    ],
  },
  {
    test: /.*\.(eot|woff|woff2|ttf|svg|png|jpe?g|gif)$/i,
    use: [
      {
        loader: 'url-loader',
        options: {
          name: 'images/[name].[hash].[ext]',
          limit: 20000,
        },
      },
      {
        loader: 'image-webpack-loader',
        options: {
          mozjpeg: {
            quality: 80,
          },
          pngquant: {
            quality: '80-90',
          },
          bypassOnDebug: true,
        },
      },
    ],
  },
];

const plugins = [
  new webpack.LoaderOptionsPlugin({
    debug: true,
  }),
  new webpack.HotModuleReplacementPlugin(),
  new webpack.NoEmitOnErrorsPlugin(),
  new StyleLintPlugin({
    configFile: path.join(__dirname, '.stylelintrc.js'),
    files: [
      '**/*.s?(a|c)ss',
      '../shared/**/*.s?(a|c)ss',
    ],
    emitErrors: false,
  }),
  new webpack.NormalModuleReplacementPlugin(/\/components\/Bundles/, './components/AsyncBundles'),
  new webpack.NormalModuleReplacementPlugin(/\/Bundles/, './AsyncBundles'),
  new webpack.optimize.CommonsChunkPlugin({
    name: 'client',
    async: 'common',
    children: true,
    minChunks: (module, count) => {
      if (module.resource && (/^.*\.(css|scss)$/).test(module.resource)) {
        return false;
      }
      return count >= 3 && module.context && !module.context.includes('node_modules');
    },
  }),
  new webpack.optimize.CommonsChunkPlugin({
    name: 'client',
    children: true,
    minChunks: module => module.context && module.context.includes('node_modules'),
  }),
  new webpack.optimize.CommonsChunkPlugin({
    name: 'vendors',
    minChunks: module => module.context && module.context.includes('node_modules'),
  }),
  new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
  // eslint-disable-next-line func-names
  function () {
    this.plugin('done', (stats) => {
      notifier.notify({
        title: 'Webpack : Build Succeeded',
        message: `${stats.compilation.errors.length} Error(s) - ${stats.compilation.warnings.length} Warning(s)`,
      });
    });
    this.plugin('failed', () => {
      notifier.notify({
        title: 'Webpack',
        message: 'Build Failed HARD',
      });
    });
  },
];

const config = {
  name: 'client',
  target: 'web',
  devtool: 'inline-source-map',
  entry: ['webpack-hot-middleware/client', 'react-hot-loader/patch', './client/src/entry/js/polyfills', './client/src/entry/js/client'],
  output: {
    filename: 'client/[name].js',
    chunkFilename: 'client/chunks/[name].chunk.js',
    path: path.join(__dirname, 'public/dist'),
    publicPath: '/dist/',
    pathinfo: true,
  },
  module: {
    rules,
  },
  plugins,
  resolve: {
    modules: roots,
  },
  resolveLoader: {
    modules: roots,
  },
  node: {
    fs: 'empty',
    net: 'empty',
    tls: 'empty',
  },
};

module.exports = config;

可能与您的 webpack.config 文件有关?您的条目中是否设置了热门内容?

const config = {
    entry: [
        'babel-polyfill',

        'react-hot-loader/patch', // activate HMR for React
        `webpack-hot-middleware/client?path=http://${HOST}:${PORT}/__webpack_hmr`, // bundle the client for webpack-hot-middleware and connect to the provided endpoint

        './src/client.jsx',
    ],

我使用 Hapi 作为带有 webpack-hot-middleware 和 webpack-dev-middleware 的服务器时运气更好。这是一个片段

import Webpack from 'webpack';
import HapiWebpackPlugin from 'hapi-webpack-plugin';

const config = require('../../../webpack.config.js'); // eslint-disable-line global-require
const compiler = Webpack(config);

const options = {
    assets: {
        // webpack-dev-middleware options - https://github.com/webpack/webpack-dev-middleware
        index: '/public/index.html',
    },
    hot: {
        // webpack-hot-middleware options - https://github.com/glenjamin/webpack-hot-middleware
    },
    compiler,
};

server.register({
    register: HapiWebpackPlugin,
    options,
}, (error) => {
    if (error) {
        console.error(error);
    }
});

如果你想尝试 Hapi,请查看我的hapi-react-hot-loader-example

这是我的全文webpack.config

const path = require('path');
const webpack = require('webpack');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ProgressBarPlugin = require('progress-bar-webpack-plugin');
const HtmlWebpackHarddiskPlugin = require('html-webpack-harddisk-plugin');
const RobotstxtPlugin = require('robotstxt-webpack-plugin').default;

const PORT = process.env.PORT || 3000;
const HOST = process.env.HOST || 'localhost';
const NODE_ENV = process.env.NODE_ENV || 'production';
const isProduction = (NODE_ENV === 'production');
const isDevelopment = (NODE_ENV === 'development');

const config = {
    entry: isDevelopment
        ? [
            'babel-polyfill',

            'react-hot-loader/patch', // activate HMR for React
            `webpack-hot-middleware/client?path=http://${HOST}:${PORT}/__webpack_hmr`, // bundle the client for webpack-hot-middleware and connect to the provided endpoint

            './src/client.jsx',
        ]
        : [
            'babel-polyfill',

            './src/client.jsx',
        ],

    resolve: {
        extensions: ['.js', '.jsx', '.json'],
    },

    output: {
        path: path.join(__dirname, 'dist/public/'),
        filename: isDevelopment
            ? 'main.js'
            : 'assets/scripts/[name].[chunkhash].js',
    },

    module: {
        rules: [
            {
                test: /\.css$/,
                use: ['css-hot-loader'].concat(
                    ExtractTextPlugin.extract({
                        fallback: 'style-loader',
                        use: [
                            {
                                loader: 'css-loader',
                                options: {minimize: true},
                            },
                            {
                                loader: 'postcss-loader',
                            },
                        ],
                    })
                ),
            },
            {
                test: /\.jsx?$/,
                use: ['babel-loader'],
                include: path.join(__dirname, 'src'),
            },
        ],
    },

    plugins: [
        new ProgressBarPlugin(),

        new webpack.DefinePlugin({
            'process.env.NODE_ENV': JSON.stringify(NODE_ENV),
        }),

        isDevelopment
            ? null
            : new webpack.optimize.ModuleConcatenationPlugin(),

        isDevelopment
            ? new webpack.HotModuleReplacementPlugin() // enable HMR globally
            : null,
        isDevelopment
            ? new webpack.NamedModulesPlugin() // prints more readable module names in the browser console on HMR updates
            : null,
        isDevelopment
            ? new webpack.NoEmitOnErrorsPlugin() // do not emit compiled assets that include errors
            : null,

        new ExtractTextPlugin({
            filename: isDevelopment
                ? 'assets/styles/main.css'
                : 'assets/styles/[name].[chunkhash].css',
        }),

        isDevelopment
            ? null
            : new webpack.optimize.CommonsChunkPlugin({
                name: 'vendor',
                minChunks: module => /node_modules/.test(module.resource),
            }),

        isDevelopment
            ? null
            : new webpack.optimize.CommonsChunkPlugin({name: 'manifest'}),

        isProduction
            ? new webpack.optimize.UglifyJsPlugin()
            : null,

        new HtmlWebpackPlugin({
            template: path.resolve(__dirname, 'src/index.html'),
            minify: isProduction ? {collapseWhitespace: true, collapseInlineTagWhitespace: true} : false,
            alwaysWriteToDisk: true,
        }),
        new HtmlWebpackHarddiskPlugin(),

        new CopyWebpackPlugin([
            {
                context: 'src/assets/media',
                from: '**/*',
                to: 'assets/media',
            },
        ]),

        new RobotstxtPlugin({
            policy: [
                isProduction
                    ? {userAgent: '*', allow: '/'}
                    : {userAgent: '*', disallow: '/'},
            ],
        }),
    ].filter(Boolean),

    devtool: isProduction
        ? 'none'
        : 'cheap-module-eval-source-map',

    performance: {
        maxAssetSize: 500000,
    },
};

module.exports = config;