热重载在 webpack 5.91.0 和 webpack-dev-server 5.0.4 上不起作用

问题描述 投票:0回答:1

将 webpack 从版本 4.46.0 更新到版本 5.91.0 后,热重载停止工作(不要与实时重载混淆)。 以前,热加载器可以通过库

@pmmmwh/react-refresh-webpack-plugin
工作。

webpack.config.js

const webpack = require('webpack');
const path = require('path');
const fs = require('fs');

require('dotenv').config();

const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const WorkboxPlugin = require('workbox-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');

// paths
const SOURCE = path.resolve(__dirname, 'src');
const SINK = path.resolve(__dirname, 'build');
const PUBLIC = path.resolve(__dirname, 'public');
const MODULES = path.resolve(__dirname, 'node_modules');
const PFX = path.join(__dirname, '/certificates/development.pfx');
const APPICONS = path.join(__dirname, '/electron/icons');

// variables
const analyzeBundle = process.argv.indexOf('-a') >= 0;
const isProd = process.argv.indexOf('-p') >= 0 || process.env.NODE_ENV === 'production';

const HOST = process.env.HOST || 'localhost';
const PORT = process.env.PORT || 3001;

const HTML_OPTIONS = Object.assign(
  {},
  {
    inject: true,
    template: path.resolve(__dirname, './public/index.ejs'),
    favicon: path.resolve(__dirname, './public/favicon.ico'),
  },
  isProd
    ? {
        minify: {
          removeComments: true,
          collapseWhitespace: true,
          removeRedundantAttributes: true,
          useShortDoctype: true,
          removeEmptyAttributes: true,
          removeStyleLinkTypeAttributes: true,
          keepClosingSlash: true,
          minifyJS: true,
          minifyCSS: true,
          minifyURLs: true,
        },
      }
    : undefined
);

