阅读视图
Vue 响应式原理深度解析
Vue真的是单向数据流?
深入浅出 Vue3 defineModel:极简实现组件双向绑定
深入浅出 Vue3 defineModel:极简实现组件双向绑定
在 Vue3 从 Options API 向 Composition API 演进的过程中,组件双向绑定的实现方式也经历了迭代优化。defineModel 作为 Vue3.4+ 版本推出的新语法糖,彻底简化了传统 v-model 双向绑定的实现逻辑,让开发者无需手动声明 props 和 emits 就能快速实现组件内外的数据同步。本文将从核心原理、使用场景、进阶技巧等维度,全面解析 defineModel 的使用方式。
一、为什么需要 defineModel?
在 Vue3.4 之前,实现组件双向绑定需要手动声明 props + 触发 emits,步骤繁琐且代码冗余:
<!-- 传统 v-model 实现(Vue3.4 前) -->
<template>
<input :value="modelValue" @input="handleInput" />
</template>
<script setup>
// 1. 声明接收的 props
const props = defineProps(['modelValue'])
// 2. 声明触发的事件
const emit = defineEmits(['update:modelValue'])
// 3. 手动触发事件更新值
const handleInput = (e) => {
emit('update:modelValue', e.target.value)
}
</script>
这种写法需要维护 props 和 emits 的一致性,且多字段双向绑定时代码量会成倍增加。而 defineModel 正是为解决这一痛点而生 —— 它将 props 声明、事件触发的逻辑封装为一个极简的 API,大幅降低双向绑定的开发成本。
二、defineModel 核心用法
1. 基础使用(单字段绑定)
defineModel 是一个内置的组合式 API,调用后会返回一个可响应的 ref 对象,既可以读取值,也可以直接修改(修改时会自动触发 update:modelValue 事件)。
<!-- 简化后的双向绑定 -->
<template>
<!-- 直接绑定 ref 对象,无需手动处理事件 -->
<input v-model="modelValue" />
</template>
<script setup>
// 一行代码实现双向绑定核心逻辑
const modelValue = defineModel()
</script>
父组件使用方式不变,依然是标准的 v-model:
<template>
<MyInput v-model="username" />
</template>
<script setup>
import { ref } from 'vue'
const username = ref('')
</script>
2. 自定义绑定名称(多字段绑定)
当组件需要多个双向绑定字段时,可给 defineModel 传入参数指定绑定名称,配合父组件的 v-model:xxx 语法实现多字段同步:
<!-- 子组件:多字段双向绑定 -->
<template>
<input v-model="name" placeholder="姓名" />
<input v-model="age" type="number" placeholder="年龄" />
</template>
<script setup>
// 自定义绑定名称:name 和 age
const name = defineModel('name')
const age = defineModel('age')
</script>
父组件使用带参数的 v-model:
<template>
<UserForm v-model:name="userName" v-model:age="userAge" />
</template>
<script setup>
import { ref } from 'vue'
const userName = ref('张三')
const userAge = ref(20)
</script>
3. 配置默认值与类型校验
defineModel 支持传入配置对象,设置 props 的默认值、类型校验等,等价于传统 defineProps 的配置:
<template>
<input v-model="count" type="number" />
</template>
<script setup>
// 配置默认值、类型、必填项
const count = defineModel({
type: Number,
default: 0,
required: false
})
</script>
三、defineModel 核心原理
defineModel 本质是 Vue 提供的语法糖,其底层依然是基于 props + emits 实现的,Vue 会自动完成以下操作:
-
声明一个名为
modelValue(或自定义名称)的 prop; -
声明一个名为
update:modelValue(或update:自定义名称)的 emit 事件; -
返回一个 ref 对象:
- 读取值时,取的是 props 中的值;
- 修改值时,自动触发对应的 update 事件更新父组件数据。
四、注意事项与使用场景
1. 版本要求
defineModel 是 Vue3.4+ 新增的 API,若项目版本低于 3.4,需先升级 Vue 核心包:
运行
# npm
npm install vue@latest
# yarn
yarn add vue@latest
2. 与 v-model 修饰符结合
defineModel 支持获取父组件传入的 v-model 修饰符(如 .trim、.number),通过 modelModifiers 属性访问:
<template>
<input
:value="modelValue"
@input="handleInput"
/>
</template>
<script setup>
const modelValue = defineModel()
// 获取修饰符
const { modelModifiers } = defineProps({
modelModifiers: { default: () => ({}) }
})
const handleInput = (e) => {
let value = e.target.value
// 处理 trim 修饰符
if (modelModifiers.trim) {
value = value.trim()
}
// 直接修改 ref,自动触发更新
modelValue.value = value
}
</script>
3. 适用场景
- 表单组件(输入框、选择器、开关等)的双向绑定;
- 需同步父子组件状态的通用组件(如弹窗的显隐、滑块的数值等);
- 多字段联动的复杂组件(如表单卡片、筛选面板)。
五、总结
-
defineModel是 Vue3.4+ 为简化组件双向绑定推出的语法糖,替代了传统props + emits的冗余写法; - 核心用法:调用
defineModel()返回 ref 对象,直接绑定到模板,修改 ref 自动同步父组件数据; - 支持自定义绑定名称、配置 props 校验规则,兼容 v-model 修饰符,满足复杂场景需求;
- 底层仍基于 Vue 原生的 props 和 emits 实现,无额外性能开销,是 Vue3 组件双向绑定的首选方案。
相比于传统写法,defineModel 大幅减少了模板代码量,让开发者更聚焦于业务逻辑,是 Vue3 组件开发中提升效率的重要特性。
Vue开发必考:defineComponent与defineAsyncComponent,你真的掌握吗?
vue2和vue3以及react生命周期对比
vue2和vue3响应式原理的区别
浅谈 import.meta.env 和 process.env 的区别
这是一个前端构建环境里非常核心、也非常容易混淆的问题。下面我们从来源、使用场景、编译时机、安全性四个维度来谈谈 import.meta.env 和 process.env 的区别。
一句话结论
process.env是 Node.js 的环境变量接口import.meta.env是 Vite(ESM)在构建期注入的前端环境变量
一、process.env 是什么?
1️⃣ 本质
- 来自 Node.js
- 运行时读取 服务器 / 构建机的系统环境变量
- 本身 浏览器里不存在
console.log(process.env.NODE_ENV);
2️⃣ 使用场景
- Node 服务
- 构建工具(Webpack / Vite / Rollup)
- SSR(Node 端)
3️⃣ 前端能不能用?
👉 不能直接用
浏览器里没有 process:
// 浏览器原生环境 ❌
Uncaught ReferenceError: process is not defined
4️⃣ 为什么 Webpack 项目里能用?
因为 Webpack 帮你“编译期替换”了
process.env.NODE_ENV
// ⬇️ 构建时被替换成
"production"
本质是 字符串替换,不是运行时读取。
二、import.meta.env 是什么?
1️⃣ 本质
- Vite 提供
- 基于 ES Module 的
import.meta - 构建期 + 运行期可用(但值是构建期确定的)
console.log(import.meta.env.MODE);
2️⃣ 特点
- 浏览器里 原生支持
- 不依赖 Node 的
process - 更符合现代 ESM 规范
三、两者核心区别对比(重点)
| 维度 | process.env | import.meta.env |
|---|---|---|
| 来源 | Node.js | Vite |
| 标准 | Node API | ESM 标准扩展 |
| 浏览器可用 | ❌(需编译替换) | ✅ |
| 注入时机 | 构建期 | 构建期 |
| 是否运行时读取 | ❌ | ❌ |
| 推荐前端使用 | ❌ | ✅ |
⚠️ 两者都不是“前端运行时读取服务器环境变量”
四、Vite 中为什么不用 process.env?
1️⃣ 因为 Vite 不再默认注入 process
// Vite 项目中 ❌
process.env.API_URL
会直接报错。
2️⃣ 官方设计选择
- 避免 Node 全局污染
- 更贴近浏览器真实环境
- 更利于 Tree Shaking
五、Vite 环境变量的正确用法(非常重要)
1️⃣ 必须以 VITE_ 开头
# .env
VITE_API_URL=https://api.example.com
console.log(import.meta.env.VITE_API_URL);
❌ 否则 不会注入到前端
2️⃣ 内置变量
import.meta.env.MODE // development / production
import.meta.env.DEV // true / false
import.meta.env.PROD // true / false
import.meta.env.BASE_URL
六、安全性
⚠️ 重要警告
import.meta.env里的变量 ≠ 私密
它们会:
- 被 打进 JS Bundle
- 可在 DevTools 直接看到
❌ 不要这样做
VITE_SECRET_KEY=xxxx
✅ 正确做法
- 前端:只放“公开配置”(API 域名、开关)
- 私密变量:只放在 Node / 服务端
七、SSR / 全栈项目里怎么区分?
在 Vite + SSR(如 Nuxt / 自建 SSR):
Node 端
process.env.DB_PASSWORD
浏览器端
import.meta.env.VITE_API_URL
两套环境变量是刻意分开的。
-
为什么必须分成两套?(设计原因)
1️⃣ 执行环境不同(这是根因)
| 位置 | 运行在哪 | 能访问什么 |
|---|---|---|
| SSR Server | Node.js | process.env |
| Client Bundle | 浏览器 | import.meta.env |
浏览器里 永远不可能安全地访问服务器环境变量。
2️⃣ SSR ≠ 浏览器
很多人误解:
“SSR 是不是浏览器代码先在 Node 跑一遍?”
❌ 不完全对
SSR 实际是:
Node.js 先跑一份 → 生成 HTML
浏览器再跑一份 → hydrate
这两次执行:
- 环境不同
- 变量来源不同
- 安全级别不同
-
在 Vite + SSR 中,变量的“真实流向”
1️⃣ Node 端(SSR Server)
// server.ts / entry-server.ts
const dbPassword = process.env.DB_PASSWORD;
✔️ 真实运行时读取
✔️ 不会进 bundle
✔️ 只存在于服务器内存
2️⃣ Client 端(浏览器)
// entry-client.ts / React/Vue 组件
const apiUrl = import.meta.env.VITE_API_URL;
✔️ 构建期注入
✔️ 会打进 JS
✔️ 用户可见
3️⃣ 中间那条“禁止通道”
// ❌ 绝对禁止
process.env.DB_PASSWORD → 浏览器
SSR 不会、也不允许,自动帮你“透传”环境变量
-
SSR 中最容易踩的 3 个坑(重点)
❌ 坑 1:在“共享代码”里直接用 process.env
// utils/config.ts(被 server + client 共用)
export const API = process.env.API_URL; // ❌
问题:
- Server OK
- Client 直接炸(或被错误替换)
✅ 正确方式:
export const API = import.meta.env.VITE_API_URL;
或者:
export const API =typeof window === 'undefined'
? process.env.INTERNAL_API
: import.meta.env.VITE_API_URL;
❌ 坑 2:误以为 SSR 可以“顺手用数据库变量”
// Vue/React 组件里
console.log(process.env.DB_PASSWORD); // ❌
哪怕你在 SSR 模式下,这段代码:
- 最终仍会跑在浏览器
- 会被打包
- 是严重安全漏洞
❌ 坑 3:把“环境变量”当成“运行时配置”
// ❌ 想通过部署切换 API
import.meta.env.VITE_API_URL
🚨 这是 构建期值:
build 时确定
→ CDN 缓存
→ 所有用户共享
想运行期切换?只能:
- 接口返回配置
- HTML 注入 window.CONFIG
- 拉 JSON 配置文件
-
SSR 项目里“正确的分层模型”(工程视角)
┌──────────────────────────┐
│ 浏览器 Client │
│ import.meta.env.VITE_* │ ← 公开配置
└───────────▲──────────────┘
│
HTTP / HTML
│
┌───────────┴──────────────┐
│ Node SSR Server │
│ process.env.* │ ← 私密配置
└───────────▲──────────────┘
│
内部访问
│
┌───────────┴──────────────┐
│ DB / Redis / OSS │
└──────────────────────────┘
这是一条 单向、安全的数据流。
-
Nuxt / 自建 SSR 的对应关系
| 类型 | 用途 |
|---|---|
| runtimeConfig | Server-only |
| runtimeConfig.public | Client 可见 |
| process.env | 仅 server |
👉 Nuxt 本质也是在帮你维护这条边界
八、常见误区总结
❌ 误区 1
import.meta.env是运行时读取
❌ 错,仍是构建期注入
❌ 误区 2
可以用它动态切换环境
❌ 不行,想动态只能:
- 接口返回配置
- 或运行时请求 JSON
❌ 误区 3
Vite 里还能继续用
process.env
❌ 除非你手动 polyfill(不推荐)
九、总结
- 前端(Vite)只认
import.meta.env.VITE_* - 服务端(Node)只认
process.env - 永远不要把秘密放进前端 env
历时1年,TinyEditor v4.0 正式发布!
Vue 3 中 v-for 动态组件 ref 收集失败问题排查与解决
Vue 3 中 v-for 动态组件 ref 收集失败问题排查与解决
问题描述
在开发部门管理页面的搜索栏功能时,遇到了一个奇怪的问题:在 v-for 循环中渲染的动态组件,无法正确收集到 ref 数组中。
问题现象
// schema-search-bar.vue
const searchComList = ref([]);
const getValue = () => {
let dtoObj = {};
console.log("searchComList", searchComList.value); // 输出: Proxy(Array) {}
searchComList.value.forEach((component) => {
dtoObj = { ...dtoObj, ...component.getValue() };
});
return dtoObj; // 返回: {}
};
现象:
-
searchComList.value始终是空数组[] - 无法获取到任何子组件的实例
- 导致搜索功能无法正常工作
代码结构
<template>
<el-form v-if="schema && schema.properties" :inline="true">
<el-form-item v-for="(schemaItem, key) in schema.properties" :key="key">
<!-- 动态组件 -->
<component
:ref="searchComList" <!-- ❌ 问题所在 -->
:is="SearchItemConfig[schemaItem.option?.comType]?.component"
:schemaKey="key"
:schema="schemaItem">
</component>
</el-form-item>
</el-form>
</template>
<script setup>
const searchComList = ref([]);
</script>
排查过程
1. 初步怀疑:打印时机问题
最初怀疑是打印时机不对,组件还没有挂载完成。但即使使用 nextTick 或在 onMounted 中打印,searchComList.value 仍然是空数组。
2. 对比其他正常工作的代码
在同一个项目中,发现 schema-view.vue 中类似的代码却能正常工作:
<!-- schema-view.vue - ✅ 正常工作 -->
<component
v-for="(item, key) in components"
:key="key"
:is="ComponentConfig[key]?.component"
ref="comListRef" <!-- ✅ 使用字符串形式 -->
@command="onComponentCommand">
</component>
<script setup>
const comListRef = ref([]);
// comListRef.value 能正确收集到所有组件实例
</script>
3. 发现关键差异
对比两个文件的代码,发现了关键差异:
| 文件 | ref 写法 | 结果 |
|---|---|---|
schema-view.vue |
ref="comListRef" (字符串) |
✅ 正常工作 |
schema-search-bar.vue |
:ref="searchComList" (绑定对象) |
❌ 无法收集 |
根本原因
Vue 3 中 v-for 使用 ref 的机制
在 Vue 3 中,v-for 中使用 ref 时,两种写法的行为完全不同:
1. 字符串形式的 ref(自动收集到数组)
<component v-for="item in list" ref="comListRef" />
行为:
- Vue 会自动将
ref的值设置为一个数组 - 数组中的元素按顺序对应
v-for中的每一项 - 这是 Vue 3 的特殊处理机制
2. 绑定 ref 对象(不会自动收集)
<component v-for="item in list" :ref="comListRef" />
行为:
-
:ref绑定的是一个 ref 对象,Vue 会直接赋值 - 在
v-for中,不会自动收集到数组 - 每次循环都会覆盖上一次的值
- 最终只会保留最后一个组件的引用
官方文档说明
根据 Vue 3 官方文档:
当在
v-for中使用ref时,ref 的值将是一个数组,包含所有循环项对应的组件实例。
关键点: 这个特性只适用于字符串形式的 ref,不适用于 :ref 绑定。
解决方案
方案一:使用字符串形式的 ref(推荐)
<template>
<el-form-item v-for="(schemaItem, key) in schema.properties" :key="key">
<component
ref="searchComList" <!-- ✅ 去掉冒号,使用字符串形式 -->
:is="SearchItemConfig[schemaItem.option?.comType]?.component"
:schemaKey="key"
:schema="schemaItem">
</component>
</el-form-item>
</template>
<script setup>
const searchComList = ref([]);
// 现在 searchComList.value 会自动收集到所有组件实例
</script>
方案二:使用函数形式的 ref(更灵活)
如果需要更精细的控制(比如去重、按 key 索引等),可以使用函数形式:
<template>
<el-form-item v-for="(schemaItem, key) in schema.properties" :key="key">
<component
:ref="(el) => handleRef(el, key)" <!-- ✅ 函数形式 -->
:is="SearchItemConfig[schemaItem.option?.comType]?.component"
:schemaKey="key"
:schema="schemaItem">
</component>
</el-form-item>
</template>
<script setup>
const searchComList = ref([]);
const componentMap = new Map();
const handleRef = (el, key) => {
if (el) {
// 如果已经存在,先移除旧的(避免重复)
if (componentMap.has(key)) {
const oldIndex = searchComList.value.indexOf(componentMap.get(key));
if (oldIndex > -1) {
searchComList.value.splice(oldIndex, 1);
}
}
// 添加新的组件实例
componentMap.set(key, el);
searchComList.value.push(el);
} else {
// 组件卸载时,从 Map 和数组中移除
if (componentMap.has(key)) {
const oldEl = componentMap.get(key);
const index = searchComList.value.indexOf(oldEl);
if (index > -1) {
searchComList.value.splice(index, 1);
}
componentMap.delete(key);
}
}
};
</script>
技术要点总结
1. Vue 3 ref 在 v-for 中的行为
| 写法 | 在 v-for 中的行为 | 适用场景 |
|---|---|---|
ref="xxx" |
自动收集到数组 | ✅ 推荐,简单场景 |
:ref="xxx" |
不会自动收集,会覆盖 | ❌ 不适用于 v-for |
:ref="(el) => fn(el)" |
手动控制收集逻辑 | ✅ 需要精细控制时 |
2. 最佳实践
-
在 v-for 中使用 ref 时,优先使用字符串形式
<component v-for="item in list" ref="comListRef" /> -
如果需要按 key 索引或去重,使用函数形式
<component v-for="(item, key) in list" :ref="(el) => handleRef(el, key)" /> -
避免在 v-for 中使用
:ref="refObject"<!-- ❌ 不推荐 --> <component v-for="item in list" :ref="comListRef" />
3. 调试技巧
当遇到 ref 收集问题时,可以:
- 检查 ref 的写法:确认是字符串还是绑定对象
- 使用 nextTick 延迟检查:确保组件已挂载
- 对比正常工作的代码:找出差异点
- 查看 Vue DevTools:检查组件实例是否正确创建
相关资源
总结
这个问题看似简单,但实际上涉及到 Vue 3 中 ref 在 v-for 中的特殊处理机制。关键点在于:
- 字符串形式的
ref在 v-for 中会自动收集到数组 - 绑定形式的
:ref在 v-for 中不会自动收集 - 函数形式的
:ref可以手动控制收集逻辑
记住这个规则,可以避免很多类似的坑。在开发过程中,如果遇到 ref 收集问题,首先检查是否在 v-for 中使用了错误的 ref 写法。
2026最新款Vue3+DeepSeek-V3.2+Arco+Markdown网页端流式生成AI Chat
一周左右爆肝迭代研发,最新款vite7.2+vue3接入deepseek api网页版ai系统,完结了!
技术栈
- 开发工具:vscode
- 技术框架:vite^7.2.4+vue^3.5.24+vue-router^4.6.4
- 大模型ai框架:DeepSeek-R1 + OpenAI
- UI组件库:arco-design^2.57.0 (字节桌面端组件库)
- 状态插件:pinia^3.0.4
- 本地存储:pinia-plugin-persistedstate^4.7.1
- 高亮插件:highlight.js^11.11.1
- markdown插件:markdown-it
- katex公式:@mdit/plugin-katex^0.24.1
项目功能性
- 最新框架vite7.x集成deepseek流式生成,效果更丝滑流畅
- 提供暗黑+浅色主题、侧边栏展开/收缩
- 支持丰富Markdown样式,代码高亮/复制/收缩功能
- 支持思考模式DeepSeek-R1
- 支持Katex数学公式
- 支持Mermaid各种甘特图/流程图/类图等图表
项目结构目录
项目环境变量.env
根据自己申请的deepseek apikey替换项目根目录下.env文件里的key即可体验ai流式打字效果。
# title
VITE_APP_TITLE = 'Vue3-Web-DeepSeek'
# port
VITE_PORT = 3001
# 运行时自动打开浏览器
VITE_OPEN = true
# 开启https
VITE_HTTPS = false
# 是否删除生产环境 console
VITE_DROP_CONSOLE = true
# DeepSeek API配置
VITE_DEEPSEEK_API_KEY = 替换为你的 API Key
VITE_DEEPSEEK_BASE_URL = https://api.deepseek.com
公共布局模板
项目整体结构如下图所示:
<script setup>
import Sidebar from '@/layouts/components/sidebar/index.vue'
</script>
<template>
<div class="vu__container">
<div class="vu__layout flexbox flex-col">
<div class="vu__layout-body flex1 flexbox">
<!-- 侧边区域 -->
<Sidebar />
<!-- 主面板区域 -->
<div class="vu__layout-main flex1">
<router-view v-slot="{ Component, route }">
<keep-alive>
<component :is="Component" :key="route.path" />
</keep-alive>
</router-view>
</div>
</div>
</div>
</div>
</template>
vue3集成deepseek深度思考模式
// 调用deepseek接口
const completion = await openai.chat.completions.create({
// 单一会话
/* messages: [
{role: 'user', content: editorValue}
], */
// 多轮会话
messages: props.multiConversation ? historySession.value : [{role: 'user', content: editorValue}],
// deepseek-chat对话模型 deepseek-reasoner推理模型
model: sessionstate.thinkingEnabled ? 'deepseek-reasoner' : 'deepseek-chat',
stream: true, // 流式输出
max_tokens: 8192, // 一次请求中模型生成 completion 的最大 token 数(默认使用 4096)
temperature: 0.4, // 严谨采样
})
vue3-deepseek集成katex和mermaid插件
import { katex } from "@mdit/plugin-katex"; // 支持数学公式
import 'katex/dist/katex.min.css'
// 渲染mermaid图表
import { markdownItMermaidPlugin } from '@/components/markdown/plugins/mermaidPlugin'
解析markdown结构
<Markdown
:source="item.content"
:html="true"
:linkify="true"
:typographer="true"
:plugins="[
[katex, {delimiters: 'all'}],
[markdownItMermaidPlugin, { ... }]
]"
@copy="onCopy"
/>
vue3调用deepseek api流式对话
// 调用deepseek接口
const completion = await openai.chat.completions.create({
// 单一会话
// messages: [{role: 'user', content: editorValue}],
// 多轮会话
messages: props.multiConversation ? historySession.value : [{role: 'user', content: editorValue}],
// deepseek-chat对话模型 deepseek-reasoner推理模型
model: sessionstate.thinkingEnabled ? 'deepseek-reasoner' : 'deepseek-chat',
stream: true, // 流式输出
max_tokens: 8192,
temperature: 0.4
})
处理流式生成内容。
for await (const chunk of completion) {
// 检查是否已终止
if(sessionstate.aborted) break
const content = chunk.choices[0]?.delta?.content || ''
// 获取推理内容
const reasoningContent = chunk.choices[0]?.delta?.reasoning_content || ''
if(content || reasoningContent) {
answerText += content
reasoningText += reasoningContent
// 限制更新频率:每100ms最多更新一次
const now = Date.now()
if(now - lastUpdate > 100) {
lastUpdate = now
requestAnimationFrame(() => {
// ...
})
}
}
if(chunk.choices[0]?.finish_reason === 'stop') {
// ...
}
}
2026最新款Vite7+Vue3+DeepSeek-V3.2+Markdown流式输出AI会话
基于uniapp+vue3+deepseek+markdown搭建app版流式输出AI模板
electron38.2-vue3os系统|Vite7+Electron38+Pinia3+ArcoDesign桌面版OS后台管理
基于electron38+vite7+vue3 setup+elementPlus电脑端仿微信/QQ聊天软件
2025最新款Electron38+Vite7+Vue3+ElementPlus电脑端后台系统Exe
自研2025版flutter3.38实战抖音app短视频+聊天+直播商城系统
基于uni-app+vue3+uvui跨三端仿微信app聊天模板【h5+小程序+app】
基于uniapp+vue3+uvue短视频+聊天+直播app系统
基于flutter3.32+window_manager仿macOS/Wins风格桌面os系统
flutter3.27+bitsdojo_window电脑端仿微信Exe应用
自研tauri2.0+vite6.x+vue3+rust+arco-design桌面版os管理系统Tauri2-ViteOS
如何取消Vue Watch监听
权限封装不是写个指令那么简单:一次真实项目的反思
如何在Vue中传递函数作为Prop
解析ElementPlus打包源码(三、打包类型)
runTask('generateTypesDefinitions')
我们定位到相关的代码
import path from 'path'
import { readFile, writeFile } from 'fs/promises'
import glob from 'fast-glob'
import { copy, remove } from 'fs-extra'
import { buildOutput } from '@element-plus/build-utils'
import { pathRewriter, run } from '../utils'
export const generateTypesDefinitions = async () => {
await run(
'npx vue-tsc -p tsconfig.web.json --declaration --emitDeclarationOnly --declarationDir dist/types'
)
const typesDir = path.join(buildOutput, 'types', 'packages')
const filePaths = await glob(`**/*.d.ts`, {
cwd: typesDir,
absolute: true,
})
const rewriteTasks = filePaths.map(async (filePath) => {
const content = await readFile(filePath, 'utf8')
await writeFile(filePath, pathRewriter('esm')(content), 'utf8')
})
await Promise.all(rewriteTasks)
const sourceDir = path.join(typesDir, 'element-plus')
await copy(sourceDir, typesDir)
await remove(sourceDir)
}
打包类型文件
对于npx vue-tsc -p tsconfig.web.json --declaration --emitDeclarationOnly --declarationDir dist/types,-p tsconfig.web.json指定要使用的编译配置文件,--declaration 指定生成声明文件,--emitDeclarationOnly表示只生成声明文件不会进行代码编译,--declarationDir dist/types指定了输出目录
然后运行pnpm buid,可以看到生成了对应的类型文件
重写类型文件
主要就是针对路径进行处理,把开发环境的路径处理成了打包之后的路径
重写类型文件之前
重写类型文件之后
可以看到这里import的是es文件目录下的类型文件,之前打包的es下面并没有对应的类型啊???其实后面还有处理,会把types的相关类型移动到对应目录下,需要往后面看 copyTypesDefinitions
提升package/element-plus的类型文件到上层
我们之前打包的配置 preserveModules 值为 true 时,不会有element-plus的目录结构了,下面的文件会提升到上层。
这里就是为了把类型提到上层,使得其和之前的组件打包的结构一致
提升前:
提升后:
copyTypesDefinitions
export const copyTypesDefinitions: TaskFunction = (done) => {
const src = path.resolve(buildOutput, 'types', 'packages')
const copyTypes = (module: Module) =>
withTaskName(`copyTypes:${module}`, () =>
copy(src, buildConfig[module].output.path, { recursive: true })
)
return parallel(copyTypes('esm'), copyTypes('cjs'))(done)
}
这是要把types下面的各种文件,copy到es和lib的相关文件下
JavaScript 数组中删除偶数下标值的多种方法
- 使用 filter 方法(推荐)
// 方法1: 使用filter保留奇数下标元素
const arr = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
// 删除偶数下标(0, 2, 4, 6, 8...),保留奇数下标
const result1 = arr.filter((_, index) => index % 2 !== 0);
console.log(result1); // [1, 3, 5, 7, 9]
// 或删除奇数下标,保留偶数下标
const result2 = arr.filter((_, index) => index % 2 === 0);
console.log(result2); // [0, 2, 4, 6, 8]
2. 使用 for 循环(原地修改)
// 方法2: 从后向前遍历,原地删除
function removeEvenIndexes(arr) {
// 从后向前遍历,避免索引错乱
for (let i = arr.length - 1; i >= 0; i--) {
if (i % 2 === 0) { // 删除偶数下标
arr.splice(i, 1);
}
}
return arr;
}
const arr = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
console.log(removeEvenIndexes(arr)); // [1, 3, 5, 7, 9]
console.log(arr); // 原数组也被修改
- 使用 reduce 方法
// 方法3: 使用reduce
const arr = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
const result = arr.reduce((acc, cur, index) => {
if (index % 2 !== 0) { // 只保留奇数下标
acc.push(cur);
}
return acc;
}, []);
console.log(result); // [1, 3, 5, 7, 9]
- 使用 for 循环创建新数组
// 方法4: 遍历奇数下标创建新数组
const arr = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
function removeEvenIndexes(arr) {
const result = [];
for (let i = 0; i < arr.length; i++) {
if (i % 2 !== 0) { // 只取奇数下标
result.push(arr[i]);
}
}
return result;
}
console.log(removeEvenIndexes(arr)); // [1, 3, 5, 7, 9]
- while 循环 + splice
// 方法5: 使用while循环
const arr = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
function removeEvenIndexes(arr) {
let i = 0;
while (i < arr.length) {
if (i % 2 === 0) { // 删除偶数下标
arr.splice(i, 1);
} else {
i++; // 只有不删除时才递增
}
}
return arr;
}
console.log(removeEvenIndexes(arr)); // [1, 3, 5, 7, 9]
- 性能优化版本(大数据量)
// 方法6: 高性能版本,避免splice
function removeEvenIndexesOptimized(arr) {
const result = [];
const length = arr.length;
// 从第一个奇数下标开始,步长为2
for (let i = 1; i < length; i += 2) {
result.push(arr[i]);
}
return result;
}
// 测试大数据量
const largeArr = Array.from({ length: 1000000 }, (_, i) => i);
console.time('优化版');
const optimizedResult = removeEvenIndexesOptimized(largeArr);
console.timeEnd('优化版'); // 大约 5-10ms
console.log('结果长度:', optimizedResult.length); // 500000
- 原地修改的高效方法
function removeEvenIndexesInPlace(arr) {
let writeIndex = 0;
for (let readIndex = 0; readIndex < arr.length; readIndex++) {
if (readIndex % 2 !== 0) { // 只保留奇数下标
arr[writeIndex] = arr[readIndex];
writeIndex++;
}
}
// 截断数组
arr.length = writeIndex;
return arr;
}
const arr = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
console.log(removeEvenIndexesInPlace(arr)); // [1, 3, 5, 7, 9]
console.log(arr); // 原数组被修改
- Vue 3 响应式数组处理
<template>
<div>
<h3>原始数组: {{ originalArray }}</h3>
<h3>处理后: {{ processedArray }}</h3>
<button @click="processArray">删除偶数下标</button>
<button @click="resetArray">重置数组</button>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
// 原始响应式数组
const originalArray = ref([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
// 计算属性:删除偶数下标
const processedArray = computed(() => {
return originalArray.value.filter((_, index) => index % 2 !== 0)
})
// 方法处理
const processArray = () => {
// 方法1: 创建新数组(推荐,不修改原数组)
originalArray.value = originalArray.value.filter((_, index) => index % 2 !== 0)
// 方法2: 原地修改(会触发响应式更新)
// let writeIndex = 0
// for (let i = 0; i < originalArray.value.length; i++) {
// if (i % 2 !== 0) {
// originalArray.value[writeIndex] = originalArray.value[i]
// writeIndex++
// }
// }
// originalArray.value.length = writeIndex
}
const resetArray = () => {
originalArray.value = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
}
</script>
- 通用工具函数
// 工具函数集合
const ArrayUtils = {
/**
* 删除偶数下标元素
* @param {Array} arr - 输入数组
* @param {boolean} inPlace - 是否原地修改
* @returns {Array} 处理后的数组
*/
removeEvenIndexes: function(arr, inPlace = false) {
if (!Array.isArray(arr)) {
throw new TypeError('输入必须是数组')
}
if (inPlace) {
return this._removeEvenIndexesInPlace(arr)
} else {
return this._removeEvenIndexesNew(arr)
}
},
/**
* 创建新数组(不修改原数组)
*/
_removeEvenIndexesNew: function(arr) {
return arr.filter((_, index) => index % 2 !== 0)
},
/**
* 原地修改
*/
_removeEvenIndexesInPlace: function(arr) {
let writeIndex = 0
for (let i = 0; i < arr.length; i++) {
if (i % 2 !== 0) {
arr[writeIndex] = arr[i]
writeIndex++
}
}
arr.length = writeIndex
return arr
},
/**
* 删除奇数下标元素
*/
removeOddIndexes: function(arr, inPlace = false) {
if (inPlace) {
let writeIndex = 0
for (let i = 0; i < arr.length; i++) {
if (i % 2 === 0) {
arr[writeIndex] = arr[i]
writeIndex++
}
}
arr.length = writeIndex
return arr
} else {
return arr.filter((_, index) => index % 2 === 0)
}
},
/**
* 删除指定下标的元素
* @param {Array} arr - 输入数组
* @param {Function} condition - 条件函数,返回true则删除
*/
removeByIndexCondition: function(arr, condition, inPlace = false) {
if (inPlace) {
let writeIndex = 0
for (let i = 0; i < arr.length; i++) {
if (!condition(i)) {
arr[writeIndex] = arr[i]
writeIndex++
}
}
arr.length = writeIndex
return arr
} else {
return arr.filter((_, index) => !condition(index))
}
}
}
// 使用示例
const arr = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
// 删除偶数下标
console.log(ArrayUtils.removeEvenIndexes(arr)) // [1, 3, 5, 7, 9]
console.log(arr) // 原数组不变 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
// 原地修改
console.log(ArrayUtils.removeEvenIndexes(arr, true)) // [1, 3, 5, 7, 9]
console.log(arr) // 原数组被修改 [1, 3, 5, 7, 9]
// 删除奇数下标
const arr2 = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
console.log(ArrayUtils.removeOddIndexes(arr2)) // [0, 2, 4, 6, 8]
// 自定义条件
const arr3 = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
console.log(ArrayUtils.removeByIndexCondition(
arr3,
index => index % 3 === 0
)) // 删除下标是3的倍数的元素
- ES6+ 高级写法
// 使用生成器函数
function* filterByIndex(arr, condition) {
for (let i = 0; i < arr.length; i++) {
if (condition(i)) {
yield arr[i]
}
}
}
const arr = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
const result = [...filterByIndex(arr, i => i % 2 !== 0)]
console.log(result) // [1, 3, 5, 7, 9]
// 使用箭头函数和三元运算符
const removeEvenIndexes = arr => arr.filter((_, i) => i % 2 ? arr[i] : null).filter(Boolean)
// 使用位运算(性能更好)
const removeEvenIndexesBit = arr => {
const result = []
for (let i = 1; i < arr.length; i += 2) {
result.push(arr[i])
}
return result
}
// 使用 Array.from
const removeEvenIndexesFrom = arr =>
Array.from(
{ length: Math.ceil(arr.length / 2) },
(_, i) => arr[i * 2 + 1]
).filter(Boolean)
- TypeScript 版本
// TypeScript 类型安全的版本
class ArrayProcessor {
/**
* 删除偶数下标元素
* @param arr 输入数组
* @param inPlace 是否原地修改
* @returns 处理后的数组
*/
static removeEvenIndexes<T>(arr: T[], inPlace: boolean = false): T[] {
if (inPlace) {
return this.removeEvenIndexesInPlace(arr)
} else {
return this.removeEvenIndexesNew(arr)
}
}
private static removeEvenIndexesNew<T>(arr: T[]): T[] {
return arr.filter((_, index) => index % 2 !== 0)
}
private static removeEvenIndexesInPlace<T>(arr: T[]): T[] {
let writeIndex = 0
for (let i = 0; i < arr.length; i++) {
if (i % 2 !== 0) {
arr[writeIndex] = arr[i]
writeIndex++
}
}
arr.length = writeIndex
return arr
}
/**
* 泛型方法:根据下标条件过滤数组
*/
static filterByIndex<T>(
arr: T[],
predicate: (index: number) => boolean
): T[] {
return arr.filter((_, index) => predicate(index))
}
}
// 使用示例
const numbers: number[] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
const strings: string[] = ['a', 'b', 'c', 'd', 'e', 'f']
console.log(ArrayProcessor.removeEvenIndexes(numbers)) // [1, 3, 5, 7, 9]
console.log(ArrayProcessor.filterByIndex(strings, i => i % 2 === 0)) // ['a', 'c', 'e']
- 性能对比测试
// 性能测试
function testPerformance() {
const largeArray = Array.from({ length: 1000000 }, (_, i) => i)
// 测试 filter 方法
console.time('filter')
const result1 = largeArray.filter((_, i) => i % 2 !== 0)
console.timeEnd('filter')
// 测试 for 循环
console.time('for loop')
const result2 = []
for (let i = 1; i < largeArray.length; i += 2) {
result2.push(largeArray[i])
}
console.timeEnd('for loop')
// 测试 while 循环
console.time('while loop')
const arrCopy = [...largeArray]
let i = 0
while (i < arrCopy.length) {
if (i % 2 === 0) {
arrCopy.splice(i, 1)
} else {
i++
}
}
console.timeEnd('while loop')
// 测试 reduce
console.time('reduce')
const result4 = largeArray.reduce((acc, cur, i) => {
if (i % 2 !== 0) acc.push(cur)
return acc
}, [])
console.timeEnd('reduce')
}
testPerformance()
// 结果通常: for loop 最快, filter 次之, reduce 较慢, while+splice 最慢
总结
推荐方法:
-
最简洁:
filter方法arr.filter((_, i) => i % 2 !== 0) -
最高性能:
for循环const result = [] for (let i = 1; i < arr.length; i += 2) { result.push(arr[i]) } -
原地修改:使用写指针
let writeIndex = 0 for (let i = 0; i < arr.length; i++) { if (i % 2 !== 0) { arr[writeIndex] = arr[i] writeIndex++ } } arr.length = writeIndex
注意事项:
- 索引从0开始:JavaScript 数组下标从0开始
-
避免splice:在循环中使用
splice会改变数组长度,容易出错 -
性能考虑:大数据量时避免使用
splice和filter链式调用 - 不变性:根据需求选择是否修改原数组
扩展应用:
- 删除奇数下标:
i % 2 === 0 - 删除特定模式:
i % 3 === 0删除3的倍数下标 - 保留特定范围:
i >= 2 && i <= 5
**
给 Ant Design Vue 装上"涡轮增压":虚拟列表封装实践
前言
如果你用过 Ant Design Vue 的 Table 组件渲染过上万条数据,你一定体验过那种"浏览器在思考人生"的感觉。页面卡顿、滚动掉帧,仿佛在对你说:"兄弟,我真的尽力了。"
问题的根源很简单:Ant Design Vue 的 Table 组件并不支持虚拟列表。当你塞给它 10 万条数据时,它会老老实实地把这 10 万个 DOM 节点全部渲染出来。浏览器:我谢谢你啊。
于是,这个项目诞生了——基于 Ant Design Vue 二次封装,让它也能享受虚拟列表的"涡轮增压"。
什么是虚拟列表?
简单来说,虚拟列表就是一种"障眼法"。用户看起来在滚动一个包含 10 万条数据的列表,但实际上浏览器只渲染了可视区域内的那几十条数据。当你滚动时,组件会动态计算应该显示哪些数据,然后快速替换 DOM。
这就像是火车车窗外的风景:你坐在车厢里,透过窗户看到的永远只是窗外那一小片景色,但随着火车移动,窗外的景色不断变化,给你一种穿越了整片大地的感觉。虚拟列表也是如此,可视区域就是那扇窗户,数据就是窗外的风景。
核心实现:站在巨人的肩膀上
这个项目的核心是 @vueuse/core 提供的 useVirtualList hook。VueUse 是一个优秀的 Vue 组合式 API 工具集,而 useVirtualList 就是其中专门用来实现虚拟列表的工具。
让我们看看关键代码:
const { list, containerProps, wrapperProps } = useVirtualList(
computed(() => props.dataSource),
{
itemHeight: props.rowHeight,
overscan: 10,
},
)
必要参数解析
1. 数据源(第一个参数)
这里传入的是一个响应式的数据源。 computed(() => props.dataSource),当父组件传入的数据变化时,虚拟列表会自动更新。
2. itemHeight(行高)
这是虚拟列表的"灵魂参数"。它告诉 useVirtualList:"嘿,我的每一行有多高。"有了这个信息,它才能准确计算出:
- 可视区域能显示多少行
- 滚动到某个位置时应该显示哪些数据
- 整个列表的总高度是多少
重要提示:这个值必须尽可能准确。如果你设置的行高和实际渲染的行高不一致,滚动时就会出现"跳跃"或"错位"的问题。在我们的实现中,默认值是 55px,这是 Ant Design Vue Table 的默认行高。
3. overscan(预渲染数量)
这是一个性能优化参数。它的意思是:"除了可视区域的数据,我还要多渲染上下各 10 条数据。"
为什么要这么做?想象一下,如果只渲染可视区域的数据,当用户快速滚动时,新的数据可能来不及渲染,就会出现短暂的空白。通过预渲染一些数据,可以让滚动更加流畅。
当然,这个值也不能设置太大,否则就失去了虚拟列表的意义。10 是一个比较平衡的值。
返回值解析:虚拟列表的"三剑客"
useVirtualList 返回了三个关键对象,它们各司其职,共同完成虚拟列表的魔法。
1. list - 数据的"精选集"
这是当前应该渲染的数据列表。注意,这不是完整的数据源,而是经过计算后,当前可视区域(加上 overscan)应该显示的数据。
数据结构如下:
[
{ data: 原始数据, index: 在完整列表中的索引 },
{ data: 原始数据, index: 在完整列表中的索引 },
...
]
比如你有 10 万条数据,但 list 可能只包含 20-30 条数据,这就是虚拟列表的核心优势。
2. containerProps - 滚动的"指挥官"
这是绑定到外层容器的属性对象,它的结构大概是这样的:
{
ref: containerRef, // 容器的引用
onScroll: () => { // 滚动事件监听
calculateRange(); // 重新计算应该显示哪些数据
},
style: {
overflowY: "auto" // 允许垂直滚动
}
}
核心作用:
- ref:VueUse 需要获取容器的 DOM 引用,以便计算可视区域的大小和滚动位置
-
onScroll:这是虚拟列表的"心跳"。每次滚动时,它会触发
calculateRange()函数,重新计算当前应该显示哪些数据 - style:确保容器可以滚动
在我们的实现中,这样使用:
<div v-bind="containerProps" class="virtual-scroll-container">
<!-- 内容 -->
</div>
通过 v-bind 直接绑定,所有的属性和事件监听都会自动应用到容器上。
3. wrapperProps - 高度的"撑杆"
这是绑定到内层包裹元素的属性对象,它的结构是这样的:
{
width: "100%",
height: "5500000px", // 注意这个惊人的高度!
marginTop: "0px" // 动态调整,实现滚动偏移
}
为什么高度这么夸张?
假设你有 10 万条数据,每条高度 55px,那么完整列表的总高度就是:
100000 × 55 = 5,500,000px = 5500000px
这个高度是计算出来的"虚拟高度"。虽然实际只渲染了几十条数据,但通过设置这个巨大的高度,可以让滚动条的长度和滚动范围与真实的 10 万条数据保持一致。
marginTop 的妙用:
当你滚动到列表中间时,比如滚动到第某条数据,marginTop 会动态调整为正值(比如 2915px),这样可以:
- 让当前渲染的数据显示在正确的位置
- 保持滚动条的位置准确
这就像是一个"移动的窗口",窗口内的内容在不断变化,但窗口的位置始终准确。通过动态调整 marginTop,VueUse 将实际渲染的少量数据"推"到了应该显示的位置,从而实现了虚拟滚动的效果。
在我们的实现中:
<div v-bind="wrapperProps">
<a-table ... />
</div>
这个包裹层撑起了整个虚拟列表的"骨架",让浏览器以为真的有 10 万条数据在那里。
封装
1. 容器高度的控制
虚拟列表需要一个固定高度的容器来触发滚动。我们通过 CSS 变量动态绑定容器高度:
<style scoped>
.virtual-scroll-container {
height: v-bind(containerHeight + 'px');
overflow-y: auto;
/* 其他样式... */
}
</style>
这样,父组件可以通过 container-height prop 灵活控制可视区域的高度。
2. 插槽的完整透传
为了保持 Ant Design Vue Table 的灵活性,我们需要把所有插槽都透传给内部的 Table 组件:
<template v-for="(_, name) in $slots" #[name]="slotData">
<slot :name="name" v-bind="slotData || {}"></slot>
</template>
这样,父组件可以像使用原生 Table 一样使用自定义列、自定义单元格等功能。
3. 必须注意的细节
在使用这个组件时,有一个容易被忽略的细节:表格列的 ellipsis 必须设置为 true。
为什么?因为虚拟列表的行高是固定的,如果内容超出了单元格,就会撑高行高,导致计算错位。设置 ellipsis: true 可以确保内容超出时显示省略号,而不是换行。
const columns = [
{ title: '类型', key: 'type', width: 400, ellipsis: true },
// ...
]
性能对比
在测试页面中,我们生成了 10 万条数据:
const tableData = ref(generateMockData(100000))
如果用原生的 Ant Design Vue Table 渲染,浏览器会直接"去世"。但使用虚拟列表封装后,滚动依然丝滑流畅。
这就是虚拟列表的魅力:无论数据有多少,实际渲染的 DOM 节点永远只有那么几十个。
使用方式
使用这个组件非常简单,和原生 Table 几乎一样:
<v-table
:data-source="tableData"
:columns="columns"
:row-height="55"
:container-height="600"
row-key="id"
/>
唯一的区别是多了两个参数:
-
row-height:行高,必须准确 -
container-height:容器高度,决定可视区域大小
总结
这个项目的核心思路很简单:
- 借助 VueUse 的
useVirtualList实现虚拟列表逻辑 - 将虚拟列表的数据转换为 Ant Design Vue Table 需要的格式
- 通过 props 和插槽保持组件的灵活性
项目地址:ant-virtual-table
让 JSON 数据可视化:两款 Vue 组件实战解析
最近的项目中正好遇到JSON格式化展示的需求,需要在前端清晰美观的展示JSON数据结构。
调研了下Vue生态中有两款出色的组件:vue-json-pretty和vue-json-viewer,它们都能将JSON数据变得直观易读。
组件定位与核心差异
vue-json-pretty更像是功能全面的JSON编辑器。它采用树形结构展示数据,支持节点编辑、虚拟滚动和深度自定义。如果你需要用户交互或处理大型数据集,这是更好的选择。
vue-json-viewer则定位为简洁高效的查看器。它专注于快速展示和便捷复制,API简单直接。对于只需展示不需编辑的场景,它更加轻量实用。
实际选择时,问问自己:需要编辑功能吗?数据量有多大?需要多深的自定义?回答这些问题后,选择就清晰了。
vue-json-pretty:美观实用的JSON编辑器
基础使用
安装很简单:
# Vue 3
npm install vue-json-pretty --save
# Vue 2
npm install vue-json-pretty@v1-latest --save
基本集成:
<template>
<vue-json-pretty :data="apiResponse" />
</template>
<script>
import VueJsonPretty from 'vue-json-pretty'
import 'vue-json-pretty/lib/styles.css'
export default {
components: { VueJsonPretty },
data() {
return {
apiResponse: {
user: { name: '张三', id: 123 },
status: 'active',
permissions: ['read', 'write']
}
}
}
}
</script>
核心优势
树形结构清晰:缩进和连接线让嵌套数据一目了然。数组和对象会显示长度,快速掌握数据结构。
编辑功能实用:开启编辑模式后,用户可以直接修改数值(ps:所以我最后选了它):
<vue-json-pretty
:data="configData"
:editable="true"
@data-change="handleConfigUpdate"
/>
这对于配置编辑器、主题定制器等场景特别有用。
虚拟滚动处理大数据:
<vue-json-pretty
:data="largeDataset"
:virtual="true"
:item-height="24"
/>
即使渲染数千节点,依然保持流畅。
高度可定制:控制展示细节的选项丰富:
<vue-json-pretty
:data="complexData"
:show-length="true"
:show-line="true"
:deep="3"
:highlight-selected="true"
:custom-value="renderTimestamp"
/>
实战:API调试面板
在实际开发中,我常用它构建API调试工具:
<template>
<div class="api-debugger">
<div class="request-panel">
<h4>请求参数</h4>
<vue-json-pretty :data="requestParams" :deep="2" />
</div>
<div class="response-panel">
<h4>响应数据</h4>
<vue-json-pretty
:data="responseData"
:highlight-selected="true"
@node-click="copyNodeValue"
/>
</div>
</div>
</template>
vue-json-viewer:轻量高效的查看利器
快速集成
按Vue版本选择安装:
# Vue 2
npm install vue-json-viewer@2 --save
# Vue 3
npm install vue-json-viewer@3 --save
基本使用:
<template>
<json-viewer
:value="logData"
:expand-depth="2"
copyable
boxed
/>
</template>
<script>
import JsonViewer from 'vue-json-viewer'
import 'vue-json-viewer/style.css'
export default {
components: { JsonViewer },
data() {
return {
logData: {
timestamp: Date.now(),
level: 'error',
message: '数据库连接失败',
details: { retryCount: 3 }
}
}
}
}
</script>
设计特点
极简但实用:没有多余功能,但复制、折叠、主题切换都做得很好。默认样式清爽,颜色区分明显。
一键复制:添加copyable属性,每个值旁都会出现复制按钮,调试时特别方便。
性能优化好:采用延迟加载策略,大文件初始加载快。但要注意,这会影响浏览器的全局搜索功能(Ctrl+F可能找不到未渲染内容)。
主题支持:轻松切换明暗主题:
<json-viewer :value="data" theme="dark" />
实战:系统日志查看器
对于日志查看场景,vue-json-viewer非常合适:
<template>
<div class="log-viewer">
<div v-for="(log, index) in filteredLogs" :key="index">
<div class="log-meta">
<span class="level-tag">{{ log.level }}</span>
<span class="time">{{ formatTime(log.timestamp) }}</span>
</div>
<json-viewer
:value="log.data"
:expand-depth="log.level === 'error' ? 3 : 1"
copyable
/>
</div>
</div>
</template>
决策指南:如何选择?
根据我的使用经验,选择建议如下:
选vue-json-pretty当:
- 需要编辑JSON数据
- 处理超大型数据集(>5MB)
- 要求深度自定义样式和交互
- 构建开发者工具或管理后台
选vue-json-viewer当:
- 只需查看不可编辑
- 需要频繁复制字段值
- 项目对包体积敏感
- 快速集成,最小配置
实用技巧
处理循环引用:两个组件遇到循环引用都会出错。传递数据前先处理:
function safeStringify(obj) {
const cache = new Set()
return JSON.stringify(obj, (key, value) => {
if (typeof value === 'object' && value !== null) {
if (cache.has(value)) return '[Circular]'
cache.add(value)
}
return value
})
}
自定义样式:使用深度选择器覆盖默认样式:
::v-deep .vjs-tree {
font-family: 'Monaco', 'Menlo', monospace;
font-size: 13px;
}
总结
vue-json-pretty和vue-json-viewer都是优秀的Vue JSON组件,选择取决于具体需求。 需要功能全面、支持编辑、处理大数据?选vue-json-pretty。只需简单展示、快速集成、便捷复制?选vue-json-viewer。
你是否有更好的JSON展示组件推荐?欢迎评论区留言。
关注微信公众号" 大前端历险记",掌握更多前端开发干货姿势!