Skip to content

第三方插件

批量检测并更新依赖

批量检测并更新依赖工具 【npm专用】

npm-check-updates(简称 ncu)是批量检测并更新依赖的工具,它可以检查并更新package.json中的依赖到最新版本。

  1. 安装 npm-check-updates(全局或项目本地): 全局安装(推荐)
bash
npm install -g npm-check-updates

或 项目本地安装(需在项目中使用 npx)

bash
npm install -D npm-check-updates
  1. 扫描并更新 package.json中的依赖:-u 表示直接修改 package.json 中的版本号为最新
bash
ncu -u
  1. 安装更新后的依赖:建议把node_modules文件删除掉,再执行安装命令
bash
npm install

Yarn专用

yarn upgrade-interactive

yarn upgrade-interactive --latest

说明:勾选需要更新的包(重点勾选 vue、vite、@vitejs/plugin-vue等),确认后自动升级。

less

bash
npm install less -D

由于vite内置less-loader则不需要再次安装

bash
npm i less less-loader --save

scss

bash
npm install sass -D

由于vite内置sass-loader则不需要再次安装

bash
npm i less less-loader --save

为什么用的scss却要安装sass?

我们通常使用scss作为预处理器,但安装的包是sass。这是因为Sass有两种语法: SCSS(Sassy CSS)使用.scss扩展名,语法和CSS类似,使用大括号和分号。 缩进语法(Sass)使用.sass扩展名,使用缩进和换行而不是大括号和分号。 在Vue项目中,我们通常使用SCSS语法,因为更接近CSS,更容易上手。而安装的包sass是两种语法的实现。

mitt轻量事件总线库

bash
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()清空所有事件监听(慎用)

封装全局事件总线

ts
// 导入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(事件名, 数据) 发送事件,支持任意类型数据:

ts
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(事件名, 回调函数) 监听,必须在组件卸载时取消监听(避免内存泄漏)

ts
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) // 需传入同一个处理函数
})

监听所有事件(调试 / 全局日志)

ts
// 监听所有事件的触发,type 是事件名,data 是传递的数据
emitter.on('*', (type, data) => {
  console.log(`触发事件:${type},数据:`, data)
})

一次性事件(监听一次后自动移除)

ts
// 手动实现一次性监听(mitt 本身无 once 方法,可通过 off 实现)
const handleOnce = (data) => {
  console.log('仅执行一次:', data)
  emitter.off('once-event', handleOnce) // 执行后立即取消监听
}
emitter.on('once-event', handleOnce)

清空所有监听(慎用,会移除所有事件的监听)

ts
emitter.all.clear()

axios

安装

bash
npm install axios

在src根目录下创建新的文件,复制以下代码即可

ts
// 导入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,
});

导出封装的方法

ts
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图片懒加载

安装

bash
npm install vue3-lazyload

main.js入口文件注册插件

ts
//引入图片懒加载插件
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。

在页面/组件中使用懒加载

ts
 <img v-lazy="图片地址" />

nprogress进度条

安装

bash
npm install nprogress

main.js入口文件导入nprogress以及nprogress样式

ts
// 进度条进度
import nprogress from 'nprogress';
// 导入进度条进度
import 'nprogress/nprogress.css';

在js或ts中使用nprogress

ts
// 进度条进度开启
nprogress.start();
// 进度条进度关闭
nprogress.done();

animate动画库

安装

bash
npm install animate.css

main.js入口文件导入animate

ts
// 导入animate动画库
import 'animate.css';

在页面/组件中使用懒加载

vue
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

安装

bash
npm install echarts

在全局或组件中导入echarts(或在main.js/main.ts中)

ts
// 导入echarts
import * as echarts from 'echarts';

// 或全局注册 --- 可设置echarts全局属性9(通过vue3组合式api"getCurrentInstance"中获取)
app.config.globalProperties.echarts = echarts;

在页面/组件中使用

ts
/* 在组件或页面中 */
// 第一步,初始化
const myChart = appInst.echarts.init(html元素/html标签(div标签即可));

myChart.setOption({
    // echarts官网查询配置项即可
})

示例:(柱状图)

ts
*** 注:测试环境 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],
            },
        ],
    });
}

示例:(饼状图)

ts
*** 注:测试环境 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" },
                ],
            },
        ],
    });
}

代码高亮

安装

bash
npm install highlight.js

在main.js/main.ts中配置如下

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)
  })
});

组件/页面中使用

html
<div v-highlight>
  <pre style="padding: 0; height: 0">
      <code class="当前语言的名称,如(js,java,md,json,ts...)">{{ 代码富文本 }}</code>
  </pre>
