第三方插件
批量检测并更新依赖
批量检测并更新依赖工具 【npm专用】
npm-check-updates(简称 ncu)是批量检测并更新依赖的工具,它可以检查并更新package.json中的依赖到最新版本。
- 安装 npm-check-updates(全局或项目本地): 全局安装(推荐)
npm install -g npm-check-updates或 项目本地安装(需在项目中使用 npx)
npm install -D npm-check-updates- 扫描并更新 package.json中的依赖:-u 表示直接修改 package.json 中的版本号为最新
ncu -u- 安装更新后的依赖:建议把node_modules文件删除掉,再执行安装命令
npm installYarn专用
yarn upgrade-interactive
yarn upgrade-interactive --latest
说明:勾选需要更新的包(重点勾选 vue、vite、@vitejs/plugin-vue等),确认后自动升级。
less
npm install less -D由于vite内置less-loader则不需要再次安装
npm i less less-loader --savescss
npm install sass -D由于vite内置sass-loader则不需要再次安装
npm i less less-loader --save为什么用的scss却要安装sass?
我们通常使用scss作为预处理器,但安装的包是sass。这是因为Sass有两种语法: SCSS(Sassy CSS)使用.scss扩展名,语法和CSS类似,使用大括号和分号。 缩进语法(Sass)使用.sass扩展名,使用缩进和换行而不是大括号和分号。 在Vue项目中,我们通常使用SCSS语法,因为更接近CSS,更容易上手。而安装的包sass是两种语法的实现。
mitt轻量事件总线库
npm install mitt核心 API
| API | 作用 | 示例 |
|---|---|---|
| emitter.on(name, fn) | 监听指定事件 | emitter.on('msg', (data) => {}) |
| emitter.emit(name, data) | 触发指定事件并传递数据 | emitter.emit('msg', { content: 'hi' }) |
| emitter.off(name, fn) | 取消指定事件的监听 | emitter.off('msg', handleMsg) |
| emitter.on('*', (type, data) => {}) | 监听所有事件 | |
| emitter.all.clear() | 清空所有事件监听(慎用) |
封装全局事件总线
// 导入mitt
import mitt from 'mitt';
// 定义所有事件的类型(键:事件名,值:参数类型)
type Events = {
// 'totalContainerScrollbar'事件名,'void':值的类型,void表示没有值,无参数的事件
'totalContainerScrollbar': void;
// 'scrollbar-endReached'事件名,'string': 表示传递的值类型为字符串
'scrollbar-endReached': string;
// [key: string]: any
// 'cross-page-msg': { content: string; time: string } // 带参数的事件
// 'refresh-data': void // 无参数的事件
// 'user-login': { userId: string; name: string } // 复杂参数事件
}
// 创建类型约束的 mitt 实例, 用于【绑定事件】和【触发事件】
const emitter = mitt<Events>();
// 导出实例,供全项目使用
export default emitter;触发事件(发送方:页面 / 组件 A)
关键点
通过 emitter.emit(事件名, 数据) 发送事件,支持任意类型数据:
import { ref } from 'vue';
import emitter from '@/utils/emitter';
const inputVal = ref('');
// 触发事件(可在点击、接口回调等场景调用)
const sendEvent = () => {
// 第一个参数:事件名称(自定义,需与监听方一致)
// 第二个参数:要传递的数据(任意类型:字符串、对象、数组等)
emitter.emit('cross-page-msg', {
content: inputVal.value,
time: new Date().toLocaleTimeString()
})
// 也可触发无参数事件
// emitter.emit('refresh-data')
}监听 / 取消监听事件(接收方:页面 / 组件 B)
关键点
通过 emitter.on(事件名, 回调函数) 监听,必须在组件卸载时取消监听(避免内存泄漏)
import { onMounted, onUnmounted, ref } from 'vue'
import emitter from '@/utils/emitter'
// 响应式数据,用于实时展示接收的事件数据
const receivedData = ref(null)
// 定义事件处理函数(参数为触发方传递的数据)
const handleEvent = (data) => {
console.log('接收到事件:', data)
receivedData.value = data // 实时更新视图
}
// 组件挂载时,开始监听事件
onMounted(() => {
emitter.on('cross-page-msg', handleEvent) // 事件名必须与触发方一致
})
// 组件卸载时,取消监听(核心!防止内存泄漏)
onUnmounted(() => {
emitter.off('cross-page-msg', handleEvent) // 需传入同一个处理函数
})监听所有事件(调试 / 全局日志)
// 监听所有事件的触发,type 是事件名,data 是传递的数据
emitter.on('*', (type, data) => {
console.log(`触发事件:${type},数据:`, data)
})一次性事件(监听一次后自动移除)
// 手动实现一次性监听(mitt 本身无 once 方法,可通过 off 实现)
const handleOnce = (data) => {
console.log('仅执行一次:', data)
emitter.off('once-event', handleOnce) // 执行后立即取消监听
}
emitter.on('once-event', handleOnce)清空所有监听(慎用,会移除所有事件的监听)
emitter.all.clear()axios
安装
npm install axios在src根目录下创建新的文件,复制以下代码即可
// 导入axios
import axios from 'axios';
// 导入进度
import nprogress from 'nprogress'
// 创建新的axios
const $axios = axios.create({
// 开发板请求地址,需要vite配置跨域
baseURL: "http://localhost:9999/api",
// 正式版请求地址,例如:
baseURL: "https://fc-mp-ef.bspapp.com",
// 请求时间
timeout: 8000
});
// 枚举错误信息
enum error {
"请求成功" = 200,
'路径错误' = 404,
"客户端请求的语法错误,服务器无法理解" = 400,
"请求要求用户的身份认证" = 401,
"服务器拒绝执行此请求" = 403,
"服务器无法根据客户端的请求找到资源" = 404,
"客客户端请求中的方法被禁止" = 405,
"服务器无法根据客户端请求的内容特性完成请求" = 406,
"请求要求代理的身份认证,未授权" = 407,
"请求超时" = 408,
"服务器完成客户端的 PUT 请求时可能返回此代码,服务器处理请求时发生了冲突" = 409,
"服务器错误" = 500,
"服务器不支持请求的功能,无法完成请求" = 501,
"作为网关或者代理工作的服务器尝试执行请求时,从远程服务器接收到了一个无效的响应" = 502,
"由于超载或系统维护,服务器暂时的无法处理客户端的请求" = 503,
"充当网关或代理的服务器,未及时从远端服务器获取请求" = 504,
"服务器不支持请求的HTTP协议的版本,无法完成处理" = 505,
}
// 创建请求拦截器
$axios.interceptors.request.use((config) => {
// 进度条进度开启
progress.start();
---------------
// 用于添加token等等
---------------
// 一定要返回相应头,否则无法请求
return config;
});
// 创建相应拦截器
$axios.interceptors.response.use((response) => {
// 进度条进度关闭
nprogress.done();
// 返回的数据没有错误
if (response.status == 200) {
return response.data;
}
// 暴露提示信息
console.log(new Error(error[response.status]));
// 用于处理数据
return response;
}, err => {
// 错误信息
console.log(new Error(err));
console.log(error[err.response.status]);
// 错误提示
return new Error(error[err.response.status]);
});
// 暴露数据($http就是axios)
export const $http = $axios;
// get请求
export const $get = (url: string) => $axios.get(url);
// post请求
export const $post = (obj: { url: string, data?: object, headers?: object}) => $axios({
method: "POST",
...obj,
});导出封装的方法
import { $http, $post, $get } from './request/index';
// get请求使用方法
export const login = () => $get('/message.json'); // "/message.json"为请求地址
// post请求使用方法
export const getFilesHttp = (parameter) => $post({
// 请求地址
url: "/getFile",
// 请求体
data: parameter
});vue3-lazyload图片懒加载
安装
npm install vue3-lazyloadmain.js入口文件注册插件
//引入图片懒加载插件
import Lazyload from "vue3-lazyload";
app.use(Lazyload);
//注册插件
app.use(Lazyload, {
//可以指定加载中的图像
loading: "@/assets/images/default.png",
//可以指定加载失败的图像
error: "@/assets/images/err.png",
});
备注:自定义指令为"v-lazy", v-lazy == src。在页面/组件中使用懒加载
<img v-lazy="图片地址" />nprogress进度条
安装
npm install nprogressmain.js入口文件导入nprogress以及nprogress样式
// 进度条进度
import nprogress from 'nprogress';
// 导入进度条进度
import 'nprogress/nprogress.css';在js或ts中使用nprogress
// 进度条进度开启
nprogress.start();
// 进度条进度关闭
nprogress.done();animate动画库
安装
npm install animate.cssmain.js入口文件导入animate
// 导入animate动画库
import 'animate.css';在页面/组件中使用懒加载
1. 单个html元素时使用transition效果更好:
<transition
<!-- animate.css名称,不需要修改 -->
name="custom-classes"
<!-- 进入时的该元素的动画效果,animate__animated是固定不变的,修改animate__animated后面即可 -->
enter-active-class="animate__animated animate__bounceIn"
<!-- 离开时的该元素的动画效果 -->
leave-active-class="animate__animated animate__zoomOutRight">
<div></div>
</transition>
-------------------------------------------------------------
2. 多个列表时使用TransitionGroup效果更好:
<TransitionGroup
<!-- TransitionGroup在编译时,编译成的html标签 tag="ul"时,编译为“ul”标签 -->
tag="ul"
<!-- animate.css名称,不需要修改 -->
name="custom-classes"
<!-- 进入时的该元素的动画效果,animate__animated是固定不变的,修改animate__animated后面即可 -->
enter-active-class="animate__animated animate__bounceIn"
<!-- 离开时的该元素的动画效果 -->
leave-active-class="animate__animated animate__zoomOutRight">
<li></li>
</TransitionGroup>echarts
安装
npm install echarts在全局或组件中导入echarts(或在main.js/main.ts中)
// 导入echarts
import * as echarts from 'echarts';
// 或全局注册 --- 可设置echarts全局属性9(通过vue3组合式api"getCurrentInstance"中获取)
app.config.globalProperties.echarts = echarts;在页面/组件中使用
/* 在组件或页面中 */
// 第一步,初始化
const myChart = appInst.echarts.init(html元素/html标签(div标签即可));
myChart.setOption({
// echarts官网查询配置项即可
})示例:(柱状图)
*** 注:测试环境 vue3
// html元素(并设置容器的宽度和高度)
<div ref="columnarShape" style="width: 600px;height: 200px;"></div>
// 获取柱状图的容器(div即可)
const columnarShape = ref(null);
// 柱形图
function columnar() {
// 创建图标实例
const myChart = echarts.init(columnarShape.value);
// 配置项
myChart.setOption({
// echarts官网查询配置项即可
// width: "85%",
// height: "250px",
// 背景颜色
backgroundColor: "rgba(0,0,0,0)",
// backgroundColor: "#2f3349",
tooltip: {
trigger: "axis",
axisPointer: {
type: "shadow",
},
},
// 标题
legend: {
// 可也添加多个
data: ["Files", "Click"],
textStyle: {
color: '#000',
},
},
// x轴
xAxis: {
data: ['2024-1-21', '2024-1-20', '2024-1-19', '2024-1-18', '2024-1-17', '2024-1-16', '2024-1-15'],
axisLine: {
lineStyle: {
color: '#000',
},
},
},
// y轴
yAxis: {
splitLine: { show: false },
axisLine: {
lineStyle: {
color: '#000',
},
},
},
series: [
// 线条
{
name: "Click",
type: "line",
smooth: true,
showAllSymbol: true,
symbol: "emptyCircle",
symbolSize: 15,
data: [80, 40, 50, 80, 40, 50, 10],
},
// 实色背景
{
name: "Files",
type: "bar",
barWidth: 10,
itemStyle: {
borderRadius: 5,
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: "#14c8d4" },
{ offset: 1, color: "#43eec6" },
]),
},
data: [80, 40, 50, 80, 40, 50, 10],
},
{
name: "downLoad",
type: "pictorialBar",
symbol: "rect",
itemStyle: {
color: "#0f375f",
},
symbolRepeat: true,
symbolSize: [12, 4],
symbolMargin: 1,
z: -10,
data: [80, 40, 50, 80, 40, 50, 10],
},
],
});
}示例:(饼状图)
*** 注:测试环境 vue3
// html元素(并设置容器的宽度和高度)
<div ref="pieChart" style="width: 200px;height: 200px;"></div>
// 获取饼状图的容器(div即可)
const pieChart = ref(null);
// 饼状图
function cakeShaped() {
// 图标实例
const myChart = echarts.init(pieChart.value);
// 配置项
myChart.setOption({
// width: "100%",
// height: "150px",
// 背景颜色
backgroundColor: "#fff",
// echarts官网查询配置项即可
legend: {
top: "bottom",
textStyle: {
color: "#000",
},
},
textStyle: {
color: "#000",
fontFamily: "宋体",
},
// 圆盘颜色
color: ["#fc8452", "#73c0de", "#5470c6", "#fac858"],
// 工具菜单
toolbox: {
show: false,
feature: {
mark: { show: false },
saveAsImage: { show: true },
},
},
series: [
{
name: "Nightingale Chart",
type: "pie",
radius: [0, 50],
center: ["50%", "50%"],
roseType: "area",
itemStyle: {
borderRadius: 8,
},
data: [
{ value: 40, name: "Click" },
{ value: 38, name: "Files" },
{ value: 32, name: "DownLoad" },
{ value: 10, name: "People" },
],
},
],
});
}代码高亮
安装
npm install highlight.js在main.js/main.ts中配置如下
// 引入代码高亮,并进行全局注册
import 'highlight.js/styles/atom-one-dark.css';
//导入代码高亮文件
import hljs from 'highlight.js';
//自定义一个代码高亮指令
app.directive('highlight', function (el) {
const blocks = el.querySelectorAll('pre code');
blocks.forEach((block: any) => {
hljs.highlightBlock(block)
})
});组件/页面中使用
<div v-highlight>
<pre style="padding: 0; height: 0">
<code class="当前语言的名称,如(js,java,md,json,ts...)">{{ 代码富文本 }}</code>
</pre>
</div>bootstarp5
安装
npm install --save bootstrap @popperjs/core安装开发依赖 SASS (由于使用scss必须开发环境安装SASS)
npm install sass -Dnpm i sass --save-dev 或 npm i sass -D修改 vite.config.js 文件 (增加一个alias映射bootstrap)
import { fileURLToPath, URL } from 'node:url';
// 导入path
import path from 'node:path';
// 配置依赖
export default defineConfig({
mode: 'production',
plugins: [vue()],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
// 增加~bootstrap
'~bootstrap': path.resolve(__dirname, 'node_modules/bootstrap'),
}
},
server: {
open: true
}
})在style.css中导入样式 或 在 assets目录中新建 my.scss,再导入样式
@import "../node_modules/bootstrap";在src/main.js 中导入 my.scss
// 导入自己的scss文件
import './style.css' //路径根据这个文件地址来(在style.css中导入样式 或 在 assets目录中新建 my.scss,再导入样式)
// 导入所有bootstrapjs
import * as bootstrap from 'bootstrap'pako高性能的压缩 / 解压库
安装
npm install pako完整导入 和 按需导入
// 完整导入
import * as pako from 'pako';
// 按需导入(只导入需要的即可)
import { ungzip, ungzip, deflate, inflate } from 'pako';TypeScript 支持(可选)
pako 自带类型声明,TS 项目无需额外安装 @types/pako,直接导入即可使用
核心 API 说明:pako主要处理 gzip 和 deflate 两种压缩格式
| 方法 | 作用 | 适用场景 |
|---|---|---|
| pako.gzip() | 压缩数据为 gzip 格式 | 网络传输、大文件压缩 |
| pako.ungzip() | 解压 gzip 格式数据 | 解析服务端压缩响应 |
| pako.deflate() | 压缩数据为 deflate 格式 | 本地存储(体积更小) |
| pako.inflate() | 解压 deflate 格式数据 | 读取本地压缩存储 |
gzip压缩和ungzip解压
// 文本
const text = "Hello, Vue 3 with Pako!";
// 压缩
const gzipResult = pako.gzip(text);
console.log('压缩结果:', gzipResult);
// 解压
const ungzipResult = pako.ungzip(gzipResult, { to: "string" });
console.log('解压结果:', ungzipResult);
*******************************************************************************
// 对象
const obj = { msg: "Hello, Vue 3 with Pako", id: 17, ars: [1 ,'2', true] };
// 压缩
const gzipResult = pako.gzip(JSON.stringify(obj));
console.log('压缩结果:', gzipResult);
// 解压
const ungzipResult = pako.ungzip(gzipResult, { to: "string" });
console.log('解压结果:', ungzipResult);deflate压缩和inflate解压
// 文本
const text = "Hello, Vue 3 with Pako!";
// 压缩字符串
const deflateResult = pako.deflate(text);
console.log('压缩结果:', deflateResult);
// 解压回字符串
const inflateReResult = pako.inflate(deflateResult, { to: 'string' });
console.log('解压结果:', inflateReResult);
*******************************************************************************
// 压缩 ArrayBuffer
const input = new Uint8Array([1, 2, 3]);
const compressed = pako.deflate(input);
// 解压回 ArrayBuffer
const output = pako.inflate(compressed);示例
场景 1:压缩 / 解压 JSON 数据(本地存储优化)
<template>
<div>
<button @click="compressAndSave">压缩并存储数据</button>
<button @click="decompressAndRead">解压并读取数据</button>
</div>
</template>
<script setup>
import pako from 'pako';
// 模拟大体积JSON数据
const bigData = {
list: Array.from({ length: 1000 }, (_, i) => ({ id: i, name: `测试项${i}`, value: Math.random() })),
time: new Date().getTime()
};
// 压缩并存储到localStorage
const compressAndSave = () => {
// 1. JSON转字符串
const jsonStr = JSON.stringify(bigData);
// 2. 字符串转Uint8Array(UTF-8编码)
const uint8Array = new TextEncoder().encode(jsonStr);
// 3. pako压缩(deflate格式,体积更小)
const compressed = pako.deflate(uint8Array);
// 4. 转Base64存储(避免二进制乱码)
const base64Str = btoa(String.fromCharCode(...compressed));
localStorage.setItem('compressedData', base64Str);
console.log('压缩完成:', `原大小${jsonStr.length}字节 → 压缩后${base64Str.length}字节`);
};
// 解压并读取数据
const decompressAndRead = () => {
// 1. 读取Base64数据
const base64Str = localStorage.getItem('compressedData');
if (!base64Str) return alert('无压缩数据');
// 2. Base64转Uint8Array
const compressed = new Uint8Array(atob(base64Str).split('').map(char => char.charCodeAt(0)));
try {
// 3. pako解压
const decompressed = pako.inflate(compressed);
// 4. Uint8Array转JSON字符串
const jsonStr = new TextDecoder().decode(decompressed);
// 5. 转JSON对象
const data = JSON.parse(jsonStr);
console.log('解压后数据:', data);
alert(`解压成功,数据条数:${data.list.length}`);
} catch (err) {
console.error('解压失败:', err);
alert('数据解压失败');
}
};
</script>场景 2:压缩文本后上传(减少网络传输体积)
<template>
<textarea v-model="textContent" placeholder="输入大文本内容"></textarea>
<button @click="uploadCompressedText">压缩并上传</button>
</template>
<script setup>
import pako from 'pako';
import axios from 'axios';
const textContent = ref(''); // 绑定文本框内容
// 压缩文本并上传
const uploadCompressedText = async () => {
if (!textContent.value) return alert('请输入文本');
try {
// 1. 文本转Uint8Array
const uint8Array = new TextEncoder().encode(textContent.value);
// 2. gzip压缩(网络传输推荐gzip,服务端兼容性更好)
const compressed = pako.gzip(uint8Array);
// 3. 转Blob(方便FormData上传)
const blob = new Blob([compressed], { type: 'application/gzip' });
// 4. 构建FormData上传
const formData = new FormData();
formData.append('compressedText', blob, 'text.gz'); // 命名为.gz便于服务端识别
// 5. 上传到后端
const res = await axios.post('/api/upload-compressed', formData, {
headers: { 'Content-Type': 'multipart/form-data' }
});
console.log('上传成功:', res.data);
alert('上传成功');
} catch (err) {
console.error('上传失败:', err);
alert('上传失败');
}
};
</script>场景 3:解压服务端返回的 gzip 数据
<script setup>
import pako from 'pako';
import axios from 'axios';
// 请求并解压服务端gzip数据
const fetchAndDecompress = async () => {
try {
// 1. 请求二进制数据(responseType设为arraybuffer)
const res = await axios.get('/api/get-compressed-data', {
responseType: 'arraybuffer'
});
// 2. ArrayBuffer转Uint8Array
const uint8Array = new Uint8Array(res.data);
// 3. 解压gzip数据
const decompressed = pako.ungzip(uint8Array);
// 4. 转字符串/JSON
const dataStr = new TextDecoder().decode(decompressed);
const data = JSON.parse(dataStr);
console.log('解压后接口数据:', data);
} catch (err) {
console.error('请求/解压失败:', err);
}
};
</script>注意事项
- Vite/Webpack 兼容 Vite:无需额外配置,直接导入 pako 即可(Vite 会自动处理 ES 模块); Webpack:pako 是纯 JS 库,无依赖,webpack 打包时无需配置 externals。
- 处理中文 / 特殊字符 压缩前用 TextEncoder.encode()(UTF-8 编码),解压后用 TextDecoder.decode(),避免中文乱码; 禁止直接用 unescape/encodeURI 处理二进制数据,会导致压缩 / 解压失败。 localStorage 存储二进制数据乱码→ 压缩后转 Base64 存储(btoa/atob),不要直接存储 Uint8Array。
- 大文件处理 pako 适合处理 10MB 以内 的数据,超大文件(>50MB)建议分片压缩,避免主线程阻塞;
- 错误处理 压缩 / 解压失败(如数据损坏)会抛出异常,务必用 try/catch 包裹; 空数据压缩前需判空,避免 pako 处理空 Uint8Array 报错。
Web Worker 异步压缩 / 解压,不阻塞 Vue 组件渲染
// 示例:Web Worker中压缩(避免页面卡顿)
// worker.js
import pako from 'pako';
self.onmessage = (e) => {
const { data } = e;
const compressed = pako.deflate(new TextEncoder().encode(data));
self.postMessage(compressed);
};
// 组件中使用
const worker = new Worker(new URL('./worker.js', import.meta.url));
worker.postMessage(bigDataStr);
worker.onmessage = (e) => {
console.log('Worker压缩完成:', e.data);
};crypto-js加密/解密库
crypto-js是什么插件?
crypto-js 是一个 JavaScript 加密库,封装了常见的加密算法,如 AES、MD5、SHA 系列、DES 等。
安装
npm install crypto-js完整导入 和 按需导入
// 完整导入
import CryptoJS from 'crypto-js';
// 按需导入,只引入需要的模块
import AES from 'crypto-js/aes';
import encUtf8 from 'crypto-js/enc-utf8';
import encBase64 from 'crypto-js/enc-base64';
import MD5 from 'crypto-js/md5';
import SHA256 from 'crypto-js/sha256';
// 其他加密算法...简单示例
加密方法
/**
* 加密数据
* @param obj 需要加密的数据
* @returns
*/
function encryption(obj: any) {
// 设置密钥(建议至少16个字符)
const secretKey = "mySecretKey507298738";
// 1. 压缩:JSON字符串 → GZIP压缩
const jsonString = JSON.stringify(obj);
// const compressedData = pako.gzip(jsonString);
// console.log('压缩成功,数据长度:', compressedData.length, compressedData);
// 2. 将Uint8Array直接转换为CryptoJS可识别的格式
// 关键:正确转换Uint8Array到WordArray
const wordArray = Crypto.lib.WordArray.create(compressedData);
// 3. 加密
const encrypted = Crypto.AES.encrypt(wordArray, secretKey);
// 返回结果
return encrypted.toString();
}解密方法
/**
* 解密函数
* @param encryptedText 需要解密的数据
* @param secretKey 解密的密钥
* @returns
*/
function decrypt(encryptedText: String, secretKey: String) {
// 解密
// 4. 解密
const decrypted = Crypto.AES.decrypt(encryptedText, secretKey);
// 5. 将WordArray转回Uint8Array
const decryptedWords = decrypted.words;
const decryptedBytes = new Uint8Array(decryptedWords.length * 4);
for (let i = 0; i < decryptedWords.length; i++) {
const word = decryptedWords[i];
decryptedBytes[i * 4] = (word >> 24) & 0xff;
decryptedBytes[i * 4 + 1] = (word >> 16) & 0xff;
decryptedBytes[i * 4 + 2] = (word >> 8) & 0xff;
decryptedBytes[i * 4 + 3] = word & 0xff;
}
// 6. 去除可能的多余字节(CryptoJS填充)
// const actualLength = decrypted.sigBytes;
// const finalBytes = decryptedBytes.slice(0, actualLength);
// 7. 解压
// const decompressedData = pako.ungzip(finalBytes, { to: "string" });
// 判断当前返回的结构是否为空,为空的话返回基本的数据结构
// if (Boolean(decompressedData)) {
// // 返回数据
// return JSON.parse(decompressedData);
// } else {
// // 默认数据
// return {
// // 用户所有账号
// account: [],
// // 文件数据列表
// fileDataList: [],
// };
// }
return decryptedBytes
}TypeScript 支持
declare module 'crypto-js' {
export interface CipherParams {
ciphertext: WordArray;
salt?: string;
iv?: WordArray;
algorithm?: string;
mode?: any;
padding?: any;
blockSize?: number;
formatter?: any;
}
export class WordArray {
constructor(words?: number[], sigBytes?: number);
words: number[];
sigBytes: number;
toString(encoder?: any): string;
}
// 核心加密函数
export function AES: {
encrypt(
message: string | WordArray,
key: string | WordArray,
cfg?: any
): CipherParams;
decrypt(
ciphertext: string | CipherParams,
key: string | WordArray,
cfg?: any
): WordArray;
};
// 哈希函数
export function MD5(message: string | WordArray): WordArray;
export function SHA1(message: string | WordArray): WordArray;
export function SHA256(message: string | WordArray): WordArray;
export function HmacSHA256(
message: string | WordArray,
key: string | WordArray
): WordArray;
// 编码器
export const enc: {
Utf8: {
parse(str: string): WordArray;
stringify(wordArray: WordArray): string;
};
Base64: {
parse(str: string): WordArray;
stringify(wordArray: WordArray): string;
};
Hex: {
parse(str: string): WordArray;
stringify(wordArray: WordArray): string;
};
};
// 模式和填充
export const mode: {
CBC: any;
ECB: any;
};
export const pad: {
Pkcs7: any;
};
}封装加密工具函数1
函数
import CryptoJS from 'crypto-js';
// 加密密钥(在生产环境中应该从环境变量读取)
const CRYPTO_KEY = import.meta.env.VITE_CRYPTO_KEY || 'your-secret-key-123456';
const IV = CryptoJS.enc.Utf8.parse('1234567890123456'); // 16位偏移量
/**
* 加密工具类
*/
class CryptoUtils {
/**
* AES 加密
* @param {string} data - 要加密的数据
* @param {string} key - 密钥
* @returns {string} 加密后的字符串
*/
static aesEncrypt(data, key = CRYPTO_KEY) {
if (!data) return '';
const keyHex = CryptoJS.enc.Utf8.parse(key);
const encrypted = CryptoJS.AES.encrypt(data, keyHex, {
iv: IV,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
});
return encrypted.toString();
}
/**
* AES 解密
* @param {string} encrypted - 加密后的字符串
* @param {string} key - 密钥
* @returns {string} 解密后的字符串
*/
static aesDecrypt(encrypted, key = CRYPTO_KEY) {
if (!encrypted) return '';
try {
const keyHex = CryptoJS.enc.Utf8.parse(key);
const decrypted = CryptoJS.AES.decrypt(encrypted, keyHex, {
iv: IV,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
});
return decrypted.toString(CryptoJS.enc.Utf8);
} catch (error) {
console.error('AES解密失败:', error);
return '';
}
}
/**
* Base64 加密
* @param {string} data - 要加密的数据
* @returns {string} Base64字符串
*/
static base64Encode(data) {
if (!data) return '';
const wordArray = CryptoJS.enc.Utf8.parse(data);
return CryptoJS.enc.Base64.stringify(wordArray);
}
/**
* Base64 解密
* @param {string} data - Base64字符串
* @returns {string} 解密后的字符串
*/
static base64Decode(data) {
if (!data) return '';
try {
const parsedWordArray = CryptoJS.enc.Base64.parse(data);
return parsedWordArray.toString(CryptoJS.enc.Utf8);
} catch (error) {
console.error('Base64解密失败:', error);
return '';
}
}
/**
* MD5 哈希
* @param {string} data - 要哈希的数据
* @returns {string} MD5字符串
*/
static md5(data) {
if (!data) return '';
return CryptoJS.MD5(data).toString();
}
/**
* SHA256 哈希
* @param {string} data - 要哈希的数据
* @returns {string} SHA256字符串
*/
static sha256(data) {
if (!data) return '';
return CryptoJS.SHA256(data).toString();
}
/**
* SHA1 哈希
* @param {string} data - 要哈希的数据
* @returns {string} SHA1字符串
*/
static sha1(data) {
if (!data) return '';
return CryptoJS.SHA1(data).toString();
}
/**
* HMAC SHA256 签名
* @param {string} data - 要签名的数据
* @param {string} secret - 密钥
* @returns {string} 签名
*/
static hmacSha256(data, secret) {
if (!data || !secret) return '';
return CryptoJS.HmacSHA256(data, secret).toString();
}
/**
* 生成随机字符串
* @param {number} length - 长度
* @returns {string} 随机字符串
*/
static generateRandomString(length = 16) {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let result = '';
const randomValues = new Uint8Array(length);
crypto.getRandomValues(randomValues);
for (let i = 0; i < length; i++) {
result += chars[randomValues[i] % chars.length];
}
return result;
}
/**
* 对象加密
* @param {object} obj - 要加密的对象
* @param {string} key - 密钥
* @returns {string} 加密后的字符串
*/
static encryptObject(obj, key = CRYPTO_KEY) {
if (!obj) return '';
const jsonString = JSON.stringify(obj);
return this.aesEncrypt(jsonString, key);
}
/**
* 对象解密
* @param {string} encrypted - 加密的字符串
* @param {string} key - 密钥
* @returns {object} 解密后的对象
*/
static decryptObject(encrypted, key = CRYPTO_KEY) {
if (!encrypted) return null;
try {
const decrypted = this.aesDecrypt(encrypted, key);
return JSON.parse(decrypted);
} catch (error) {
console.error('对象解密失败:', error);
return null;
}
}
}
export default CryptoUtils;组件中使用示例
<template>
<div class="crypto-demo">
<h2>Crypto-JS 加密演示</h2>
<!-- 输入区域 -->
<div class="input-section">
<textarea v-model="inputText" placeholder="输入要加密的文本" rows="4" />
<input v-model="secretKey" placeholder="加密密钥(可选)" type="password" />
</div>
<!-- 操作按钮 -->
<div class="actions">
<button @click="handleAES">AES 加密/解密</button>
<button @click="handleBase64">Base64 编码/解码</button>
<button @click="handleHash">哈希计算</button>
<button @click="handleObject">对象加密</button>
</div>
<!-- 结果显示 -->
<div class="results">
<div v-if="aesResult">
<h3>AES 结果:</h3>
<p>加密: {{ aesResult.encrypted }}</p>
<p>解密: {{ aesResult.decrypted }}</p>
</div>
<div v-if="base64Result">
<h3>Base64 结果:</h3>
<p>编码: {{ base64Result.encoded }}</p>
<p>解码: {{ base64Result.decoded }}</p>
</div>
<div v-if="hashResult">
<h3>哈希结果:</h3>
<p>MD5: {{ hashResult.md5 }}</p>
<p>SHA256: {{ hashResult.sha256 }}</p>
<p>SHA1: {{ hashResult.sha1 }}</p>
</div>
<div v-if="objectResult">
<h3>对象加密结果:</h3>
<p>原始对象: {{ JSON.stringify(objectResult.original) }}</p>
<p>加密后: {{ objectResult.encrypted }}</p>
<p>解密后: {{ JSON.stringify(objectResult.decrypted) }}</p>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue';
import CryptoUtils from '@/utils/crypto';
// 响应式数据
const inputText = ref('Hello Crypto-JS!');
const secretKey = ref('my-secret-key');
const aesResult = ref(null);
const base64Result = ref(null);
const hashResult = ref(null);
const objectResult = ref(null);
// AES 加密解密
const handleAES = () => {
if (!inputText.value.trim()) {
alert('请输入文本');
return;
}
const encrypted = CryptoUtils.aesEncrypt(inputText.value, secretKey.value);
const decrypted = CryptoUtils.aesDecrypt(encrypted, secretKey.value);
aesResult.value = { encrypted, decrypted };
console.log('AES 加密:', encrypted);
console.log('AES 解密:', decrypted);
};
// Base64 编码解码
const handleBase64 = () => {
if (!inputText.value.trim()) {
alert('请输入文本');
return;
}
const encoded = CryptoUtils.base64Encode(inputText.value);
const decoded = CryptoUtils.base64Decode(encoded);
base64Result.value = { encoded, decoded };
};
// 哈希计算
const handleHash = () => {
if (!inputText.value.trim()) {
alert('请输入文本');
return;
}
hashResult.value = {
md5: CryptoUtils.md5(inputText.value),
sha256: CryptoUtils.sha256(inputText.value),
sha1: CryptoUtils.sha1(inputText.value)
};
};
// 对象加密
const handleObject = () => {
const testObject = {
id: 1,
name: '测试用户',
email: 'user@example.com',
timestamp: Date.now(),
data: {
role: 'admin',
permissions: ['read', 'write', 'delete']
}
};
const encrypted = CryptoUtils.encryptObject(testObject, secretKey.value);
const decrypted = CryptoUtils.decryptObject(encrypted, secretKey.value);
objectResult.value = {
original: testObject,
encrypted,
decrypted
};
};
</script>
<style scoped>
.crypto-demo {
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.input-section {
margin-bottom: 20px;
}
textarea, input {
width: 100%;
margin-bottom: 10px;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 16px;
}
.actions {
display: flex;
gap: 10px;
margin-bottom: 20px;
flex-wrap: wrap;
}
button {
padding: 10px 20px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
transition: background-color 0.3s;
}
button:hover {
background-color: #45a049;
}
.results > div {
margin-top: 20px;
padding: 15px;
background-color: #f5f5f5;
border-radius: 4px;
border-left: 4px solid #4CAF50;
}
h3 {
margin-top: 0;
color: #333;
}
p {
margin: 5px 0;
word-break: break-all;
font-family: monospace;
background-color: #fff;
padding: 5px;
border-radius: 3px;
}
</style>敏感数据本地存储
// 加密存储到 localStorage
const saveSecureData = (key, data) => {
const encrypted = CryptoUtils.aesEncrypt(
JSON.stringify(data),
'your-secret-key'
);
localStorage.setItem(key, encrypted);
};
// 从 localStorage 解密读取
const getSecureData = (key) => {
const encrypted = localStorage.getItem(key);
if (!encrypted) return null;
return CryptoUtils.decryptObject(encrypted, 'your-secret-key');
};封装加密工具函数2
// src/utils/crypto.js
import CryptoJS from 'crypto-js'
// 🔒 密钥和偏移量(建议从环境变量读取,避免硬编码!)
// AES-128 需 16 位密钥,AES-256 需 32 位密钥;CBC 模式必须 16 位 iv
const AES_KEY = CryptoJS.enc.Utf8.parse(import.meta.env.VITE_AES_KEY || '1234567890abcdef') // 16位
const AES_IV = CryptoJS.enc.Utf8.parse(import.meta.env.VITE_AES_IV || 'abcdef1234567890') // 16位
/**
* AES 加密(CBC 模式 + PKCS7 填充,前端后端需一致)
* @param {string} data - 待加密的原始数据(字符串)
* @returns {string} 加密后的 Base64 字符串(便于传输)
*/
export function aesEncrypt(data) {
if (!data || typeof data !== 'string') return ''
const src = CryptoJS.enc.Utf8.parse(data)
const encrypted = CryptoJS.AES.encrypt(src, AES_KEY, {
iv: AES_IV,
mode: CryptoJS.mode.CBC, // 加密模式(常用 CBC/ECB,ECB 无需 iv)
padding: CryptoJS.pad.Pkcs7 // 填充方式(与后端匹配!)
})
return encrypted.ciphertext.toString(CryptoJS.enc.Base64)
}
/**
* AES 解密(与加密配置完全对应,否则解密失败)
* @param {string} encryptedData - 加密后的 Base64 字符串
* @returns {string} 解密后的原始数据
*/
export function aesDecrypt(encryptedData) {
if (!encryptedData) return ''
const encryptedHex = CryptoJS.enc.Base64.parse(encryptedData)
const src = CryptoJS.enc.Base64.stringify(encryptedHex)
const decrypted = CryptoJS.AES.decrypt(src, AES_KEY, {
iv: AES_IV,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
})
// 转换为 UTF-8 字符串并去除多余填充
return decrypted.toString(CryptoJS.enc.Utf8).trim()
}
/**
* MD5 加密(不可逆,适用于密码存储、数据校验)
* @param {string} data - 待加密数据
* @returns {string} 32位小写 MD5 哈希值
*/
export function md5Encrypt(data) {
if (!data) return ''
return CryptoJS.MD5(data).toString()
}
/**
* SHA256 加密(不可逆,安全性高于 MD5)
* @param {string} data - 待加密数据
* @returns {string} SHA256 哈希值
*/
export function sha256Encrypt(data) {
if (!data) return ''
return CryptoJS.SHA256(data).toString()
}关键提示
在 Vue3 中,环境变量需以 VITE_ 前缀开头(Vite 构建工具要求),避免密钥硬编码到代码中(防止泄露)。
在项目根目录创建 .env.development(开发环境)和 .env.production(生产环境)文件:
# .env.development(开发环境)
VITE_AES_KEY=1234567890abcdef # 16位密钥(AES-128)
VITE_AES_IV=abcdef1234567890 # 16位偏移量(CBC模式用)
# .env.production(生产环境)
VITE_AES_KEY=prod1234567890abc # 生产环境密钥(需与后端一致)
VITE_AES_IV=prodabcdef12345678 # 生产环境偏移量Vue3 组件中使用(Composition API)在需要加密 / 解密的组件中,直接导入工具函数使用,示例如下
<!-- src/components/CryptoDemo.vue -->
<template>
<div class="crypto-demo">
<h3>Vue3 + Crypto-JS 示例</h3>
<!-- AES 加密/解密 -->
<div class="aes-section">
<input
v-model="rawData"
placeholder="输入待加密数据(AES)"
class="input"
/>
<button @click="handleAesEncrypt" class="btn">AES 加密</button>
<button @click="handleAesDecrypt" class="btn">AES 解密</button>
<p>加密后:<code>{{ aesEncrypted }}</code></p>
<p>解密后:<code>{{ aesDecrypted }}</code></p>
</div>
<!-- MD5 加密(不可逆) -->
<div class="md5-section">
<input
v-model="pwdData"
placeholder="输入密码(MD5 加密)"
class="input"
/>
<button @click="handleMd5Encrypt" class="btn">MD5 加密</button>
<p>MD5 结果:<code>{{ md5Result }}</code></p>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
// 导入封装的加密函数
import { aesEncrypt, aesDecrypt, md5Encrypt } from '@/utils/crypto'
// 响应式数据
const rawData = ref('hello crypto-js') // AES 原始数据
const aesEncrypted = ref('') // AES 加密结果
const aesDecrypted = ref('') // AES 解密结果
const pwdData = ref('123456') // MD5 原始密码
const md5Result = ref('') // MD5 加密结果
// AES 加密触发
const handleAesEncrypt = () => {
aesEncrypted.value = aesEncrypt(rawData.value)
aesDecrypted.value = '' // 清空解密结果
}
// AES 解密触发
const handleAesDecrypt = () => {
aesDecrypted.value = aesDecrypt(aesEncrypted.value)
}
// MD5 加密触发
const handleMd5Encrypt = () => {
md5Result.value = md5Encrypt(pwdData.value)
}
</script>
<style scoped>
.crypto-demo {
padding: 20px;
max-width: 800px;
margin: 0 auto;
}
.input {
width: 300px;
padding: 8px;
margin-right: 10px;
margin-bottom: 15px;
}
.btn {
padding: 8px 16px;
margin-right: 10px;
cursor: pointer;
background: #42b983;
color: white;
border: none;
border-radius: 4px;
}
code {
color: #e53e3e;
background: #f5f5f5;
padding: 2px 4px;
border-radius: 4px;
}
section {
margin-bottom: 30px;
}
</style>file-saver文件下载核心插件
安装
npm install file-saverfile-saver是什么插件?
FileSaver.js是一个流行的 JavaScript 库,用于在浏览器中保存文件/本地下载(如文本、图片、PDF 、JSON、Blob等)到用户本地设备。
TypeScript 支持(可选)
npm install @types/file-saver --save-dev核心 API 与基础用法
saveAs(blob/文件对象/URL, 文件名, 可选配置);封装复用
函数
// 导入
import { saveAs } from 'file-saver';
/**
* 通用文件下载函数
* @param {string/object} content 下载内容(文本/JSON 对象)
* @param {string} fileName 文件名(含后缀,如:data.json)
* @param {string} type 文件 MIME 类型,默认:text/plain
*/
export const useFileDownload = (content, fileName, type = 'text/plain;charset=utf-8') => {
let blob;
// 区分 JSON 对象和普通文本
if (typeof content === 'object') {
blob = new Blob([JSON.stringify(content, null, 2)], {
type: 'application/json;charset=utf-8'
});
} else {
blob = new Blob([content], { type });
}
// 处理中文文件名乱码(可选)
const safeFileName = encodeURIComponent(fileName);
saveAs(blob, safeFileName);
};组件中调用封装函数
<template>
<button @click="downloadDemo">调用封装函数下载</button>
</template>
<script setup>
import { useFileDownload } from '@/composables/useFileDownload';
const downloadDemo = () => {
// 下载 JSON 示例
useFileDownload(
{ id: 1, name: '测试数据' },
'封装下载示例.json',
'application/json;charset=utf-8'
);
};
</script>示例
场景 1:下载文本 / JSON 文件(最常用)
<template>
<button @click="downloadText">下载文本文件</button>
<button @click="downloadJSON">下载JSON数据</button>
</template>
<script setup>
import { saveAs } from 'file-saver';
// 下载纯文本
const downloadText = () => {
// 1. 构建文本内容
const content = 'Vue3 + file-saver 文本下载示例\n自定义换行\n中文正常显示';
// 2. 转为 Blob(指定 UTF-8 编码避免中文乱码)
const blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
// 3. 触发下载
saveAs(blob, '自定义文本文件名.txt');
};
// 下载JSON文件
const downloadJSON = () => {
// 1. 构建JSON数据
const jsonData = {
list: [{ name: '张三', age: 25 }, { name: '李四', age: 30 }],
time: new Date().toLocaleString()
};
// 2. JSON转字符串并生成Blob(缩进2格便于阅读)
const blob = new Blob(
[JSON.stringify(jsonData, null, 2)],
{ type: 'application/json;charset=utf-8' }
);
// 3. 下载
saveAs(blob, '用户数据.json');
};
</script>场景 2:下载图片 / 二进制文件
<template>
<button @click="downloadImage">下载在线图片</button>
</template>
<script setup>
import { saveAs } from 'file-saver';
// 下载图片(支持在线/本地图片)
const downloadImage = async () => {
try {
// 1. 拉取图片为 Blob(fetch 支持跨域需服务端允许)
const response = await fetch('https://picsum.photos/200/100');
const blob = await response.blob();
// 2. 下载(指定文件名和格式)
saveAs(blob, '风景图.png', { autoBom: false }); // autoBom:禁用自动编码(图片无需)
} catch (err) {
console.error('图片下载失败:', err);
alert('下载失败,请检查网络');
}
};
</script>场景 3:下载 CSV(模拟 Excel)
<template>
<button @click="downloadCSV">下载Excel格式数据</button>
</template>
<script setup>
import { saveAs } from 'file-saver';
// 下载 CSV(Excel 可直接打开)
const downloadCSV = () => {
// 1. 构建 CSV 内容(逗号分隔,换行用 \n)
const csvContent = `姓名,年龄,职业
张三,25,前端开发
李四,30,后端开发
王五,28,UI设计`;
// 2. 生成 Blob(指定 CSV 类型)
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8' });
// 3. 下载
saveAs(blob, '人员信息.csv');
};
</script>带样式 + 新窗口下载
<template>
<div style="padding: 20px;">
<button
style="padding: 8px 16px; cursor: pointer;"
@click="downloadWithStyle"
>
带样式下载(新窗口)
</button>
</div>
</template>
<script setup>
import { saveAs } from 'file-saver';
// 新窗口下载 + 样式优化
const downloadWithStyle = () => {
const content = '新窗口下载示例,点击后保留原页面';
const blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
// 新窗口下载(通过 a 标签模拟)
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = '新窗口下载.txt';
a.target = '_blank'; // 新窗口打开
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url); // 释放 URL 资源
};
</script>@tanstack/vue-virtual 虚拟化列表
TanStack Virtual是什么插件?
TanStack Virtual 是一款无头界面工具,用于虚拟化 JS/TS、React、Vue、Svelte、Solid、Lit 和 Angular 中的大量元素列表。anStack Virtual 的核心是虚拟化器。虚拟化器可以选择垂直(默认)或水平轴,这使得通过将两个轴配置结合实现垂直、水平甚至网格状虚拟化。
npm install @tanstack/vue-virtual基本配置项:
/*************** 配置项 ***************/
const virtualizer = useVirtualizer(computed(()=>({
count: 1000, // 总项目数量
getScrollElement: () => parentRef.value, // 滚动容器元素
estimateSize: () => 50, // 估算每项高度
overscan: 5, // 缓冲区项目数量
horizontal: false, // 是否水平滚动
paddingStart: 0, // 起始填充
paddingEnd: 0, // 结束填充
scrollMargin: 0, // 滚动边距【滚动边界预留空间(像素)】
gap: 0, // 项目间距
indexAttribute: 'data-index', // 索引属性名
initialOffset: 0, // 初始偏移量
getItemKey: (index) => index, // 获取项目key的函数
rangeExtractor: defaultRangeExtractor, // 范围提取器
measureElement: undefined, // 测量元素函数
scrollToFn: elementScrollToFn, // 滚动函数
// ...其余的配置可以到官网进行查询
})));
/*************** 属性 ***************/
// 获取当前虚拟项目列表(每一项的索引值,key值,起始位置或结束位置等等...)
virtualizer.value.getVirtualItems(); // 用于循环渲染列表项【***实际要渲染的dom参数以及数量***】
// 获取总的虚拟尺寸 return<number>
virtualizer.value.getTotalSize();
/*************** 方法 ***************/
// 滚动到指定索引
virtualizer.value.scrollToIndex(index, { index: number, options?: { align: 'start'\|'center'\|'end', behavior: 'auto'\|'smooth' })
// 滚动到指定偏移量
virtualizer.value.scrollToOffset(offset, { offset: number, options?: { align: 'start'\|'center'\|'end', behavior: 'auto'\|'smooth' });
// 重新测量所有项目
virtualizer.value.measure();
// 重新测量某一项的高度
virtualizer.value.measureElement(el);
// 强制重新计算布局
virtualizer.value.update();
// 重置状态
virtualizer.value.reset();【固定高度】使用示例:
<template>
<!-- 外层滚动容器(必须添加)【滚动条层】 -->
<div @scroll="handleScroll" ref="customAudioVirtualList" class="custom-audio-virtualList" :style="{ height: `${160}px` }">
<!-- 虚拟滚动占位层【item项采用transform,必需需要外层去撑开盒子】 -->
<ul :style="{ height: `${totalSize}px`, position: 'relative' }">
<li class="item"
v-for="row in virtualRows"
:key="row.index"
:style="{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
transform: `translateY(${row.start}px)`,
}"
>
项目 {{ row.index }}:{{ row }}
</li>
</ul>
</div>
</template>
<script setup>
import { onMounted, ref, nextTick, computed } from 'vue'
import { useVirtualizer } from '@tanstack/vue-virtual'
const itemsList = ref([1,2,3,4,5,6,7,8]);
setTimeout(() => {
// 数据
itemsList.value = Array.from({ length: 100000 }, (_, i) => i)
}, 3000);
// 滚动容器
const customAudioVirtualList = ref(null);
// 创建虚拟滚动器的函数 ---- 使用 computed - 修复响应式问题
const virtualizer = useVirtualizer(
computed(()=>({
// 使用最新数据长度
count: itemsList.value.length,
// 绑定滚动容器
getScrollElement: ()=> customAudioVirtualList.value,
// 可视区域上下额外渲染5项,优化滚动过渡体验
overscan: 2,
// 固定高度场景:直接返回单个项高度(性能最优)
estimateSize: () => 77,
// containerHeight: 160, // 容器固定高度
// 起始填充
paddingStart: 10,
// 项目间距
gap: 10, // item项之间的距离
}))
)
onMounted(async ()=>{
// 确保 DOM 完全渲染
await nextTick()
// 定位到某一个索引
const index = 6;
// 定位
virtualizer.value.scrollToIndex(index ,{ align: 'center' });
})
// 总数据 【***实际要渲染的dom参数以及数量***】
const virtualRows = computed(() => virtualizer.value.getVirtualItems() || [])
// item项总体高度
const totalSize = computed(() => virtualizer.value.getTotalSize() || 0)
// 滚动同步div盒子
const handleScroll = ({ target }) => virtualizer.value.scrollToOffset(target.scrollTop);
</script>
<style scoped>
.custom-audio-virtualList{
background: pink;
overflow-y: auto;
}
.item {
margin-top: 5px;
padding: 12px;
border-bottom: 1px solid #eee;
background: white;
box-sizing: border-box;
background-color: aqua;
height: 77px;
overflow: hidden;
}
</style>【动态高度】使用示例:
<template>
<!-- 关键点: -->
<li
v-for="row in virtualRows"
:key="row.index"
:data-index='row.index'
:ref="measureElement">
</li>
<!-- /*** 必须要有data-index属性,并赋值。且并指定ref ***/ -->
</template>
<script setup>
// 动态计算
const measureElement = (el: HTMLElement | null) => {
// 测量实际高度(通知virtualizer重新测量当前dom元素)
if (el) virtualizer.value.measureElement(el);
}
</script>