前言
生命不息,折腾不止。我很喜欢那些搞事情的人,比如说创业者,发明者。从大处说,他们不是解决了我们的就业,就是推动了社会的进步。从小处说,生活需要目标和想法,不然就会像一杯白开水一样,索然无味。最近萌生了想开发一个列表项固定高度虚拟滚动工具库的想法。对于项目中高频使用的库,若有时间和精力的话,最好自己实现一下。这样做至少有两个好处,第一,做定制化业务开发很容易,第二,工具出现问题很快能定位到症结之所在。沿着这个思路,趁着热乎劲在,说干就干。
动手实现
项目初始化
用脚手架生成一个vue3+vite项目, 下面四种命令,都能生成一个vue3+vite4项目。
npm init vite
# 等价于
npm create vite
# 使用yarn
yarn create vite
# 使用pnpm
pnpm create vite
本文使用的是pnpm,因为pnpm安装工具包既快,也节省空间。选择 vue3 + ts 模式,
同时添加声明文件 vite-env.d.ts ,处理引入 .vue 文件编码软件标示红色波浪线问题,需要确保包含在 tsconfig.json 的include中,包含 vite-env.d.ts文件路径。
pnpm init vite vue3-virtual-list
开发功能
我们开发一个每项固定高度的虚拟滚动列表组件,要想使外层容器可以滚动,需要满足两个条件:
- 外层容器的滚动属性要设置为
overflow:auto
, - 内层容器的高度要大于外层容器
<style lang="less">
.virtual-list-scroll-box {
position: relative;
overflow: auto;
border: 1px solid #ccc;
cursor: default;
// 为了美观,隐藏滚动条
&::-webkit-scrollbar {
display: none;
}
}
</style>
<template>
<div class="virtual-list-scroll-box" :style="scrollBoxStyle" @scroll.passive="handleScroll">
<div :style="contentBoxStyle">
<div v-for="(item, index) in fixedList" :style="item.style" :key="index">
<slot name="listItem" :itemData="item.itemData"></slot>
</div>
</div>
</div>
</template>
实现虚拟列表的关键逻辑是容器滚动后变更可见区域的显示条目,其中具体步骤如下:
- 计算可视区域可以显示的元素数量
- 计算可视区域滚动时数据的起始缓冲索引 startBuffIndex
- 计算可视区域滚动时数据的结束缓冲索引 endBuffIndex
- 计算出startBuffIndex到endBuffIndex对应每项数据在整个列表中的偏移位置 Offset,设置到列表项上
并进行渲染
Vue版本的代码实现如下:
<script setup lang="ts">
import { ref, computed, watchEffect } from 'vue';
const props = withDefaults(
defineProps<{
data: any[];
height: number;
width: number;
itemSize: number;
itemCount: number;
}>(),
{
data: () => [],
height: 500,
width: 200,
itemSize: 50,
itemCount: 10,
}
);
// 记录滚动卷去的高度
const scrollOffset = ref(0);
// 外部容器高度
const scrollBoxStyle = computed(() => {
const { height, width } = props;
return {
width: `${width}px`,
height: `${height}px`,
};
});
// 元素撑起盒子的实际高度
const contentBoxStyle = computed(() => {
const { itemSize, itemCount } = props;
return {
height: `${itemSize * itemCount}px`,
width: '100%',
};
});
const fixedList = computed(() => {
const arr = [];
const { data, height, itemSize, itemCount } = props;
const Buff_Size=2;
// 可视区能展示的元素的最大个数
const visibleCount = Math.ceil(height / itemSize);
// 可视区起始索引
const startIndex = Math.floor(scrollOffset.value / itemSize);
// 缓冲区起始索引
const startBuffIndex = Math.max(0, startIndex - Buff_Size);
// 缓冲区结束索引
const endBuffIndex = Math.min(itemCount - 1, startIndex + visibleCount + Buff_Size);
// 根据上面计算的索引值,不断添加元素给container
for (let i = startBuffIndex; i <= endBuffIndex; i++) {
arr.push({
style: {
position: 'absolute',
height: `${itemSize}px`,
width: '100%',
// 计算每个元素在container中的top值
top: `${itemSize * i}px`,
},
itemData: data[i],
} as const);
}
return arr;
});
// watchEffect(() => {
// console.log(fixedList, props.data);
// });
// 当触发滚动就重新计算
const handleScroll = (evt: UIEvent) => {
scrollOffset.value = (evt.currentTarget as HTMLDivElement).scrollTop;
};
</script>
开发模式改进
将组件的代码和调试页面代码放在同一个工程中,每次部署的时候,得进行手动拆分,把demo演示示例功能移除,感觉不是很方便。之前看到过pnpm支持workspace功能,可以把一个项目按照功能拆分成多个子项目,每个子项目可以引用别的子项目,每个子项目可以单独运行,单独打包。这正好是我们所需要的。
我们把项目拆分成两个子项目。组件的实现放在core子项目,组件的调试放在demo子项目。
新建一个 pnpm-workspace.yaml 文件,配置内容如下:
packages:
- "core/**"
- "demo/**"
pnpm的workspace依赖包的安装分两种方式,一种是在根目录下安装,所有子项目都共享,另外一种是给每个子项目独自安装依赖包。特别要说明的是,一个子项目可以把另外一个子项目当做依赖包,进行安装。不同方式的安装依赖包命令如下:
# 安装公共npm依赖包
pnpm i typescript -w -D
# 给某个项目安装npm依赖包
pnpm install 包名 -r --filter 某个项目中package.json中定义的name字段
# 把A项目当做依赖包,安装到B,C项目
pnpm i A -r --filter B C
相信你也和我一样心中会有疑问:当这样的工具包被发布后,如果引用了同一个仓库下的子项目,外网如何找到形如"@A": "workspace:^1.0.0"
这样的依赖包。当执行了pnpm publish
后,pnpm会把基于workspace的依赖变成外部依赖,如:
// 执行pnpm publish之前
"dependencies": {
"@A": "workspace:^1.0.0"
},
// 执行pnpm publish之后
"dependencies": {
"@A": "^1.0.0"
},
另外,还要说一下pnpm的workspace模式,每个子项目的启动/打包命令是:
pnpm -C 子项目路径 子项目的package.json中的scripts配置的命令
举例:pnpm -C ./demo start
, 有了这些知识垫底,相信拆分子项目对你而言就很Easy了,如果你还不会,可以下载文末的代码。
发布自己的npm包
- 在 npm 官网 注册一个账号
- 发布npm包时,要将自己配置的别的npm镜像源切换到npm官方镜像源
nrm ls
# 切换镜像源
nrm use npm
- 在终端登陆账号
npm login
npm notice Log in on https://registry.npmjs.org/
Username: // 用户名
Password: // 密码(只能手输,不能复制粘贴)
Email: (this IS public) // 注册邮箱
Enter one-time password from your authenticator app: // 注册邮箱收到的EOTP code(当次有效)
- 发布
执行npm publish
,看下面的错误提示,猜测是与别人发布的包重名了
去npm官网查了一下,果不其然
改个名字,重新发布,这次看到发布成功了
- 更新
// patch--补丁号,修复bug,小变动,如 v1.0.0->v1.0.1
npm version patch
// minor--次版本号,增加与修改功能,如 v1.0.0->v1.1.0
npm version minor
// major--主版本号,不兼容的修改,如 v1.0.0->v2.0.0
npm version major
结语
本文的代码已经分享到码云,你可以点击这里下载学习。本以为会把时间消耗在学习npm包的发布流程方面,实际开发下来,发现在pnpm+workspace这一块花费的时间最多,所以自认为是难点的地方,可能并不见得是难点。只有自己动手做一下,自己薄弱的环节才会暴露出来。在还用不上的时候提前暴露,总比事到临头,带着压力去攻克难关人在心态和开发体验上要好很多,这就是爱折腾的意义之所在。