CRA创建的React应用Service Worker报Unexpected token '<'错误
这个报错的核心逻辑很明确:当新版本Service Worker还处于等待激活状态时,旧Service Worker仍然在控制当前页面,你刷新页面后,旧Service Worker会去请求已经被新版本构建删除的带hash的JS/CSS chunk文件,而服务端的SPA fallback规则把找不到的静态资源请求都返回了index.html,浏览器把HTML内容当成JS执行,就出现了Unexpected token '<'和MIME类型不匹配的错误。
具体触发流程:
- CRA的生产构建产物里所有静态chunk都带唯一hash,每次发新版本都会生成新hash的chunk,旧hash的chunk默认会被清理掉。
- 你轮询检测到新版本的时候,新的Service Worker只是安装完成处于等待激活状态,只要没调用
skipWaiting,旧Service Worker就会一直接管当前打开的页面。 - 这时候你手动刷新页面,旧Service Worker先返回缓存里的旧版index.html,这个HTML里引用的都是旧hash的chunk地址。
- 旧Service Worker请求这些旧chunk的时候,服务端找不到文件就触发SPA fallback,返回了index.html的内容,自然就报语法错。
几个可直接落地的方案,按优先级选就行:
1. 改服务端的fallback规则(最根本的解法)
把服务端的配置改了,只有非静态资源的请求才fallback返回index.html,/static/路径下的所有静态资源找不到就直接返回404,不要返回HTML。
这样就算旧Service Worker请求了已经删除的旧chunk,只会拿到404,不会出现JS语法错误,你可以再加个全局监听,遇到静态资源加载失败就自动触发更新:
// 全局监听资源加载错误 window.addEventListener('error', (e) => { // 只处理JS和CSS加载失败的情况 if (['SCRIPT', 'LINK'].includes(e.target?.tagName)) { navigator.serviceWorker.ready.then(reg => { // 给等待状态的新SW发消息让它跳过等待 reg.waiting?.postMessage({ type: 'SKIP_WAITING' }); }); } }, true)
2. 部署时保留历史版本的静态资源
每次发新版本不要直接清空之前的静态文件,至少留最近2个版本的/static/目录下的chunk,这样旧Service Worker请求旧chunk的时候能正常拿到资源,不会触发fallback,用户下次打开页面就会自动切到新版本。
3. 优化更新提示的交互
在onUpdate弹出更新提示之后加个超时逻辑,比如用户30秒没点更新按钮,就自动调用skipWaiting激活新SW再刷新页面,避免用户手动刷新触发报错。
4. 调整SW的缓存策略
如果你修改了CRA默认的Workbox配置,可以把静态资源的缓存规则从默认的CacheFirst(缓存优先)改成NetworkFirst(网络优先),或者给静态资源缓存加个短的过期时间,降低旧缓存命中的概率。
内容的提问来源于stack exchange,提问作者mordecai