module.exports = (env) => {
  const defaultLanguage = env && env.DEFAULT_LANGUAGE ? env.DEFAULT_LANGUAGE : 'en';
  const copyrightText = env && env.COPYRIGHT_TEXT ? env.COPYRIGHT_TEXT : '© Copyright placeholder';
  let productName = env && env.PRODUCT_NAME;
  let productEdition = env && env.PRODUCT_EDITION;
  const isElectronMode = env.PACKAGE_MODE === 'electron';

  if (!productEdition) {
    productEdition = productName ? '' : 'Develop';
  }
  if (!productName) {
    productName = 'ProductName';
  }
  let product = productName;
  if (productEdition !== '') {
    product += ` ${productEdition}`;
  }

  return {
    mode: !isProd ? 'development' : 'production',
    context: SOURCE,
    entry: {
      app: ['./index.tsx'],
      vendor: ['react', 'react-dom', 'lodash', 'webrtc-adapter', '@mui/material', '@mui/icons-material', '@mui/styles', 'reflect-metadata'],
    },
    output: {
      pathinfo: !isProd,
      filename: 'static/js/[name].[hash:8].bundle.js',
      chunkFilename: 'static/js/[name].[hash:8].bundle.js',
      path: SINK,
      publicPath: isElectronMode ? './' : '/',
      clean: true,
    },
    optimization: {
      moduleIds: 'named',
      minimize: true,
      minimizer: [
        new TerserPlugin({
          minify: TerserPlugin.swcMinify,
          parallel: false,
          include: SOURCE,
          exclude: /node_modules/,
          terserOptions: {
            sourceMap: !isProd,
          },
        }),
      ],
      splitChunks: {
        chunks: 'all',
        cacheGroups: {
          vendor: {
            chunks: 'initial',
            name: 'vendor',
            test: 'vendor',
            enforce: true,
          },
        },
      },
    },
    target: !isProd ? 'web' : 'browserslist',
    devtool: isProd ? 'source-map' : 'inline-cheap-module-source-map',
    resolve: {
      alias: {
        '@mui/styles': path.join(MODULES, '/@mui/styles'),
        '@App': SOURCE,
        Actions: path.join(SOURCE, '/actions/'),
        API: path.join(SOURCE, '/api/'),
        Common: path.join(SOURCE, '/common/'),
        Enums: path.join(SOURCE, '/enums/'),
        Factories: path.join(SOURCE, '/factories/'),
        Models: path.join(SOURCE, '/models/'),
        Reducers: path.join(SOURCE, '/reducers/'),
        Sagas: path.join(SOURCE, '/sagas/'),
        Selectors: path.join(SOURCE, '/selectors/'),
        Services: path.join(SOURCE, '/services/'),
        View: path.join(SOURCE, '/view/'),
        Translations: path.join(SOURCE, '/translations/'),
        Store: path.join(SOURCE, '/store/'),
      },
      extensions: ['.js', '.jsx', '.json', '.ts', '.tsx'],
      modules: [SOURCE, 'node_modules'],
      fallback: {
        url: false,
        crypto: false,
      },
    },
    module: {
      rules: [
        {
          test: /\.tsx?$/,
          exclude: MODULES,
          use: [
            {
              loader: 'ts-loader',
              options: {
                compilerOptions: {
                  exclude: ['node_modules', 'src/__tests__'],
                },
              },
            },
          ],
        },
        { enforce: 'pre', test: /\.js$/, loader: 'source-map-loader' },
        {
          test: /\.(jpg|jpeg|png|woff|woff2|eot|ttf|svg|ogg|mp3|wav)$/,
          use: [
            {
              loader: 'file-loader',
              options: {
                name: 'static/media/[name].[ext]',
              },
            },
          ],
        },
        {
          test: /\.css$/,
          use: [
            {
              loader: MiniCssExtractPlugin.loader,
              options: {
                hmr: !isProd,
                reloadAll: true,
              },
            },
            'css-loader',
          ],
        },
        {
          test: /\.(sa|sc)ss$/,
          use: [
            {
              loader: MiniCssExtractPlugin.loader,
              options: {
                hmr: !isProd,
                reloadAll: true,
              },
            },
            'css-loader',
            'postcss-loader',
            'sass-loader',
          ],
        },
      ],
    },
    plugins: [
      new CleanWebpackPlugin(),
      new MiniCssExtractPlugin({
        filename: 'static/css/[name].[hash:8].css',
        chunkFilename: 'static/css/[id].[hash:8].css',
      }),
      new HtmlWebpackPlugin(HTML_OPTIONS),
      new CopyWebpackPlugin({
        patterns: [
          {
            from: PUBLIC,
            to: SINK,
            globOptions: {
              ignore: ['index.ejs', 'manifest.json'],
            },
          },
          {
            from: path.resolve(PUBLIC, 'manifest.json'),
            to: path.resolve(SINK, 'manifest.json'),
            transform(content) {
              let manifest = JSON.parse(content);

              manifest['name'] = `${product} Web Client`;
              manifest['short_name'] = product;

              return JSON.stringify(manifest, null, 2);
            },
          },
          {
            from: APPICONS,
            to: path.join(SINK, '/icons'),
            toType: 'dir',
          },
        ],
      }),
      new WorkboxPlugin.InjectManifest({
        swSrc: path.join(__dirname, '/serviceWorker/service-worker.js'),
        maximumFileSizeToCacheInBytes: 50 * 1024 * 1024,
      }),
      new webpack.DefinePlugin({
        'process.env.DEFAULT_LANGUAGE': JSON.stringify(defaultLanguage),
        'process.env.COPYRIGHT_TEXT': JSON.stringify(copyrightText),
        'process.env.PRODUCT': JSON.stringify(product),
      }),
      analyzeBundle && new BundleAnalyzerPlugin(),
      !isProd && new webpack.HotModuleReplacementPlugin(),
      !isProd && new ReactRefreshWebpackPlugin(),
    ].filter(Boolean),
    watchOptions: {
      aggregateTimeout: 600,
      poll: 3000,
      ignored: [MODULES],
    },
    devServer: {
      watchFiles: SOURCE,
      liveReload: false,
      hot: true,
      static: {
        watch: false,
        staticOptions: {
          contentBase: PUBLIC,
        },
      },
      compress: true,
      devMiddleware: {
        publicPath: '/',
      },
      historyApiFallback: true,
      host: HOST,
      port: PORT,
      server: {
        type: 'https',
        options: {
          pfx: fs.readFileSync(PFX),
        },
      },
      client: {
        progress: true,
        logging: 'none',
        overlay: {
          warnings: false,
          errors: true,
        },
      },
    },
  };
};

package.json

