无法让 React Hot Loader 工作

Can't get React Hot Loader to work

我正在尝试将 react-hot-loader 配置到我的应用程序中。它基于 webpack 2、webpack-dev-middleware、browser-sync、express 和服务器端渲染。

这是我的 webpack.config

require('dotenv').config()
import path from 'path'
import webpack from 'webpack'
import extend from 'extend'
import AssetsPlugin from 'assets-webpack-plugin'
import ExtractTextPlugin from 'extract-text-webpack-plugin'
import bundles from '../src/bundles'
import merge from 'lodash/merge'
import fs from 'fs'

const isDebug = !process.argv.includes('--release');
const isVerbose = process.argv.includes('--verbose');
const GLOBALS = {
  'process.env.NODE_ENV': isDebug ? '"development"' : '"production"',
  'process.env.MORTGAGE_CALCULATOR_API': process.env.MORTGAGE_CALCULATOR_API ? `"${process.env.MORTGAGE_CALCULATOR_API}"` : null,
  'process.env.API_HOST': process.env.BROWSER_API_HOST ? `"${process.env.BROWSER_API_HOST}"` : process.env.API_HOST ? `"${process.env.API_HOST}"` : null,
  'process.env.GOOGLE_ANALYTICS_ID': process.env.GOOGLE_ANALYTICS_ID ? `"${process.env.GOOGLE_ANALYTICS_ID}"` : null,
  'process.env.OMNITURE_SUITE_ID': process.env.OMNITURE_SUITE_ID ? `"${process.env.OMNITURE_SUITE_ID}"` : null,
  'process.env.COOKIE_DOMAIN': process.env.COOKIE_DOMAIN ? `"${process.env.COOKIE_DOMAIN}"` : null,
  'process.env.FEATURE_FLAG_BAZAAR_VOICE': process.env.FEATURE_FLAG_BAZAAR_VOICE ? `"${process.env.FEATURE_FLAG_BAZAAR_VOICE}"` : null,
  'process.env.FEATURE_FLAG_REALTIME_RATING': process.env.FEATURE_FLAG_REALTIME_RATING ? `"${process.env.FEATURE_FLAG_REALTIME_RATING}"` : null,
  'process.env.SALE_RESULTS_PAGE_FLAG': process.env.SALE_RESULTS_PAGE_FLAG ? `"${process.env.SALE_RESULTS_PAGE_FLAG}"` : null,
  'process.env.SALE_RELOADED_RESULTS_PAGE_FLAG': process.env.SALE_RELOADED_RESULTS_PAGE_FLAG ? `"${process.env.SALE_RELOADED_RESULTS_PAGE_FLAG}"` : null,
  'process.env.TRACKER_DOMAIN': process.env.TRACKER_DOMAIN ? `"${process.env.TRACKER_DOMAIN}"` : null,
  'process.env.USER_SERVICE_ENDPOINT': process.env.USER_SERVICE_ENDPOINT ? `"${process.env.USER_SERVICE_ENDPOINT}"` : null,
  __DEV__: isDebug
};

//
// Common configuration chunk to be used for both
// client-side (client.js) and server-side (server.js) bundles
// -----------------------------------------------------------------------------

const config = {
  output: {
    publicPath: '/blaze-assets/',
  },

  cache: isDebug,

  stats: {
    colors: true,
    reasons: isDebug,
    hash: isVerbose,
    version: isVerbose,
    timings: true,
    chunks: isVerbose,
    chunkModules: isVerbose,
    cached: isVerbose,
    cachedAssets: isVerbose,
  },

  plugins: [
    new ExtractTextPlugin({
      filename: isDebug ? '[name].css' : '[name].[chunkhash].css',
      allChunks: true,
    }),
    new webpack.LoaderOptionsPlugin({
      minimize: true,
      debug: isDebug,
    }),
  ],

  resolve: {
    extensions: ['.webpack.js', '.web.js', '.js', '.jsx', '.json'],
    modules: [
      path.resolve('./src'),
      'node_modules',
    ]
  },

  module: {
    rules: [
      {
        test: /\.jsx?$/,
        include: [
          path.resolve(__dirname, '../src'),
        ],
        loader: 'babel-loader',
      }, {
        test: /\.(scss|css)$/,
        exclude: ['node_modules'],
        use: ExtractTextPlugin.extract({
          fallback: 'style-loader',
          use: [
            'css-loader',
            'sass-loader',
          ]
        })
      }, {
        test: /\.txt$/,
        loader: 'raw-loader',
      }, {
        test: /\.(otf|png|jpg|jpeg|gif|svg|woff|woff2)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
        loader: 'url-loader?limit=10000',
      }, {
        test: /\.(eot|ttf|wav|mp3)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
        loader: 'file-loader',
      }, {
        test: /\.jade$/,
        loader: 'jade-loader',
      }
    ],
  },
};

