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 暂停、触摸暂停、点击条目回调。
- 支持
append和replace两种数据更新模式。 - 支持 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>
默认 renderMode 为 auto:没有插槽时使用 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' |
渲染模式;插槽内容建议使用 auto 或 dom |
数据不足和是否滚动
组件不会修改用户传入的 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
当数据从很多更新成很少时,如果新的内容长度不再超过容器,并且 forceScroll 为 false,组件会把当前滚动偏移归零并切换为静态展示,避免空白、跳动或停在错误位置。
事件
| 事件名 | 参数 | 说明 |
|---|---|---|
item-click |
{ item, index, event } |
点击可视条目时触发 |
暴露方法
组件通过 defineExpose 暴露以下方法:
| 方法 | 说明 |
|---|---|
updateData(newData, mode) |
更新数据,mode 为 append 或 replace |
start() |
启动动画 |
stop() |
停止动画 |
pause() |
暂停滚动,不销毁动画循环 |
resume() |
恢复滚动 |
getSnapshot() |
获取当前 offset、运行状态和数据长度 |
无感更新原理
组件内部把动画状态和数据状态解耦:
offset只由动画循环维护,不跟随数据变化重置。updateData只替换或追加数据数组,然后把当前offset归一化到新的循环长度内。- 每帧根据
offset / (itemSize + gap)计算可视区起始槽位,再用取模映射到真实数据下标。 - 绘制时只渲染可视区数量加少量缓冲项,因此数据总量增加不会线性增加每帧绘制成本。
追加模式适合长轮询、WebSocket 等增量数据场景。替换模式适合全量刷新,组件会保留当前偏移量,尽量避免视觉跳动。
本地开发
npm install
npm run dev
npm run build