{
  "name": "product_name1.0.0",
  "version": "1.0.0",
  "private": true,
  "main": "electron/main.js",
  "productName": "ProductName",
  "description": "ProductName Description",
  "author": {
    "name": "Company placeholder",
    "email": "[email protected]"
  },
  "homepage": ".",
  "dependencies": {
    "@emotion/react": "^11.11.4",
    "@emotion/styled": "^11.11.0",
    "@mui/icons-material": "^5.15.14",
    "@mui/lab": "^5.0.0-alpha.137",
    "@mui/material": "^5.15.14",
    "@mui/styles": "^5.15.14",
    "@mui/x-data-grid-pro": "^6.19.3",
    "@mui/x-date-pickers": "^6.19.3",
    "@mui/x-license-pro": "^6.10.2",
    "@mui/x-tree-view": "^6.17.0",
    "@pmmmwh/react-refresh-webpack-plugin": "^0.5.11",
    "@types/leaflet": "1.7.9",
    "@types/leaflet-editable": "^1.2.4",
    "@types/react-dnd": "^3.0.2",
    "@types/react-dom": "^18.2.0",
    "@types/react-leaflet": "^3.0.0",
    "@types/react-leaflet-markercluster": "^3.0.4",
    "@types/resize-observer-browser": "0.1.7",
    "again": "0.0.1",
    "axios": "^1.6.8",
    "class-validator": "0.11.0",
    "classnames": "^2.5.1",
    "cross-env": "7.0.3",
    "dayjs": "^1.11.10",
    "dnd-core": "^16.0.1",
    "file-saver": "2.0.5",
    "history": "4.10.1",
    "inversify": "^6.0.2",
    "inversify-inject-decorators": "3.1.0",
    "leaflet": "1.7.1",
    "leaflet-editable": "1.2.0",
    "lodash": "^4.17.21",
    "react": "^18.2.0",
    "react-color": "^2.19.3",
    "react-dnd": "10.0.2",
    "react-dnd-html5-backend": "10.0.2",
    "react-dnd-touch-backend": "10.0.2",
    "react-dom": "^18.2.0",
    "react-intl": "^6.6.4",
    "react-leaflet": "^4.2.1",
    "react-leaflet-editable": "^0.2.2",
    "react-leaflet-markercluster": "^3.0.0-rc1",
    "react-redux": "^7.1.3",
    "react-refresh": "^0.14.0",
    "react-resize-detector": "^7.1.2",
    "react-router": "5.1.2",
    "react-router-dom": "5.1.2",
    "react-virtualized": "^9.22.5",
    "redux": "4.0.5",
    "redux-devtools-extension": "^2.13.8",
    "redux-first-history": "^5.2.0",
    "redux-saga": "1.1.3",
    "reflect-metadata": "^0.2.2",
    "reselect": "^5.1.0",
    "tss-react": "^4.9.6",
    "typeface-roboto": "^1.1.13",
    "validator": "^13.11.0",
    "webrtc-adapter": "^9.0.1"
  },
  "scripts": {
    "clean:all": "npm run clean:build && npm run clean:modules && npm run clean:electron",
    "clean:build": "npx rimraf build",
    "clean:modules": "npx rimraf package-lock.json node_modules",
    "clean:electron": "npx rimraf dist",
    "start": "cross-env NODE_OPTIONS=\"--openssl-legacy-provider --max-old-space-size=4096\" webpack-dev-server --mode development --env PACKAGE_MODE=web --progress --color --open",
    "start:no-open": "cross-env NODE_OPTIONS=\"--openssl-legacy-provider --max-old-space-size=4096\" webpack-dev-server --mode development --env PACKAGE_MODE=electron --progress --color",
    "prebuild": "npm run lint:fix",
    "ci-build": "cross-env NODE_OPTIONS=\"--openssl-legacy-provider --max-old-space-size=4096\"  webpack --mode production --env PACKAGE_MODE=web --display-error-details --display-cached",
    "build": "cross-env NODE_OPTIONS=\"--openssl-legacy-provider --max-old-space-size=4096\" webpack --mode production --progress --env PACKAGE_MODE=web  --colors --display-error-details --display-cached",
    "build:electron": "cross-env NODE_OPTIONS=\"--openssl-legacy-provider --max-old-space-size=4096\" webpack --mode production --env PACKAGE_MODE=electron --progress --colors --display-error-details --display-cached",
    "ci-build:electron": "cross-env NODE_OPTIONS=\"--openssl-legacy-provider --max-old-space-size=4096\" webpack --mode production --env PACKAGE_MODE=electron --colors --display-error-details --display-cached",
    "build:analyze": "webpack --mode production --progress --colors --display-error-details --display-cached -a",
    "test": "jest --detectOpenHandles --logHeapUsage",
    "test:watch": "jest --detectOpenHandles --watch",
    "test:prod": "npm run lint:fix && npm run test -- --no-cache",
    "test:gui": "majestic --app",
    "ci-test": "jest --detectOpenHandles --env=jsdom --collectCoverage",
    "lint": "eslint \"src/**/*.{ts,tsx}\" --config .eslintrc.json",
    "lint:fix": "npm run lint -- --fix",
    "prettier:base": "prettier \"src/**/*.{ts,tsx}\"",
    "prettier:check": "npm run prettier:base -- --list-different",
    "prettier:write": "npm run prettier:base -- --write --list-different",
    "electron": "electron .",
    "electron:start": "concurrently  \"npm run start:no-open\" \"wait-on https://localhost:3001 && electron . \"",
    "electron:build-deb": "electron-builder build --linux --publish never",
    "electron:build-win": "electron-builder build --win --publish never",
    "electron:rebuild-win": "npm run build:electron && electron-builder build --win --publish never"
  },
  "husky": {
    "hooks": {
      "pre-commit": "lint-staged",
      "post-commit": "git update-index --again"
    }
  },
  "lint-staged": {
    "*.{ts,tsx}": [
      "npm run prettier:write",
      "npm run lint:fix",
      "git add"
    ]
  },
  "devDependencies": {
    "@babel/core": "7.24.3",
    "@babel/preset-env": "7.24.3",
    "@cfaester/enzyme-adapter-react-18": "^0.7.1",
    "@testing-library/jest-dom": "5.1.1",
    "@testing-library/react": "9.4.0",
    "@types/classnames": "2.2.9",
    "@types/enzyme": "3.10.5",
    "@types/file-saver": "2.0.6",
    "@types/google-maps": "3.2.1",
    "@types/googlemaps": "3.39.2",
    "@types/history": "4.7.5",
    "@types/jest": "26.0.14",
    "@types/jsdom": "12.2.4",
    "@types/lodash": "4.14.149",
    "@types/mocha": "10.0.5",
    "@types/node": "^20.12.6",
    "@types/react": "^17.0.67",
    "@types/react-color": "3.0.1",
    "@types/react-hot-loader": "4.1.1",
    "@types/react-redux": "7.1.7",
    "@types/react-resize-detector": "4.2.0",
    "@types/react-router": "5.1.4",
    "@types/react-router-dom": "5.1.3",
    "@types/react-test-renderer": "^18.0.7",
    "@types/react-virtualized": "9.21.8",
    "@types/redux": "3.6.0",
    "@types/redux-logger": "3.0.7",
    "@types/redux-mock-store": "1.0.2",
    "@types/webpack-env": "^1.18.4",
    "@types/webrtc": "0.0.26",
    "@typescript-eslint/eslint-plugin": "5.62.0",
    "@typescript-eslint/parser": "5.62.0",
    "autoprefixer": "9.7.4",
    "babel-core": "6.26.3",
    "babel-preset-env": "1.7.0",
    "clean-webpack-plugin": "^4.0.0",
    "concurrently": "8.2.1",
    "copy-webpack-plugin": "^12.0.2",
    "css-loader": "^7.1.1",
    "dotenv": "8.2.0",
    "electron": "26.3.0",
    "electron-builder": "24.6.4",
    "enzyme": "3.11.0",
    "enzyme-to-json": "3.4.4",
    "eslint": "7.32.0",
    "eslint-config-prettier": "6.10.0",
    "eslint-config-react": "1.1.7",
    "eslint-plugin-prettier": "3.1.2",
    "eslint-plugin-react": "7.32.2",
    "eslint-plugin-react-hooks": "4.3.0",
    "file-loader": "^6.2.0",
    "html-webpack-plugin": "^5.6.0",
    "husky": "4.2.3",
    "identity-obj-proxy": "3.0.0",
    "jest": "26.6.3",
    "jest-dom": "4.0.0",
    "jest-environment-jsdom": "25.1.0",
    "jest-environment-jsdom-fourteen": "1.0.1",
    "jest-enzyme": "7.1.2",
    "jsdom": "16.1.0",
    "lint-staged": "10.0.7",
    "majestic": "1.6.2",
    "mini-css-extract-plugin": "0.9.0",
    "mock-browser": "0.92.14",
    "mock-socket": "9.0.3",
    "npm-force-resolutions": "0.0.3",
    "npm-run-all": "4.1.5",
    "postcss-loader": "^8.1.1",
    "prettier": "^1.19.1",
    "react-dnd-test-backend": "10.0.2",
    "react-test-renderer": "^18.2.0",
    "react-testing-library": "8.0.1",
    "redux-mock-store": "1.5.4",
    "redux-saga-test-plan": "4.0.0-rc.3",
    "rimraf": "5.0.5",
    "sass-loader": "^14.1.1",
    "source-map-loader": "^5.0.0",
    "style-loader": "^4.0.0",
    "terser-webpack-plugin": "^5.3.10",
    "testcafe": "1.8.1",
    "ts-jest": "26.5.4",
    "ts-loader": "^9.5.1",
    "ts-node": "10.6.0",
    "typescript": "^5.4.4",
    "wait-on": "7.0.1",
    "web-audio-test-api": "0.5.2",
    "webpack": "^5.91.0",
    "webpack-bundle-analyzer": "^4.10.2",
    "webpack-cli": "^5.1.4",
    "webpack-dev-server": "^5.0.4",
    "webpack-node-externals": "^3.0.0",
    "workbox-webpack-plugin": "^7.0.0"
  },
  "build": {
    "appId": "",
    "productName": "Product name",
    "copyright": "2023 Company placeholder",
    "win": {
      "icon": "/build/icons/appIcon-256.png",
      "target": [
        {
          "target": "nsis",
          "arch": [
            "x64"
          ]
        }
      ]
    },
    "linux": {
      "target": [
        "deb",
        "rpm"
      ],
      "icon": "build/icons"
    },
    "deb": {},
    "rpm": {},
    "files": [
      "build",
      "electron",
      "!node_modules"
    ],
    "extraResources": [
      {
        "from": "build/static/media",
        "to": "static/media"
      }
    ],
    "extends": null
  },
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  }
}