//
// Configuration for the client-side bundles
// -----------------------------------------------------------------------------
let clientBundles = {}

Object.keys(bundles).forEach(function (bundle) {
  clientBundles[bundle] = [
    'bootstrap-loader',
    `./src/bundles/${bundle}/index.js`
  ]
})

merge(
  clientBundles,
  {
    'embedWidget': ['./src/components/Widgets/EmbedWidget/widgetLoader.js']
  },
)

const clientConfig = extend(true, {}, config, {
  entry: clientBundles,
  output: {
    path: path.join(__dirname, '../build/public/blaze-assets/'),
    filename: isDebug ? '[name].js' : '[name].[chunkhash].js',
    chunkFilename: isDebug ? '[name].chunk.js' : '[name].[chunkhash].chunk.js',
  },
  node: {
    fs: "empty"
  },
  // Choose a developer tool to enhance debugging
  // http://webpack.github.io/docs/configuration.html#devtool
  // devtool: isDebug ? 'cheap-module-source-map' : false,
  plugins: [
    ...config.plugins,
    ...(isDebug ? [
        new webpack.EvalSourceMapDevToolPlugin({
          filename: '[file].map',
          exclude: /\.(css)($)/i,
        }),
      ] : []),
    new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
    new webpack.DefinePlugin({
      ...GLOBALS,
      'process.env.BROWSER': true
    }),
    ...(!isDebug ? [
      new webpack.optimize.UglifyJsPlugin({
        sourceMap: true,
        compress: {
          // jscs:disable requireCamelCaseOrUpperCaseIdentifiers
          screw_ie8: true,

          // jscs:enable requireCamelCaseOrUpperCaseIdentifiers
          warnings: isVerbose,
          unused: true,
          dead_code: true,
        },
      }),
      new webpack.optimize.AggressiveMergingPlugin(),
    ] : []),
    new AssetsPlugin({
      path: path.join(__dirname, '../build'),
      filename: 'assets.json',
      prettyPrint: true,
    }),
  ],
});

//
// Configuration for the server-side bundle (server.js)
// -----------------------------------------------------------------------------

var srcDirs = {};
fs.readdirSync('src').forEach(function(path) {
  srcDirs[path] = true
});

function isExternalFile(context, request, callback) {
  var isExternal = request.match(/^[@a-z][a-z\/\.\-0-9]*$/i) && !srcDirs[request.split("/")[0]]
  callback(null, Boolean(isExternal));
}

const serverConfig = extend(true, {}, config, {
  entry: './src/server.js',
  output: {
    path: path.join(__dirname, '../build/public/blaze-assets/'),
    filename: '../../server.js',
    libraryTarget: 'commonjs2',
  },
  target: 'node',
  externals: [
    /^\.\/assets\.json$/,
    isExternalFile
  ],
  node: {
    console: false,
    global: false,
    process: false,
    Buffer: false,
    __filename: false,
    __dirname: false,
  },
  devtool: isDebug ? 'cheap-module-source-map' : 'source-map',
  plugins: [
    ...config.plugins,
    new webpack.DefinePlugin({
      ...GLOBALS,
      'process.env.BROWSER': false,
      'process.env.API_HOST': process.env.API_HOST ? `"${process.env.API_HOST}"` : null
    }),
    new webpack.NormalModuleReplacementPlugin(/\.(scss|css|eot|ttf|woff|woff2)$/, 'node-noop'),
    new webpack.BannerPlugin({
      banner: `require('dotenv').config(); require('newrelic'); require('source-map-support').install();`,
      raw: true,
      entryOnly: false
    })
  ],
});

export default [clientConfig, serverConfig];

start.js 文件

import Browsersync from 'browser-sync'
import webpack from 'webpack'
import webpackMiddleware from 'webpack-dev-middleware'
import webpackHotMiddleware from 'webpack-hot-middleware'
import WriteFilePlugin from 'write-file-webpack-plugin'
import run from './run'
import runServer from './runServer'
import webpackConfig from './webpack.config'
import clean from './clean'
import copy from './copy'

const isDebug = !process.argv.includes('--release')
const [, serverConfig] = webpackConfig
process.argv.push('--watch')
/**
 * Launches a development web server with "live reload" functionality -
 * synchronizing URLs, interactions and code changes across multiple devices.
 */
