npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

unrefresh

v1.2.0

Published

A lightweight pull-to-refresh plugin for mobile web pages.

Downloads

436

Readme

安装

npm install unrefresh

在应用入口引入样式:

import 'unrefresh/css'

Vanilla JS

import { createRefresh } from 'unrefresh/vanilla'
import 'unrefresh/css'

const refresh = createRefresh({
  target: document,
  pullDownLength: 96,
  animation: 'magnetic',
  animationDuration: 720,
  bounce: true,
  completeDuration: 460,
  haptics: true,
  minLoadingDuration: 520,
  mouse: true,
  onStateChange(state) {
    updateRefreshIndicator(state.status, state.progress)
  },
  async onRefresh({ signal }) {
    const response = await fetch('/api/feed', { signal })
    const feed = await response.json()
    renderFeed(feed)
  },
})

refreshButton.addEventListener('click', () => {
  refresh.refresh()
})

cancelButton.addEventListener('click', () => {
  refresh.cancel()
})

toggleButton.addEventListener('click', () => {
  if (refresh.getState().refreshing)
    refresh.cancel()

  refresh.disable()
})

Resource 数据层

unrefresh/resource 适合信息流、收件箱、列表页和仪表盘。它把下拉刷新和真实数据请求绑定在一起,统一管理加载、取消、错误、重试、旧数据保留和更新时间。

import { createRefreshResource } from 'unrefresh/resource'
import 'unrefresh/css'

const feed = createRefreshResource({
  auto: true,
  cache: {
    key: 'feed',
    ttl: 5 * 60_000,
  },
  skeleton: {
    animation: 'shimmer',
    count: 6,
    variant: 'feed-card',
    when: 'empty',
  },
  target: document,
  pullDownLength: 96,
  bounce: true,
  completeDuration: 460,
  minLoadingDuration: 520,
  retry: 1,
  retryDelay: 300,
  staleTime: 30_000,
  async load({ signal }) {
    const response = await fetch('/api/feed', { signal })
    return await response.json()
  },
  onChange(state) {
    if (state.showSkeleton)
      renderSkeleton(state.skeletonCount, state.skeletonVariant, state.skeletonAnimation)
    else if (state.status === 'error')
      renderError(state.error)
    else if (state.data)
      renderFeed(state.data)

    if (state.isCached)
      showCachedBadge()
  },
})

feed.reload({ force: true })
feed.cancel()
feed.clearCache()
feed.markStale()
feed.setOptions({
  animation: 'magnetic',
  animationDuration: 640,
  pullDownLength: 112,
  skeleton: {
    animation: 'wave',
    count: 4,
    variant: 'feed-card',
    when: 'empty',
  },
})
feed.controller.disable()
feed.controller.enable()

Resource state 包含:

| 字段 | 说明 | | --- | --- | | data | 当前数据。 | | error | 最近一次加载错误。 | | status | idleloadingsuccesserror。 | | isCached | 当前数据是否来自缓存。 | | cacheKey | 当前使用的缓存 key。 | | isLoading | 是否正在加载。 | | isStale | 数据是否过期。 | | showSkeleton | 当前是否应该显示骨架屏。 | | skeletonAnimation | 骨架屏动画,例如 shimmerpulsewave。 | | skeletonCount | 建议渲染的骨架屏数量。 | | skeletonVariant | 页面级骨架屏类型,例如 feed-card。 | | failureCount | 当前加载过程中的失败次数。 | | updatedAt | 最近一次成功更新时间。 | | refresh | 下拉手势状态,例如 progressstatusdistance。 |

Vue

unrefresh/vue 是 Vue 兼容插件,但不会把 Vue 打进包里。

import { createApp } from 'vue'
import UnrefreshVuePlugin from 'unrefresh/vue'
import 'unrefresh/css'

createApp(App).use(UnrefreshVuePlugin, {
  target: document,
  async onRefresh({ signal }) {
    await reloadData(signal)
  },
})

