npm.io
0.1.1 • Published yesterday

h-marquee-x

Licence
MIT
Version
0.1.1
Deps
0
Size
46 kB
Vulns
0
Weekly
0

HMarqueeX

HMarqueeX 是一个基于 Canvas 的 Vue 3 无限滚动走马灯组件。核心动画引擎不依赖 Vue,可以单独使用;Vue 组件负责生命周期、props、事件和插件注册。

特性

  • Canvas 渲染,适合 1000+ 条数据的长列表滚动场景。
  • 使用 requestAnimationFrame 驱动动画。
  • 只绘制可视区域内的条目,避免每帧遍历完整数据。
  • 支持水平和垂直滚动。
  • 支持 hover 暂停、触摸暂停、点击条目回调。
  • 支持 appendreplace 两种数据更新模式。
  • 支持 ESM 和 UMD 打包格式。

安装

npm install h-marquee-x

Vue 3 是 peer dependency,项目中需要已有 Vue 3。

Vue 3 使用

<template>
  <HMarqueeX
    ref="marqueeRef"
    class="marquee"
    :data="items"
    :speed="1.2"
    :item-width="170"
    :item-height="76"
    :gap="12"
    :render-item="renderItem"
    @item-click="handleClick"
  />
</template>

<script setup lang="ts">
import { ref } from 'vue';
import { HMarqueeX, type RenderItem, type MarqueeClickPayload } from 'h-marquee-x';
import 'h-marquee-x/style.css';

interface Item {
  title: string;
  value: number;
}

const marqueeRef = ref<InstanceType<typeof HMarqueeX> | null>(null);
const items = ref<Item[]>([
  { title: 'A', value: 100 },
  { title: 'B', value: 200 },
]);

const renderItem: RenderItem<Item> = (ctx, item, x, y, width, height) => {
  ctx.fillStyle = '#ffffff';
  ctx.strokeStyle = '#e2e8f0';
  ctx.beginPath();
  ctx.rect(x, y, width, height);
  ctx.fill();
  ctx.stroke();

  ctx.fillStyle = '#0f172a';
  ctx.font = '14px system-ui';
  ctx.textBaseline = 'middle';
  ctx.fillText(`${item.title}: ${item.value}`, x + 12, y + height / 2, width - 24);
};

function handleClick(payload: MarqueeClickPayload<Item>) {
  console.log(payload.item, payload.index);
}

function appendItems(next: Item[]) {
  marqueeRef.value?.updateData(next, 'append');
}
</script>

<style scoped>
.marquee {
  width: 100%;
  height: 120px;
}
</style>

插槽自定义内容

如果需要让业务方完全自定义内部 DOM 和 CSS,可以直接使用默认插槽:

<template>
  <HMarqueeX
    class="marquee"
    :data="items"
    :speed="1"
    :item-width="180"
    :item-height="82"
    :gap="12"
    @item-click="handleClick"
  >
    <template #default="{ item, index }">
      <div class="marquee-card">
        <strong>{{ item.title }}</strong>
        <span>{{ item.value }}</span>
        <small>#{{ index }}</small>
      </div>
    </template>
  </HMarqueeX>
</template>

<script setup lang="ts">
import { ref } from 'vue';
import { HMarqueeX } from 'h-marquee-x';
import 'h-marquee-x/style.css';

const items = ref([
  { title: '商品 A', value: 100 },
  { title: '商品 B', value: 200 },
]);

function handleClick(payload) {
  console.log(payload.item, payload.index);
}
</script>

<style scoped>
.marquee {
  width: 100%;
  height: 120px;
}

.marquee-card {
  display: grid;
  align-content: center;
  width: 100%;
  height: 100%;
  border: 1px solid #e2e8f0;
  border-radius: 8px;
  background: #ffffff;
  padding: 12px;
}
</style>

默认 renderModeauto:没有插槽时使用 Canvas 模式,有默认插槽时自动使用 DOM 模式。Canvas 不能直接渲染 Vue 插槽里的 HTML/CSS,因此插槽模式会用 DOM 节点承载内容,仍由组件内部用 requestAnimationFrame 控制滚动、暂停和点击。

也可以注册为全局插件:

import { createApp } from 'vue';
import HMarqueeX from 'h-marquee-x';
import 'h-marquee-x/style.css';

createApp(App).use(HMarqueeX).mount('#app');

纯 JavaScript 使用

import { CanvasMarquee } from 'h-marquee-x';

const canvas = document.querySelector<HTMLCanvasElement>('#marquee')!;

const marquee = new CanvasMarquee(canvas, {
  data: ['A', 'B', 'C'],
  speed: 1,
  itemWidth: 150,
  itemHeight: 80,
  gap: 10,
});

marquee.updateData(['D', 'E'], 'append');

Props

名称 类型 默认值 说明
data T[] [] 初始/响应式数据
direction 'horizontal' | 'vertical' 'horizontal' 滚动方向
speed number 1 每帧移动像素数
itemWidth number 150 条目宽度
itemHeight number 80 条目高度
gap number 10 条目间距
autoStart boolean true 挂载后自动开始
pauseOnHover boolean true 鼠标悬停暂停
touchPause boolean true 触摸/指针按下暂停
forceScroll boolean true 是否强制滚动;为 false 时仅内容主轴长度超过容器才滚动
backgroundColor string undefined Canvas 背景色
renderItem RenderItem<T> 内置文本渲染 自定义条目绘制函数
updateMode 'append' | 'replace' 'replace' props 数据变化时的更新策略
renderMode 'auto' | 'canvas' | 'dom' 'auto' 渲染模式;插槽内容建议使用 autodom

数据不足和是否滚动

组件不会修改用户传入的 data。为了做到无缝循环,内部只在渲染层做虚拟重复:

  • forceScroll: true:只要有数据就滚动。数据量不足以填满容器时,内部会重复映射原始数据进行绘制或渲染,但点击事件返回的仍然是原始 item/index
  • forceScroll: false:组件会计算内容主轴长度是否超过容器。没有超过时静态展示;超过时才开启循环滚动。

判断方式:

const itemStep = direction === 'horizontal'
  ? itemWidth + gap
  : itemHeight + gap

const contentSize = data.length * itemStep
const viewportSize = direction === 'horizontal'
  ? containerWidth
  : containerHeight

const shouldScroll = forceScroll || contentSize > viewportSize

当数据从很多更新成很少时,如果新的内容长度不再超过容器,并且 forceScrollfalse,组件会把当前滚动偏移归零并切换为静态展示,避免空白、跳动或停在错误位置。

事件

事件名 参数 说明
item-click { item, index, event } 点击可视条目时触发

暴露方法

组件通过 defineExpose 暴露以下方法:

方法 说明
updateData(newData, mode) 更新数据,modeappendreplace
start() 启动动画
stop() 停止动画
pause() 暂停滚动,不销毁动画循环
resume() 恢复滚动
getSnapshot() 获取当前 offset、运行状态和数据长度

无感更新原理

组件内部把动画状态和数据状态解耦:

  1. offset 只由动画循环维护,不跟随数据变化重置。
  2. updateData 只替换或追加数据数组,然后把当前 offset 归一化到新的循环长度内。
  3. 每帧根据 offset / (itemSize + gap) 计算可视区起始槽位,再用取模映射到真实数据下标。
  4. 绘制时只渲染可视区数量加少量缓冲项,因此数据总量增加不会线性增加每帧绘制成本。

追加模式适合长轮询、WebSocket 等增量数据场景。替换模式适合全量刷新,组件会保留当前偏移量,尽量避免视觉跳动。

本地开发

npm install
npm run dev
npm run build

Keywords