fix(build): SSR 安全化 PlaylistCardGenerator canvasHeight 避免 Nitro 构建 OOM(exit 137)
构建并部署 BYW VoiceHub / Preflight 语法与构建检查 (push) Failing after 4m19s
构建并部署 BYW VoiceHub / 镜像构建·预热(与 Preflight 并发:系统/镜像源/Builder 初始化) (push) Successful in 4m30s
构建并部署 BYW VoiceHub / 构建并推送容器镜像 (push) Canceled after 0s
构建并部署 BYW VoiceHub / 部署到生产服务器 (push) Canceled after 0s

上版在 canvasHeight computed 里对每首歌 document.createElement('canvas')+getContext('2d'),Nitro prerender/rollup bundler 在 Node 环境下要么 document undefined 抛错 fallback,要么 prerender 时对大歌单反复创建 2D context 撑爆虚拟内存 -> 5min 后 OOM Killer(exit 137)。

修复:
- 新增 isBrowser = typeof document !== 'undefined' 判断
- getSharedCtx(): 浏览器环境才创建 1 个共享 2D context 并跨次复用(不再每首歌 createElement)
- fitText(ctx, ...) 抽成双分支:有 ctx 用 measureText(客户端精确),无 ctx 用 CJK>0x2e80 Unicode 判断+经验字符宽度(16px 中文 1.05em/英文 0.55em)估算换行,保守估算高度
- 同时 fitText 支持文本中带 \\n 换行手动切分
- canvasHeight 中不再 new canvas/context,SSR 环境也能安全执行+算出估算高度
This commit is contained in:
2026-09-13 12:32:51 +08:00
parent ac4727551c
commit a55fa96952
+52 -13
View File
@@ -91,30 +91,69 @@ const songLeftX = 44
const songRightX = 360 const songRightX = 360
const songAreaWidth = songRightX - songLeftX const songAreaWidth = songRightX - songLeftX
const isBrowser = typeof document !== 'undefined'
let _sharedCtx = null
const getSharedCtx = () => {
if (!isBrowser) return null
if (_sharedCtx) return _sharedCtx
try {
const c = document.createElement('canvas')
_sharedCtx = c.getContext('2d')
} catch { _sharedCtx = null }
return _sharedCtx
}
const fitText = (ctx, text, fontSize, maxWidth, lineHeight) => { const fitText = (ctx, text, fontSize, maxWidth, lineHeight) => {
ctx.font = `${fontSize}px "Inter", "system-ui", "PingFang SC", "Microsoft YaHei", sans-serif` const hasCtx = !!ctx
const chars = Array.from(text) const fontFamily = `"Inter", "system-ui", "PingFang SC", "Microsoft YaHei", sans-serif`
const avgCharWidth = fontSize * 0.55
const cjkCharWidth = fontSize * 1.05
const splitLines = String(text || '').split(/\r?\n/)
const lines = [] const lines = []
let current = '' for (const rawLine of splitLines) {
for (const ch of chars) { if (hasCtx) {
const tryStr = current + ch ctx.font = `${fontSize}px ${fontFamily}`
const w = ctx.measureText(tryStr).width const chars = Array.from(rawLine)
if (w > maxWidth && current) { let current = ''
lines.push(current) for (const ch of chars) {
current = ch const tryStr = current + ch
const w = ctx.measureText(tryStr).width
if (w > maxWidth && current) {
lines.push(current)
current = ch
} else {
current = tryStr
}
}
if (current) lines.push(current)
} else { } else {
current = tryStr const chars = Array.from(rawLine)
let current = ''
let curW = 0
for (const ch of chars) {
const code = ch.codePointAt(0) || 0
const isCJK = code > 0x2e80
const w = isCJK ? cjkCharWidth : avgCharWidth
if (curW + w > maxWidth && current) {
lines.push(current)
current = ch
curW = w
} else {
current += ch
curW += w
}
}
if (current) lines.push(current)
} }
} }
if (current) lines.push(current)
return { lines, lineHeight } return { lines, lineHeight }
} }
const canvasHeight = computed(() => { const canvasHeight = computed(() => {
const count = Math.min(props.songs.length, maxDisplaySongs) const count = Math.min(props.songs.length, maxDisplaySongs)
if (!count) return 600 if (!count) return 600
const canvas = document.createElement('canvas') const ctx = getSharedCtx()
const ctx = canvas.getContext('2d')
let totalH = 0 let totalH = 0
const maxWidthTitle = songAreaWidth const maxWidthTitle = songAreaWidth
const maxWidthArtist = Math.max(100, songAreaWidth - 10) const maxWidthArtist = Math.max(100, songAreaWidth - 10)