如何通过Webpack API实现ESM与Node.js的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

