我有一个单页应用,我在每条路线上进行代码分割。当我部署新版本的应用时,如果用户仍然打开该页面并访问之前未访问过的路由,则用户通常会收到错误。
如果应用程序启用了服务工作程序,则还会出现这种情况。当用户在新部署后访问页面时,服务工作者将从缓存中提供服务。然后,如果用户尝试访问不在其缓存中的页面,他们将获得块加载失败。
目前我在我的应用程序中禁用了代码拆分以避免这种情况,但我一直很好奇处理这个问题的最佳方法是什么。我已经考虑过在用户完成加载初始页面后预加载所有其他路由,我相信这可能会解决路由上代码拆分的问题。但是,假设我想对组件进行代码拆分,那么这意味着我必须尝试确定何时以及如何预加载所有这些组件。
所以我想知道人们如何处理单页应用程序的这个问题?谢谢! (我目前正在使用create-react-app)
答案 0 :(得分:6)
我更喜欢让用户刷新而不是自动刷新(这避免了无限刷新循环错误的可能性)。
以下策略对于React应用程序来说效果很好,代码是按路线拆分的:
将index.html设置为从不缓存。这样可以确保请求您的初始资产的主文件始终是新文件(并且通常不大,因此不进行缓存就不会成为问题)。参见MDN Cache Control。
对块使用一致的块哈希。这样可以确保只有更改的块才会具有不同的哈希。 (请参见下面的webpack.config.js代码段)
在部署时不要使CDN的缓存无效,因此在部署新版本时,旧版本不会丢失其块。
在路线之间导航时检查应用程序版本,以通知用户它们是否在旧版本上运行并请求刷新。
最后,以防万一发生ChunkLoadError :添加一个错误边界。 (请参见下面的错误边界)
optimization: {
moduleIds: 'hashed',
splitChunks: {
cacheGroups: {
default: false,
vendors: false,
// vendor chunk
vendor: {
name: 'vendor',
// async + async chunks
chunks: 'all',
// import file path containing node_modules
test: /node_modules/,
priority: 20
},
}
}
import React, { Component } from 'react'
export default class ErrorBoundary extends Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
// Update state so the next render will show the fallback UI.
return { hasError: true, error };
}
componentDidCatch(error, errorInfo) {
// You can also log the error to an error reporting service
console.error('Error Boundary Caught:', error, errorInfo);
}
render() {
const {error, hasError} = this.state
if (hasError) {
// You can render any custom fallback UI
return <div>
<div>
{error.name === 'ChunkLoadError' ?
<div>
This application has been updated, please refresh your browser to see the latest content.
</div>
:
<div>
An error has occurred, please refresh and try again.
</div>}
</div>
</div>
}
return this.props.children;
}
}
注意:请确保在内部导航事件中清除错误(例如,如果您使用react-router),否则错误边界将持续通过内部导航,并且只会在实际导航或页面刷新时消失
答案 1 :(得分:1)
create-react-app中的问题是脚本标记所引用的块不存在,因此在我们的index.html中引发了错误。这是我们得到的错误。
Uncaught SyntaxError: Unexpected token < 9.70df465.chunk.js:1
让它工作的方法是在index.html中所有脚本标签上方添加一个window.onerror函数。
<script>
window.onerror = function (message, source, lineno, colno, error) {
if (error && error.name === 'SyntaxError') {
window.location.reload(true);
}
};
</script>
我希望有一个更好的方法,但是这是我能想到的最好的方法,并认为这是一个非常安全的解决方案,因为create-react-app将不会编译或生成任何语法错误,这应该是唯一的方法出现语法错误的情况。
答案 2 :(得分:1)
我正在使用AsyncComponent HOC延迟加载块,并且遇到了相同的问题。 我要做的工作是确定错误并进行一次硬重新加载。
.catch(error => {
if (error.toString().indexOf('ChunkLoadError') > -1) {
console.log('[ChunkLoadError] Reloading due to error');
window.location.reload(true);
}
});
完整的HOC文件如下:
export default class Async extends React.Component {
componentWillMount = () => {
this.cancelUpdate = false;
this.props.load
.then(c => {
this.C = c;
if (!this.cancelUpdate) {
this.forceUpdate();
}
})
.catch(error => {
if (error.toString().indexOf('ChunkLoadError') > -1) {
console.log('[ChunkLoadError] Reloading due to error');
window.location.reload(true);
}
});
};
componentWillUnmount = () => {
this.cancelUpdate = true;
};
render = () => {
const props = this.props;
return this.C ? (
this.C.default ? (
<this.C.default {...props} />
) : (
<this.C {...props} />
)
) : null;
};
}
答案 3 :(得分:0)
尽管非常简单,但我们还是用一个难看的方法解决了这个问题。目前可能暂时还可以,但可能会对某人有所帮助。
我们创建了一个AsyncComponent来加载块(即路由组件)。当该组件加载一个块并接收到错误时,我们只需执行一次简单的页面重新加载即可更新index.html及其对主块的引用。难看的原因是,根据页面的外观或加载方式,用户可能会在刷新之前看到空白页面的短暂闪烁。可能会造成刺激,但这也许是因为我们不希望SPA自发刷新。
App.js
// import the component for the route just like you would when
// doing async components
const ChunkedRoute = asyncComponent(() => import('components/ChunkedRoute'))
// use it in the route just like you normally would
<Route path="/async/loaded/route" component={ChunkedRoute} />
asyncComponent.js
import React, { Component } from 'react'
const asyncComponent = importComponent => {
return class extends Component {
state = {
component: null,
}
componentDidMount() {
importComponent()
.then(cmp => {
this.setState({ component: cmp.default })
})
.catch(() => {
// if there was an error, just refresh the page
window.location.reload(true)
})
}
render() {
const C = this.state.component
return C ? <C {...this.props} /> : null
}
}
}
export default asyncComponent