封装基于canvas实现的长列表组件(1)

前言

作为使用React前端开发,可能vue也一样,应该会经常遇到长列表的功能需求,如果不做性能优化,那会导致渲染性能低,带来网页卡顿的问题。常见的解决方案应该是使用虚拟列表,只渲染可见的部分。但是对于虚拟列表的功能,经常不仅仅只需要渲染,还会有拖拽排序,多选组合等功能,这会带来很多的业务复杂度。首先,因为很少会自己出造轮子,所以光相关组件库的引用,就需要虚拟列表库,拖拽库等,因为虚拟列表无法渲染树结构,所以还需要讲树结构展平,然后还需要维护父子关系。那除了使用虚拟列表,还有哪些高性能渲染长列表的方式呢?这里介绍一种使用canvas画布绘制长列表的方法。先看下成品的效果,

这个是我们编辑器项目的canvas列表效果,这里的icon和滚动条也是使用canvas绘制的,使用体验跟dom体验相差无几,甚至应该比虚拟列表的体验好。

如果你的项目恰好也是一个基于canvas的产品,那使用canvas应该也是更好的选择。

接下来,本文会教大家封装一个开源的canvas列表的组件,这篇文章主要讲下列表的渲染 以及滚动条的实现

技术分析

1. 渲染部分

css 复制代码
 每个列表项就是矩形,矩形里面绘制一个icon,绘制一个文字,如果是父元素,那需要绘制一个文件夹图标和一个折叠图标
 滚动条也是一个矩形,在弄个圆角美观点

2. 交互部分

复制代码
 点击的时候需要知道点击的时候需要点击了哪个部分
 需要拖拽功能   

3. 技术选型

css 复制代码
 为了更快的实现上面的功能,选个canvas库进行开发,常见的canvas库有fabricjs,konvajs,pixijs
 fabricjs文档不友好,
 pixijs是基于webgl的,性能比较好,但是demo 没适合这个组件的
 konvajs 文档挺好,官方demo 提供了很多这个组件需要的功能
 所以这个组件选择基于konvajs进行开发
 

实现步骤

数据结构

常见的树结构

ini 复制代码
export type TreeItemOption = {
	label: string;
	id: string | number;
	data?: any;
	children?: TreeItemOption[];
};

canvas列表类

kotlin 复制代码
export class CanvasTree {
	private _stage: Stage;
	private _layer: Layer;
	private scrollBar: Rect;
	private totalHeight = 30;
	private _renderIndex = 0;
	constructor(container: HTMLElement, private _data: TreeItemOption[] = []) {
        this._stage = new Stage({
                container: container as HTMLDivElement,
                width: container.clientWidth,
                height: container.clientHeight,
        });

        const layer = new Layer();
        this._layer = layer;

        this._stage.add(layer);
        
            // 渲染滚动条
        const rect = new Rect({
                name: SCROLL_BAR_NAME,
                height: 100,
                width: SCROLL_BAR_WIDTH,
                cornerRadius: 5,
                fill: "#ccc",
                ID: SCROLL_BAR_NAME,
                x: this._stage.width() - SCROLL_BAR_WIDTH,
                hitStrokeWidth: 10,
                y: 0,
                draggable: true,
                dragBoundFunc: (pos) => {
                        return {
                                x: this.scrollBar.x(),
                                y: clamp(pos.y, 0, this.Height - this.scrollBar.height()),
                        };
                },
		});

		layer.add(rect);

		this.scrollBar = rect;

		this.render();

		this.register();
    }
    get Width() {
            return this._stage.width();
    }
    get Height() {
            return this._stage.height();
    }
  }

以上代码主要是初始化konvajs的画布场景,场景中加入一个层,层里面添加一个rect 作为滚动条。滚动条固定在画布的最右边,拖拽的时候固定x坐标,y的范围限制在画布内。

滚动条的高度为 (场景高度/列表总高度)*场景高度

绘制列表项

arduino 复制代码
renderNodeItem(item: TreeItemOption, depth: number) {
        const x = depth * 30;
        const y = this._renderIndex * 30;
        const group = new Group({
                x,
                y,
        });

        const background = new Rect({
                name: "Backgroud",
                width: this.Width,
                height: 30,
                stroke: "black",
                strokeWidth: 1,
        });
        group.add(background);

        const text = new Text({
                text: item.label,
                x: 30,
                height: 30,
                verticalAlign: "middle",
        });
        group.add(text);

        this._layer.add(group);

        this._renderIndex++;

        if (item.children) {
                for (const element of item.children) {
                        this.renderNodeItem(element, depth + 1);
                }
        }
}

列表项使用Group渲染,每个Group 加入Rect作为背景。使用·renderIndex记录项的索引,每个项的y值根据索引决定,每个项的x值由项所在的深度决定

监听滚动行为

kotlin 复制代码
this._stage.on("wheel", (e) => {
        if (this.totalHeight <= this.Height) return;

        e.evt.preventDefault();

        const direction = -Math.sign(e.evt.deltaY);

        if (direction < 0 && -this._stage.y() + this.Height >= this.totalHeight) {
                return;
        }

        if (direction > 0 && this._stage.y() >= 0) {
                return;
        }

        let y = this._stage.y();
        y += direction * (itemHeight + 1) * (Math.abs(e.evt.deltaY) / 100);

        if (direction < 0 && -y + this.Height >= this.totalHeight) {
                y = this.Height - this.totalHeight;
        }

        if (direction > 0 && y >= 0) {
                y = 0;
        }

        this._stage.y(y);

        this.updateScrollBar(y);
});

updateScrollBar(y: number) {
        if (this.totalHeight <= this.Height) return;
        const scale = Math.abs(y) / this.totalHeight;

        const dist = scale * this.Height;
        this.scrollBar.y(-y + dist);
}

监听场景的 wheel事件,如果场景的y 值在有效范围内(即在0 到列表总高度之间,总高度就是列表项数*列表项高度),那更新场景的y值,让场景上下滑动,同时需要更新滚动条的位置。

滚动条的位置 根据滚动高度和总列表高度的比例决定

拖拽滚动条

kotlin 复制代码
scrollY2StageY() {
        const y = this.scrollBar.y();
        const stageY = Math.abs(this._stage.y());
        const dist = y - stageY;
        const scale = dist / this.Height;
        this._stage.y(-this.totalHeight * scale);
        this.updateScrollBar(-this.totalHeight * scale);
}

将滚动条的y值 转换为场景的y值,然后在根据最新的场景高度,更新下滚动条的位置

效果

完整代码

请前往代码仓库查看

总结

以上基于konvajs实现了长列表的渲染以及对滚动条的模拟,下一步讲实现列表项的拖动,编组等。

参考资料

-Konvajs

相关推荐
一只大侠的侠6 小时前
Flutter开源鸿蒙跨平台训练营 Day 10特惠推荐数据的获取与渲染
flutter·开源·harmonyos
崔庆才丨静觅7 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60618 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了8 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅8 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅9 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
猫头虎9 小时前
如何排查并解决项目启动时报错Error encountered while processing: java.io.IOException: closed 的问题
java·开发语言·jvm·spring boot·python·开源·maven
崔庆才丨静觅9 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment9 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅10 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端