You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何通过Webpack API实现ESM与Node.js的HMR功能

通过Webpack API实现NestJS的HMR功能

问题描述

我看过很多示例,知道怎么用Webpack CLI实现HMR,但想直接通过Webpack API实现却找不到可行方案。我尝试了以下代码:

const compiler = webpack(await createWebpackConfig(entry))

if (runDevServer) {
  compiler.watch(
    {
      aggregateTimeout: 1000,
    },
    (err, stats) => {
      process.stdout.write(`${stats.toString({ colors: true })}\n\n`)
    }
  )

同时把入口设置为['webpack/hot/poll?100', entry],但没生效。应用实例报错:

[HMR] Update failed: TypeError: fetch failed
    at Object.fetch (node:internal/deps/undici/undici:11457:11)

看起来是需要运行HMR服务器才能让应用接收更新,但我试了各种方法都没用,比如改入口加webpack-dev-server,或者直接实例化webpackDevServer(const server = new webpackDevServer({ hot: false, client: false }, compiler))都不行。是不是我的思路错了?

我的Webpack配置如下:

const isProduction = process.env.NODE_ENV === 'production'
const runDevServer = !!process.env.RUN_DEV_SERVER

export async function createWebpackConfig(entry: string): Promise<Configuration> {
  const packages = await findMonorepoPackages()

  const workspaces = packages.map(name => new RegExp(`^${name}(/.*)?$`))
  const allowedPackages = [...workspaces, /webpack\/hot\/poll\?100/]

  if (isProduction) {
    console.log('Making production build')
  }

  if (runDevServer) {
    console.log(`Going to run dev server of ${path.basename(entry, '.ts')}.mjs`)
  }
  const baseConfig: Configuration = {
    context: process.cwd(),
    watch: runDevServer,
    mode: isProduction ? 'production' : 'development',
    node: {
      __dirname: false,
      __filename: false,
    },
    devtool: 'source-map',
    ignoreWarnings: [
      {
        module: /@nestjs\//,
      },
    ],
    ...(runDevServer
      ? {
          devServer: {
            static: './dist',
          },
        }
      : {}),
    stats: {
      colors: true,
    },
    performance: {
      hints: false,
    },
    optimization: {
      minimize: false,
      splitChunks: {
        // include all types of chunks
        chunks: 'all',
      },
    },
    output: {
      globalObject: `typeof self !== 'undefined' ? self : this`,
      filename: '[name].mjs',
      path: path.join(process.cwd(), 'dist'),
      library: {
        // do not specify a `name` here
        type: 'module',
      },
    },
    experiments: {
      outputModule: true,
    },
    module: {
      rules: [
        {
          test: /.tsx?$/,
          use: {
            loader: 'swc-loader',
            options: {
              minify: isProduction,
              jsc: {
                target: 'es2022',
                parser: {
                  syntax: 'typescript',
                  decorators: true,
                  dynamicImport: true,
                },
                transform: {
                  legacyDecorator: true,
                  decoratorMetadata: true,
                },
              },
            },
          },
        },
      ],
    },
    plugins: [
      new BannerPlugin({
        banner: "import 'source-map-support/register.js';",
        raw: true,
        entryOnly: false,
      }),
      new BannerPlugin({
        banner: '/* eslint-disable */\n//prettier-ignore',
        raw: true,
      }),
      ...(runDevServer
        ? [
            new webpack.HotModuleReplacementPlugin(),
            new webpack.WatchIgnorePlugin({
              paths: [/\.js$/, /\.d\.ts$/],
            }),
            new RunScriptWebpackPlugin({ name: `${path.basename(entry, '.ts')}.mjs`, autoRestart: false }),
          ]
        : []),
    ],
    resolve: {
      extensions: ['.tsx', '.ts', '.js', '.mjs'],
    },
    externals: [
      'perf_hooks',
      'fs/promises',
      ({ request, context }, callback) => {
        if (
          /^[^/.][a-zA-Z\-0-9./]+$/.test(request) &&
          !allowedPackages.some(regexp => regexp.test(request)) &&
          !includedInBundleExternals.includes(request)
        ) {
          return callback(null, `module ${request}`)
        }

        // Continue without externalizing the import
        return callback()
      },
    ],
  }

  return {
    ...baseConfig,
    entry: {
      [path.basename(entry, '.ts')]: runDevServer ? ['webpack/hot/poll?100', entry] : entry,
    },
  }
}

我用的是标准NestJS应用:

import 'reflect-metadata'
import { NestFactory }          from '@nestjs/core'

import { QualificationsModule } from './qualifications.module'

async function bootstrap() {
  const app = await NestFactory.create(QualificationsModule)
  await app.listen(3000)

  if (module.hot) {
    module.hot.accept()
    module.hot.dispose(() => app.close())
  }
}

bootstrap()

解决方案

1. 明确Node.js HMR的两种模式

Node.js环境下HMR分两种模式:

  • 轮询模式:无需额外服务器,通过文件系统轮询检测更新(你用的webpack/hot/poll?100就属于这种)
  • WebSocket模式:依赖webpack-dev-server提供WebSocket服务推送更新

你遇到的fetch错误,核心原因是开启了outputModule: true(ES模块输出),而轮询模式的HMR逻辑在ES模块环境下存在兼容问题——module.hot的实现依赖CommonJS模块系统的特性,ES模块下无法正常工作。

2. 关键配置调整

(1)切换到CommonJS输出

关闭ES模块实验特性,改用CommonJS输出:

output: {
  filename: '[name].js', // 移除.mjs后缀,避免Node.js自动识别为ES模块
  path: path.join(process.cwd(), 'dist'),
  library: {
    type: 'commonjs2', // 改为CommonJS格式
  },
},
experiments: {
  outputModule: false, // 关闭ES模块输出
},

(2)简化Webpack API启动逻辑

轮询模式不需要启动webpack-dev-server,只需保持compiler.watch监听文件变更即可:

const webpack = require('webpack');
const createWebpackConfig = require('./path-to-your-config');

(async () => {
  const compiler = webpack(await createWebpackConfig('./src/main.ts'));

  if (process.env.RUN_DEV_SERVER) {
    compiler.watch(
      {
        aggregateTimeout: 1000,
        ignored: /node_modules/, // 忽略node_modules的变更
      },
      (err, stats) => {
        if (err) {
          console.error(err.stack || err);
          if (err.details) console.error(err.details);
          return;
        }
        process.stdout.write(`${stats.toString({ colors: true })}\n\n`);
      }
    );
  } else {
    // 生产构建逻辑
    compiler.run((err, stats) => {
      if (err) {
        console.error(err);
        return;
      }
      console.log(stats.toString({ colors: true }));
    });
  }
})();

(3)调整RunScriptWebpackPlugin配置

开启自动重启作为热替换失败的 fallback:

new RunScriptWebpackPlugin({ 
  name: `${path.basename(entry, '.ts')}.js`, 
  autoRestart: true 
}),

(4)确认externals配置

确保webpack/hot/poll?100没有被外部化,你的allowedPackages已经包含对应正则,无需修改,但要注意切换到CommonJS后路径解析是否正常。

3. 验证NestJS的HMR逻辑

确保bootstrap函数中的热替换处理正确,对于NestJS,还需保证模块变更能被正确捕获:

async function bootstrap() {
  const app = await NestFactory.create(QualificationsModule);
  await app.listen(3000);

  if (module.hot) {
    // 接受当前模块的更新
    module.hot.accept();
    // 热替换前关闭应用实例
    module.hot.dispose(async () => {
      await app.close();
    });
  }
}

bootstrap();

为什么之前的方法无效?

  • ES模块输出与HMR轮询模式不兼容,导致更新请求失败(fetch错误)
  • 尝试启动webpack-dev-server但配置不符合Node.js环境需求,WebSocket模式需要额外配置devServer.hot: 'only'及对应入口,反而不如轮询模式简洁

内容的提问来源于stack exchange,提问作者blits

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.19 02:07:13