Vue3视图渲染技术与JavaWeb开发实践
1. Vue3视图渲染技术概述
在JavaWeb开发中,前端工程化已经成为现代Web应用开发的标准实践。Vue3作为当前最主流的前端框架之一,其视图渲染技术为开发者提供了高效、灵活的开发体验。与传统的JSP、Thymeleaf等服务器端渲染技术不同,Vue3的客户端渲染方式能够实现更丰富的交互效果和更流畅的用户体验。
Vue3的视图渲染核心基于虚拟DOM技术,通过高效的Diff算法最小化DOM操作。相比Vue2,Vue3在渲染性能上有显著提升,主要体现在编译时优化(如静态节点提升)、更高效的组件更新机制等方面。这些改进使得Vue3特别适合复杂的企业级JavaWeb应用开发。
提示:虽然Vue3支持选项式API,但在新项目中建议优先使用组合式API,它能更好地组织代码逻辑,特别是在处理复杂业务场景时。
2. Vue3模板语法深度解析
2.1 基础模板语法
Vue3的模板语法是连接JavaScript逻辑和HTML展示的桥梁。最基本的模板语法包括:
- 文本插值:使用双大括号
{{ }}包裹数据
<p>{{ message }}</p>- 属性绑定:使用
v-bind或简写:指令
<img :src="imagePath" alt="产品图片">- 事件绑定:使用
v-on或简写@指令
<button @click="submitForm">提交</button>在实际JavaWeb项目中,这些基础语法通常用于构建CRUD界面。例如,在管理系统中的表单页面:
<div class="form-group"> <label>用户名</label> <input type="text" v-model="user.username" class="form-control" placeholder="请输入用户名"> </div>2.2 条件渲染与列表渲染
条件渲染(v-if/v-else/v-show)和列表渲染(v-for)是业务开发中最常用的指令。
性能考虑:
v-if是真正的条件渲染,会销毁和重建元素v-show只是切换CSS的display属性- 在频繁切换的场景使用
v-show,运行时条件很少改变时使用v-if
列表渲染示例(结合Java后端数据):
<table class="table"> <thead> <tr> <th>ID</th> <th>姓名</th> <th>操作</th> </tr> </thead> <tbody> <tr v-for="(user, index) in userList" :key="user.id"> <td>{{ user.id }}</td> <td>{{ user.name }}</td> <td> <button @click="editUser(index)">编辑</button> </td> </tr> </tbody> </table>注意:使用
v-for时必须提供唯一的key属性,这对Vue的虚拟DOM diff算法至关重要。在JavaWeb项目中,通常使用数据库ID作为key。
2.3 表单输入绑定
Vue3的v-model指令实现了表单输入和应用状态之间的双向绑定。在JavaWeb开发中,这极大简化了表单处理逻辑。
常见表单元素绑定示例:
<!-- 文本输入 --> <input v-model="formData.username" type="text"> <!-- 单选按钮 --> <input v-model="formData.gender" type="radio" value="male"> 男 <input v-model="formData.gender" type="radio" value="female"> 女 <!-- 下拉选择 --> <select v-model="formData.department"> <option value="dev">开发部</option> <option value="test">测试部</option> </select>对于复杂表单验证,可以结合Vuelidate或VeeValidate等验证库使用。
3. Vue3组合式API在视图渲染中的应用
3.1 ref与reactive的使用
Vue3的组合式API提供了ref和reactive两种创建响应式数据的方式:
import { ref, reactive } from 'vue' // 基本类型使用ref const count = ref(0) // 对象类型可以使用reactive const formState = reactive({ username: '', password: '' }) // 在模板中使用 // <p>{{ count }}</p> // <input v-model="formState.username">在JavaWeb项目中,通常使用reactive来管理表单数据和页面状态:
export default { setup() { const pageData = reactive({ loading: false, tableData: [], pagination: { current: 1, size: 10, total: 0 } }) // 从Java后端获取数据 const fetchData = async () => { pageData.loading = true try { const res = await axios.get('/api/users', { params: { page: pageData.pagination.current, size: pageData.pagination.size } }) pageData.tableData = res.data.list pageData.pagination.total = res.data.total } finally { pageData.loading = false } } return { pageData, fetchData } } }3.2 计算属性和侦听器
计算属性(computed)和侦听器(watch)是处理衍生数据和副作用的重要工具。
计算属性示例:
const filteredUsers = computed(() => { return pageData.tableData.filter(user => user.status === 'active' ) })侦听器示例:
watch( () => pageData.pagination.current, (newVal) => { fetchData() } )在JavaWeb项目中,这些特性常用于:
- 表格数据的过滤和排序
- 表单字段的联动和验证
- 路由参数变化时重新获取数据
4. 组件化开发实践
4.1 组件基础
Vue3的组件系统是构建大型JavaWeb应用的基石。一个典型的单文件组件(SFC)结构:
<template> <!-- 组件模板 --> </template> <script> export default { // 组件逻辑 } </script> <style scoped> /* 组件样式 */ </style>在JavaWeb项目中,通常会按照功能模块划分组件,例如:
src/ components/ layout/ Header.vue Sidebar.vue business/ UserTable.vue UserForm.vue4.2 组件通信
Vue3提供了多种组件通信方式:
- Props/Events:父子组件通信的基础方式
<!-- 父组件 --> <ChildComponent :title="pageTitle" @submit="handleSubmit"/> <!-- 子组件 --> <script> export default { props: ['title'], emits: ['submit'], methods: { submitForm() { this.$emit('submit', formData) } } } </script>- provide/inject:跨层级组件通信
// 祖先组件 provide('userContext', { userId: 123, userName: '张三' }) // 后代组件 const userContext = inject('userContext')- Pinia/Vuex:全局状态管理
4.3 插槽(Slot)的高级用法
插槽是创建灵活可复用组件的重要特性。Vue3中插槽的主要类型:
- 默认插槽
<!-- 组件定义 --> <div class="card"> <slot></slot> </div> <!-- 组件使用 --> <CardComponent> <p>这是卡片内容</p> </CardComponent>- 具名插槽
<!-- 组件定义 --> <div class="layout"> <header> <slot name="header"></slot> </header> <main> <slot></slot> </main> </div> <!-- 组件使用 --> <LayoutComponent> <template #header> <h1>页面标题</h1> </template> <p>主要内容区域</p> </LayoutComponent>- 作用域插槽
<!-- 组件定义 --> <ul> <li v-for="item in items" :key="item.id"> <slot :item="item"></slot> </li> </ul> <!-- 组件使用 --> <ItemList :items="userList"> <template #default="{ item }"> {{ item.name }} - {{ item.email }} </template> </ItemList>在JavaWeb后台管理系统中,插槽常用于构建可复用的布局组件和业务组件。
5. 性能优化技巧
5.1 虚拟滚动优化长列表
当渲染大量数据时(如后台管理系统中的大数据表格),虚拟滚动可以显著提升性能:
<template> <RecycleScroller class="scroller" :items="largeList" :item-size="50" key-field="id" v-slot="{ item }" > <div class="user-item"> {{ item.name }} - {{ item.email }} </div> </RecycleScroller> </template> <script> import { RecycleScroller } from 'vue-virtual-scroller' import 'vue-virtual-scroller/dist/vue-virtual-scroller.css' export default { components: { RecycleScroller }, data() { return { largeList: [] // 从Java后端获取的大量数据 } } } </script>5.2 异步组件与代码分割
Vue3的defineAsyncComponent可以实现组件的异步加载,配合Webpack的代码分割功能:
const AsyncComponent = defineAsyncComponent(() => import('./components/HeavyComponent.vue') )在JavaWeb项目中,可以按路由进行代码分割:
const routes = [ { path: '/report', component: () => import('@/views/ReportView.vue') } ]5.3 其他优化策略
- v-once:只渲染元素和组件一次
<footer v-once>© 2024 公司名称</footer>- v-memo:Vue3.2+新增,可以记住模板树
<div v-memo="[valueA, valueB]"> <!-- 只有当valueA或valueB变化时才会更新 --> </div>- 合理使用shallowRef/shallowReactive:对于不需要深度响应式的大对象
6. 常见问题与解决方案
6.1 模板编译警告处理
在开发过程中,可能会遇到各种模板编译警告。常见问题及解决方法:
| 问题类型 | 可能原因 | 解决方案 |
|---|---|---|
| 缺少key属性 | v-for没有提供key | 为每个v-for项添加唯一key |
| 组件命名冲突 | 组件名与HTML元素冲突 | 使用多单词组件名,如UserTable |
| 属性大小写不匹配 | HTML不区分大小写 | 模板中使用kebab-case,如<user-info> |
6.2 响应式数据问题
Vue3的响应式系统虽然强大,但也有一些需要注意的陷阱:
- 解构丢失响应性:
// 错误做法 const { x, y } = reactive({ x: 1, y: 2 }) // 正确做法 const pos = reactive({ x: 1, y: 2 }) const { x, y } = toRefs(pos)- 数组变化检测:
// 这些方法会触发视图更新 state.list.push(item) state.list.splice(index, 1) // 这些不会触发更新 state.list[index] = newItem state.list.length = 0 // 解决方案 state.list = [...state.list, newItem]6.3 与Java后端集成问题
在JavaWeb项目中,Vue3前端与Java后端集成时常见问题:
- 跨域问题:开发环境下配置代理
// vite.config.js export default defineConfig({ server: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } })- 日期时间处理:Java后端返回的日期可能需要格式化
import { format } from 'date-fns' const formattedDate = format(new Date(backendDateStr), 'yyyy-MM-dd')- 大整数处理:Java的Long类型可能超出JS安全整数范围
// 使用json-bigint处理大整数 import JSONbig from 'json-bigint' axios.interceptors.response.use(response => { if (response.data) { response.data = JSONbig.parse(response.data) } return response })7. 实战:构建JavaWeb管理系统用户模块
7.1 用户列表页实现
结合上述技术,实现一个典型的用户管理列表页:
<template> <div class="user-management"> <div class="toolbar"> <button @click="showAddDialog">新增用户</button> <input v-model="searchQuery" placeholder="搜索用户..."> </div> <el-table :data="filteredUsers" v-loading="loading"> <el-table-column prop="id" label="ID" width="80"></el-table-column> <el-table-column prop="username" label="用户名"></el-table-column> <el-table-column prop="role" label="角色"></el-table-column> <el-table-column label="操作" width="180"> <template #default="scope"> <button @click="editUser(scope.row)">编辑</button> <button @click="deleteUser(scope.row.id)">删除</button> </template> </el-table-column> </el-table> <el-pagination v-model:currentPage="pagination.current" :page-size="pagination.size" :total="pagination.total" @current-change="fetchUsers" ></el-pagination> <UserDialog v-model:visible="dialogVisible" :user="currentUser" @submit="handleSubmit" /> </div> </template> <script> import { ref, computed, onMounted } from 'vue' import axios from '@/utils/axios' import UserDialog from './UserDialog.vue' export default { components: { UserDialog }, setup() { const loading = ref(false) const users = ref([]) const searchQuery = ref('') const dialogVisible = ref(false) const currentUser = ref(null) const pagination = ref({ current: 1, size: 10, total: 0 }) const filteredUsers = computed(() => { return users.value.filter(user => user.username.includes(searchQuery.value) || user.role.includes(searchQuery.value) ) }) const fetchUsers = async () => { loading.value = true try { const res = await axios.get('/api/users', { params: { page: pagination.value.current, size: pagination.value.size } }) users.value = res.data.list pagination.value.total = res.data.total } finally { loading.value = false } } const showAddDialog = () => { currentUser.value = null dialogVisible.value = true } const editUser = (user) => { currentUser.value = { ...user } dialogVisible.value = true } const deleteUser = async (id) => { if (confirm('确定删除此用户吗?')) { await axios.delete(`/api/users/${id}`) await fetchUsers() } } const handleSubmit = async (userData) => { if (userData.id) { await axios.put(`/api/users/${userData.id}`, userData) } else { await axios.post('/api/users', userData) } dialogVisible.value = false await fetchUsers() } onMounted(fetchUsers) return { loading, users, searchQuery, pagination, dialogVisible, currentUser, filteredUsers, fetchUsers, showAddDialog, editUser, deleteUser, handleSubmit } } } </script>7.2 用户表单对话框组件
<template> <el-dialog :title="user ? '编辑用户' : '新增用户'" v-model="visible" width="50%" > <el-form :model="form" label-width="100px"> <el-form-item label="用户名" required> <el-input v-model="form.username"></el-input> </el-form-item> <el-form-item label="密码" required v-if="!user"> <el-input v-model="form.password" type="password"></el-input> </el-form-item> <el-form-item label="角色"> <el-select v-model="form.role"> <el-option v-for="role in roleOptions" :key="role.value" :label="role.label" :value="role.value" ></el-option> </el-select> </el-form-item> </el-form> <template #footer> <el-button @click="visible = false">取消</el-button> <el-button type="primary" @click="submitForm">确定</el-button> </template> </el-dialog> </template> <script> import { ref, watch } from 'vue' export default { props: { visible: Boolean, user: Object }, emits: ['update:visible', 'submit'], setup(props, { emit }) { const form = ref({ username: '', password: '', role: '' }) const roleOptions = [ { value: 'admin', label: '管理员' }, { value: 'user', label: '普通用户' } ] watch(() => props.user, (newVal) => { if (newVal) { form.value = { ...newVal, password: '' } } else { form.value = { username: '', password: '', role: '' } } }, { immediate: true }) const submitForm = () => { emit('submit', form.value) emit('update:visible', false) } return { form, roleOptions, submitForm } } } </script>8. 进阶技巧与最佳实践
8.1 自定义指令的应用
Vue3的自定义指令可以封装DOM操作逻辑。例如,实现一个权限控制指令:
// directives/permission.js export const permission = { mounted(el, binding) { const { value } = binding const permissions = store.getters.permissions if (value && !permissions.includes(value)) { el.parentNode?.removeChild(el) } } } // 在组件中使用 <button v-permission="'user:create'">新增用户</button>8.2 渲染函数与JSX
在某些复杂场景下,渲染函数或JSX比模板更灵活。例如,动态生成表格列:
export default { setup() { const columns = ref([ { prop: 'name', label: '姓名' }, { prop: 'age', label: '年龄' } ]) const renderTable = () => { return h('el-table', { data: tableData.value }, columns.value.map(col => { return h('el-table-column', { prop: col.prop, label: col.label }) })) } return { renderTable } } }8.3 性能监控与分析
使用Vue DevTools和浏览器性能工具分析组件渲染性能:
- 识别渲染时间过长的组件
- 检查不必要的重新渲染
- 使用
<KeepAlive>缓存组件状态 - 使用
v-once标记静态内容
在开发环境中,可以添加性能标记:
import { markRaw } from 'vue' const heavyComponent = markRaw(import('./HeavyComponent.vue'))8.4 与JavaWeb传统技术集成
在混合项目中,可能需要与JSP等传统技术集成:
- 数据传递:通过全局变量或API接口
<script> window.globalConfig = { userId: "${userId}", userName: "${userName}" } </script>- 渐进式迁移:逐步替换旧有页面
- 共享认证信息:统一处理Session和Token
9. 测试与调试策略
9.1 单元测试
使用Jest或Vitest测试Vue组件:
import { mount } from '@vue/test-utils' import UserForm from '@/components/UserForm.vue' test('提交表单时触发submit事件', async () => { const wrapper = mount(UserForm) await wrapper.find('form').trigger('submit') expect(wrapper.emitted()).toHaveProperty('submit') })9.2 E2E测试
使用Cypress进行端到端测试:
describe('用户管理', () => { it('可以创建新用户', () => { cy.visit('/users') cy.contains('新增用户').click() cy.get('#username').type('testuser') cy.get('#password').type('123456') cy.contains('确定').click() cy.contains('testuser').should('exist') }) })9.3 调试技巧
- 源码映射:确保生成正确的sourcemap
- 错误追踪:集成Sentry等错误监控工具
- API Mock:使用Mock.js或MSW模拟后端接口
// src/mocks/handlers.js import { rest } from 'msw' export const handlers = [ rest.get('/api/users', (req, res, ctx) => { return res( ctx.json({ list: [{ id: 1, name: 'Mock User' }], total: 1 }) ) }) ]10. 部署与持续集成
10.1 构建优化
- 分包策略:根据路由拆分代码
// vite.config.js export default defineConfig({ build: { rollupOptions: { output: { manualChunks(id) { if (id.includes('node_modules')) { return 'vendor' } } } } } })- Gzip压缩:减小资源体积
// vite.config.js import viteCompression from 'vite-plugin-compression' export default defineConfig({ plugins: [ viteCompression() ] })10.2 部署到JavaWeb项目
- 独立部署:前端部署到CDN/Nginx,通过API与Java后端交互
- 集成部署:将构建产物放入Java项目的
src/main/resources/static目录 - Docker化:构建前端Docker镜像
# 前端Dockerfile示例 FROM nginx:alpine COPY dist /usr/share/nginx/html COPY nginx.conf /etc/nginx/conf.d/default.conf EXPOSE 80 CMD ["nginx", "-g", "daemon off;"]10.3 CI/CD集成
在GitLab CI或Jenkins中配置自动化流程:
# .gitlab-ci.yml示例 stages: - build - test - deploy build: stage: build image: node:16 script: - npm install - npm run build artifacts: paths: - dist/ test: stage: test image: node:16 script: - npm install - npm run test:unit - npm run test:e2e deploy: stage: deploy image: alpine script: - apk add rsync - rsync -avz dist/ user@server:/var/www/html/11. 安全最佳实践
11.1 常见前端安全问题
XSS防护:
- 使用
v-html时要对内容进行消毒 - 设置Content Security Policy (CSP)
- 避免在模板中直接使用用户输入
- 使用
CSRF防护:
- 确保所有修改操作使用POST/PUT/DELETE方法
- 从Java后端获取CSRF Token并包含在请求头中
// axios拦截器示例 axios.interceptors.request.use(config => { const token = getCookie('XSRF-TOKEN') if (token) { config.headers['X-XSRF-TOKEN'] = token } return config })- API安全:
- 使用HTTPS加密通信
- 实现适当的认证和授权
- 对敏感数据进行加密
11.2 依赖安全
- 定期检查项目依赖的安全漏洞:
npm audit- 使用依赖锁定文件(package-lock.json)
- 及时更新有安全漏洞的依赖
11.3 生产环境加固
- 禁用开发者工具和调试信息
// vite.config.js export default defineConfig({ define: { __VUE_PROD_DEVTOOLS__: false } })- 配置正确的HTTP安全头
- 实现敏感操作的二次确认
12. 项目结构与代码组织
12.1 推荐的项目结构
对于JavaWeb项目中的Vue3前端,推荐如下结构:
src/ assets/ # 静态资源 components/ # 公共组件 ui/ # 基础UI组件 business/ # 业务组件 composables/ # 组合式函数 router/ # 路由配置 stores/ # 状态管理 utils/ # 工具函数 views/ # 页面组件 App.vue # 根组件 main.js # 应用入口12.2 代码风格指南
命名规范:
- 组件名:PascalCase (如
UserForm.vue) - 变量/函数:camelCase
- 常量:UPPER_CASE
- 组件名:PascalCase (如
组件设计原则:
- 单一职责原则
- 合理的props设计
- 明确的emits定义
代码分割:
- 按功能拆分组合式函数
- 避免过大的单文件组件
12.3 文档与注释
- 为复杂组件添加JSDoc注释
/** * 用户表单组件 * @prop {Object} user - 要编辑的用户对象 * @emits {submit} - 表单提交事件 */ export default { props: ['user'], emits: ['submit'] }- 使用README.md记录项目配置和约定
- 维护CHANGELOG.md记录重要变更
13. 国际化与主题定制
13.1 多语言支持
使用Vue I18n实现国际化:
// src/i18n.js import { createI18n } from 'vue-i18n' const messages = { en: { user: { name: 'Name', create: 'Create User' } }, zh: { user: { name: '姓名', create: '创建用户' } } } export default createI18n({ locale: 'zh', messages })在组件中使用:
<template> <h1>{{ $t('user.create') }}</h1> </template>13.2 动态主题切换
实现CSS变量主题切换:
/* 定义主题变量 */ :root { --primary-color: #409eff; --bg-color: #ffffff; } .dark { --primary-color: #79bbff; --bg-color: #1d1e1f; }在Vue中切换主题:
const toggleTheme = () => { document.documentElement.classList.toggle('dark') }13.3 响应式设计考虑
- 使用CSS媒体查询适配不同屏幕
- 考虑移动端触摸操作
- 使用rem/em单位实现弹性布局
@media (max-width: 768px) { .form-container { width: 100%; padding: 10px; } }14. 与Java后端深度集成
14.1 认证与授权
- JWT认证集成:
// axios拦截器示例 axios.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = `Bearer ${token}` } return config }) axios.interceptors.response.use(response => { return response }, error => { if (error.response.status === 401) { // 跳转到登录页 } return Promise.reject(error) })- 权限控制:基于后端返回的权限数据控制界面元素
14.2 文件上传与下载
- 文件上传:
<template> <input type="file" @change="handleUpload"> </template> <script> const handleUpload = async (e) => { const file = e.target.files[0] const formData = new FormData() formData.append('file', file) try { await axios.post('/api/upload', formData, { headers: { 'Content-Type': 'multipart/form-data' } }) } catch (error) { console.error('上传失败', error) } } </script>- 文件下载:
const downloadFile = async (fileId) => { const response = await axios.get(`/api/files/${fileId}`, { responseType: 'blob' }) const url = window.URL.createObjectURL(new Blob([response.data])) const link = document.createElement('a') link.href = url link.setAttribute('download', 'filename.ext') document.body.appendChild(link) link.click() link.remove() }14.3 WebSocket实时通信
与Java后端的WebSocket集成:
// 建立WebSocket连接 const socket = new WebSocket('ws://localhost:8080/ws') // 监听消息 socket.addEventListener('message', (event) => { const message = JSON.parse(event.data) // 处理消息 }) // 发送消息 const sendMessage = (content) => { socket.send(JSON.stringify({ type: 'chat', content })) }15. 微前端架构考虑
15.1 微前端方案选型
在大型JavaWeb项目中,可能需要考虑微前端架构:
- qiankun:阿里开源的微前端框架
- Module Federation:Webpack 5原生支持
- iframe:简单但隔离性强的方案
15.2 qiankun集成示例
主应用配置:
import { registerMicroApps, start } from 'qiankun' registerMicroApps([ { name: 'userApp', entry: '//localhost:7101', container: '#user-container', activeRule: '/user' } ]) start()子应用(Vue3)配置:
// main.js import { createApp } from 'vue' import App from './App.vue' import { renderWithQiankun, qiankunWindow } from 'vite-plugin-qiankun/dist/helper' let app function render(props = {}) { const { container } = props app = createApp(App) app.mount(container ? container.querySelector('#app') : '#app') } renderWithQiankun({ mount(props) { render(props) }, bootstrap() {}, unmount() { app.unmount() } }) if (!qiankunWindow.__POWERED_BY_QIANKUN__) { render() }15.3 样式隔离方案
- CSS命名空间:为每个微应用添加特定前缀
- Shadow DOM:实现真正的DOM隔离
- CSS Modules:组件级作用域
16. 移动端适配方案
16.1 响应式布局
使用flex/grid布局和媒体查询:
.container { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 20px; } @media (max-width: 768px) { .container { grid-template-columns: 1fr; } }16.2 手势操作支持
添加触摸事件处理:
<div @touchstart="handleTouchStart" @touchmove="handleTouchMove" @touchend="handleTouchEnd" ></div>16.3 PWA支持
配置Vite PWA插件:
// vite.config.js import { VitePWA } from 'vite-plugin-pwa' export default defineConfig({ plugins: [ VitePWA({ registerType: 'autoUpdate', manifest: { name: 'JavaWeb管理系统', short_name: '管理系统', theme_color: '#ffffff' } }) ] })17. 状态管理进阶
17.1 Pinia状态管理
Pinia是Vue3推荐的状态管理库:
// stores/user.js import { defineStore } from 'pinia' export const useUserStore = defineStore('user', { state: () => ({ currentUser: null, token: '' }), actions: { async login(credentials) { const res = await axios.post('/api/login', credentials) this.currentUser = res.data.user this.token = res.data.token } } })在组件中使用:
import { useUserStore } from '@/stores/user' const userStore = useUserStore() const login = () => { userStore.login({ username: 'admin', password: '123456' }) }17.2 状态持久化
使用插件实现状态持久化:
// stores/persist.js import { createPinia } from 'pinia' import piniaPluginPersistedstate from 'pinia-plugin-persistedstate' const pinia = createPinia() pinia.use(piniaPluginPersistedstate) export default pinia在store中启用:
export const useUserStore = defineStore('user', { persist: true, // ... })17.3 状态共享模式
- Provide/Inject:适合小范围状态共享
- Event Bus:简单的事件通信
- Composables:可复用的状态逻辑
18. 动画与过渡效果
18.1 Vue过渡组件
使用Vue内置的过渡组件:
<transition name="fade"> <div v-if="show">内容</div> </transition> <style> .fade-enter-active, .fade-leave-active { transition: opacity 0.5s; } .fade-enter-from, .fade-leave-to { opacity: 0; } </style>