Node 18+React+Express渲染Gzip静态资源时内容解码失败200
问题:升级Node 18与Webpack 5后静态资源加载异常(net::Err_content_decoding_failed)
将React+Express应用升级至Node 18和Webpack 5后,执行yarn dev时控制台出现错误failed to load resource: net::Err_content_decoding_failed,应用CSS无法正常加载。
移除代码中的etag header,将原配置:
app.use(express.static('public', {...config.cacheConfig, setHeaders}));
改为:
app.set('Content-Encoding', 'gzip') app.use(express.static('public' ))
后错误消失,但CSS/JS仍无法正常工作。
附当前Webpack配置代码:
const { merge } = require('webpack-merge'); const webpack = require('webpack'); const { DefinePlugin, IgnorePlugin } = require('webpack'); const CopyWebpackPlugin = require('copy-webpack-plugin'); const LoadablePlugin = require('@loadable/webpack-plugin'); const CompressionPlugin = require('compression-webpack-plugin'); function getEnvironmentConfig(envConfig, env) { return {}; } const copyPlugin = new CopyWebpackPlugin( { patterns: [{ from: './src/public/favicon.ico' }] }, { copyUnmodified: true, }, ); const reactLoadablePlugin = new LoadablePlugin({ filename: '../loadable.json', }); const DashboardPlugin = require('webpack-dashboard/plugin'); const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer'); const commonConfig = require('./webpack.base'); /** * Gets the Webpack configuration for the React bundle. * * @param {{ ENV: string }} env * The Webpack environment. */ const compressionPlugin = new CompressionPlugin({ test: /\.(js|map|css|json|html)$/i, algorithm: 'gzip', // [path] is a path relative to output.path exclude: '../loadable-stats.json', minRatio: Number.MAX_SAFE_INTEGER, }); function reactConfig(env) { return merge( commonConfig(env), { target: 'web', devtool: 'source-map', entry: { main: path.resolve(__dirname, 'src/client.js'), landingPage: path.resolve( __dirname, 'src/mycomponent.js', ), Wrapper: path.resolve(__dirname, 'src/wrapper.js'), ), }, output: { filename: '[name].js', path: path.resolve(__dirname, 'public'), publicPath: '', }, module: { rules: [ { test: /\.css$/, use: [ { loader: 'style-loader', }, { loader: 'css-loader', options: { url: false, }, }, ], }, ], }, plugins: [ new webpack.ProvidePlugin({ process: 'process/browser', }), reactLoadablePlugin, new DefinePlugin({ 'process.env.WEBPACK_TARGET': JSON.stringify('web'), 'process.env.FLAVOR': JSON.stringify(env.BUILD_ENV), }), new IgnorePlugin({ resourceRegExp: /^\.\/cpme\/content/, contextRegExp: /@some-library\/component\/dist$/, }), compressionPlugin, ], optimization: { splitChunks: { chunks: 'initial', maxInitialRequests: 1, }, }, }, getEnvironmentConfig( { prod: { plugins: [copyPlugin], }, dev: { plugins: [ copyPlugin, new DashboardPlugin(), new BundleAnalyzerPlugin(), ], }, }, env, ), ); } module.exports = (env = { BUILD_ENV: 'dev' }) => { if (!env.BUILD_ENV || env.BUILD_ENV === '$FLAVOR') { env.BUILD_ENV = 'dev'; } const config = [reactConfig(env)]; if (env.WEBPACK_LOG_LEVEL === 'VERBOSE') { console.log( 'Final config', JSON.stringify( config, (key, value) => value instanceof RegExp ? `[RegExp(${value.toString()})]` : value, 2, ), ); } return config; };
解决方案
1. 开发环境禁用CompressionPlugin
CompressionPlugin主要用于生产环境预生成压缩文件,开发环境启用会导致Express静态资源服务无法正确匹配原始文件与.gz文件,引发解码错误。修改Webpack配置,仅在生产环境加载该插件:
// 修改reactConfig中的plugins数组 plugins: [ new webpack.ProvidePlugin({ process: 'process/browser', }), reactLoadablePlugin, new DefinePlugin({ 'process.env.WEBPACK_TARGET': JSON.stringify('web'), 'process.env.FLAVOR': JSON.stringify(env.BUILD_ENV), }), new IgnorePlugin({ resourceRegExp: /^\.\/cpme\/content/, contextRegExp: /@some-library\/component\/dist$/, }), // 仅在生产环境添加compressionPlugin ...(env.BUILD_ENV === 'prod' ? [compressionPlugin] : []), ],
2. 用express-static-gzip修复静态资源响应头
如果需要保留压缩功能,使用express-static-gzip替代原生express.static,它会自动识别.gz文件并设置正确的响应头:
首先安装依赖:
yarn add express-static-gzip
然后修改Express配置:
const expressStaticGzip = require('express-static-gzip'); // 替换原express.static调用 app.use(expressStaticGzip('public', { ...config.cacheConfig, enableBrotli: false, // 不需要brotli压缩可关闭 customCompressions: [{ encodingName: 'gzip', fileExtension: '.gz' }] }));
3. 修复原setHeaders函数(保留ETag场景)
如果原setHeaders函数导致ETag与压缩文件冲突,调整逻辑确保为.gz文件添加对应响应头:
const setHeaders = (res, path) => { // 保留原缓存配置的ETag逻辑 if (config.cacheConfig.setHeaders) { config.cacheConfig.setHeaders(res, path); } // 为.gz文件设置正确的编码与类型 if (path.endsWith('.gz')) { res.setHeader('Content-Encoding', 'gzip'); if (path.endsWith('.css.gz')) { res.setHeader('Content-Type', 'text/css'); } else if (path.endsWith('.js.gz')) { res.setHeader('Content-Type', 'application/javascript'); } } }; app.use(express.static('public', {...config.cacheConfig, setHeaders}));
4. 校验Webpack路径配置
确保Webpack的output.publicPath与Express静态资源访问路径一致,当前publicPath: ''为相对路径,需确认Express服务的静态资源根目录正确指向public文件夹。
内容的提问来源于stack exchange,提问作者uidev76
相关产品推荐
相关产品推荐