React

unrefresh/react 提供 Hook。React 是可选 peer dependency,只有导入这个入口时才需要。

import { useRef } from 'react'
import { useRefresh } from 'unrefresh/react'
import 'unrefresh/css'

export function Feed() {
  const pageRef = useRef<HTMLElement | null>(null)

  useRefresh(pageRef, {
    async onRefresh({ signal }) {
      await reloadFeed(signal)
    },
  }, [])

  return <main ref={pageRef}>{/* feed */}</main>
}

如果团队偏好对象式 Hook 配置,也可以使用:

import { useRefreshController } from 'unrefresh/react'

const refreshRef = useRefreshController({
  target: pageRef,
  options: {
    async onRefresh({ signal }) {
      await reloadFeed(signal)
    },
  },
  deps: [],
})

Vue 注入

unrefresh/vue 也导出 UNREFRESH_VUE_KEY,适合需要显式注入的 Vue 项目。

import { UNREFRESH_VUE_KEY } from 'unrefresh/vue'

app.provide(UNREFRESH_VUE_KEY, refresh)

常用选项

| 选项 | 默认值 | 说明 | | --- | --- | --- | | animation | spin | 刷新加载指示器动画,可选 spinpulseorbitmagneticbounceflipnonemagnetic 是更有辨识度的 App 风格磁吸、轨道和扩散环动效。 | | animationDuration | 720 | 加载指示器动画时长,单位毫秒。 | | animationIcon | auto | 内置加载图标风格,可选 autoloopdotorbitmagnetarrowdiamondsparkboltarcauto 会根据动画自动切换不同图标。 | | target / dom | document.documentElement | 监听触摸事件的目标。 | | pullDownLength | 80 | 触发刷新的下拉距离。 | | bounce | true | 刷新锁定时启用回弹动画。 | | bounceDuration | 420 | 回弹动画时长,单位毫秒。 | | completeDuration | 0 | 成功或失败反馈显示时长。 | | disabled | false | 临时禁用手势。 | | haptics | false | 开启震动反馈。 | | minLoadingDuration | 0 | 最小加载显示时长。 | | mouse | false | 开启鼠标拖拽,适合桌面演示和测试。 | | onRefresh | undefined | 刷新回调,可使用 context.signal 取消请求。 | | onStateChange | undefined | 监听 idlepullingreadyrefreshingsuccesserror。 |

每种刷新动画都有独立的下拉帧格式。拖拽过程中,unrefresh 会实时更新 --unrefresh-progress--unrefresh-distance--unrefresh-frame-rotate--unrefresh-frame-scale--unrefresh-frame-orbit--unrefresh-frame-flip--unrefresh-frame-magnet 等 CSS 变量;内置样式会为 spinpulseorbitmagneticbounceflip 分别映射不同的当前帧效果,而不是统一使用同一种旋转。

内置动画默认也会使用不同图标风格:循环、脉冲点、轨道、磁吸、箭头、钻石和闪光等。用 animationIcon 可以指定某个内置图标;如果需要完全自定义图片 URL,继续使用 loadingImage,它的优先级高于 animationIcon

也可以传入自定义帧动画。frames 是离散进度帧,onFrame 可以返回每一帧的样式或 CSS 变量,用来做完全自定义的插值逻辑:

createRefresh({
  animation: {
    name: 'elastic-arc',
    frames: [
      { progress: 0, spinner: { opacity: 0.4, transform: 'scale(0.72)' } },
      { progress: 0.5, spinner: { opacity: 0.8, transform: 'scale(0.96) rotate(72deg)' } },
      { progress: 1, spinner: { opacity: 1, transform: 'scale(1.12) rotate(180deg)' } },
    ],
    onFrame({ frame }) {
      return {
        top: {
          transform: `translateY(${(1 - frame.progress) * -8}px) scale(${1 + frame.progress * 0.1})`,
        },
        variables: {
          '--brand-refresh-progress': frame.progress.toFixed(3),
        },
      }
    },
  },
})

