From 09322ecbec8a0e5eb70e8aa54bc1eda1f7bb97d7 Mon Sep 17 00:00:00 2001 From: Blizzard Date: Thu, 30 Jul 2026 08:30:59 +0800 Subject: [PATCH] =?UTF-8?q?chore(fe):=20=E9=9D=99=E6=80=81=E8=87=AA?= =?UTF-8?q?=E6=9F=A5=E6=8C=89=E4=BD=9C=E7=94=A8=E5=9F=9F=E7=AE=97=20class?= =?UTF-8?q?=EF=BC=8C=E5=BD=93=E5=9C=BA=E6=8A=93=E5=87=BA=E7=AC=AC=E4=B8=89?= =?UTF-8?q?=E4=BE=8B=E8=B7=A8=E9=A1=B5=E8=A3=B8=E5=A5=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 原来的检查是错的 它把所有 wxss 并成一个全局集合再看「这个 class 有没有人定义过」。 可这个项目连着栽的两次都是「有人定义过,但定义在别的页面里」: .post-card 留在 community.wxss,用户主页用它 → 帖子完全没样式 .profile-row 留在 settings.wxss,「我的」用它 → 四个入口整个散架 两次它都报「✓ 全部对得上」。全局并一起看,恰好检查不出这一类问题。 ## 改成按作用域 app.wxss 全局;页面 wxss 只对自己那页生效;组件 wxss 只对组件自己的节点 生效,且要开了 addGlobalClass 才看得到全局。@import 跟着递归收 (pt-icon 的图标就是这么来的)。 class 不在作用域内时分两种报法——「没有任何定义」是拼错了, 「只定义在 xxx.wxss」是跨页借用,后者直接把定义位置指出来。 ## 跑起来当场又抓到一个 pages/relations 的关注按钮 .follow-btn / .follow-btn.on 只定义在 community.wxss —— 关注/粉丝列表里那个按钮一直是裸的。已提到 app.wxss。 ## 自测过 往 learn.wxml 塞一个只在 decorate.wxss 里定义的 class,确认报错并指对了 位置,再还原确认恢复绿。不然这检查本身就是个装饰。 脚本从临时目录挪进 pets-fe/scripts/check.js(原来只活在会话的临时目录里, 会话一结束就没了),路径改成相对自己。scripts/ 加进 packOptions.ignore, 不进小程序包体。 Co-Authored-By: Claude Opus 5 --- pets-fe/app.wxss | 8 ++ pets-fe/pages/community/community.wxss | 5 - pets-fe/project.config.json | 4 + pets-fe/scripts/check.js | 127 +++++++++++++++++++++++++ 4 files changed, 139 insertions(+), 5 deletions(-) create mode 100644 pets-fe/scripts/check.js diff --git a/pets-fe/app.wxss b/pets-fe/app.wxss index 780d74b..4a903bd 100644 --- a/pets-fe/app.wxss +++ b/pets-fe/app.wxss @@ -419,3 +419,11 @@ page{scrollbar-width:none;-ms-overflow-style:none} .pr-label{flex:1;min-width:0} .pr-val{color:var(--muted);font-weight:var(--fw);font-size:var(--fs-sm)} .pr-arrow{color:var(--muted2);display:flex;align-items:center;flex:none} + +/* ===== 关注按钮 ===== + 社区列表和关注/粉丝列表都在用。同上:两页共用的就不能留在任一页的 wxss 里 */ +.follow-btn{ + flex:none;font-size:var(--fs-cap);font-weight:var(--fw-b);padding:6rpx var(--sp-3);border-radius:var(--r-full); + color:var(--primary-dark);background:var(--primary-soft);border:1rpx solid var(--primary-line); +} +.follow-btn.on{color:var(--muted);background:#F4F1EC;border-color:var(--line)} diff --git a/pets-fe/pages/community/community.wxss b/pets-fe/pages/community/community.wxss index 4d53082..23a07b2 100644 --- a/pets-fe/pages/community/community.wxss +++ b/pets-fe/pages/community/community.wxss @@ -2,11 +2,6 @@ /* 关注按钮 */ -.follow-btn{ - flex:none;font-size:var(--fs-cap);font-weight:var(--fw-b);padding:6rpx var(--sp-3);border-radius:var(--r-full); - color:var(--primary-dark);background:var(--primary-soft);border:1rpx solid var(--primary-line); -} -.follow-btn.on{color:var(--muted);background:#F4F1EC;border-color:var(--line)} .retry-btn{ margin-top:var(--sp-3);display:inline-block;padding:var(--sp-2) var(--sp-6);border-radius:var(--r-full); diff --git a/pets-fe/project.config.json b/pets-fe/project.config.json index 77d4908..8b137dc 100644 --- a/pets-fe/project.config.json +++ b/pets-fe/project.config.json @@ -21,6 +21,10 @@ { "type": "file", "value": "assets/logo-lockup.svg" + }, + { + "type": "folder", + "value": "scripts" } ], "include": [] diff --git a/pets-fe/scripts/check.js b/pets-fe/scripts/check.js new file mode 100644 index 0000000..654120d --- /dev/null +++ b/pets-fe/scripts/check.js @@ -0,0 +1,127 @@ +// 用法:node scripts/check.js(在 pets-fe 目录下跑,或直接给全路径) +// +// 静态自查:class 有没有人定义、而且是不是「在能生效的地方」定义、pt-icon 的 name +// 是否存在、绑的事件方法是否存在、组件是否已注册。 +// +// 防的是「漏改一处 → 元素完全裸奔」这种不报错、不在 diff 里、只在特定页面/弹层才暴露 +// 的问题。这个项目已经踩过两次同一个坑:.post-card 留在 community.wxss 却被用户主页 +// 用(帖子没样式)、.profile-row 留在 settings.wxss 却被「我的」用(入口整个散架)。 +// 所以 class 检查是按作用域来的,不是全局并一起看——那样恰好检查不出这类问题。 +const fs = require('fs'); +const path = require('path'); + +const FE = path.resolve(__dirname, '..'); +const walk = (d, out = []) => { + for (const e of fs.readdirSync(d, { withFileTypes: true })) { + const p = path.join(d, e.name); + if (e.isDirectory()) { if (!['node_modules', '.git'].includes(e.name)) walk(p, out); } + else out.push(p); + } + return out; +}; +const files = walk(FE); +const read = (p) => fs.readFileSync(p, 'utf8'); +const rel = (p) => path.relative(FE, p); + +// ── 一个 wxss 里定义的 class,跟着 @import 一起收 ──────────────────────────── +function classesOf(wxss, seen = new Set()) { + const out = new Set(); + if (!fs.existsSync(wxss) || seen.has(wxss)) return out; + seen.add(wxss); + const src = read(wxss); + for (const m of src.matchAll(/\.([a-zA-Z][\w-]*)/g)) out.add(m[1]); + for (const m of src.matchAll(/@import\s+["']([^"']+)["']/g)) { + for (const c of classesOf(path.resolve(path.dirname(wxss), m[1]), seen)) out.add(c); + } + return out; +} + +// ── 作用域 ───────────────────────────────────────────────────────────────── +// app.wxss 是全局的;页面 wxss 只对自己那一页生效;组件 wxss 只对组件自己的节点生效。 +// 页面之间互不可见——这正是前面两次事故的成因。 +const GLOBAL = classesOf(path.join(FE, 'app.wxss')); + +// 谁定义了这个 class,报错时好指路 +const whereDefined = new Map(); +for (const f of files.filter((f) => f.endsWith('.wxss'))) { + for (const c of classesOf(f)) { + if (!whereDefined.has(c)) whereDefined.set(c, []); + whereDefined.get(c).push(rel(f)); + } +} + +const iconNames = new Set(); +for (const m of read(path.join(FE, 'styles/iconfont.wxss')).matchAll(/\.pt-i-([\w-]+)\{/g)) iconNames.add(m[1]); + +const problems = []; +for (const f of files.filter((f) => f.endsWith('.wxml'))) { + const src = read(f); + const r = rel(f); + const isComponent = /^(components|custom-tab-bar)\//.test(r); + + // 这个 wxml 能看到的 class:自己那个同名 wxss,加上全局(组件要开了 addGlobalClass 才有) + const own = classesOf(f.replace(/\.wxml$/, '.wxss')); + const js = f.replace(/\.wxml$/, '.js'); + const seesGlobal = !isComponent || + (fs.existsSync(js) && read(js).includes('addGlobalClass')); + const scope = seesGlobal ? new Set([...own, ...GLOBAL]) : own; + + for (const m of src.matchAll(/class="([^"]*)"/g)) { + // 先把 {{...}} 整段挖掉(里面是表达式不是 class),再看剩下的静态部分; + // 表达式里的三元字面量 'active' 单独收集 + const dyn = [...m[1].matchAll(/\{\{([^}]*)\}\}/g)].map((x) => x[1]).join(' '); + const statics = m[1].replace(/\{\{[^}]*\}\}/g, ' '); + const names = statics.split(/\s+/).filter(Boolean); + // 只认三元表达式里的字面量(? 'x' : 'y');wxs 函数的字符串入参不是 class + for (const q of dyn.matchAll(/[?:]\s*'([\w-]+)'/g)) names.push(q[1]); + // 纯布局壳,没样式是故意的 + const LAYOUT_ONLY = new Set(['greeting', 'tab-text', 'aiPlan']); + for (const raw of names) { + if (LAYOUT_ONLY.has(raw)) continue; + // class="row-{{item.role}}" 挖掉表达式后剩个 row-,是拼接前缀不是类名 + if (raw.endsWith('-')) continue; + if (scope.has(raw)) continue; + const at = whereDefined.get(raw); + if (at && at.length) { + // 最坑的一种:class 有定义,但定义在别的页面/组件里,这一页拿不到,元素裸奔 + problems.push(`${r}: class "${raw}" 只定义在 ${at.join(' / ')} —— 页面级 wxss 跨不了页,这里是裸的。两页共用就提到 app.wxss`); + } else { + problems.push(`${r}: class "${raw}" 没有任何 wxss 定义`); + } + } + } + + // 事件绑定的方法必须真的存在——绑了个不存在的方法,点了没反应且不报错 + if (fs.existsSync(js)) { + const code = read(js); + const handlers = new Set(); + for (const m of src.matchAll(/\b(?:bind|catch)(?::)?[a-z]+="([A-Za-z_$][\w$]*)"/g)) handlers.add(m[1]); + for (const h of handlers) { + if (!new RegExp(`(^|[\\s,{])${h}\\s*[(:]`, 'm').test(code)) { + problems.push(`${r}: 绑了 ${h},但 ${path.basename(js)} 里没有这个方法 —— 点了没反应`); + } + } + } + + // pt-icon 的静态 name + for (const m of src.matchAll(/]*name="([^"{]*)"/g)) { + if (m[1] && !iconNames.has(m[1])) problems.push(`${r}: pt-icon name="${m[1]}" 图标不存在`); + } + // 用到的自定义组件是否在同名 .json 里注册 + const json = f.replace(/\.wxml$/, '.json'); + const reg = fs.existsSync(json) ? Object.keys(JSON.parse(read(json)).usingComponents || {}) : []; + const known = ['nav-bar', 'pet-switch', 'bottom-sheet', 'fab', 'pt-icon', 'seg-tabs', 'stat-ring', 'profile-head']; + for (const tag of known) { + if (new RegExp('<' + tag + '[\\s>]').test(src) && !reg.includes(tag)) { + problems.push(`${r}: 用了 <${tag}> 但 ${path.basename(json)} 没有注册`); + } + } +} + +if (problems.length) { + console.log('发现 ' + problems.length + ' 处问题:'); + problems.forEach((p) => console.log(' ✗ ' + p)); + process.exit(1); +} +console.log('✓ class 作用域 / 图标名 / 事件方法 / 组件注册 全部对得上(检查了 ' + + files.filter((f) => f.endsWith('.wxml')).length + ' 个 wxml,' + iconNames.size + ' 个图标)');