Browse Source

feat: add offline-friendly error handling and network status awareness

Add ErrorBoundary to catch render errors (including chunk load failures from
React.lazy), replacing the previous white-screen crash with a friendly Result
page. Add a network status model (useModel('network')) and OfflineBanner that
shows when the user goes offline. Make request error messages offline-aware.
Clean up dead PWA/ServiceWorker code in global.tsx that never fired.

- ErrorBoundary: catches all render errors, distinguishes chunk load failures
  (offline vs online messages), uses antd Result with retry/home buttons
- OfflineBanner: Alert banner in ProLayout content area when offline
- network model: navigator.onLine + online/offline event listeners
- requestErrorConfig: offline-aware error messages in errorHandler
- global.tsx: remove dead sw.offline/sw.updated event listeners
- defaultSettings.ts: remove unused pwa flag
- locales: replace dead PWA i18n keys with network/error keys (8 locales)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
pull/11756/head
afc163 4 months ago
parent
commit
83c00f474e
  1. 2
      config/defaultSettings.ts
  2. 2
      config/md-raw-loader.cjs
  3. 7
      src/app.tsx
  4. 79
      src/components/ErrorBoundary/index.tsx
  5. 20
      src/components/OfflineBanner/index.tsx
  6. 5
      src/components/index.ts
  7. 93
      src/global.tsx
  8. 18
      src/locales/bn-BD/pwa.ts
  9. 19
      src/locales/en-US/pwa.ts
  10. 19
      src/locales/fa-IR/pwa.ts
  11. 19
      src/locales/id-ID/pwa.ts
  12. 19
      src/locales/ja-JP/pwa.ts
  13. 19
      src/locales/pt-BR/pwa.ts
  14. 15
      src/locales/zh-CN/pwa.ts
  15. 16
      src/locales/zh-TW/pwa.ts
  16. 20
      src/models/network.ts
  17. 13
      src/requestErrorConfig.ts

2
config/defaultSettings.ts

