手摸手带你开发一个实用的浏览器插件
先上代码,别整虚的
我最近在搞一个移动端的下拉刷新插件,需求很简单:用户下拉页面,触发刷新动作。但实际做起来才发现,这玩意儿坑真不少。尤其是 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 执行时机的问题。但我没深究,反正不影响功能。
以上是我踩坑后的总结,希望对你有帮助。这个技巧的拓展用法还有很多,后续会继续分享这类博客。如果有更优的实现方式,欢迎评论区交流,我也一直在找更好的写法。

暂无评论