我们目前正在部署与Webpack捆绑在一起的React应用程序。我们的Webpack配置如下。
我们正在经历一些 - 对我来说 - 非常奇怪的缓存行为;每次部署后,我们的用户都被迫进行硬刷新。如果未进行硬刷新,则会显示index.html
模板,并且不会加载JS。
这种情况不会每次都发生,但它足以成为一个问题。
我们的index.html
头脑中有以下内容:
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
<meta http-equiv="Pragma" content="no-cache" />
<meta http-equiv="Expires" content="0" />
index.html基于模板,我们使用HtmlPlugin在编译时注入(散列)脚本和样式表。
的index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
<meta http-equiv="Pragma" content="no-cache" />
<meta http-equiv="Expires" content="0" />
<title>...</title>
<meta name="description" content="...">
<link rel="shortcut icon" type="image/x-icon" href="..." />
<link href="/portal/styles/app.d4e88655c7001362ca091666c25a9284.css" rel="stylesheet">
</head>
<body>
<div id="root">Loading...</div>
<script type="text/javascript" src="/portal/scripts/vendor.a2b9a0c858a0cf1aac42.js"></script>
<script type="text/javascript" src="/portal/scripts/app.42d841eebfd48ad70010.js"></script>
</body>
</html>
如webpack.config中所示,我们在构建时散列每个文件名。
我的下一步行动(也许)不会将JS分成app
和vendor
,但我宁愿不这样做。
编辑:添加HTML响应标题
编辑2:这主要发生在Windows上的Chrome中,但我们已经看到它发生在OSX上的Safari和Chrome中。
编辑3:添加了有关HTML文件的信息。
HTML响应标题:
Cache-Control:no-cache
Cache-Control:max-age=0, no-cache, no-store, must-revalidate
Connection:keep-alive
Content-Encoding:gzip
Content-Length:821
Content-Type:text/html
Date:Wed, 17 Jan 2018 17:00:09 GMT
Expires:Wed, 17 Jan 2018 17:00:08 GMT
Last-Modified:Wed, 17 Jan 2018 11:15:46 GMT
Pragma:no-cache
Server:nginx/1.7.9
Webpack配置:
const config = {
entry: {
app: [
'babel-polyfill',
'./src/portal/js/App.js'
],
vendor: [
'axios',
'es6-promise',
'prop-types',
'react',
'react-dom',
'react-ga',
'react-redux',
'react-router',
'redux',
'redux-logger',
'redux-thunk',
'shortid',
]
}, // entry point
output: {
filename: 'scripts/[name].[chunkhash].js', // output filename with hash'd filename
path: path.resolve(__dirname, 'dist/portal'),
publicPath: '/portal/'
},
module: {
rules: [
{test: /\.json$/, loader: 'json-loader'},
{
test: /\.(js|jsx)$/,
use: [{loader: 'babel-loader'}],
exclude: /node_modules/
},
{
test: /\.(scss|css)$/,
use: ExtractTextPlugin.extract({ // extract all styles to a file instead of inlining
use: [
{loader: "css-loader", options: {sourceMap: (isDev || isStaging)}},
{loader: "sass-loader", options: {sourceMap: (isDev || isStaging)}}
],
fallback: "style-loader"
})
},
{
test: /\.(jpe?g|png|gif|svg)$/i,
loaders: [
'file-loader?hash=sha512&digest=hex&name=assets/images/[hash].[ext]',
{
loader: 'image-webpack-loader',
query: {
mozjpeg: {progressive: true},
gifsicle: {interlaced: false},
optipng: {optimizationLevel: 4},
pngquant: {quality: '75-90', speed: 3}
}
}
]
},
{
test: /\.woff?(\?v=[0-9]\.[0-9]\.[0-9])?$/,
use: ['url-loader?name=[name].[ext]&limit=100000&mimetype=application/font-woff&name=assets/fonts/[name].[ext]']
},
{
test: /\.woff2?(\?v=[0-9]\.[0-9]\.[0-9])?$/,
use: ['url-loader?name=[name].[ext]&limit=100000&mimetype=application/font-woff2&name=assets/fonts/[name].[ext]']
},
{
test: /\.(ttf|eot)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
use: ['file-loader?name=[name].[ext]&limit=100000&mimetype=application/octet-stream&name=assets/fonts/[name].[ext]']
},
{
test: /\.otf(\?.*)?$/,
loader: 'file-loader?name=[name].[ext]&limit=10000&mimetype=font/opentype&name=assets/fonts/[name].[ext]'
}
]
},
devtool: 'source-map',
plugins: function() {
// plugins to be used for dev, staging and prod
let basePlugins = [
// new CleanPlugin(['dist']),
new WebpackMd5Hash(),
new webpack.ProvidePlugin({
'Promise': 'es6-promise'
}),
new ExtractTextPlugin({
filename: "styles/[name].[contenthash].css",
allChunks: true,
}),
new webpack.optimize.CommonsChunkPlugin({
names: ['vendor'],
minChunks: Infinity,
filename: `scripts/[name].js`
}),
];
// only prod and staging but not dev - things like minification and copying static files
if (isProduction || isStaging && !isDev) {
basePlugins.push(
new webpack.optimize.UglifyJsPlugin({
sourceMap: true,
mangle: true,
compress: {
warnings: false
},
output: {
comments: false,
}
}),
new OptimizeCssAssetsPlugin(),
new FileManagerPlugin({
onStart: [
{
delete: [
'dist'
]
}
],
onEnd: [
{
copy: [
// static html
{ source: 'src/index.html', destination: 'dist/index.html' },
{ source: 'src/success.html', destination: 'dist/success.html' },
// static css
{ source: 'src/styles', destination: 'dist/styles' },
// hide contango build for now until we're ready
{ source: 'src/book/public', destination: 'dist/book' }
]
}
]
}),
new webpack.optimize.CommonsChunkPlugin({
names: ['vendor'],
minChunks: Infinity,
filename: `scripts/[name].[chunkhash].js`
})
);
}
if (isProduction) {
basePlugins.push(
new HtmlPlugin({
cache: true,
template: 'src/portal/index.prod.html'
}),
new webpack.DefinePlugin({
'process.env.NODE_ENV': '"production"',
API_ENV: '"production"'
})
);
} else if (isStaging) {
basePlugins.push(
new HtmlPlugin({
cache: true,
template: 'src/portal/index.staging.html'
}),
new webpack.DefinePlugin({
'process.env.NODE_ENV': '"production"',
API_ENV: '"staging"'
})
);
} else if (isDev) {
basePlugins.push(
new HtmlPlugin({
template: 'src/portal/index.staging.html'
}),
new webpack.DefinePlugin({
'process.env.NODE_ENV': '"staging"',
API_ENV: '"staging"'
})
);
}
return basePlugins;
}(),
resolve: {
modules: [
path.join(__dirname, "src/portal"),
"node_modules",
]
}
};