diff --git a/src/router/chunk-load-error.js b/src/router/chunk-load-error.js new file mode 100644 index 00000000..062743a3 --- /dev/null +++ b/src/router/chunk-load-error.js @@ -0,0 +1,66 @@ +const chunkLoadErrorReloadKey = 'vue-element-admin:chunk-load-error-reload' +const chunkLoadErrorRetryWindow = 10 * 1000 + +export function isChunkLoadError(error) { + if (!error) { + return false + } + + const name = error.name || '' + const message = error.message || String(error) + + return name === 'ChunkLoadError' || + /Loading (CSS )?chunk [\w-]+ failed/i.test(message) || + /ChunkLoadError/i.test(message) +} + +export function shouldReloadForChunkLoadError({ + href = window.location.href, + storage = window.sessionStorage, + now = Date.now() +} = {}) { + if (!storage) { + return true + } + + let previousReload + + try { + previousReload = JSON.parse(storage.getItem(chunkLoadErrorReloadKey) || 'null') + } catch (error) { + previousReload = null + } + + if ( + previousReload && + previousReload.href === href && + now - previousReload.time < chunkLoadErrorRetryWindow + ) { + return false + } + + try { + storage.setItem(chunkLoadErrorReloadKey, JSON.stringify({ href, time: now })) + } catch (error) { + // Ignore storage failures; a single reload is still the best recovery path. + } + + return true +} + +export function handleChunkLoadError(error, { + href = window.location.href, + storage = window.sessionStorage, + reload = window.location.replace.bind(window.location), + now = Date.now() +} = {}) { + if (!isChunkLoadError(error)) { + return false + } + + if (shouldReloadForChunkLoadError({ href, storage, now })) { + reload(href) + } + + return true +} diff --git a/src/router/index.js b/src/router/index.js index 2be959d2..4b1e5e85 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -1,5 +1,6 @@ import Vue from 'vue' import Router from 'vue-router' +import { handleChunkLoadError } from './chunk-load-error' Vue.use(Router) @@ -395,6 +396,10 @@ const createRouter = () => new Router({ const router = createRouter() +router.onError(error => { + handleChunkLoadError(error) +}) + // Detail see: https://github.com/vuejs/vue-router/issues/1234#issuecomment-357941465 export function resetRouter() { const newRouter = createRouter() diff --git a/tests/unit/router/chunk-load-error.spec.js b/tests/unit/router/chunk-load-error.spec.js new file mode 100644 index 00000000..e892c06a --- /dev/null +++ b/tests/unit/router/chunk-load-error.spec.js @@ -0,0 +1,48 @@ +import { + handleChunkLoadError, + isChunkLoadError, + shouldReloadForChunkLoadError +} from '@/router/chunk-load-error' + +describe('router chunk-load error handling', () => { + test('detects Webpack chunk load failures', () => { + expect(isChunkLoadError(new Error('Loading chunk 12 failed.'))).toBe(true) + expect(isChunkLoadError(new Error('Loading CSS chunk app failed.'))).toBe(true) + expect(isChunkLoadError({ name: 'ChunkLoadError', message: 'missing' })).toBe(true) + expect(isChunkLoadError(new Error('NavigationDuplicated'))).toBe(false) + }) + + test('allows one reload for the same url in the retry window', () => { + const storage = window.sessionStorage + const href = 'http://localhost/#/dashboard' + + storage.clear() + + expect(shouldReloadForChunkLoadError({ href, storage, now: 1000 })).toBe(true) + expect(shouldReloadForChunkLoadError({ href, storage, now: 2000 })).toBe(false) + expect(shouldReloadForChunkLoadError({ href, storage, now: 12000 })).toBe(true) + }) + + test('reloads the current page for a chunk load failure', () => { + const reload = jest.fn() + const storage = window.sessionStorage + const href = 'http://localhost/#/permission/page' + + storage.clear() + + expect(handleChunkLoadError(new Error('Loading chunk 1 failed.'), { + href, + storage, + reload, + now: 1000 + })).toBe(true) + expect(reload).toHaveBeenCalledWith(href) + }) + + test('ignores unrelated router errors', () => { + const reload = jest.fn() + + expect(handleChunkLoadError(new Error('NavigationDuplicated'), { reload })).toBe(false) + expect(reload).not.toHaveBeenCalled() + }) +})