fix(admin-web): 添加 global-error.tsx 修复部署后 Server Action 崩溃
问题:每次重新部署容器后,浏览器持有旧 bundle,Next.js App Router 内部 Server Action ID("r"/"multi")与新服务器不匹配,导致客户端抛出 未捕获异常,触发全屏 "Application error"(周期性崩溃根因)。 修复: - 添加 src/app/global-error.tsx(根级错误边界),检测到 stale bundle 相关错误时自动调用 window.location.reload(),无感知恢复 - 添加 src/app/(admin)/error.tsx(admin 路由段错误边界),同样自动刷新 - 两个边界均提供「立即刷新」「重试」按钮,防止极端情况下自动刷新失效 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
4878449f8c
commit
f642ef1d56
|
|
@ -0,0 +1,92 @@
|
|||
'use client';
|
||||
|
||||
import { useEffect } from 'react';
|
||||
|
||||
/**
|
||||
* Admin 路由段错误边界
|
||||
*
|
||||
* 捕获 (admin) 路由内的客户端异常。
|
||||
* 当检测到 Next.js stale bundle / Server Action 不匹配错误时自动刷新页面。
|
||||
*/
|
||||
export default function AdminError({
|
||||
error,
|
||||
reset,
|
||||
}: {
|
||||
error: Error & { digest?: string };
|
||||
reset: () => void;
|
||||
}) {
|
||||
useEffect(() => {
|
||||
const msg = error?.message ?? '';
|
||||
const isStaleBundle =
|
||||
msg.includes('Server Action') ||
|
||||
msg.includes('Failed to find') ||
|
||||
msg.includes('An unexpected response was received') ||
|
||||
(error?.digest ?? '').startsWith('NEXT_');
|
||||
|
||||
if (isStaleBundle) {
|
||||
window.location.reload();
|
||||
}
|
||||
}, [error]);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
height: '100%',
|
||||
minHeight: '400px',
|
||||
fontFamily: 'system-ui, sans-serif',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
textAlign: 'center',
|
||||
padding: '40px',
|
||||
background: '#fff',
|
||||
borderRadius: '12px',
|
||||
boxShadow: '0 2px 16px rgba(0,0,0,0.08)',
|
||||
maxWidth: '400px',
|
||||
}}
|
||||
>
|
||||
<div style={{ fontSize: '40px', marginBottom: '16px' }}>⚠️</div>
|
||||
<h2 style={{ margin: '0 0 8px', fontSize: '18px', color: '#1a1a1a' }}>
|
||||
页面加载出错
|
||||
</h2>
|
||||
<p style={{ margin: '0 0 24px', color: '#666', fontSize: '14px' }}>
|
||||
检测到新版本,正在自动刷新…
|
||||
</p>
|
||||
<div style={{ display: 'flex', gap: '12px', justifyContent: 'center' }}>
|
||||
<button
|
||||
onClick={() => window.location.reload()}
|
||||
style={{
|
||||
padding: '8px 20px',
|
||||
background: '#2563eb',
|
||||
color: '#fff',
|
||||
border: 'none',
|
||||
borderRadius: '6px',
|
||||
cursor: 'pointer',
|
||||
fontSize: '14px',
|
||||
}}
|
||||
>
|
||||
立即刷新
|
||||
</button>
|
||||
<button
|
||||
onClick={() => reset()}
|
||||
style={{
|
||||
padding: '8px 20px',
|
||||
background: '#f5f5f5',
|
||||
color: '#333',
|
||||
border: '1px solid #e0e0e0',
|
||||
borderRadius: '6px',
|
||||
cursor: 'pointer',
|
||||
fontSize: '14px',
|
||||
}}
|
||||
>
|
||||
重试
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,105 @@
|
|||
'use client';
|
||||
|
||||
/**
|
||||
* Next.js App Router 根级错误边界
|
||||
*
|
||||
* 捕获未处理的客户端异常,避免显示 Next.js 默认的
|
||||
* "Application error: a client-side exception has occurred" 全屏崩溃页。
|
||||
*
|
||||
* 常见触发场景:
|
||||
* - 重新部署后,浏览器仍持有旧 bundle,内部 Server Action ID("r" / "multi")
|
||||
* 已不匹配新服务器构建 → Failed to find Server Action → 客户端异常
|
||||
* - 此时自动刷新页面即可恢复正常(加载新 bundle)
|
||||
*
|
||||
* global-error.tsx 必须自带 <html><body>,因为它会替换根 layout。
|
||||
*/
|
||||
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export default function GlobalError({
|
||||
error,
|
||||
reset,
|
||||
}: {
|
||||
error: Error & { digest?: string };
|
||||
reset: () => void;
|
||||
}) {
|
||||
useEffect(() => {
|
||||
// 检测 Next.js stale bundle 导致的 Server Action 不匹配错误
|
||||
// 自动刷新页面以加载新版本
|
||||
const msg = error?.message ?? '';
|
||||
const isStaleBundle =
|
||||
msg.includes('Server Action') ||
|
||||
msg.includes('Failed to find') ||
|
||||
msg.includes('An unexpected response was received') ||
|
||||
(error?.digest ?? '').startsWith('NEXT_');
|
||||
|
||||
if (isStaleBundle) {
|
||||
window.location.reload();
|
||||
}
|
||||
}, [error]);
|
||||
|
||||
return (
|
||||
<html lang="zh-CN">
|
||||
<body
|
||||
style={{
|
||||
margin: 0,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
height: '100vh',
|
||||
background: '#f5f5f5',
|
||||
fontFamily: 'system-ui, sans-serif',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
textAlign: 'center',
|
||||
padding: '40px',
|
||||
background: '#fff',
|
||||
borderRadius: '12px',
|
||||
boxShadow: '0 2px 16px rgba(0,0,0,0.08)',
|
||||
maxWidth: '400px',
|
||||
}}
|
||||
>
|
||||
<div style={{ fontSize: '40px', marginBottom: '16px' }}>⚠️</div>
|
||||
<h2 style={{ margin: '0 0 8px', fontSize: '18px', color: '#1a1a1a' }}>
|
||||
页面加载出错
|
||||
</h2>
|
||||
<p style={{ margin: '0 0 24px', color: '#666', fontSize: '14px' }}>
|
||||
检测到新版本,正在自动刷新…
|
||||
</p>
|
||||
<div style={{ display: 'flex', gap: '12px', justifyContent: 'center' }}>
|
||||
<button
|
||||
onClick={() => window.location.reload()}
|
||||
style={{
|
||||
padding: '8px 20px',
|
||||
background: '#2563eb',
|
||||
color: '#fff',
|
||||
border: 'none',
|
||||
borderRadius: '6px',
|
||||
cursor: 'pointer',
|
||||
fontSize: '14px',
|
||||
}}
|
||||
>
|
||||
立即刷新
|
||||
</button>
|
||||
<button
|
||||
onClick={() => reset()}
|
||||
style={{
|
||||
padding: '8px 20px',
|
||||
background: '#f5f5f5',
|
||||
color: '#333',
|
||||
border: '1px solid #e0e0e0',
|
||||
borderRadius: '6px',
|
||||
cursor: 'pointer',
|
||||
fontSize: '14px',
|
||||
}}
|
||||
>
|
||||
重试
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in New Issue