Resource 选项

| 选项 | 默认值 | 说明 | | --- | --- | --- | | auto | false | 创建后自动执行首次加载。 | | cache | undefined | 从本地缓存恢复数据,并把成功加载结果写回缓存。 | | initialData | undefined | 首次请求前展示的初始数据。 | | keepPreviousData | true | 刷新和失败时保留旧数据。 | | load | 必填 | 数据加载函数。 | | onChange | undefined | 资源状态变化回调。 | | onLoadSuccess | undefined | 加载成功回调。 | | onLoadError | undefined | 全部重试失败后的错误回调。 | | retry | 0 | 失败重试次数,true 表示重试 2 次。 | | retryDelay | 指数退避 | 重试间隔。 | | skeleton | { animation: 'shimmer', count: 6, variant: 'default', when: 'empty' } | 控制骨架屏状态。 | | staleTime | undefined | 数据多久后标记为过期。 |

可以用 resource.setOptions() 在运行时更新刷新参数、骨架屏参数、重试、过期时间和旧数据保留策略,适合做调参面板、产品设置页或不同业务页面的动态手感调整。

设置 staleTime 后,reload() 会复用仍然新鲜的数据;手动刷新按钮或明确的用户操作可以使用 reload({ force: true }) 强制重新请求。

skeleton 支持 boolean、数字或对象。数字表示骨架屏数量;对象支持 animationcountenabledvariantwhenanimation 可选 shimmerpulsewavenone,用于匹配当前页面加载风格。variant 用来渲染页面级骨架屏,例如 feed-cardmessage-rowdashboard-tilewhen: 'empty' 只在首屏无数据加载时显示,when: 'loading' 则会在已有数据的刷新过程中也暴露骨架屏状态。

cache 支持字符串或对象。字符串会作为 localStorage key;对象支持 keyttl 和自定义 storage。缓存超过 ttl 后会被忽略并删除。

导出入口

| 入口 | 用途 | | --- | --- | | unrefresh | 默认通用入口。 | | unrefresh/vanilla | Vanilla JavaScript 工具。 | | unrefresh/resource | 框架无关的数据资源层。 | | unrefresh/vue | Vue 插件适配器。 | | unrefresh/react | React Hook 适配器。 | | unrefresh/css | 必需样式。 | | unrefresh/assets/logo.svg | 静态 logo。 | | unrefresh/assets/logo.gif | 动态 logo。 |

示例

Playground 使用真实网络请求,并内置参数调节面板,可实时调节下拉距离、动画时长、回弹、骨架屏动画、数据新鲜时间和震动反馈。动画选择器里也包含 Custom Arc,它使用同一套 framesonFrame 自定义动画 API 实现。

npm --prefix playground run dev

项目检查

提交 PR 或发布前建议跑完整检查:

npm run check

发布

GitHub Release 由 tag 触发。release workflow 会校验版本、执行 lint/typecheck/test、构建 package、构建 playground、生成 npm tarball、把 tarball 挂到 GitHub Release,并在配置 NPM_TOKEN 后发布到 npm。

npm run release

tag 必须和 package.json 版本一致,例如版本是 1.2.0 时 tag 必须是 v1.2.0。npm 发布启用了 GitHub Actions provenance,需要在 GitHub Secrets 中配置 NPM_TOKEN

如果要在本地手动发布 npm,使用:

npm publish --access public

provenance 只在 GitHub Actions release workflow 中启用。本地终端没有受支持的 provenance provider,所以 npm publish --provenance 可能会报 Automatic provenance generation not supported for provider: null

发布日志维护在 CHANGELOG.md,GitHub 自动生成 Release Notes 的分组规则在 .github/release.yml

License

MIT License © 2022 Song wuk