手摸手带你开发一个实用的浏览器插件

UP主~瑞芳 移动 阅读 2,884
赞 10 收藏
二维码
手机扫码查看
反馈

先上代码,别整虚的

我最近在搞一个移动端的下拉刷新插件,需求很简单:用户下拉页面,触发刷新动作。但实际做起来才发现,这玩意儿坑真不少。尤其是 touch 事件那部分,浏览器兼容性简直反人类。下面这段核心代码,是我折腾了三天后亲测有效的方案,直接贴出来,你们拿去改改就能用。

手摸手带你开发一个实用的浏览器插件

class PullToRefresh {
  constructor(container, onRefresh) {
    this.container = container;
    this.onRefresh = onRefresh;
    this.isRefreshing = false;
    this.startY = 0;
    this.currentY = 0;
    this.diffY = 0;

    this.init();
  }

  init() {
    this.container.addEventListener('touchstart', this.handleTouchStart.bind(this));
    this.container.addEventListener('touchmove', this.handleTouchMove.bind(this));
    this.container.addEventListener('touchend', this.handleTouchEnd.bind(this));
  }

  handleTouchStart(e) {
    if (this.isRefreshing || this.container.scrollTop > 0) return;
    this.startY = e.touches[0].clientY;
  }

  handleTouchMove(e) {
    if (this.isRefreshing || this.container.scrollTop > 0) return;

    this.currentY = e.touches[0].clientY;
    this.diffY = this.currentY - this.startY;

    if (this.diffY <= 0) return; // 只处理向下拉

    e.preventDefault(); // 关键!阻止默认滚动

    this.container.style.transform = translateY(${this.diffY}px);
    this.container.style.transition = 'none';
  }

  handleTouchEnd() {
    if (this.diffY < 80) {
      this.reset();
      return;
    }

    this.isRefreshing = true;
    this.container.style.transition = 'transform 0.3s ease';
    this.container.style.transform = 'translateY(60px)';

    this.onRefresh(this.finish.bind(this));
  }

  finish() {
    this.isRefreshing = false;
    this.reset();
  }

  reset() {
    this.container.style.transition = 'transform 0.3s ease';
    this.container.style.transform = 'translateY(0)';
    this.diffY = 0;
  }
}

怎么用?简单得很

你只要传入容器和回调函数就行。比如我现在要给一个列表加下拉刷新:

<div id="list-container" style="height: 100vh; overflow-y: auto;">
  <ul id="list">
    <!-- 列表项 -->
  </ul>
</div>
const container = document.getElementById('list-container');

new PullToRefresh(container, async (done) => {
  try {
    const res = await fetch('https://jztheme.com/api/list');
    const data = await res.json();
    updateList(data); // 更新 DOM 的逻辑自己写
  } catch (err) {
    console.error('加载失败', err);
  } finally {
    done(); // 一定要调 done,否则动效卡住
  }
});

这个场景最好用

说实话,这种插件最适合用在 SPA 里,特别是 Vue 或 React 的项目中,你可以把它封装成组件。我在 Vue 项目里是这么用的:

// PullToRefresh.vue
export default {
  mounted() {
    this.refreshPlugin = new PullToRefresh(this.$refs.container, this.loadMore);
  },
  methods: {
    async loadMore(done) {
      await this.fetchData();
      done();
    }
  },
  beforeDestroy() {
    // 注意清理事件监听器(虽然上面没写 addEventListener 的 remove)
    // 实际项目中建议保存 handler 引用,便于移除
  }
}

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

  • touchmove 默认行为不阻止,整个页面都会跟着抖。我最开始漏了 e.preventDefault(),结果一拉就页面卡顿,还以为是性能问题,折腾半天发现是浏览器在尝试滚动顶层视窗。
  • 判断是否允许下拉时,scrollTop > 0 是关键。意思是只有当容器已经滚到顶部的时候才允许触发下拉。不然用户在中间位置稍微往上滑一点,也会触发,体验极差。
  • transition 不要写在 CSS 里,要动态控制。如果一开始就写了 transition,拖动过程会变得粘滞,必须在 handleTouchMove 中设置 transition: none,结束时再加回来。

高级技巧:怎么让它更顺滑

上面的基础版能跑,但手感还是有点生硬。我后来优化了一下阻尼效果,模拟原生 App 那种“越拉越难拉”的感觉。原理就是对位移做个非线性映射:

getDampedDistance(diff) {
  return diff * 0.5 + (diff ** 2) / 100; // 简单的二次阻尼
}

然后在 handleTouchMove 里替换原来的 this.diffY

const dampedY = this.getDampedDistance(this.diffY);
this.container.style.transform = translateY(${dampedY}px);

这样拉得越多,视觉上的增量就越小,不会一下子甩出去一大截,体验立马提升一个档次。亲测有效。

又来了个新需求:支持配置项

团队里其他人想复用这个插件,但有人想要 50px 触发,有人想要 100px。于是我干脆加上 options 参数:

constructor(container, onRefresh, options = {}) {
  this.threshold = options.threshold || 80;
  this.bounceHeight = options.bounceHeight || 60;
  this.container = container;
  this.onRefresh = onRefresh;
  this.isRefreshing = false;
  this.startY = 0;
  this.currentY = 0;
  this.diffY = 0;

  this.init();
}

然后把所有写死的数值换成 this.threshold 这类变量。虽然看起来小改动,但后期维护省太多事了。建议大家写插件一开始就把配置项设计好,别等别人提需求才改。

关于 destroy 方法,我没写全

上面的代码里没有提供 destroy 方法,理论上应该解绑事件监听器。但在实际项目中,我发现大多数情况下页面跳转或组件销毁时,DOM 直接被干掉了,事件自然也就没了。所以为了代码简洁,我省略了这部分。

不过如果你要做通用库发布到 npm,那就必须补上 destroy 方法,并且保存每个 handler 的引用,方便 removeEventListener。这块我不展开,毕竟这篇重点是实战不是发包。

能不能用第三方库?当然能,但我为啥不用

其实有现成的库,比如 pulltorefreshjs,我也试过。问题是它太重了,还依赖 DOM 查询,不好集成到现代框架里。而且定制样式麻烦,还得覆盖它的 class 名。

我自己写的这个,核心逻辑不到 100 行,完全可控。改个动画、加个 loading 图标都方便。所以我现在基本都是手撸一套,比引入第三方还快。

拓展玩法:不止是刷新

这个模式其实可以复用到很多场景。比如:

  • 上拉加载更多(reverse 版本)
  • 侧滑菜单(监听 touchmove X 轴位移)
  • 长按拖拽排序

只要你掌握了 touch 事件的基本套路,这些都能基于类似的结构扩展出来。关键是理解 touchstart → touchmove → touchend 这套流程,以及如何正确地阻止默认行为。

结尾碎碎念

这个插件我已经在三个项目里用了,iOS 和 Android 主流机型都没问题。唯一还有点小瑕疵是:在微信内置浏览器里,有时候第一次加载会有轻微卡顿,可能是 JS 执行时机的问题。但我没深究,反正不影响功能。

以上是我踩坑后的总结,希望对你有帮助。这个技巧的拓展用法还有很多,后续会继续分享这类博客。如果有更优的实现方式,欢迎评论区交流,我也一直在找更好的写法。

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

暂无评论