TDesign 组件库实战:从接入到深度定制的完整指南

怡博 ☘︎ 框架 阅读 2,276
赞 6 收藏
二维码
手机扫码查看
反馈

又踩坑了,TDesign 的 Dialog 关闭后页面滚动失效

上周在用 TDesign Vue 写一个管理后台的时候,突然发现:弹窗关掉之后,页面再也滚不动了。一开始我还以为是自己加了什么全局样式冲突,结果删了一堆代码也没用。折腾了半天才定位到是 TDesign 的 Dialog 组件搞的鬼。

TDesign 组件库实战:从接入到深度定制的完整指南

具体表现是:打开 Dialog 时,body 自动加上了 overflow: hidden,防止背景滚动;但关掉 Dialog 后,这个样式没被移除,导致整个页面“卡死”在当前位置,上拉下拉都没反应。用户反馈说“点完确定就动不了了”,我一脸懵,本地测试居然没复现?后来才发现——只有在移动端 Safari 或某些安卓 WebView 里才稳定出现,Chrome 桌面版反而正常。这坑埋得有点深。

试了三种方法,前两种都翻车了

第一反应是手动在关闭回调里恢复滚动:

const handleClose = () => {
  document.body.style.overflow = '';
};

但问题来了:如果页面里有多个 Dialog 嵌套,或者同时开了 Toast、Drawer 等其他组件,你根本不知道是不是该恢复滚动。万一另一个弹层还在显示,你把 overflow 清了,背景就能滚动了,体验更差。所以这种“粗暴清空”方式不可靠。

接着我翻了 TDesign 的源码(GitHub 上 tdesign-vue-next 仓库),看到 Dialog 内部确实用了 useLockScroll 这个 hook 来管理 body 的滚动锁定。逻辑大概是:每次打开 Dialog 就加锁,关闭就解锁。但为什么有时候解锁失败?

继续 debug,发现关键在于“计数器”机制。TDesign 用了一个全局变量 lockCount 记录当前有多少个组件锁住了滚动。每开一个 Dialog,lockCount++;每关一个,lockCount--,只有当 lockCount === 0 时才真正移除 overflow: hidden。听起来很合理,对吧?

但问题出在:**如果 Dialog 是通过 v-if 动态销毁的,它的 unmount 阶段可能没触发解锁逻辑**。比如我在父组件里这么写:

<template>
  <t-dialog v-if="showDialog" @close="handleClose" />
</template>
<script>
export default {
  data() {
    return { showDialog: false };
  },
  methods: {
    handleClose() {
      this.showDialog = false; // 直接销毁组件
    }
  }
}
</script>

这时候,Dialog 组件在 beforeUnmount 钩子里会调用 unlock,但因为 Vue 的更新是异步的,可能 unlock 执行时,lockCount 已经被其他操作干扰了,或者 cleanup 时机不对,导致最终 lockCount 没归零,body 的样式就一直挂着。

核心代码就这几行:用 visible 控制,别用 v-if

后来我试了官方文档里的 demo,发现他们全都是用 visible 属性控制显隐,而不是 v-if。改完立马就好了!

正确的写法应该是:

<template>
  <t-dialog :visible="showDialog" @close="handleClose" />
</template>
<script>
export default {
  data() {
    return { showDialog: false };
  },
  methods: {
    handleClose() {
      this.showDialog = false;
    }
  }
}
</script>

这样 Dialog 组件不会被销毁,只是隐藏。TDesign 内部的 lock/unlock 逻辑能完整走完生命周期,lockCount 也能正确维护。亲测有效,各种嵌套场景下都没再出现滚动卡死。

其实 TDesign 文档里隐约提过一句“建议使用 visible 控制”,但我之前没在意,总觉得 v-if 更“干净”。现在看来,在涉及全局状态(比如 body 样式、事件监听)的组件里,**保持组件实例存活比销毁重建更安全**。这也算是 Vue 组件设计的一个小经验了。

踩坑提醒:这三点一定注意

  • 别用 v-if 控制 Dialog/Drawer 等带全局副作用的组件。它们依赖完整的生命周期来清理副作用,v-if 会跳过部分钩子。
  • 如果非要动态创建,记得手动处理 unlock。虽然不推荐,但万一项目架构限制,你可以在关闭时手动调用 TDesign 内部的 unlock 方法(不推荐,因为内部 API 可能变):
    import { unlock } from 'tdesign-vue-next/es/_util/useLockScroll';
        // 在 close 回调里
        handleClose() {
          this.showDialog = false;
          unlock(); // 危险操作,仅作应急
        }

    但这种方法耦合了内部实现,升级 TDesign 版本可能就崩了,慎用。

  • 移动端测试不能只靠 Chrome DevTools。很多滚动行为在桌面浏览器里表现正常,但在真机或 WebView 里会出问题。这次就是 Safari 的 scroll behavior 和 Chrome 不一致导致的。

改完之后,大部分情况都正常了。不过还有个小瑕疵:如果用户在 Dialog 打开时快速连续点击关闭按钮,偶尔还是会出现 lockCount 负数(虽然不影响功能,但 console 会报 warning)。不过这个属于极端操作,产品说“用户不会这么傻”,我就没深究了。毕竟优先级不高,先上线再说。

以上是我踩坑后的总结,如果你有更好的方案欢迎评论区交流。比如有没有办法在 v-if 场景下自动补全 unlock 逻辑?或者 TDesign 后续会不会优化这个机制?反正我现在是老老实实用 visible 了,省心。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论