fix(miniapp): 修复底部 tabBar 图标全部消失(开发者工具误删 app.json 专属图标)
根因:微信开发者工具(>=1.05.2112301)的 ignoreDevUnusedFiles 会按 WXML/JS 依赖分析过滤 "无依赖"文件。tabBar 图标仅在 app.json 中声明,不属于任何页面 WXML/JS 依赖,被误判为无用 文件从开发包剥离,导致 5 个图标全部加载失败(渲染为破图占位)。 修复: - 在 project.config.json 与 miniapp/project.config.json 的 setting 中显式关闭 ignoreDevUnusedFiles / ignoreUploadUnusedFiles(false),阻止图标被过滤。 - 同步关闭本地 gitignore 的 project.private.config.json(DevTools 运行时实际读取项)。 - 预览通道 scripts/preview-miniapp.mjs:补全 /assets 绝对路径→相对路径重写,并手动复刻 原生 tabBar(原生 tabBar 不在任何页面 WXML 中,静态预览需自行复刻)。 - 回归守卫 ui-tests/tabbar-icons.mjs 扩充为 5 段,覆盖根因(配置开关)+ 预览复刻。 - miniapp/assets/tabbar/DESIGN.md 增加根因警示,防止后续被误开开关。 验证:UI 契约 23/23 全绿(tabbar-icons.mjs 位于 [19/23]);CDP 渲染实测 127 张图 0 破图, 5 个 tabBar 图标 naturalW=81 全部加载,选中色 rgb(233,107,107)、未选 rgb(153,153,153), 与 app.json selectedColor/color 一致。
This commit is contained in:
+5482
-2674
File diff suppressed because it is too large
Load Diff
@@ -4,6 +4,20 @@
|
||||
|
||||
为小程序 5 个底部导航 Tab 设计的圆润线性风格图标,包含未选中(灰色)和选中(玫瑰红)两种状态,适配三种屏幕密度(1x/2x/3x)。
|
||||
|
||||
> ⚠️ **改动本目录前必读 —— 图标整排变「破图」的头号原因**
|
||||
>
|
||||
> 微信开发者工具自 `1.05.2112301` 起默认开启**「过滤无依赖文件」**(`ignoreDevUnusedFiles`)。
|
||||
> 它的依赖分析只看 **WXML/JS**,而 **tabBar 图标只在 `app.json` 里被引用**,
|
||||
> 于是被判定为「无依赖文件」并从**开发包中剔除** → 模拟器/真机上 5 个图标**全部加载失败**,
|
||||
> 表现为底部导航整排破图(而 WXML 引用的图片一切正常,极易误判为图片损坏)。
|
||||
>
|
||||
> 本目录的 `assets/tabbar/*.png` 是**全仓库唯一「只被 app.json 引用」的图片资源**,因此是唯一受害者。
|
||||
>
|
||||
> 处置:`project.config.json`(仓库根 + `miniapp/`)与本地 `project.private.config.json`
|
||||
> 中 `setting.ignoreDevUnusedFiles` / `setting.ignoreUploadUnusedFiles` **必须为 `false`**。
|
||||
> 已由守卫 `ui-tests/tabbar-icons.mjs` 强制校验,请勿改回 `true`。
|
||||
|
||||
|
||||
## 设计规范
|
||||
|
||||
| 属性 | 规范值 |
|
||||
|
||||
@@ -15,7 +15,9 @@
|
||||
"useCompilerPlugins": [
|
||||
"typescript"
|
||||
],
|
||||
"minifyWXML": true
|
||||
"minifyWXML": true,
|
||||
"ignoreDevUnusedFiles": false,
|
||||
"ignoreUploadUnusedFiles": false
|
||||
},
|
||||
"compileType": "miniprogram",
|
||||
"simulatorPluginLibVersion": {},
|
||||
|
||||
+3
-1
@@ -24,7 +24,9 @@
|
||||
"disableUseStrict": false,
|
||||
"condition": false,
|
||||
"swc": false,
|
||||
"disableSWC": true
|
||||
"disableSWC": true,
|
||||
"ignoreDevUnusedFiles": false,
|
||||
"ignoreUploadUnusedFiles": false
|
||||
},
|
||||
"compileType": "miniprogram",
|
||||
"simulatorPluginLibVersion": {},
|
||||
|
||||
@@ -31,6 +31,47 @@ const targets = requested.length ? requested : PAGES
|
||||
const read = (p) => readFileSync(resolve(root, p), 'utf8')
|
||||
const readIf = (p) => (existsSync(resolve(root, p)) ? read(p) : '')
|
||||
|
||||
// ============================================================
|
||||
// 资源路径改写
|
||||
// WXML/WXSS 写的是小程序根绝对路径 /assets/...;浏览器以 file:// 打开本预览时
|
||||
// /assets 会被解析到文件系统根目录 → 图片全部 404(表现为破图)。
|
||||
// preview.html 位于 artifacts/ui-redesign/miniapp/,向上三级即仓库根。
|
||||
// ============================================================
|
||||
const ASSET_PREFIX = '../../../miniapp'
|
||||
const rewriteAssetUrls = (s) => s
|
||||
.replace(/(\bsrc=")\/assets\//g, `$1${ASSET_PREFIX}/assets/`)
|
||||
.replace(/(\bsrc=')\/assets\//g, `$1${ASSET_PREFIX}/assets/`)
|
||||
.replace(/url\((["']?)\/assets\//g, `url($1${ASSET_PREFIX}/assets/`)
|
||||
|
||||
// ============================================================
|
||||
// 原生 tabBar 复刻
|
||||
// 小程序 tabBar 由 app.json 声明、由客户端原生渲染,**不存在于任何 WXML**。
|
||||
// 静态预览若不补这一层,会让人误判为「底部导航图标丢了」。
|
||||
// 5 项与 app.json 的 tabBar.list 一一对应(含选中态图标)。
|
||||
// ============================================================
|
||||
const TAB_ACTIVE_BY_PAGE = {
|
||||
index: 'home', discover: 'discover', messages: 'message', activity: 'activity', mine: 'mine',
|
||||
}
|
||||
const TAB_ITEMS = [
|
||||
{ key: 'home', label: '首页' },
|
||||
{ key: 'discover', label: '找对象' },
|
||||
{ key: 'message', label: '消息' },
|
||||
{ key: 'activity', label: '活动' },
|
||||
{ key: 'mine', label: '我的' },
|
||||
]
|
||||
function renderTabbar(page) {
|
||||
const active = TAB_ACTIVE_BY_PAGE[page]
|
||||
if (!active) return ''
|
||||
const items = TAB_ITEMS.map((it) => {
|
||||
const on = it.key === active
|
||||
const file = on ? `${it.key}-active.png` : `${it.key}.png`
|
||||
return `<div class="mini-tabbar__item${on ? ' is-active' : ''}">`
|
||||
+ `<img class="mini-tabbar__icon" src="${ASSET_PREFIX}/assets/tabbar/${file}" alt="${it.label}" />`
|
||||
+ `<span class="mini-tabbar__label">${it.label}</span></div>`
|
||||
}).join('')
|
||||
return `<div class="mini-tabbar" role="tablist">${items}</div>`
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// mock 数据:让各页在静态预览下呈现「有数据」的真实布局
|
||||
// 键为页面名,值为该页 data 中会被 WXML 消费的字段
|
||||
@@ -702,7 +743,7 @@ function buildPage(page, deviceWidth = 375) {
|
||||
} catch (e) {
|
||||
body = `<pre style="color:#c00;padding:16px">渲染失败: ${e.message}</pre>`
|
||||
}
|
||||
return { html: `<div class="wx-page reference-page">${body}</div>`, css, missing: false }
|
||||
return { html: `<div class="wx-page reference-page">${body}${renderTabbar(page)}</div>`, css, missing: false }
|
||||
}
|
||||
|
||||
function main() {
|
||||
@@ -762,6 +803,16 @@ function main() {
|
||||
.phone { background:#fff; border-radius:36px; padding:12px; box-shadow:0 20px 60px rgba(28,30,88,.18); }
|
||||
.phone-screen { width:100%; height:760px; overflow-y:auto; border-radius:26px; background:#F4F5FA; }
|
||||
.wx-page { min-height:100%; }
|
||||
/* 原生 tabBar 复刻。颜色/边框取 app.json 的 tabBar 字段:
|
||||
color #999999 / selectedColor #E96B6B / backgroundColor #FFFFFF / borderStyle black。
|
||||
图标按微信客户端实际渲染尺寸(约 26px)呈现,与 81px 源图尺寸不同;文字 10px 同理。 */
|
||||
.mini-tabbar { position:sticky; bottom:0; z-index:20; display:flex; align-items:stretch;
|
||||
background:#FFFFFF; border-top:1px solid rgba(0,0,0,.2); padding-bottom:20px; }
|
||||
.mini-tabbar__item { flex:1 1 0; min-width:0; display:flex; flex-direction:column;
|
||||
align-items:center; justify-content:center; gap:2px; padding:6px 0 0; }
|
||||
.mini-tabbar__icon { width:26px; height:26px; display:block; }
|
||||
.mini-tabbar__label { font-size:10px; line-height:14px; color:#999999; }
|
||||
.mini-tabbar__item.is-active .mini-tabbar__label { color:#E96B6B; }
|
||||
/* ====== 以下为页面 WXSS 转换结果(rpx 已按 375 宽换算) ====== */
|
||||
${cssAll}
|
||||
</style>
|
||||
@@ -770,7 +821,7 @@ function main() {
|
||||
<div class="banner">
|
||||
<h1>小程序 11 页 · 静态预览</h1>
|
||||
${nav}
|
||||
<span class="note">本预览为 WXML/WXSS 静态还原,非真机渲染,不含真实接口数据</span>
|
||||
<span class="note">本预览为 WXML/WXSS 静态还原,非真机渲染,不含真实接口数据;底部 tabBar 按 app.json 复刻</span>
|
||||
</div>
|
||||
<div class="stage">${sections}</div>
|
||||
<script>
|
||||
@@ -785,7 +836,7 @@ function main() {
|
||||
</html>`
|
||||
|
||||
const outFile = resolve(OUT_DIR, 'preview.html')
|
||||
writeFileSync(outFile, html, 'utf8')
|
||||
writeFileSync(outFile, rewriteAssetUrls(html), 'utf8')
|
||||
const ok = built.filter((b) => !b.missing).length
|
||||
console.log(`已生成: ${outFile}`)
|
||||
console.log(`页面数: ${ok}/${targets.length}`)
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { readFileSync, existsSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
|
||||
const root = join(process.cwd(), 'miniapp', 'assets', 'tabbar')
|
||||
const repoRoot = process.cwd()
|
||||
const root = join(repoRoot, 'miniapp', 'assets', 'tabbar')
|
||||
const icons = {
|
||||
home: '爱心小屋',
|
||||
discover: '双人缘分',
|
||||
@@ -11,6 +12,7 @@ const icons = {
|
||||
mine: '个人守护',
|
||||
}
|
||||
|
||||
// ── 1. SVG 源文件:主题化标题 + 两态配色 ──────────────────────────────
|
||||
for (const [name, title] of Object.entries(icons)) {
|
||||
const normal = readFileSync(join(root, 'svg', `${name}.svg`), 'utf8')
|
||||
const active = readFileSync(join(root, 'svg', `${name}-active.svg`), 'utf8')
|
||||
@@ -19,6 +21,7 @@ for (const [name, title] of Object.entries(icons)) {
|
||||
assert.match(normal, /#999999/i, `${name} 默认态颜色错误`)
|
||||
assert.match(active, /#E96B6B/i, `${name} 选中态颜色错误`)
|
||||
|
||||
// ── 2. PNG 规格:正方形、1x/2x/3x、透明 RGBA ────────────────────────
|
||||
for (const suffix of ['', '_2x', '_3x']) {
|
||||
const expectedSize = suffix === '' ? 81 : suffix === '_2x' ? 162 : 243
|
||||
for (const state of ['', '-active']) {
|
||||
@@ -30,4 +33,85 @@ for (const [name, title] of Object.entries(icons)) {
|
||||
}
|
||||
}
|
||||
|
||||
// ── 3. app.json ↔ 磁盘一致性 ─────────────────────────────────────────
|
||||
// 历史缺陷:tabBar 图标只被 app.json 引用(不经 WXML/JS),
|
||||
// 一旦配置漂移,不会有任何 WXML 侧症状,只会「底部导航整排变破图」。
|
||||
const appJsonPath = join(repoRoot, 'miniapp', 'app.json')
|
||||
const appJson = JSON.parse(readFileSync(appJsonPath, 'utf8'))
|
||||
const tabList = appJson.tabBar?.list ?? []
|
||||
assert.equal(tabList.length, 5, 'tabBar.list 必须为 5 项(微信支持 2~5)')
|
||||
|
||||
for (const item of tabList) {
|
||||
assert.ok(appJson.pages.includes(item.pagePath), `tabBar pagePath 未在 pages 中注册: ${item.pagePath}`)
|
||||
for (const key of ['iconPath', 'selectedIconPath']) {
|
||||
const rel = item[key]
|
||||
assert.ok(rel, `tabBar ${item.pagePath} 缺少 ${key}`)
|
||||
assert.ok(!rel.startsWith('/'), `tabBar ${key} 不应以 / 开头(微信按小程序根解析): ${rel}`)
|
||||
assert.ok(
|
||||
existsSync(join(repoRoot, 'miniapp', rel)),
|
||||
`tabBar ${key} 指向不存在的文件: ${rel}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ── 4. 「无依赖文件过滤」必须关闭(tabBar 图标整排变破图的根因)────────
|
||||
// 微信开发者工具自 1.05.2112301 起默认开启该能力:它按 WXML/JS 依赖分析,
|
||||
// app.json 的 tabBar 图标不被视为依赖 → 被从开发包中剔除 → 图标全部加载失败。
|
||||
// 官方缓解方式即 ignoreDevUnusedFiles / ignoreUploadUnusedFiles 置 false。
|
||||
const CONFIGS = [
|
||||
'project.config.json',
|
||||
'miniapp/project.config.json',
|
||||
'project.private.config.json',
|
||||
'miniapp/project.private.config.json',
|
||||
]
|
||||
const requiredOff = ['project.config.json', 'miniapp/project.config.json']
|
||||
|
||||
for (const rel of CONFIGS) {
|
||||
const p = join(repoRoot, rel)
|
||||
if (!existsSync(p)) continue // 私有配置被 gitignore,缺失属正常
|
||||
const setting = JSON.parse(readFileSync(p, 'utf8')).setting ?? {}
|
||||
assert.notEqual(
|
||||
setting.ignoreDevUnusedFiles, true,
|
||||
`${rel} 开启了无依赖文件过滤(ignoreDevUnusedFiles),会剔除 app.json 独占引用的 tabBar 图标,导致底部导航整排破图`,
|
||||
)
|
||||
assert.notEqual(
|
||||
setting.ignoreUploadUnusedFiles, true,
|
||||
`${rel} 开启了上传期无依赖文件过滤(ignoreUploadUnusedFiles),体验版/正式版会丢失 tabBar 图标`,
|
||||
)
|
||||
}
|
||||
|
||||
for (const rel of requiredOff) {
|
||||
const p = join(repoRoot, rel)
|
||||
assert.ok(existsSync(p), `缺少共享配置 ${rel}`)
|
||||
const setting = JSON.parse(readFileSync(p, 'utf8')).setting ?? {}
|
||||
assert.equal(
|
||||
setting.ignoreDevUnusedFiles, false,
|
||||
`${rel} 必须显式关闭 ignoreDevUnusedFiles(新克隆默认值为 true 会复现破图)`,
|
||||
)
|
||||
assert.equal(
|
||||
setting.ignoreUploadUnusedFiles, false,
|
||||
`${rel} 必须显式关闭 ignoreUploadUnusedFiles`,
|
||||
)
|
||||
}
|
||||
|
||||
// ── 5. 静态预览必须改写资源路径 ──────────────────────────────────────
|
||||
// WXML/WXSS 写的是小程序根绝对路径 /assets/...,浏览器 file:// 打开会解析到
|
||||
// 文件系统根 → 全部 404(表现为破图)。预览产出必须改写为相对路径,
|
||||
// 并复刻 app.json 原生 tabBar(它不在任何 WXML 里)。
|
||||
const previewSrc = readFileSync(join(repoRoot, 'scripts', 'preview-miniapp.mjs'), 'utf8')
|
||||
assert.match(previewSrc, /const rewriteAssetUrls\s*=/, '预览脚本缺少资源路径改写器')
|
||||
assert.match(
|
||||
previewSrc, /writeFileSync\(outFile,\s*rewriteAssetUrls\(html\)/,
|
||||
'预览脚本写盘时未应用资源路径改写 → file:// 下图片会全部 404',
|
||||
)
|
||||
assert.match(previewSrc, /function renderTabbar\(/, '预览脚本缺少原生 tabBar 复刻')
|
||||
|
||||
const previewHtml = join(repoRoot, 'artifacts', 'ui-redesign', 'miniapp', 'preview.html')
|
||||
if (existsSync(previewHtml)) {
|
||||
const html = readFileSync(previewHtml, 'utf8')
|
||||
const absoluteSrc = (html.match(/src=["']\/assets\//g) || []).length
|
||||
assert.equal(absoluteSrc, 0, `preview.html 仍有 ${absoluteSrc} 处根绝对资源路径,浏览器打开会破图`)
|
||||
assert.match(html, /class="mini-tabbar"/, 'preview.html 缺少复刻的底部 tabBar')
|
||||
}
|
||||
|
||||
console.log('Tabbar icon contracts passed')
|
||||
|
||||
Reference in New Issue
Block a user