</div>

bootstarp5

安装

bash
npm install --save bootstrap @popperjs/core

安装开发依赖 SASS (由于使用scss必须开发环境安装SASS)

bash
npm install sass -D
bash
npm i sass --save-dev npm i sass -D

修改 vite.config.js 文件 (增加一个alias映射bootstrap)

ts
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,再导入样式

css
  @import "../node_modules/bootstrap";

在src/main.js 中导入 my.scss

ts
  // 导入自己的scss文件
  import './style.css' //路径根据这个文件地址来(在style.css中导入样式 或 在 assets目录中新建 my.scss,再导入样式)

  // 导入所有bootstrapjs
  import * as bootstrap from 'bootstrap'

pako高性能的压缩 / 解压库

安装

bash
npm install pako

完整导入 和 按需导入

ts
// 完整导入
import * as pako from 'pako';

// 按需导入(只导入需要的即可)
import { ungzip, ungzip, deflate, inflate } from 'pako';

TypeScript 支持(可选)

pako 自带类型声明,TS 项目无需额外安装 @types/pako,直接导入即可使用

核心 API 说明:pako主要处理 gzipdeflate 两种压缩格式

方法作用适用场景
pako.gzip()压缩数据为 gzip 格式网络传输、大文件压缩
pako.ungzip()解压 gzip 格式数据解析服务端压缩响应
pako.deflate()压缩数据为 deflate 格式本地存储(体积更小)
pako.inflate()解压 deflate 格式数据读取本地压缩存储

gzip压缩和ungzip解压

ts
// 文本
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解压

ts
// 文本
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 数据(本地存储优化)

vue
<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:压缩文本后上传(减少网络传输体积)

vue
<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 数据

vue
<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>

注意事项

  1. Vite/Webpack 兼容 Vite:无需额外配置,直接导入 pako 即可(Vite 会自动处理 ES 模块); Webpack:pako 是纯 JS 库,无依赖,webpack 打包时无需配置 externals。
  2. 处理中文 / 特殊字符 压缩前用 TextEncoder.encode()(UTF-8 编码),解压后用 TextDecoder.decode(),避免中文乱码; 禁止直接用 unescape/encodeURI 处理二进制数据,会导致压缩 / 解压失败。 localStorage 存储二进制数据乱码→ 压缩后转 Base64 存储(btoa/atob),不要直接存储 Uint8Array。
  3. 大文件处理 pako 适合处理 10MB 以内 的数据,超大文件(>50MB)建议分片压缩,避免主线程阻塞;
  4. 错误处理 压缩 / 解压失败(如数据损坏)会抛出异常,务必用 try/catch 包裹; 空数据压缩前需判空,避免 pako 处理空 Uint8Array 报错。

Web Worker 异步压缩 / 解压,不阻塞 Vue 组件渲染

ts
// 示例: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 等。

安装

bash
npm install crypto-js

完整导入 和 按需导入

ts
// 完整导入
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';
// 其他加密算法...

简单示例

加密方法

ts
/**
 * 加密数据
 * @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();
}

解密方法

ts
/**
 * 解密函数
 * @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 支持

ts
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

函数

ts
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;

组件中使用示例

vue
<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>

敏感数据本地存储

ts
// 加密存储到 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

ts
// 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(生产环境)文件:

ini
# .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)在需要加密 / 解密的组件中,直接导入工具函数使用,示例如下

vue
<!-- 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文件下载核心插件

安装

bash
npm install file-saver

file-saver是什么插件?

FileSaver.js是一个流行的 JavaScript 库,用于在浏览器中保存文件/本地下载(如文本、图片、PDF 、JSON、Blob等)到用户本地设备。

TypeScript 支持(可选)

bash
npm install @types/file-saver --save-dev

核心 API 与基础用法

ts
saveAs(blob/文件对象/URL, 文件名, 可选配置);

封装复用

函数

ts
// 导入
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);
};

组件中调用封装函数

vue
<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 文件(最常用)

vue
<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:下载图片 / 二进制文件

vue
<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)

vue
<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>

带样式 + 新窗口下载

vue
<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 的核心是虚拟化器。虚拟化器可以选择垂直(默认)或水平轴,这使得通过将两个轴配置结合实现垂直、水平甚至网格状虚拟化。

bash
npm install @tanstack/vue-virtual

基本配置项:

ts
/*************** 配置项 ***************/
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();

【固定高度】使用示例:

vue
<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>

【动态高度】使用示例:

vue
<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>

Copyright © 2026 Luke