index.tsx

import { createBrowserHistory } from 'history';
import * as React from 'react';
import * as ReactDOM from 'react-dom/client';
import { Provider } from 'react-redux';
import 'typeface-roboto';

import 'react-virtualized/styles.css';
import 'leaflet/dist/leaflet.css';
import './index.css';

import { ElectronApiInit } from './electron/ElectronApi';
import { loadRootState } from './store/AppStateStore/AppStateStore';
import configureStore from './store/configureStore';
import { IRootState } from './store/IRootState';
import App from './view/App';

const state: IRootState = loadRootState();
const history = createBrowserHistory();
const store = configureStore(history, state);

ElectronApiInit();

const render = (): void => {
  ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
    <Provider store={store}>
      <App history={history} />
    </Provider>
  );
};

if (module.hot !== undefined) {
  module.hot.accept('./view/App', render);
}

service-worker.js

workbox.skipWaiting();
workbox.clientsClaim();
workbox.precaching.precacheAndRoute(self.__WB_MANIFEST);
workbox.routing.registerNavigationRoute('/index.html');

workbox.precaching.precacheAndRoute(
  self.__precacheManifest.concat([
    'https://fonts.googleapis.com/css?family=Source+Serif+Pro:400,700',
    'https://unpkg.com/[email protected]/umd/react.production.min.js',
    'https://unpkg.com/[email protected]/umd/react-dom.production.min.js',
    'https://unpkg.com/[email protected]/umd/react-router-dom.min.js',
  ])
);

顺便说一句,构建项目时,出现警告:

InjectManifest 中的警告已被多次调用,可能是由于在 --watch 模式下运行 webpack 所致。第一次调用后生成的预缓存清单可能不准确!请参阅 https://github.com/GoogleChrome/workbox/issues/1790 了解更多信息。

也许这会以某种方式影响热重载?

做了什么:

devServer.hot
属性设置为
true
。 将
devServer.liveReload
属性设置为
false
。 连接
webpack.HotModuleReplacementPlugin()
插件(尽管在 webpack 版本 5 中不再需要它,因为将 hot 设置为 true 就足够了)。 将
index.tsx
文件更改为调用
module.hot.accept()

webpack webpack-dev-server webpack-4 webpack-5 hot-reload
1个回答
0
投票

@pmmmwh/react-refresh-webpack-plugin 未受影响一年后,该包现在已准备好用于采用对等依赖项:) 版本 0.5.13(2024 年 4 月 28 日)已发布。

npm i @pmmmwh/react-refresh-webpack-plugin@latest

然后

npm i [email protected]
© www.soinside.com 2019 - 2024. All rights reserved.