封装基于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

相关推荐
n12352353 分钟前
Chrome 插件开发入门指南:从基础到实践
前端·chrome
前端 贾公子9 分钟前
ElementUI 中 validateField 对部分表单字段数组进行校验时多次回调问题
前端·javascript·elementui
棒棒的唐10 分钟前
vue2 elementUI 登录页面实现回车提交登录的方法
前端·javascript·elementui
CoderJia程序员甲10 分钟前
GitHub 热榜项目 - 日榜(2025-09-09)
ai·开源·大模型·github·ai教程
前端小万13 分钟前
一次紧急的现场性能问题排查
前端·性能优化
excel28 分钟前
为什么相同卷积代码在不同层学到的特征完全不同——基于 tfjs-node 猫图像识别示例的逐层解析
前端
InternLM29 分钟前
专为“超大模型而生”,新一代训练引擎 XTuner V1 开源!
人工智能·开源·xtuner·书生大模型·大模型训练框架·大模型预训练·大模型后训练
知识分享小能手29 分钟前
React学习教程,从入门到精通,React 使用属性(Props)创建组件语法知识点与案例详解(15)
前端·javascript·vue.js·学习·react.js·前端框架·vue
用户214118326360231 分钟前
dify案例分享-免费玩转即梦 4.0 多图生成!Dify 工作流从搭建到使用全攻略,附案例效果
前端
CodeSheep31 分钟前
稚晖君又开始摇人了,有点猛啊!
前端·后端·程序员