WXML 踩坑速查(小程序)
P0 · 自闭合标签吞节点(按钮"点击无反应"的头号嫌疑)
症状:页面某节点之后的所有元素点击无反应 / 不渲染 / 错位;JS 逻辑正常、事件绑定写法正确、控制台无报错。
根因:WXML 中 <textarea ... />、<input ... /> 自闭合写法在部分基础库/编译器下,会把后续所有兄弟节点当作其子节点解析,导致按钮等元素被"藏"进组件内部不可点。
修复:一律显式闭合:
<!-- 错误 -->
<textarea value="{{note}}" bindinput="onNote" auto-height />
<input value="{{v}}" bindinput="onInput" />
<!-- 正确 -->
<textarea value="{{note}}" bindinput="onNote" auto-height="{{true}}"></textarea>
<input value="{{v}}" bindinput="onInput"></input>
排查方法(Node 快速标签配对检查):用栈匹配开闭标签,出现「</view> 期望 </textarea>」即命中此坑。注意 input/textarea/image 等看似 void tag,但在 WXML 编译器里不是 void。
P1 · 直接改 this.data 不触发渲染("逻辑对但界面没变"的头号嫌疑)
症状:数据"明明改了"(this.data.xxx 打印正确、后续逻辑也按新值走),但界面纹丝不动。典型:onLoad 解析路由参数后视图没切换、程序化改状态后开关/标签没动。
根因:官方口径——直接修改 this.data 不会把数据发送到视图层,只有 setData 才会触发重渲染(setData 同时同步更新 this.data)。所以:
// 错误:逻辑态变了,视图层没收到
onLoad(options) {
if (options.view) this.data.view = MAP[options.view]; // ❌ 界面不会变
});
// 正确
onLoad(options) {
const patch = {};
if (options.view) patch.view = MAP[options.view];
if (Object.keys(patch).length) this.setData(patch); // ✅
});
最容易踩的变体:onLoad 直接赋值改了 view,而后续统一刷新函数(如 refresh())的 patch 里恰好不含这个键 → 界面永远停在初始值。加固办法:在统一 refresh 的 patch 里加一条不变量 { view: this.data.view },让渲染层始终镜像逻辑态。
测试为什么拦不住:如果测试桩的 setData 实现是"直接把 patch 写进 this.data",那么「直接赋值」和「setData」在桩里效果完全相同 → 断言 page.data.view === '周' 假绿。
正确做法:桩里维护两个模型——page.data(逻辑层,setData + 直接赋值都写)与 page.render(视图层,只随 setData 更新);断言渲染键时读 render,并额外断言 data.X === render.X("逻辑态 ≡ 渲染态")来兜住整类缺陷。
静态扫描注意:全仓搜 this.data.X = 时,要按归属页面聚合(页面本体 + 它的 modules 一起看)——模块里直接改、由页面 refresh() 的 patch 下发是常见且正确的模式,按单文件判定会大量假阳。
其他高频坑
| 坑 | 症状 | 修复 |
|---|---|---|
| textarea 是原生组件 | 层级最高,盖住弹层/按钮 | 用同层渲染(基础库 ≥2.4.4 默认)、或弹层出现时 wx:if 隐藏 textarea |
| auto-height 布尔属性 | 高度不生效 | 写 auto-height="{{true}}",不要裸写 |
| data 字段与方法重名 | 方法被 data 覆盖,bindtap 失效 | 命名隔离:data 用名词,方法用 on 动词 |
| wx:if 同区切换大块节点 | 状态残留/白屏 | 复杂区域用 <block wx:if> 包裹;preview 类内容切回时重置 loading |
| 自定义 navigationStyle 页面 | 状态栏遮挡内容 | 顶部用 status-bar 组件占位 |
wx.switchTab 打在非 tabBar 页 |
点击完全无反应(fail 回调未处理时静默);控制台 switchTab:fail can not switch to no-tabBar page |
仅当目标页在 app.json 的 tabBar.list 里才用 switchTab。自绘 tabBar 项目切 tab 用 wx.reLaunch(见下行),不要混用 switchTab/redirectTo;加 grep 校验防复现(注意先剥注释再检索) |
自绘 tabBar 用 redirectTo 切 tab |
页面栈静默累积(redirectTo 只替换栈顶):「首页 navigateTo 子页 → 子页 redirectTo 回首页」每轮留下一个旧首页;达上限 10 后 navigateTo 静默失败(按钮点了没反应) |
自绘 tabBar 切 tab 用 wx.reLaunch(清栈到目标页,标准做法),不要再用 redirectTo 兜底 |
| 路由参数用别名读取 | 静态扫描报"传了但没人读"(假阳) | onLoad 里常见 const opt = options \|\| {} 再 opt.type;检查入参契约时要连别名一起识别,别只搜 options. |
跳转 API 选择(先确认 app.json 有没有 tabBar)
| 场景 | 用法 |
|---|---|
目标在 app.json > tabBar.list |
wx.switchTab(唯一合法场景,且不能带 query) |
| 无 tabBar(自绘底部导航)· 切 tab | wx.reLaunch(清栈到目标页,根除 redirectTo 的「切一次 tab 留一个旧页」累积;可带 query)。实测项目「时格备考表」即以此收口 |
| 无 tabBar · 入子页(需返回)/局部替换当前页 | wx.navigateTo(入栈,可返回)/wx.redirectTo(关当前页,仅限非 tab 语义,反复用于切 tab 会累积栈) |
> 自绘 tabBar 时最容易漏改:从原 tabBar 项目迁移、或从模板复制来的「跳首页/跳统计页」代码里常残留 switchTab(打非 tabBar 页静默失败)、或残留 redirectTo(切 tab 累积栈)。二者都要靠全仓检索兜底,切 tab 一律收敛为 wx.reLaunch。
按钮无反应排查清单(按序)
- WXML 自闭合标签吞节点(本文件 P0)
- data 与方法重名冲突
- JS 语法错误(
node -e "new Function(js)"快速验证) - 事件名拼写 / data-* dataset 类型(+e.currentTarget.dataset.x 转数字)
- 条件提前 return(如
if (this.data.loading) return卡死) - wx:if 外层条件把按钮区域整个排除
- 跳转类「无反应」单独看:
wx.switchTab打到非 tabBar 页会静默失败(见上表),grepswitchTab核对app.json是否有tabBar.list;自绘 tabBar 还要查页面栈是否已累积到上限 10(见上表) - 「改了数据界面不动」→ 直奔 P1:查
this.data.X =直接赋值,以及统一 refresh 的 patch 里是否漏了这个键