perf(mp-weixin): 照片分两层,详情页移入 pages-poi 分包

微信主包上限 2 MB,30 张 800×600 照片单独就 2.6 MB,
整包 3.2 MB,上传被拒(错误码 80051)。

按使用场景把照片拆成两层:
- 主包缩略图 400×300 q75(678 KB),供地图、行程、打卡卡片使用,
  这些场景最大只显示 192rpx(≈288 px @DPR3),800×600 是过剩
- 分包大图 800×600 q66(1832 KB),供详情页 hero 使用,
  随 pages-poi 分包按需下载

两层都保持 4:3,PoiImage 的 aspectFill 行为不变,视觉表现与之前一致。

详情页从 pages/poi/detail 移到 pages-poi/detail:主包页面无法引用
分包资源,hero 要用大图,页面就必须在分包内。新增
coverPhotoFullUrlFor() 供分包内页面取大图,占位图仍指向主包。

verify-weixin-output.mjs 增加防回归检查:分包注册、两层照片数量
一致、主包与各分包实际体积(留 64 KB 余量)。包体再超限会在
verify 阶段失败,不必等上传才发现。

process-poi-photos.mjs 重写为双层产出,幂等,末尾报告包体占用并
在超限时退出非零。原图改从 src/photo-intake/ 读取(不提交仓库)。

改造后:主包 1522 KB、分包 pages-poi 1847 KB,均在 2048 KB 内。
mp-weixin 与 H5 均编译通过,97 个测试通过,vue-tsc 与 ESLint 无错。
This commit is contained in:
周瑞哲
2026-08-03 09:21:23 +08:00
parent 9e8c7bf908
commit f8ad5e1faf
72 changed files with 327 additions and 108 deletions
+87 -3
View File
@@ -1,4 +1,4 @@
import { existsSync, readFileSync } from 'node:fs'
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'
import { resolve } from 'node:path'
import process from 'node:process'
@@ -10,10 +10,14 @@ const expectedPages = [
'pages/assistant/index',
'pages/itinerary/index',
'pages/planner/index',
'pages/poi/detail',
'pages/check-in/index',
'pages/check-in/records',
]
// 点位详情页放在 pages-poi 分包里,连带 800×600 大图一起按需下载,
// 否则 30 张大图会把主包顶过 2 MB 上限。
const expectedSubPackageRoot = 'pages-poi'
const expectedSubPackagePages = ['detail']
const poiDetailPath = `${expectedSubPackageRoot}/detail`
const expectedTabPages = [
'pages/map/index',
'pages/assistant/index',
@@ -33,6 +37,9 @@ const requiredFiles = [
'app.wxss',
'project.config.json',
...expectedPages.flatMap(page => ['json', 'js', 'wxml', 'wxss'].map(extension => `${page}.${extension}`)),
...expectedSubPackagePages.flatMap(page => ['json', 'js', 'wxml', 'wxss'].map(
extension => `${expectedSubPackageRoot}/${page}.${extension}`,
)),
...expectedTabIcons,
]
@@ -55,7 +62,7 @@ if (!checkInPageScript.includes('requirePrivacyAuthorize')
console.error('打卡页缺少微信位置隐私授权兼容链。')
process.exit(1)
}
const poiDetailMarkup = readFileSync(resolve(outputRoot, 'pages/poi/detail.wxml'), 'utf8')
const poiDetailMarkup = readFileSync(resolve(outputRoot, `${poiDetailPath}.wxml`), 'utf8')
const checkInEntryIndex = poiDetailMarkup.indexOf('check-in-entry')
const checkInEntryStart = poiDetailMarkup.lastIndexOf('<view', checkInEntryIndex)
const checkInEntryEnd = poiDetailMarkup.indexOf('>', checkInEntryIndex)
@@ -167,4 +174,81 @@ if (!ideConfig.appid || !outputIdeConfig.appid || ideConfig.appid !== outputIdeC
process.exit(1)
}
const subPackages = appConfig.subPackages ?? appConfig.subpackages ?? []
const poiSubPackage = subPackages.find(item => item.root === expectedSubPackageRoot)
if (!poiSubPackage) {
console.error(`app.json 未注册 ${expectedSubPackageRoot} 分包,点位详情页与大图会落进主包。`)
process.exit(1)
}
const missingSubPages = expectedSubPackagePages.filter(page => !poiSubPackage.pages?.includes(page))
if (missingSubPages.length > 0) {
console.error(`${expectedSubPackageRoot} 分包未注册全部页面:`)
missingSubPages.forEach(page => console.error(`- ${page}`))
process.exit(1)
}
// 分包大图必须落在分包内,否则等于没分包
const fullPhotoDir = resolve(outputRoot, expectedSubPackageRoot, 'static/photos')
if (!existsSync(fullPhotoDir)) {
console.error(`分包缺少大图目录:${expectedSubPackageRoot}/static/photos`)
process.exit(1)
}
const thumbPhotoDir = resolve(outputRoot, 'static/poi/photos')
if (!existsSync(thumbPhotoDir)) {
console.error('主包缺少缩略图目录:static/poi/photos')
process.exit(1)
}
const fullPhotos = readdirSync(fullPhotoDir).filter(file => file.endsWith('.jpg'))
const thumbPhotos = readdirSync(thumbPhotoDir).filter(file => file.endsWith('.jpg'))
if (fullPhotos.length !== thumbPhotos.length) {
console.error(`两层照片数量不一致:分包大图 ${fullPhotos.length} 张,主包缩略图 ${thumbPhotos.length} 张。请重跑 node scripts/process-poi-photos.mjs`)
process.exit(1)
}
function dirBytes(dir, skipDirs = []) {
let total = 0
for (const entry of readdirSync(dir, { withFileTypes: true })) {
if (entry.isDirectory()) {
if (skipDirs.includes(entry.name))
continue
total += dirBytes(resolve(dir, entry.name), skipDirs)
}
else {
total += statSync(resolve(dir, entry.name)).size
}
}
return total
}
// 微信主包与单个分包各自上限 2 MB。留 64 KB 余量,避免刚好卡线时上传被拒。
const PACKAGE_LIMIT_KB = 2048
const PACKAGE_WARN_KB = PACKAGE_LIMIT_KB - 64
const subPackageRoots = subPackages.map(item => item.root)
const mainPackageKb = Math.round(dirBytes(outputRoot, subPackageRoots) / 1024)
const subPackageSizes = subPackages.map(item => ({
root: item.root,
kb: Math.round(dirBytes(resolve(outputRoot, item.root)) / 1024),
}))
console.log(`主包 ${mainPackageKb} KB / ${PACKAGE_LIMIT_KB} KB`)
subPackageSizes.forEach(({ root, kb }) => {
console.log(`分包 ${root} ${kb} KB / ${PACKAGE_LIMIT_KB} KB`)
})
if (mainPackageKb > PACKAGE_WARN_KB) {
console.error(`主包 ${mainPackageKb} KB 已超过 ${PACKAGE_WARN_KB} KB 安全线(微信上限 ${PACKAGE_LIMIT_KB} KB),上传会被拒。`)
process.exit(1)
}
const oversizedSubPackages = subPackageSizes.filter(({ kb }) => kb > PACKAGE_WARN_KB)
if (oversizedSubPackages.length > 0) {
oversizedSubPackages.forEach(({ root, kb }) => {
console.error(`分包 ${root} ${kb} KB 已超过 ${PACKAGE_WARN_KB} KB 安全线(微信上限 ${PACKAGE_LIMIT_KB} KB)。`)
})
process.exit(1)
}
console.log(`微信开发者工具产物检查通过:${outputRoot}`)