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