async function start () {
  await run(clean)
  await run(copy.bind(undefined, { watch: true }))
  await new Promise((resolve) => {

    serverConfig.plugins.push(new WriteFilePlugin({ log: false }))
    // Patch the client-side bundle configurations
    // to enable Hot Module Replacement (HMR) and React Transform
    webpackConfig.filter((x) => x.target !== 'node').forEach((config) => {
      /* eslint-disable no-param-reassign */
      Object.keys(config.entry).forEach((entryKey) => {
        if (!Array.isArray(config.entry[entryKey])) {
          config.entry[entryKey] = [config.entry[entryKey]]
        }
        config.entry[entryKey].unshift('react-hot-loader/patch', 'webpack-hot-middleware/client')
      })
      if (config.output.filename) {
        config.output.filename = config.output.filename.replace('[chunkhash]', '[hash]')
      }
      if (config.output.chunkFilename) {
        config.output.chunkFilename = config.output.chunkFilename.replace('[chunkhash]', '[hash]')
      }
      config.plugins.push(new webpack.HotModuleReplacementPlugin())
      config.plugins.push(new webpack.NoEmitOnErrorsPlugin())
      config
        .module
        .rules
        .filter((x) => x.loader === 'babel-loader')
        .forEach((x) => (x.query = {
          ...x.query,
          cacheDirectory: true,
          presets: [
            ['es2015', {modules: false}],
            'stage-0',
            'react'
          ],
          plugins: [
            ...(x.query ? x.query.plugins : []),
            [
              'react-hot-loader/babel',
            ],
          ],
        }))
      /* eslint-enable no-param-reassign */
    })

    const bundler = webpack(webpackConfig)
    const wpMiddleware = webpackMiddleware(bundler, {

      // IMPORTANT: webpack middleware can't access config,
      // so we should provide publicPath by ourselves
      publicPath: webpackConfig[0].output.publicPath,

      // Pretty colored output
      stats: webpackConfig[0].stats,

      // For other settings see
      // https://webpack.github.io/docs/webpack-dev-middleware
    })
    const hotMiddleware = webpackHotMiddleware(bundler.compilers[0])

    let handleBundleComplete = async () => {
      handleBundleComplete = (stats) => !stats.stats[1].compilation.errors.length && runServer()

      const server = await runServer()
      const bs = Browsersync.create()

      bs.init({
        ...isDebug ? {} : { notify: false, ui: false },

        proxy: {
          target: server.host,
          middleware: [wpMiddleware, hotMiddleware],
          proxyOptions: {
            xfwd: true,
          },
        },
        open: false,
        files: ['build/content/**/*.*'],
      }, resolve)
    }

    bundler.plugin('done', (stats) => handleBundleComplete(stats))
  })
}

export default start

对于热负载减速器,我已经像这样更改了我的代码,

const configureStore = (initialState = {}) => {
  const store = createStore(
    // storage.reducer is what merges storage state into redux state
    storage.reducer(rootReducer),
    inflateDecorators(initialState),
    applyMiddleware(...middleware)
  )

  if (module.hot) {
    module.hot.accept('./reducers', () => {
      const nextRootReducer = require('./reducers')

      store.replaceReducer(nextRootReducer)
    })
  }

  return store
}

我们有多个包,所以有一个共同的函数来处理入口逻辑,

import 'babel-polyfill'
import React from 'react'
import ReactDOM from 'react-dom'
import FastClick from 'fastclick'
import { Provider } from 'react-redux'
import { setUrl } from 'actions'
import Location from '../../libs/Location'
import configureStore, { loadFromLocalStorage } from '../../configureStore'
import { AppContainer } from 'react-hot-loader'

const initialState = window.__INITIAL_STATE__

const store = configureStore(initialState)

function runner (createBody) {
  return function () {
    // Make taps on links and buttons work fast on mobiles
    FastClick.attach(document.body)

    const component = (
     <AppContainer>
      <Provider store={store}>
        {createBody()}
      </Provider>
    </AppContainer>
    )

    if (!initialState) {
      store.dispatch(setUrl(`${Location.location.pathname}${Location.location.search}`))
    }

    Location.listen((location) => {
      store.dispatch(setUrl(`${location.pathname}${location.search}`))
    })

    ReactDOM.render(component, document.getElementById('app'))

    // only apply stored state after first render
    // this allows serverside and clientside rendering to agree on initial state
    loadFromLocalStorage(store)
  }
}

export default function run (createBody) {
  if (['complete', 'loaded', 'interactive'].includes(document.readyState) && document.body) {
    runner(createBody)()
  } else {
    document.addEventListener('DOMContentLoaded', runner(createBody), false)
  }
}

这是调用上述公共函数的包入口点之一,

import run from '../util/run'
import createBody from './body'

run(createBody)
if (module.hot) {
  module.hot.accept('./body', () => run(createBody))
}

不确定我还缺少什么,我尝试关注一些博客 post 并对热加载程序文档做出反应,但我无法让它工作。

认为这对具有类似设置的人会有所帮助,即使在 webpack 2 中也可以让 React 热加载器工作你的入口点代码应该是这样的,

if (module.hot) {
  module.hot.accept('./body', () => {
    const body = require('./body').default
    run(body)
  })
}

由于我正在使用 extract-text-webpack-plugin,因此无法热重载 css 更改。因为我已经在使用 browser-sync,简单的解决方法是使用 write-file-webpack-plugin 写出 css 文件并让 browser-sync 监听变化。