@ -4,7 +4,6 @@ import type { ProLayoutProps } from '@ant-design/pro-components';
* @name
*/
const Settings: ProLayoutProps & {
pwa?: boolean;
logo?: string;
} = {
navTheme: 'light',
@ -16,7 +15,6 @@ const Settings: ProLayoutProps & {
fixSiderbar: true,
colorWeak: false,
title: 'Ant Design Pro',
pwa: true,
logo: 'https://gw.alipayobjects.com/zos/rmsportal/KDpgvguMpGfqaHPjicRK.svg',
iconfontUrl: '',
token: {

2
config/md-raw-loader.cjs

@ -1,4 +1,4 @@
module.exports = function mdRawLoader(source) {
this.cacheable?.();
return `export default ${JSON.stringify(String(source))};`;
};
};

7
src/app.tsx

@ -13,8 +13,10 @@ dayjs.extend(relativeTime);
import {
AvatarDropdown,
DocLink,
ErrorBoundary,
Footer,
LangDropdown,
OfflineBanner,
VersionDropdown,
} from '@/components';
import { currentUser as queryCurrentUser } from '@/services/ant-design-pro/api';
@ -147,6 +149,7 @@ export const layout: RunTimeLayoutConfig = ({
// if (initialState?.loading) return <PageLoading />;
return (
<>
<OfflineBanner />
{children}
<SettingDrawer
disableUrlParams
@ -182,3 +185,7 @@ export const request: RequestConfig = {
baseURL: isDev ? '' : 'https://pro-api.ant-design-demo.workers.dev',
...errorConfig,
};
export function rootContainer(container: React.ReactNode) {
return <ErrorBoundary>{container}</ErrorBoundary>;
}

79
src/components/ErrorBoundary/index.tsx

@ -0,0 +1,79 @@
import { getIntl } from '@umijs/max';
import { Button, Card, Result } from 'antd';
import React from 'react';
function isChunkLoadError(error: Error): boolean {
return (
error.name === 'ChunkLoadError' ||
/loading (?:css )?chunk/i.test(error.message) ||
/imported module/i.test(error.message)
);
}
interface ErrorBoundaryState {
hasError: boolean;
error: Error | null;
}
export default class ErrorBoundary extends React.Component<
{ children: React.ReactNode },
ErrorBoundaryState
> {
state: ErrorBoundaryState = { hasError: false, error: null };
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
return { hasError: true, error };
}
componentDidCatch(error: Error, info: React.ErrorInfo) {
console.error('[ErrorBoundary]', error, info.componentStack);
}
handleRetry = () => {
if (this.state.error && isChunkLoadError(this.state.error)) {
window.location.reload();
} else {
this.setState({ hasError: false, error: null });
}
};
render() {
if (!this.state.hasError || !this.state.error) {
return this.props.children;
}
const { error } = this.state;
const intl = getIntl();
const isOffline = typeof navigator !== 'undefined' && !navigator.onLine;
const isChunkError = isChunkLoadError(error);
const title = intl.formatMessage({
id: isChunkError ? 'app.error.chunk.title' : 'app.error.render.title',
});
const subTitle = intl.formatMessage({
id: isChunkError
? isOffline
? 'app.error.chunk.description.offline'
: 'app.error.chunk.description.online'
: 'app.error.render.description',
});
return (
<Card variant="borderless">
<Result
status="error"
title={title}
subTitle={subTitle}
extra={[
<Button type="primary" key="retry" onClick={this.handleRetry}>
{intl.formatMessage({ id: 'app.error.retry' })}
</Button>,
<a href="/" key="home">
<Button>{intl.formatMessage({ id: 'app.error.home' })}</Button>
</a>,
]}
/>
</Card>
);
}
}

20
src/components/OfflineBanner/index.tsx

@ -0,0 +1,20 @@
import { useIntl, useModel } from '@umijs/max';
import { Alert } from 'antd';
const OfflineBanner: React.FC = () => {
const { isOnline } = useModel('network');
const intl = useIntl();
if (isOnline) return null;
return (
<Alert
type="warning"
banner
closable={false}
message={intl.formatMessage({ id: 'app.network.offline' })}
/>
);
};
export default OfflineBanner;

5
src/components/index.ts

@ -14,6 +14,11 @@ import { AvatarDropdown } from './RightContent/AvatarDropdown';
*/
export { default as ArticleListContent } from './ArticleListContent';
export { default as AvatarList } from './AvatarList';
/**
*
*/
export { default as ErrorBoundary } from './ErrorBoundary';
export { default as OfflineBanner } from './OfflineBanner';
export { default as StandardFormRow } from './StandardFormRow';
export { default as TagSelect } from './TagSelect';

93
src/global.tsx

@ -1,94 +1 @@
import './tailwind.css';
import { useIntl } from '@umijs/max';
import { Button, message, notification } from 'antd';
import defaultSettings from '../config/defaultSettings';
const { pwa } = defaultSettings;
const isHttps = document.location.protocol === 'https:';
const clearCache = () => {
// remove all caches
if (window.caches) {
caches
.keys()
.then((keys) => {
keys.forEach((key) => {
caches.delete(key);
});
})
.catch((e) => console.log(e));
}
};
// if pwa is true
if (pwa) {
// Notify user if offline now
window.addEventListener('sw.offline', () => {
message.warning(useIntl().formatMessage({ id: 'app.pwa.offline' }));
});
// Pop up a prompt on the page asking the user if they want to use the latest version
window.addEventListener('sw.updated', (event: Event) => {
const e = event as CustomEvent;
const reloadSW = async () => {
// Check if there is sw whose state is waiting in ServiceWorkerRegistration
// https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration
const worker = e.detail?.waiting;
if (!worker) {
return true;
}
// Send skip-waiting event to waiting SW with MessageChannel
await new Promise((resolve, reject) => {
const channel = new MessageChannel();
channel.port1.onmessage = (msgEvent) => {
if (msgEvent.data.error) {
reject(msgEvent.data.error);
} else {
resolve(msgEvent.data);
}
};
worker.postMessage({ type: 'skip-waiting' }, [channel.port2]);
});
clearCache();
window.location.reload();
return true;
};
const key = `open${Date.now()}`;
const btn = (
<Button
type="primary"
onClick={() => {
notification.destroy(key);
reloadSW();
}}
>
{useIntl().formatMessage({ id: 'app.pwa.serviceworker.updated.ok' })}
</Button>
);
notification.open({
title: useIntl().formatMessage({ id: 'app.pwa.serviceworker.updated' }),
description: useIntl().formatMessage({
id: 'app.pwa.serviceworker.updated.hint',
}),
btn,
key,
onClose: async () => null,
});
});
} else if ('serviceWorker' in navigator && isHttps) {
// unregister service worker
const { serviceWorker } = navigator;
if (serviceWorker.getRegistrations) {
serviceWorker.getRegistrations().then((sws) => {
sws.forEach((sw) => {
sw.unregister();
});
});
}
serviceWorker.getRegistration().then((sw) => {
if (sw) sw.unregister();
});
clearCache();
}

18
src/locales/bn-BD/pwa.ts

@ -1,7 +1,15 @@
export default {
'app.pwa.offline': 'আপনি এখন অফলাইন',
'app.pwa.serviceworker.updated': 'নতুন সামগ্রী উপলব্ধ',
'app.pwa.serviceworker.updated.hint':
'বর্তমান পৃষ্ঠাটি পুনরায় লোড করতে দয়া করে "রিফ্রেশ" বোতাম টিপুন',
'app.pwa.serviceworker.updated.ok': 'রিফ্রেশ',
'app.network.offline': 'আপনি এখন অফলাইন। কিছু বৈশিষ্ট্য অনুপলব্ধ হতে পারে।',
'app.error.chunk.title': 'পৃষ্ঠা লোড করতে ব্যর্থ',
'app.error.chunk.description.offline':
'আপনার নেটওয়ার্ক সংযোগ বিচ্ছিন্ন হয়েছে। আপনার সংযোগ পরীক্ষা করুন এবং পৃষ্ঠাটি রিফ্রেশ করুন।',
'app.error.chunk.description.online':
'পৃষ্ঠা সম্পদ লোড করতে ব্যর্থ। দয়া করে রিফ্রেশ করুন এবং আবার চেষ্টা করুন।',
'app.error.render.title': 'কিছু ভুল হয়েছে',
'app.error.render.description':
'দুঃখিত, এই পৃষ্ঠায় একটি ত্রুটি ঘটেছে। দয়া করে রিফ্রেশ করুন বা হোম পৃষ্ঠায় ফিরে যান।',
'app.error.retry': 'রিফ্রেশ',
'app.error.home': 'হোমে ফিরে যান',
'app.request.offline':
'নেটওয়ার্ক অনুপলব্ধ। আপনার সংযোগ পরীক্ষা করুন এবং আবার চেষ্টা করুন।',
};

19
src/locales/en-US/pwa.ts

@ -1,7 +1,16 @@
export default {
'app.pwa.offline': 'You are offline now',
'app.pwa.serviceworker.updated': 'New content is available',
'app.pwa.serviceworker.updated.hint':
'Please press the "Refresh" button to reload current page',
'app.pwa.serviceworker.updated.ok': 'Refresh',
'app.network.offline':
'You are currently offline. Some features may be unavailable.',
'app.error.chunk.title': 'Failed to load page',
'app.error.chunk.description.offline':
'Your network connection has been lost. Please check your connection and refresh.',
'app.error.chunk.description.online':
'Page resources failed to load. Please refresh and try again.',
'app.error.render.title': 'Something went wrong',
'app.error.render.description':
'Sorry, an error occurred on this page. Please refresh or go back to the home page.',
'app.error.retry': 'Refresh',
'app.error.home': 'Back Home',
'app.request.offline':
'Network unavailable. Please check your connection and try again.',
};

19
src/locales/fa-IR/pwa.ts

@ -1,7 +1,16 @@
export default {
'app.pwa.offline': 'شما اکنون آفلاین هستید',
'app.pwa.serviceworker.updated': 'مطالب جدید در دسترس است',
'app.pwa.serviceworker.updated.hint':
'لطفاً برای بارگیری مجدد صفحه فعلی ، دکمه "تازه سازی" را فشار دهید',
'app.pwa.serviceworker.updated.ok': 'تازه سازی',
'app.network.offline':
'شما اکنون آفلاین هستید. برخی از قابلیت‌ها ممکن است در دسترس نباشند.',
'app.error.chunk.title': 'بارگذاری صفحه ناموفق بود',
'app.error.chunk.description.offline':
'اتصال شبکه شما قطع شده است. اتصال خود را بررسی کنید و صفحه را بارگذاری مجدد کنید.',
'app.error.chunk.description.online':
'بارگذاری منابع صفحه ناموفق بود. لطفاً صفحه را بارگذاری مجدد کنید.',
'app.error.render.title': 'خطایی رخ داد',
'app.error.render.description':
'متأسفانه، در این صفحه خطایی رخ داد. لطفاً صفحه را بارگذاری مجدد کنید یا به صفحه اصلی بازگردید.',
'app.error.retry': 'بارگذاری مجدد',
'app.error.home': 'بازگشت به صفحه اصلی',
'app.request.offline':
'شبکه در دسترس نیست. لطفاً اتصال خود را بررسی و دوباره تلاش کنید.',
};

19
src/locales/id-ID/pwa.ts

@ -1,7 +1,16 @@
export default {
'app.pwa.offline': 'Koneksi anda terputus',
'app.pwa.serviceworker.updated': 'Konten baru sudah tersedia',
'app.pwa.serviceworker.updated.hint':
'Silahkan klik tombol "Refresh" untuk memuat ulang halaman ini',
'app.pwa.serviceworker.updated.ok': 'Memuat ulang',
'app.network.offline':
'Anda sedang offline. Beberapa fitur mungkin tidak tersedia.',
'app.error.chunk.title': 'Gagal memuat halaman',
'app.error.chunk.description.offline':
'Koneksi jaringan terputus. Periksa koneksi Anda dan muat ulang halaman.',
'app.error.chunk.description.online':
'Gagal memuat sumber daya halaman. Muat ulang dan coba lagi.',
'app.error.render.title': 'Terjadi kesalahan',
'app.error.render.description':
'Maaf, terjadi kesalahan pada halaman ini. Muat ulang halaman atau kembali ke beranda.',
'app.error.retry': 'Muat Ulang',
'app.error.home': 'Kembali ke Beranda',
'app.request.offline':
'Jaringan tidak tersedia. Periksa koneksi Anda dan coba lagi.',
};

19
src/locales/ja-JP/pwa.ts

@ -1,7 +1,16 @@
export default {
'app.pwa.offline': 'あなたは今オフラインです',
'app.pwa.serviceworker.updated': '新しいコンテンツが利用可能です',
'app.pwa.serviceworker.updated.hint':
'現在のページをリロードするには、「更新」ボタンを押してください',
'app.pwa.serviceworker.updated.ok': 'リフレッシュ',
'app.network.offline':
'現在オフラインです。一部の機能が利用できない場合があります',
'app.error.chunk.title': 'ページの読み込みに失敗しました',
'app.error.chunk.description.offline':
'ネットワーク接続が切断されています。接続を確認してページを再読み込みしてください。',
'app.error.chunk.description.online':
'ページリソースの読み込みに失敗しました。ページを再読み込みしてください。',
'app.error.render.title': 'エラーが発生しました',
'app.error.render.description':
'申し訳ありません、ページでエラーが発生しました。ページを再読み込みするか、ホームに戻ってください。',
'app.error.retry': '再読み込み',
'app.error.home': 'ホームに戻る',
'app.request.offline':
'ネットワークに接続できません。接続を確認して再試行してください。',
};

19
src/locales/pt-BR/pwa.ts

@ -1,7 +1,16 @@
export default {
'app.pwa.offline': 'Você está offline agora',
'app.pwa.serviceworker.updated': 'Novo conteúdo está disponível',
'app.pwa.serviceworker.updated.hint':
'Por favor, pressione o botão "Atualizar" para recarregar a página atual',
'app.pwa.serviceworker.updated.ok': 'Atualizar',
'app.network.offline':
'Você está offline no momento. Alguns recursos podem estar indisponíveis.',
'app.error.chunk.title': 'Falha ao carregar a página',
'app.error.chunk.description.offline':
'Sua conexão de rede foi perdida. Verifique sua conexão e atualize a página.',
'app.error.chunk.description.online':
'Falha ao carregar os recursos da página. Atualize e tente novamente.',
'app.error.render.title': 'Algo deu errado',
'app.error.render.description':
'Desculpe, ocorreu um erro nesta página. Atualize a página ou volte para a página inicial.',
'app.error.retry': 'Atualizar',
'app.error.home': 'Voltar ao Início',
'app.request.offline':
'Rede indisponível. Verifique sua conexão e tente novamente.',
};

15
src/locales/zh-CN/pwa.ts

@ -1,6 +1,13 @@
export default {
'app.pwa.offline': '当前处于离线状态',
'app.pwa.serviceworker.updated': '有新内容',
'app.pwa.serviceworker.updated.hint': '请点击“刷新”按钮或者手动刷新页面',
'app.pwa.serviceworker.updated.ok': '刷新',
'app.network.offline': '当前处于离线状态,部分功能可能不可用',
'app.error.chunk.title': '页面加载失败',
'app.error.chunk.description.offline':
'网络连接已断开,请检查网络后刷新页面。',
'app.error.chunk.description.online': '页面资源加载失败,请刷新页面重试。',
'app.error.render.title': '页面出现错误',
'app.error.render.description':
'抱歉,页面遇到了一些问题,请刷新页面或返回首页。',
'app.error.retry': '刷新页面',
'app.error.home': '返回首页',
'app.request.offline': '网络不可用,请检查网络连接后重试。',
};

16
src/locales/zh-TW/pwa.ts

@ -1,6 +1,14 @@
export default {
'app.pwa.offline': '當前處於離線狀態',
'app.pwa.serviceworker.updated': '有新內容',
'app.pwa.serviceworker.updated.hint': '請點擊“刷新”按鈕或者手動刷新頁面',
'app.pwa.serviceworker.updated.ok': '刷新',
'app.network.offline': '當前處於離線狀態,部分功能可能不可用',
'app.error.chunk.title': '頁面載入失敗',
'app.error.chunk.description.offline':
'網路連線已中斷,請檢查網路後重新整理頁面。',
'app.error.chunk.description.online':
'頁面資源載入失敗,請重新整理頁面重試。',
'app.error.render.title': '頁面出現錯誤',
'app.error.render.description':
'抱歉,頁面遇到了一些問題,請重新整理頁面或返回首頁。',
'app.error.retry': '重新整理',
'app.error.home': '返回首頁',
'app.request.offline': '網路不可用,請檢查網路連線後重試。',
};

20
src/models/network.ts

@ -0,0 +1,20 @@
import { useEffect, useState } from 'react';
export default function useNetwork() {
const [isOnline, setIsOnline] = useState(
typeof navigator !== 'undefined' ? navigator.onLine : true,
);
useEffect(() => {
const handleOnline = () => setIsOnline(true);
const handleOffline = () => setIsOnline(false);
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, []);
return { isOnline };
}

13
src/requestErrorConfig.ts

@ -77,7 +77,18 @@ export const errorConfig: RequestConfig = {
// 请求已经成功发起,但没有收到响应
// \`error.request\` 在浏览器中是 XMLHttpRequest 的实例,
// 而在node.js中是 http.ClientRequest 的实例
message.error('None response! Please retry.');
if (!navigator.onLine) {
message.error(
'Network unavailable. Please check your connection and try again.',
);
} else {
message.error('None response! Please retry.');
}
} else if (!navigator.onLine) {
// 发送请求时出了点问题(离线)
message.error(
'Network unavailable. Please check your connection and try again.',
);
} else {
// 发送请求时出了点问题
message.error('Request error, please retry.');

Loading…
Cancel
Save