Compare commits
9 Commits
b878032303
...
b73a52c022
Author | SHA1 | Date |
---|---|---|
|
b73a52c022 | |
|
90e512b977 | |
|
3872c8f29e | |
|
76ef5f7e5b | |
|
f2b0e74cd2 | |
|
e6e280d8f1 | |
|
9ecf17621e | |
|
39b773f409 | |
|
b397e00b36 |
|
@ -4,12 +4,16 @@
|
|||
"private": true,
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
"dev": "pnpm serve",
|
||||
"build": "vue-cli-service build",
|
||||
"lint": "vue-cli-service lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"animate.css": "^4.1.1",
|
||||
"core-js": "^3.8.3",
|
||||
"nanoid": "^5.1.5",
|
||||
"vue": "^2.6.14",
|
||||
"vue-router": "^3.6.5",
|
||||
"vuex": "3"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
@ -8,12 +8,21 @@ importers:
|
|||
|
||||
.:
|
||||
dependencies:
|
||||
animate.css:
|
||||
specifier: ^4.1.1
|
||||
version: 4.1.1
|
||||
core-js:
|
||||
specifier: ^3.8.3
|
||||
version: 3.43.0
|
||||
nanoid:
|
||||
specifier: ^5.1.5
|
||||
version: 5.1.5
|
||||
vue:
|
||||
specifier: ^2.6.14
|
||||
version: 2.7.16
|
||||
vue-router:
|
||||
specifier: ^3.6.5
|
||||
version: 3.6.5(vue@2.7.16)
|
||||
vuex:
|
||||
specifier: '3'
|
||||
version: 3.6.2(vue@2.7.16)
|
||||
|
@ -1028,6 +1037,9 @@ packages:
|
|||
ajv@8.17.1:
|
||||
resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==}
|
||||
|
||||
animate.css@4.1.1:
|
||||
resolution: {integrity: sha512-+mRmCTv6SbCmtYJCN4faJMNFVNN5EuCTTprDTAo7YzIGji2KADmakjVA3+8mVDkZ2Bf09vayB35lSQIex2+QaQ==}
|
||||
|
||||
ansi-colors@4.1.3:
|
||||
resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==}
|
||||
engines: {node: '>=6'}
|
||||
|
@ -2574,6 +2586,11 @@ packages:
|
|||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||
hasBin: true
|
||||
|
||||
nanoid@5.1.5:
|
||||
resolution: {integrity: sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw==}
|
||||
engines: {node: ^18 || >=20}
|
||||
hasBin: true
|
||||
|
||||
natural-compare@1.4.0:
|
||||
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
|
||||
|
||||
|
@ -3575,6 +3592,11 @@ packages:
|
|||
vue:
|
||||
optional: true
|
||||
|
||||
vue-router@3.6.5:
|
||||
resolution: {integrity: sha512-VYXZQLtjuvKxxcshuRAwjHnciqZVoXAjTjcqBTz4rKc8qih9g9pI3hbDjmqXaHdgL3v8pV6P8Z335XvHzESxLQ==}
|
||||
peerDependencies:
|
||||
vue: ^2
|
||||
|
||||
vue-style-loader@4.1.3:
|
||||
resolution: {integrity: sha512-sFuh0xfbtpRlKfm39ss/ikqs9AbKCoXZBpHeVZ8Tx650o0k0q/YCM7FRvigtxpACezfq6af+a7JeqVTWvncqDg==}
|
||||
|
||||
|
@ -5229,6 +5251,8 @@ snapshots:
|
|||
json-schema-traverse: 1.0.0
|
||||
require-from-string: 2.0.2
|
||||
|
||||
animate.css@4.1.1: {}
|
||||
|
||||
ansi-colors@4.1.3: {}
|
||||
|
||||
ansi-escapes@3.2.0: {}
|
||||
|
@ -6654,6 +6678,8 @@ snapshots:
|
|||
|
||||
nanoid@3.3.11: {}
|
||||
|
||||
nanoid@5.1.5: {}
|
||||
|
||||
natural-compare@1.4.0: {}
|
||||
|
||||
negotiator@0.6.3: {}
|
||||
|
@ -7700,6 +7726,10 @@ snapshots:
|
|||
'@vue/compiler-sfc': 3.5.16
|
||||
vue: 2.7.16
|
||||
|
||||
vue-router@3.6.5(vue@2.7.16):
|
||||
dependencies:
|
||||
vue: 2.7.16
|
||||
|
||||
vue-style-loader@4.1.3:
|
||||
dependencies:
|
||||
hash-sum: 1.0.2
|
||||
|
|
|
@ -1,15 +1,35 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
|
||||
<link
|
||||
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.7/dist/css/bootstrap.min.css"
|
||||
rel="stylesheet"
|
||||
integrity="sha384-LN+7fdVzj6u52u30Kp6M/trliBMCMKTyK833zpbD+pXdCLuTusPj697FH4R/5mcr"
|
||||
crossorigin="anonymous"
|
||||
/>
|
||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico" />
|
||||
<script
|
||||
src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.8/dist/umd/popper.min.js"
|
||||
integrity="sha384-I7E8VVD/ismYTF4hNIPjVp/Zjvgyol6VFvRkX/vR+Vc4jQkC+hVqc2pM8ODewa9r"
|
||||
crossorigin="anonymous"
|
||||
></script>
|
||||
<script
|
||||
src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.7/dist/js/bootstrap.min.js"
|
||||
integrity="sha384-7qAoOXltbVP82dhxHAUje59V5r2YsVfBafyUDxEdApLPmcdhBPg1DKg1ERo0BZlK"
|
||||
crossorigin="anonymous"
|
||||
></script>
|
||||
<title><%= htmlWebpackPlugin.options.title %></title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||
<strong
|
||||
>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work
|
||||
properly without JavaScript enabled. Please enable it to
|
||||
continue.</strong
|
||||
>
|
||||
</noscript>
|
||||
<div id="app"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
|
|
|
@ -5,7 +5,13 @@
|
|||
<Demo2 />
|
||||
<hr />
|
||||
<Demo3 /> -->
|
||||
<Demo4 />
|
||||
<!-- <Demo4 /> -->
|
||||
<!-- <Demo6 /> -->
|
||||
<!-- <Todo /> -->
|
||||
<!-- <Demo8 /> -->
|
||||
<!-- <Demo9 /> -->
|
||||
<!-- <Demo10 /> -->
|
||||
<Demo11 />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -13,11 +19,17 @@
|
|||
// import Demo1 from "@/views/demo1/index.vue";
|
||||
// import Demo2 from "@/views/demo2/index.vue";
|
||||
// import Demo3 from "@/views/demo3/index.vue";
|
||||
import Demo4 from "@/views/demo4/index.vue";
|
||||
// import Demo4 from "@/views/demo4/index.vue";
|
||||
// import Demo5 from "@/views/demo5/index.vue";
|
||||
// import Todo from "@/views/todo/index.vue";
|
||||
// import Demo8 from "@/views/demo8/index.vue";
|
||||
// import Demo9 from "@/views/demo9/index.vue";
|
||||
// import Demo10 from "@/views/demo10/index.vue";
|
||||
import Demo11 from "@/views/demo11/index.vue";
|
||||
|
||||
export default {
|
||||
name: "App",
|
||||
// components: { Demo1, Demo2, Demo3 },
|
||||
components: { Demo4 },
|
||||
components: { Demo11 },
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -1,10 +1,16 @@
|
|||
import "animate.css";
|
||||
import Vue from "vue";
|
||||
import VueRouter from "vue-router";
|
||||
import App from "./App.vue";
|
||||
import router from "./router";
|
||||
import store from "./store";
|
||||
|
||||
Vue.config.productionTip = false;
|
||||
|
||||
Vue.use(VueRouter);
|
||||
|
||||
new Vue({
|
||||
render: (h) => h(App),
|
||||
store,
|
||||
router,
|
||||
}).$mount("#app");
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
import VueRouter from "vue-router";
|
||||
|
||||
const router = new VueRouter({
|
||||
routes: [
|
||||
{
|
||||
name: "About",
|
||||
path: "/about",
|
||||
component: () => import("@/views/demo6/components/About.vue"),
|
||||
},
|
||||
{
|
||||
name: "Home",
|
||||
path: "/home",
|
||||
component: () => import("@/views/demo6/components/Home.vue"),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
export default router;
|
|
@ -1,13 +1,15 @@
|
|||
import Vue from "vue";
|
||||
import Vuex from "vuex";
|
||||
import count from "./modules/count";
|
||||
import schoolInfo from "./modules/schoolInfo";
|
||||
|
||||
Vue.use(Vuex);
|
||||
|
||||
const store = new Vuex.Store({
|
||||
state: {},
|
||||
mutations: {},
|
||||
actions: {},
|
||||
modules: { count },
|
||||
modules: { count, schoolInfo },
|
||||
});
|
||||
|
||||
export default store;
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
const schoolInfo = {
|
||||
namespaced: true,
|
||||
state: {
|
||||
schoolName: "BunnySchool",
|
||||
schoolAddress: "昆山市印象欧洲",
|
||||
},
|
||||
getters: {
|
||||
getSchoolName(state) {
|
||||
return state.schoolName + "---";
|
||||
},
|
||||
},
|
||||
actions: {},
|
||||
mutations: {},
|
||||
};
|
||||
|
||||
export default schoolInfo;
|
|
@ -0,0 +1,175 @@
|
|||
# Vue2 Transition 动画系统详解
|
||||
|
||||
## 一、基本动画实现
|
||||
|
||||
### 1. Transition 组件核心用法
|
||||
```html
|
||||
<Transition name="bunny" :appear="true">
|
||||
<div v-show="visibility">内容</div>
|
||||
</Transition>
|
||||
```
|
||||
|
||||
| 属性 | 说明 | 示例值 |
|
||||
| -------- | -------------------- | -------- |
|
||||
| `name` | 动画类名前缀 | "bunny" |
|
||||
| `appear` | 初始渲染是否应用动画 | true |
|
||||
| `mode` | 过渡模式 | "out-in" |
|
||||
|
||||
### 2. 动画类名系统
|
||||
```css
|
||||
/* 进入动画 */
|
||||
.bunny-enter-active { /* 激活阶段样式 */ }
|
||||
.bunny-enter { /* 开始状态 */ }
|
||||
.bunny-enter-to { /* 结束状态 */ }
|
||||
|
||||
/* 离开动画 */
|
||||
.bunny-leave-active { /* 激活阶段样式 */ }
|
||||
.bunny-leave { /* 开始状态 */ }
|
||||
.bunny-leave-to { /* 结束状态 */ }
|
||||
```
|
||||
|
||||
## 二、动画效果进阶
|
||||
|
||||
### 1. CSS 动画实现
|
||||
```css
|
||||
/* 关键帧动画 */
|
||||
@keyframes slip {
|
||||
from { transform: translateX(-100%); }
|
||||
to { transform: translateX(0); }
|
||||
}
|
||||
|
||||
.bunny-enter-active {
|
||||
animation: slip 0.5s ease-out;
|
||||
}
|
||||
|
||||
.bunny-leave-active {
|
||||
animation: slip 0.5s ease-in reverse;
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 过渡效果实现
|
||||
```css
|
||||
/* 过渡效果 */
|
||||
.bunny-enter-active, .bunny-leave-active {
|
||||
transition: all 0.5s cubic-bezier(0.68, -0.55, 0.27, 1.55);
|
||||
}
|
||||
|
||||
.bunny-enter, .bunny-leave-to {
|
||||
opacity: 0;
|
||||
transform: scale(0.8);
|
||||
}
|
||||
```
|
||||
|
||||
## 三、最佳实践指南
|
||||
|
||||
### 1. 性能优化建议
|
||||
- 优先使用 `transform` 和 `opacity` 属性
|
||||
- 避免动画期间触发布局重排
|
||||
- 对移动设备减少动画持续时间
|
||||
|
||||
### 2. 动画调试技巧
|
||||
```css
|
||||
/* 调试用边框 */
|
||||
.bunny-enter-active {
|
||||
outline: 2px solid red;
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 响应式动画方案
|
||||
```javascript
|
||||
computed: {
|
||||
animationDuration() {
|
||||
return this.isMobile ? 300 : 500
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 四、高级应用场景
|
||||
|
||||
### 1. 列表过渡
|
||||
```html
|
||||
<TransitionGroup name="list" tag="ul">
|
||||
<li v-for="item in items" :key="item.id">
|
||||
{{ item.text }}
|
||||
</li>
|
||||
</TransitionGroup>
|
||||
|
||||
<style>
|
||||
.list-move {
|
||||
transition: transform 0.5s ease;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
### 2. 动态过渡
|
||||
```html
|
||||
<Transition :name="transitionName">
|
||||
<!-- ... -->
|
||||
</Transition>
|
||||
|
||||
<script>
|
||||
data() {
|
||||
return {
|
||||
transitionName: isMobile ? 'slide' : 'fade'
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
### 3. JavaScript 钩子
|
||||
```html
|
||||
<Transition
|
||||
@before-enter="beforeEnter"
|
||||
@enter="enter"
|
||||
@after-enter="afterEnter"
|
||||
>
|
||||
<!-- ... -->
|
||||
</Transition>
|
||||
|
||||
<script>
|
||||
methods: {
|
||||
beforeEnter(el) {
|
||||
el.style.opacity = 0
|
||||
},
|
||||
enter(el, done) {
|
||||
anime({
|
||||
targets: el,
|
||||
opacity: 1,
|
||||
duration: 500,
|
||||
complete: done
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
## 五、常见问题解决方案
|
||||
|
||||
1. **动画不生效排查**:
|
||||
- 检查类名前缀是否匹配
|
||||
- 确认元素初始状态是否正确
|
||||
- 验证CSS属性是否可动画
|
||||
|
||||
2. **闪烁问题处理**:
|
||||
```css
|
||||
.v-enter {
|
||||
opacity: 0.01; /* 避免初始状态检测问题 */
|
||||
}
|
||||
```
|
||||
|
||||
3. **多元素过渡**:
|
||||
```html
|
||||
<Transition mode="out-in">
|
||||
<div :key="currentView">{{ currentView }}</div>
|
||||
</Transition>
|
||||
```
|
||||
|
||||
## 六、与Vue3的对比
|
||||
|
||||
| 特性 | Vue2 | Vue3 |
|
||||
| -------- | ---------------------- | ---------------- |
|
||||
| 类名前缀 | `v-` 或自定义 | 相同 |
|
||||
| 过渡模式 | 支持 `in-out`/`out-in` | 新增 `in-out-in` |
|
||||
| 性能 | 基于CSS | 基于FLIP优化 |
|
||||
| 新特性 | - | 过渡持久化 |
|
||||
|
|
@ -0,0 +1,166 @@
|
|||
# Vue2 过渡动画与 Animate.css 整合指南
|
||||
|
||||
## 一、Transition 与 TransitionGroup 对比
|
||||
|
||||
| 特性 | Transition | TransitionGroup |
|
||||
| -------- | ----------------- | --------------- |
|
||||
| 用途 | 单个元素/组件过渡 | 列表项过渡 |
|
||||
| 要求 | 需要条件渲染 | 需要 `key` 属性 |
|
||||
| 动画类 | 自动生成 | 可自定义类名 |
|
||||
| 额外效果 | - | 自动处理定位 |
|
||||
|
||||
## 二、Animate.css 集成方案
|
||||
|
||||
### 1. 基本集成方法
|
||||
```html
|
||||
<TransitionGroup
|
||||
name="animate__animated animate__bounce"
|
||||
appear
|
||||
enter-active-class="animate__flipInX"
|
||||
leave-active-class="animate__flipOutX"
|
||||
>
|
||||
<!-- 列表项 -->
|
||||
</TransitionGroup>
|
||||
```
|
||||
|
||||
### 2. 常用动画效果
|
||||
| 进入效果 | 离开效果 | 说明 |
|
||||
| ---------------------- | ------------------------ | -------- |
|
||||
| `animate__fadeIn` | `animate__fadeOut` | 淡入淡出 |
|
||||
| `animate__zoomIn` | `animate__zoomOut` | 缩放效果 |
|
||||
| `animate__slideInLeft` | `animate__slideOutRight` | 滑动效果 |
|
||||
| `animate__flipInX` | `animate__flipOutX` | 3D翻转 |
|
||||
|
||||
## 三、自定义动画与库动画结合
|
||||
|
||||
### 1. 混合使用示例
|
||||
```html
|
||||
<!-- 自定义单个元素动画 -->
|
||||
<Transition name="bunny">
|
||||
<h3 v-show="visibility">自定义动画</h3>
|
||||
</Transition>
|
||||
|
||||
<!-- Animate.css 列表动画 -->
|
||||
<TransitionGroup
|
||||
enter-active-class="animate__animated animate__fadeInUp"
|
||||
leave-active-class="animate__animated animate__fadeOutDown"
|
||||
>
|
||||
<li v-for="item in items" :key="item.id">{{ item.text }}</li>
|
||||
</TransitionGroup>
|
||||
```
|
||||
|
||||
### 2. 动画性能优化
|
||||
```css
|
||||
/* 启用GPU加速 */
|
||||
.animate__animated {
|
||||
animation-duration: 0.5s;
|
||||
backface-visibility: hidden;
|
||||
}
|
||||
|
||||
/* 限制动画影响范围 */
|
||||
.bunny-enter-active, .bunny-leave-active {
|
||||
will-change: transform;
|
||||
isolation: isolate;
|
||||
}
|
||||
```
|
||||
|
||||
## 四、最佳实践建议
|
||||
|
||||
### 1. 动画选择原则
|
||||
- **移动端**:使用较轻量的动画(如fade)
|
||||
- **重要交互**:使用明显的反馈动画(如bounce)
|
||||
- **连续操作**:保持动画风格一致
|
||||
|
||||
### 2. 时间控制技巧
|
||||
```javascript
|
||||
// 动态控制动画时长
|
||||
data() {
|
||||
return {
|
||||
duration: window.innerWidth < 768 ? 300 : 500
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 无障碍访问
|
||||
```css
|
||||
/* 减少运动偏好设置 */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.animate__animated,
|
||||
.bunny-enter-active,
|
||||
.bunny-leave-active {
|
||||
animation: none !important;
|
||||
transition: none !important;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 五、进阶应用场景
|
||||
|
||||
### 1. 路由过渡动画
|
||||
```html
|
||||
<Transition :name="transitionName" mode="out-in">
|
||||
<router-view></router-view>
|
||||
</Transition>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
transitionName: 'slide'
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'$route'(to, from) {
|
||||
this.transitionName = to.meta.transition || 'fade'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
### 2. 状态驱动的动画
|
||||
```javascript
|
||||
computed: {
|
||||
animationClass() {
|
||||
return this.isError
|
||||
? 'animate__shakeX'
|
||||
: 'animate__tada'
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 动画事件钩子
|
||||
```html
|
||||
<Transition
|
||||
@before-enter="beforeEnter"
|
||||
@after-enter="afterEnter"
|
||||
>
|
||||
<div v-if="show">内容</div>
|
||||
</Transition>
|
||||
|
||||
<script>
|
||||
methods: {
|
||||
beforeEnter(el) {
|
||||
el.style.opacity = 0
|
||||
},
|
||||
afterEnter(el) {
|
||||
// 动画结束后的处理
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
## 六、常见问题解决
|
||||
|
||||
1. **动画不连贯**:
|
||||
- 使用 `mode="out-in"` 确保顺序执行
|
||||
- 检查 `key` 属性是否唯一
|
||||
|
||||
2. **初始渲染无动画**:
|
||||
- 添加 `appear` 属性
|
||||
- 检查初始状态是否正确
|
||||
|
||||
3. **移动端性能差**:
|
||||
- 减少动画复杂度
|
||||
- 使用 `will-change` 提示浏览器优化
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
<template>
|
||||
<div class="container card">
|
||||
<div class="card-header">
|
||||
<h1>动画效果:{{ visibility }}</h1>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<Transition name="bunny" :appear="true">
|
||||
<h3 v-show="visibility">显示动画</h3>
|
||||
</Transition>
|
||||
|
||||
<ul>
|
||||
<TransitionGroup
|
||||
name="animate__animated animate__bounce"
|
||||
appear
|
||||
enter-active-class="animate__flipInX"
|
||||
leave-active-class="animate__flipOutX"
|
||||
>
|
||||
<li key="1" v-show="visibility">嗬嗬嗬嗬嗬嗬嗬嗬嗬嗬嗬</li>
|
||||
<li key="2" v-show="!visibility">哈哈哈哈哈哈哈哈哈哈哈</li>
|
||||
</TransitionGroup>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="card-footer">
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-primary" @click="visibility = true">显示</button>
|
||||
<button class="btn btn-success" @click="visibility = false">
|
||||
隐藏
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "Demo-10",
|
||||
data() {
|
||||
return {
|
||||
visibility: true,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.bunny-enter-active {
|
||||
animation: slip 0.5s linear;
|
||||
}
|
||||
|
||||
.bunny-leave-active {
|
||||
animation: slip 0.5s linear reverse;
|
||||
}
|
||||
|
||||
@keyframes slip {
|
||||
from {
|
||||
transform: translateX(-300px);
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,175 @@
|
|||
# Vue2 路由缓存与导航控制最佳实践
|
||||
|
||||
## 一、Keep-Alive 路由缓存机制
|
||||
|
||||
### 1. 基本使用方式
|
||||
```html
|
||||
<!-- 缓存单个组件 -->
|
||||
<keep-alive include="Home">
|
||||
<router-view/>
|
||||
</keep-alive>
|
||||
|
||||
<!-- 缓存多个组件 -->
|
||||
<keep-alive :include="['Home', 'About']">
|
||||
<router-view/>
|
||||
</keep-alive>
|
||||
```
|
||||
|
||||
### 2. 核心属性说明
|
||||
| 属性 | 类型 | 说明 |
|
||||
| --------- | ------------------- | ---------------------- |
|
||||
| `include` | String/Regexp/Array | 匹配的组件名将被缓存 |
|
||||
| `exclude` | String/Regexp/Array | 匹配的组件名不会被缓存 |
|
||||
| `max` | Number | 最大缓存组件数 |
|
||||
|
||||
## 二、组件生命周期变化
|
||||
|
||||
### 1. 缓存状态下的生命周期
|
||||
```
|
||||
首次加载:created → mounted → activated
|
||||
离开组件:deactivated
|
||||
再次进入:activated
|
||||
```
|
||||
|
||||
### 2. 新增生命周期钩子
|
||||
```javascript
|
||||
export default {
|
||||
activated() {
|
||||
// 组件被激活时调用
|
||||
this.fetchData()
|
||||
},
|
||||
deactivated() {
|
||||
// 组件被停用时调用
|
||||
this.clearTimer()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 三、最佳实践建议
|
||||
|
||||
### 1. 缓存策略优化
|
||||
```html
|
||||
<!-- 动态决定是否缓存 -->
|
||||
<keep-alive :include="cachedViews">
|
||||
<router-view :key="$route.fullPath"/>
|
||||
</keep-alive>
|
||||
|
||||
<script>
|
||||
computed: {
|
||||
cachedViews() {
|
||||
return this.$store.state.tagsView.cachedViews
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
### 2. 数据刷新方案
|
||||
```javascript
|
||||
// 方案1:监听路由变化
|
||||
watch: {
|
||||
'$route'(to, from) {
|
||||
if (to.name === 'Home') {
|
||||
this.refreshData()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 方案2:使用activated钩子
|
||||
activated() {
|
||||
this.loadData()
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 内存管理技巧
|
||||
```javascript
|
||||
// 清除缓存数据
|
||||
deactivated() {
|
||||
this.list = []
|
||||
this.pagination = { page: 1, size: 10 }
|
||||
}
|
||||
```
|
||||
|
||||
## 四、高级应用场景
|
||||
|
||||
### 1. 多级路由缓存
|
||||
```javascript
|
||||
// 父路由配置
|
||||
{
|
||||
path: '/nested',
|
||||
component: Layout,
|
||||
children: [
|
||||
{
|
||||
path: 'child1',
|
||||
component: Child1,
|
||||
meta: { keepAlive: true }
|
||||
},
|
||||
{
|
||||
path: 'child2',
|
||||
component: Child2
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// 动态缓存
|
||||
<keep-alive>
|
||||
<router-view v-if="$route.meta.keepAlive"/>
|
||||
</keep-alive>
|
||||
<router-view v-if="!$route.meta.keepAlive"/>
|
||||
```
|
||||
|
||||
### 2. 滚动位置保持
|
||||
```javascript
|
||||
// router配置
|
||||
scrollBehavior(to, from, savedPosition) {
|
||||
if (savedPosition) {
|
||||
return savedPosition
|
||||
} else if (to.meta.keepAlive) {
|
||||
return { x: 0, y: to.meta.scrollTop || 0 }
|
||||
}
|
||||
return { x: 0, y: 0 }
|
||||
}
|
||||
|
||||
// 组件内记录位置
|
||||
deactivated() {
|
||||
this.$route.meta.scrollTop = document.documentElement.scrollTop
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 缓存状态管理
|
||||
```javascript
|
||||
// 动态移除缓存
|
||||
this.$vnode.parent.componentInstance.cache = {}
|
||||
this.$vnode.parent.componentInstance.keys = []
|
||||
```
|
||||
|
||||
## 五、常见问题解决方案
|
||||
|
||||
1. **缓存不生效排查**:
|
||||
- 检查组件`name`是否与`include`匹配
|
||||
- 验证路由配置是否正确
|
||||
- 确保没有使用`v-if`干扰
|
||||
|
||||
2. **数据不更新问题**:
|
||||
```javascript
|
||||
// 使用路由参数作为key强制更新
|
||||
<router-view :key="$route.fullPath"/>
|
||||
```
|
||||
|
||||
3. **内存泄漏预防**:
|
||||
```javascript
|
||||
deactivated() {
|
||||
// 清除定时器/事件监听
|
||||
clearInterval(this.timer)
|
||||
window.removeEventListener('resize', this.handleResize)
|
||||
}
|
||||
```
|
||||
|
||||
## 六、与Vue3的区别
|
||||
|
||||
| 特性 | Vue2 | Vue3 |
|
||||
| --------- | ----------------------- | ------------------------- |
|
||||
| 缓存控制 | `include/exclude` | 新增`max`属性 |
|
||||
| 生命周期 | `activated/deactivated` | 需要导入`onActivated`钩子 |
|
||||
| 路由缓存 | 需要`keep-alive` | 内置路由组件缓存 |
|
||||
| 组合式API | 不支持 | 提供`onActivated`等钩子 |
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
<template>
|
||||
<div class="container card">
|
||||
<div class="card-header btn-group">
|
||||
<!-- 路由导航 -->
|
||||
<button class="btn btn-success" @click="$router.push({ name: 'About' })">
|
||||
显示About
|
||||
</button>
|
||||
<button class="btn btn-success" @click="$router.push({ name: 'Home' })">
|
||||
显示Home
|
||||
</button>
|
||||
|
||||
<!-- 前进和后退 -->
|
||||
<button class="btn btn-primary" @click="back">后退</button>
|
||||
<button class="btn btn-dark" @click="forward">前进</button>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<!-- include 包含的是组件名:让不展示的路由保持挂载,不被销毁 -->
|
||||
<!-- <keep-alive include="Home"> -->
|
||||
<keep-alive :include="['Home', 'About']">
|
||||
<router-view />
|
||||
</keep-alive>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "Demo-11",
|
||||
data() {
|
||||
return {};
|
||||
},
|
||||
methods: {
|
||||
/* 后退 */
|
||||
back() {
|
||||
this.$router.back();
|
||||
},
|
||||
|
||||
/* 前进 */
|
||||
forward() {
|
||||
this.$router.forward();
|
||||
},
|
||||
|
||||
/* 跳转指定步数 */
|
||||
go() {
|
||||
this.$router.go(-2);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -0,0 +1,199 @@
|
|||
# Vuex 状态映射与模块化最佳实践
|
||||
|
||||
## 一、状态访问方式对比
|
||||
|
||||
### 1. 直接访问方式
|
||||
```javascript
|
||||
// 原始访问方式(不推荐)
|
||||
this.$store.state.schoolInfo.schoolName
|
||||
this.$store.getters['schoolInfo/getSchoolName']
|
||||
```
|
||||
|
||||
### 2. 映射辅助函数
|
||||
```javascript
|
||||
// 推荐使用mapState/mapGetters
|
||||
import { mapState, mapGetters } from 'vuex'
|
||||
|
||||
computed: {
|
||||
// 数组写法(同名映射)
|
||||
...mapState('schoolInfo', ['schoolName']),
|
||||
...mapGetters('schoolInfo', ['getSchoolName']),
|
||||
|
||||
// 对象写法(重命名)
|
||||
...mapState('schoolInfo', {
|
||||
mySchoolName: 'schoolName'
|
||||
}),
|
||||
...mapGetters('schoolInfo', {
|
||||
formattedName: 'getSchoolName'
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## 二、模块化配置详解
|
||||
|
||||
### 1. 模块定义规范
|
||||
```javascript
|
||||
const schoolInfo = {
|
||||
namespaced: true, // 必须开启命名空间
|
||||
state: () => ({ // 使用函数返回状态对象
|
||||
schoolName: "BunnySchool",
|
||||
schoolAddress: "昆山市印象欧洲"
|
||||
}),
|
||||
getters: {
|
||||
getSchoolName(state) {
|
||||
// 可组合其他getters
|
||||
return `${state.schoolName}---${state.schoolAddress}`
|
||||
}
|
||||
},
|
||||
mutations: {
|
||||
// 使用常量类型
|
||||
UPDATE_NAME(state, payload) {
|
||||
state.schoolName = payload
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
async fetchSchoolInfo({ commit }) {
|
||||
const res = await api.getSchoolInfo()
|
||||
commit('UPDATE_NAME', res.data.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 模块注册方式
|
||||
```javascript
|
||||
// store/index.js
|
||||
import schoolInfo from './modules/schoolInfo'
|
||||
|
||||
export default new Vuex.Store({
|
||||
modules: {
|
||||
schoolInfo // 键名将作为命名空间前缀
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## 三、最佳实践指南
|
||||
|
||||
### 1. 命名规范建议
|
||||
| 类型 | 命名规则 | 示例 |
|
||||
| -------- | ----------- | ----------------- |
|
||||
| 模块文件 | kebab-case | `school-info.js` |
|
||||
| state | camelCase | `studentCount` |
|
||||
| mutation | 大写+下划线 | `UPDATE_INFO` |
|
||||
| getter | 动词短语 | `getFilteredList` |
|
||||
| action | 业务动作 | `fetchSchoolData` |
|
||||
|
||||
### 2. 组件中使用建议
|
||||
```javascript
|
||||
export default {
|
||||
computed: {
|
||||
// 优先使用映射辅助函数
|
||||
...mapState('schoolInfo', ['schoolName']),
|
||||
|
||||
// 复杂计算使用本地计算属性
|
||||
formattedAddress() {
|
||||
return this.$store.state.schoolInfo.schoolAddress.replace('市', '')
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapMutations('schoolInfo', ['UPDATE_NAME']),
|
||||
|
||||
// 组合多个action
|
||||
async refreshData() {
|
||||
await this.$store.dispatch('schoolInfo/fetchSchoolInfo')
|
||||
this.loadComplete()
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 代码组织技巧
|
||||
```
|
||||
store/
|
||||
├── index.js # 主入口
|
||||
├── modules/
|
||||
│ ├── school-info.js # 学校模块
|
||||
│ └── user.js # 用户模块
|
||||
└── types.js # mutation类型常量
|
||||
```
|
||||
|
||||
## 四、高级应用场景
|
||||
|
||||
### 1. 动态模块注册
|
||||
```javascript
|
||||
// 按需加载模块
|
||||
export default {
|
||||
created() {
|
||||
import('./store/modules/school-info').then(module => {
|
||||
this.$store.registerModule('schoolInfo', module.default)
|
||||
})
|
||||
},
|
||||
destroyed() {
|
||||
this.$store.unregisterModule('schoolInfo')
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 模块复用
|
||||
```javascript
|
||||
// 创建可复用模块工厂
|
||||
function createSchoolModule(initialName) {
|
||||
return {
|
||||
namespaced: true,
|
||||
state: () => ({
|
||||
schoolName: initialName
|
||||
}),
|
||||
// ...其他配置
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 插件开发
|
||||
```javascript
|
||||
// 状态持久化插件
|
||||
const persistPlugin = store => {
|
||||
// 初始化时读取
|
||||
const savedState = localStorage.getItem('vuex_state')
|
||||
if (savedState) {
|
||||
store.replaceState(JSON.parse(savedState))
|
||||
}
|
||||
|
||||
// 订阅mutation变化
|
||||
store.subscribe((mutation, state) => {
|
||||
localStorage.setItem('vuex_state', JSON.stringify(state))
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## 五、常见问题解决方案
|
||||
|
||||
1. **模块热更新**:
|
||||
```javascript
|
||||
if (module.hot) {
|
||||
module.hot.accept(['./modules/school-info'], () => {
|
||||
store.hotUpdate({
|
||||
modules: {
|
||||
schoolInfo: require('./modules/school-info').default
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
2. **循环依赖处理**:
|
||||
- 使用全局getter解决跨模块访问
|
||||
- 通过rootState参数访问根状态
|
||||
|
||||
3. **TypeScript支持**:
|
||||
```typescript
|
||||
// 定义模块类型
|
||||
interface SchoolState {
|
||||
schoolName: string
|
||||
schoolAddress: string
|
||||
}
|
||||
|
||||
const schoolInfo: Module<SchoolState, RootState> = {
|
||||
namespaced: true,
|
||||
state: () => ({ /* 初始状态 */ })
|
||||
}
|
||||
```
|
|
@ -0,0 +1,30 @@
|
|||
<template>
|
||||
<div>
|
||||
<h3>{{ $store.state.schoolInfo.schoolName }}</h3>
|
||||
<h3>{{ schoolName }}</h3>
|
||||
<h3>{{ $store.getters["schoolInfo/getSchoolName"] }}</h3>
|
||||
<h3>{{ getSchoolName }}</h3>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState, mapGetters } from "vuex";
|
||||
export default {
|
||||
name: "Demo-5",
|
||||
data() {
|
||||
return {};
|
||||
},
|
||||
computed: {
|
||||
// mapState和mapGetters对象写法
|
||||
// ...mapState("schoolInfo", { schoolName: "schoolName" }),
|
||||
|
||||
// mapState和mapGetters数组写法
|
||||
...mapState("schoolInfo", ["schoolName"]),
|
||||
...mapGetters("schoolInfo", ["getSchoolName"]),
|
||||
},
|
||||
mounted() {
|
||||
const data = mapState("schoolInfo", { schoolName: "schoolName" });
|
||||
console.log(data);
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -0,0 +1,17 @@
|
|||
<template>
|
||||
<div>
|
||||
<h2>About内容</h2>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "AboutPage",
|
||||
beforeDestroy() {
|
||||
console.log("AboutPage销毁");
|
||||
},
|
||||
mounted() {
|
||||
console.log("AboutPage挂载");
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -0,0 +1,17 @@
|
|||
<template>
|
||||
<div>
|
||||
<h3>Home内容</h3>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "HomePage",
|
||||
beforeDestroy() {
|
||||
console.log("HomePage销毁");
|
||||
},
|
||||
mounted() {
|
||||
console.log("HomePage挂载");
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -0,0 +1,39 @@
|
|||
<template>
|
||||
<div class="container mt-3">
|
||||
<div class="row">
|
||||
<div class="col-sm-2 col-xs-offset-2 text-center">
|
||||
<!-- 路由跳转内容 -->
|
||||
<div class="list-group">
|
||||
<router-link
|
||||
class="list-group-item"
|
||||
active-class="active"
|
||||
to="/about"
|
||||
>
|
||||
about
|
||||
</router-link>
|
||||
<router-link class="list-group-item" active-class="active" to="/home"
|
||||
>home</router-link
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 内容显示区域 -->
|
||||
<div class="col-sm-6">
|
||||
<div class="panel">
|
||||
<div class="panel-body">
|
||||
<router-view />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "Demo-6",
|
||||
data() {
|
||||
return {};
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -0,0 +1,44 @@
|
|||
<template>
|
||||
<div
|
||||
class="card-footer d-flex justify-content-between align-items-center"
|
||||
v-if="count"
|
||||
>
|
||||
<div @click="onSelectAll(selectAll)">
|
||||
<input type="checkbox" class="me-2" :checked="selectAll" />
|
||||
<span>已完成 {{ accomplish }} / 全部 {{ count }}</span>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-danger" @click="onDeletedCommpleted">
|
||||
清除已完成
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "TodoFooter",
|
||||
props: ["list", "onSelectAll", "deletedTodo"],
|
||||
computed: {
|
||||
/* 完成的数量 */
|
||||
accomplish() {
|
||||
return this.list.filter((todo) => todo.completed).length;
|
||||
},
|
||||
|
||||
/* 总数量 */
|
||||
count() {
|
||||
return this.list.length;
|
||||
},
|
||||
|
||||
/* 是否已经全选 */
|
||||
selectAll() {
|
||||
const flag = this.list.filter((todo) => todo.completed === true);
|
||||
return flag.length === this.list.length;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onDeletedCommpleted() {
|
||||
this.$emit("deleted-commpleted");
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -0,0 +1,43 @@
|
|||
<template>
|
||||
<div class="card-header my-3">
|
||||
<input
|
||||
type="text"
|
||||
v-model.trim="content"
|
||||
class="form-control"
|
||||
placeholder="用户的输入..."
|
||||
@keyup.enter="onAdd"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { nanoid } from "nanoid";
|
||||
|
||||
export default {
|
||||
name: "TodoHeader",
|
||||
data() {
|
||||
return {
|
||||
content: "",
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
onAdd(ev) {
|
||||
if (!this.content) {
|
||||
alert("没有内容!!!");
|
||||
return;
|
||||
}
|
||||
|
||||
const todoObj = {
|
||||
id: nanoid(),
|
||||
content: ev.target.value.trim(),
|
||||
completed: false,
|
||||
};
|
||||
|
||||
this.$emit("onAdd", todoObj);
|
||||
|
||||
// 清空输入框
|
||||
this.content = "";
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -0,0 +1,47 @@
|
|||
<template>
|
||||
<li
|
||||
class="list-group-item d-flex justify-content-between align-items-center list-group-item-action"
|
||||
>
|
||||
<div class="py-2" @click="onChange">
|
||||
<input type="checkbox" class="me-2" :checked="todo.completed" />
|
||||
<span> {{ todo.content }}</span>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-danger deleted" @click="onDeleted">删除</button>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "TodoItem",
|
||||
props: ["todo"],
|
||||
methods: {
|
||||
/* 修改todo的状态 */
|
||||
onChange() {
|
||||
const id = this.todo.id;
|
||||
|
||||
this.$root.$emit("change-todo-status", id);
|
||||
},
|
||||
|
||||
/* 删除todo */
|
||||
onDeleted() {
|
||||
const id = this.todo.id;
|
||||
|
||||
const result = confirm(`确认删除:${id}`);
|
||||
if (result) {
|
||||
this.$root.$emit("delete-todo", id);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.deleted {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
li:hover .deleted {
|
||||
visibility: visible !important;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,15 @@
|
|||
<template>
|
||||
<ul class="list-group card-body">
|
||||
<todo-item v-for="todo in list" :key="todo.id" :todo="todo" />
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import TodoItem from "./todo-item";
|
||||
|
||||
export default {
|
||||
name: "TodoList",
|
||||
components: { TodoItem },
|
||||
props: ["list"],
|
||||
};
|
||||
</script>
|
|
@ -0,0 +1,89 @@
|
|||
<template>
|
||||
<div class="container card">
|
||||
<todo-header @onAdd="onAdd" />
|
||||
<todo-list :list="todolist" />
|
||||
<todo-footer
|
||||
:list="todolist"
|
||||
:on-select-all="onSelectAll"
|
||||
@deleted-commpleted="onDeletedCommpleted"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import TodoFooter from "./components/todo-footer";
|
||||
import TodoHeader from "./components/todo-header";
|
||||
import TodoList from "./components/todo-list";
|
||||
|
||||
export default {
|
||||
name: "TODOList",
|
||||
components: { TodoHeader, TodoList, TodoFooter },
|
||||
data() {
|
||||
return {
|
||||
todolist: [
|
||||
{ id: 1, content: "吃饭", completed: true },
|
||||
{ id: 2, content: "吃饭", completed: false },
|
||||
{ id: 3, content: "吃饭", completed: true },
|
||||
],
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
/* 添加Todo内容 */
|
||||
onAdd(todo) {
|
||||
this.todolist.unshift(todo);
|
||||
},
|
||||
|
||||
/* 修改todo状态是否完成 */
|
||||
changeTodoStatus() {
|
||||
// 先移除旧的监听器避免重复
|
||||
this.$root.$off("change-todo-status");
|
||||
this.$root.$on("change-todo-status", (id) => {
|
||||
this.todolist.forEach((todo) => {
|
||||
if (todo.id === id) {
|
||||
todo.completed = !todo.completed;
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/* 删除todo */
|
||||
deletedTodo() {
|
||||
this.$root.$off("delete-todo");
|
||||
this.$root.$on("delete-todo", (id) => {
|
||||
const list = this.todolist.filter((todo) => todo.id !== id);
|
||||
this.todolist = list;
|
||||
});
|
||||
},
|
||||
|
||||
/* 删除已完成的todo */
|
||||
onDeletedCommpleted() {
|
||||
const ids = this.todolist
|
||||
.filter((todo) => todo.completed === true)
|
||||
.map((todo) => todo.id);
|
||||
|
||||
const completedList = this.todolist.filter(
|
||||
(todo) => todo.completed === false
|
||||
);
|
||||
|
||||
// 删除时的提示
|
||||
const result = confirm(`是否确认删除:${ids}`);
|
||||
if (result) {
|
||||
this.todolist = completedList;
|
||||
alert(`清除已完成id:${ids}`);
|
||||
}
|
||||
},
|
||||
|
||||
/* 选择全部 */
|
||||
onSelectAll(selectAll) {
|
||||
this.todolist = this.todolist.map((todo) => ({
|
||||
...todo,
|
||||
completed: !selectAll,
|
||||
}));
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.changeTodoStatus();
|
||||
this.deletedTodo();
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -0,0 +1,174 @@
|
|||
# Vue2 组件通信与事件处理最佳实践
|
||||
|
||||
## 一、事件系统核心机制
|
||||
|
||||
### 1. 事件修饰符对比
|
||||
| 修饰符 | 说明 | 示例 |
|
||||
| ---------- | ------------ | --------------------------- |
|
||||
| `.once` | 只触发一次 | `@click.once="handler"` |
|
||||
| `.native` | 监听原生事件 | `@click.native="handler"` |
|
||||
| `.stop` | 阻止事件冒泡 | `@click.stop="handler"` |
|
||||
| `.prevent` | 阻止默认行为 | `@submit.prevent="handler"` |
|
||||
|
||||
### 2. 事件绑定与解绑
|
||||
```javascript
|
||||
// 子组件 StudentInfo.vue
|
||||
methods: {
|
||||
emitEvent() {
|
||||
this.$emit('custom-event', payload)
|
||||
},
|
||||
unbind() {
|
||||
this.$off('custom-event') // 解绑特定事件
|
||||
this.$off() // 解绑所有事件
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 二、组件通信模式详解
|
||||
|
||||
### 1. Props 向下传递
|
||||
```javascript
|
||||
// 父组件
|
||||
<Child :title="pageTitle" />
|
||||
|
||||
// 子组件
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
default: '默认标题'
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Events 向上通信
|
||||
```javascript
|
||||
// 子组件
|
||||
this.$emit('update', newValue)
|
||||
|
||||
// 父组件
|
||||
<Child @update="handleUpdate" />
|
||||
```
|
||||
|
||||
### 3. 高级通信方式
|
||||
| 方式 | 适用场景 | 示例 |
|
||||
| -------------- | ---------- | ------------------------------------- |
|
||||
| ref | 父调子方法 | `this.$refs.child.method()` |
|
||||
| provide/inject | 跨级组件 | `provide() { return { key: value } }` |
|
||||
| Event Bus | 任意组件 | `Vue.prototype.$bus = new Vue()` |
|
||||
| Vuex | 全局状态 | `this.$store.commit('mutation')` |
|
||||
|
||||
## 三、最佳实践指南
|
||||
|
||||
### 1. 事件命名规范
|
||||
- 使用 kebab-case 命名(如 `user-updated`)
|
||||
- 避免与原生事件重名(如 `click`)
|
||||
- 语义化命名(如 `form-submitted`)
|
||||
|
||||
### 2. 性能优化建议
|
||||
```javascript
|
||||
// 1. 适时解绑事件
|
||||
beforeDestroy() {
|
||||
this.$off('custom-event')
|
||||
}
|
||||
|
||||
// 2. 防抖处理高频事件
|
||||
methods: {
|
||||
handleInput: _.debounce(function() {
|
||||
// 处理逻辑
|
||||
}, 500)
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 安全通信模式
|
||||
```javascript
|
||||
// 添加事件存在性检查
|
||||
if (this._events['custom-event']) {
|
||||
this.$emit('custom-event', data)
|
||||
}
|
||||
```
|
||||
|
||||
## 四、高级应用场景
|
||||
|
||||
### 1. 动态事件处理
|
||||
```javascript
|
||||
// 根据条件绑定不同处理函数
|
||||
<template>
|
||||
<Child @event="handlerMap[handlerType]" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
handlerType: 'typeA',
|
||||
handlerMap: {
|
||||
typeA: this.handleTypeA,
|
||||
typeB: this.handleTypeB
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
### 2. 自定义事件总线
|
||||
```javascript
|
||||
// event-bus.js
|
||||
import Vue from 'vue'
|
||||
export const EventBus = new Vue()
|
||||
|
||||
// 组件A
|
||||
EventBus.$emit('message', 'Hello')
|
||||
|
||||
// 组件B
|
||||
EventBus.$on('message', (msg) => {
|
||||
console.log(msg)
|
||||
})
|
||||
```
|
||||
|
||||
### 3. 异步事件处理
|
||||
```javascript
|
||||
// 返回Promise的事件处理
|
||||
methods: {
|
||||
async submitForm() {
|
||||
try {
|
||||
await this.$emitAsync('form-submit')
|
||||
this.showSuccess()
|
||||
} catch (error) {
|
||||
this.showError(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 五、常见问题解决方案
|
||||
|
||||
1. **事件未触发排查**:
|
||||
- 检查事件名称大小写是否一致
|
||||
- 确认子组件是否正确调用 `$emit`
|
||||
- 验证父组件监听的事件名是否匹配
|
||||
|
||||
2. **内存泄漏预防**:
|
||||
```javascript
|
||||
// 清除所有监听器
|
||||
beforeDestroy() {
|
||||
this.$off()
|
||||
EventBus.$off('event', this.handler)
|
||||
}
|
||||
```
|
||||
|
||||
3. **跨组件通信优化**:
|
||||
```javascript
|
||||
// 使用Vue.observable实现轻量状态管理
|
||||
const state = Vue.observable({ count: 0 })
|
||||
```
|
||||
|
||||
## 六、与Vue3的对比
|
||||
|
||||
| 特性 | Vue2 | Vue3 |
|
||||
| -------- | ----------- | ---------------- |
|
||||
| 事件系统 | `$emit/$on` | `emits` 选项声明 |
|
||||
| 移除API | - | `$off`, `$once` |
|
||||
| 新特性 | - | `v-model` 参数 |
|
||||
| 性能 | 基于观察者 | 基于Proxy |
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
<template>
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="card-header-tabs">
|
||||
<h3>{{ title }}</h3>
|
||||
</div>
|
||||
|
||||
<div class="card-header-pills">
|
||||
<h3>名称:{{ userinfo.sutdentName }}</h3>
|
||||
<h3>地址:{{ userinfo.address }}</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body btn-group">
|
||||
<button class="btn btn-primary" @click="getUserinfo">获取用户信息</button>
|
||||
<button class="btn btn-danger" @click="unbind">解除绑定</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "StudentInfo",
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
default: "标题显示",
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
userinfo: {
|
||||
sutdentName: "Bunny",
|
||||
address: "昆山印象花园",
|
||||
},
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
/* 触发用户事件 */
|
||||
getUserinfo() {
|
||||
this.$emit("getUserinfo", this.userinfo);
|
||||
},
|
||||
|
||||
/* 接触绑定 */
|
||||
unbind() {
|
||||
console.info("解除绑定");
|
||||
|
||||
// 解绑单个
|
||||
this.$off("getUserinfo");
|
||||
|
||||
// 解绑多个
|
||||
// this.$off(["getUserinfo"]);
|
||||
|
||||
// 解绑所有
|
||||
// this.$off();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -0,0 +1,25 @@
|
|||
<template>
|
||||
<div class="container">
|
||||
<!-- 可以只触发一次 -->
|
||||
<Student @getUserinfo.once="getUserinfo" title="可以只触发一次" />
|
||||
<Student @getUserinfo="getUserinfo" title="普通触发" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Student from "./components/Sutdent";
|
||||
export default {
|
||||
name: "Demo-8",
|
||||
components: { Student },
|
||||
data() {
|
||||
return {};
|
||||
},
|
||||
methods: {
|
||||
/* 获取用户信息 */
|
||||
getUserinfo(userinfo) {
|
||||
const data = { ...userinfo };
|
||||
console.log(data);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -0,0 +1,186 @@
|
|||
# Vue2 组件通信与事件处理深度指南
|
||||
|
||||
## 一、组件通信核心方式
|
||||
|
||||
### 1. 父子组件通信模式
|
||||
|
||||
#### Props 向下传递
|
||||
```javascript
|
||||
// 父组件
|
||||
<Child :title="pageTitle" />
|
||||
|
||||
// 子组件
|
||||
props: {
|
||||
title: String
|
||||
}
|
||||
```
|
||||
|
||||
#### Events 向上传递
|
||||
```javascript
|
||||
// 子组件
|
||||
this.$emit('update', newValue)
|
||||
|
||||
// 父组件
|
||||
<Child @update="handleUpdate" />
|
||||
```
|
||||
|
||||
### 2. 引用直接访问
|
||||
```javascript
|
||||
// 父组件
|
||||
<Child ref="childRef" />
|
||||
|
||||
methods: {
|
||||
callChildMethod() {
|
||||
this.$refs.childRef.childMethod()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 二、事件系统高级用法
|
||||
|
||||
### 1. 原生事件绑定
|
||||
```html
|
||||
<!-- 必须添加.native修饰符 -->
|
||||
<StudentInfo @click.native="handleClick" />
|
||||
```
|
||||
|
||||
### 2. 手动事件管理
|
||||
```javascript
|
||||
// 绑定事件(推荐在mounted钩子中)
|
||||
mounted() {
|
||||
this.$refs.studenInfo.$on("getStudentName", this.getStudentName)
|
||||
}
|
||||
|
||||
// 解绑事件(必须在beforeDestroy中清理)
|
||||
beforeDestroy() {
|
||||
this.$refs.studenInfo.$off("getStudentName")
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 事件参数传递
|
||||
```javascript
|
||||
// 子组件触发时可带多个参数
|
||||
this.$emit('event-name', arg1, arg2, ...)
|
||||
|
||||
// 父组件接收所有参数
|
||||
handler(arg1, arg2, ...rest) {
|
||||
// 处理参数
|
||||
}
|
||||
```
|
||||
|
||||
## 三、最佳实践建议
|
||||
|
||||
### 1. 事件命名规范
|
||||
- 使用 kebab-case 命名(如 `student-updated`)
|
||||
- 避免与原生事件重名(添加业务前缀)
|
||||
- 语义化命名(如 `form-submitted`)
|
||||
|
||||
### 2. 安全通信模式
|
||||
```javascript
|
||||
// 添加事件存在性检查
|
||||
if (this._events['custom-event']) {
|
||||
this.$emit('custom-event', data)
|
||||
}
|
||||
|
||||
// 使用try-catch包裹可能出错的事件处理
|
||||
try {
|
||||
this.$emit('critical-action', payload)
|
||||
} catch (error) {
|
||||
console.error('事件处理失败:', error)
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 性能优化方案
|
||||
```javascript
|
||||
// 防抖处理高频事件
|
||||
methods: {
|
||||
handleInput: _.debounce(function() {
|
||||
this.$emit('input-changed', this.value)
|
||||
}, 300)
|
||||
}
|
||||
```
|
||||
|
||||
## 四、高级通信场景
|
||||
|
||||
### 1. 跨级组件通信
|
||||
```javascript
|
||||
// 祖先组件
|
||||
provide() {
|
||||
return {
|
||||
appData: this.sharedData
|
||||
}
|
||||
}
|
||||
|
||||
// 后代组件
|
||||
inject: ['appData']
|
||||
```
|
||||
|
||||
### 2. 动态事件处理器
|
||||
```javascript
|
||||
// 根据状态切换处理函数
|
||||
<Child @custom-event="handlerMap[currentHandler]" />
|
||||
|
||||
data() {
|
||||
return {
|
||||
currentHandler: 'default',
|
||||
handlerMap: {
|
||||
default: this.handleDefault,
|
||||
special: this.handleSpecial
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 事件总线模式
|
||||
```javascript
|
||||
// event-bus.js
|
||||
import Vue from 'vue'
|
||||
export default new Vue()
|
||||
|
||||
// 组件A
|
||||
EventBus.$emit('data-updated', payload)
|
||||
|
||||
// 组件B
|
||||
EventBus.$on('data-updated', handler)
|
||||
```
|
||||
|
||||
## 五、常见问题解决方案
|
||||
|
||||
1. **事件未触发排查**:
|
||||
- 检查组件引用是否正确(`ref` 命名)
|
||||
- 确认事件名称完全匹配(大小写敏感)
|
||||
- 验证事件绑定时机(确保在 mounted 之后)
|
||||
|
||||
2. **内存泄漏预防**:
|
||||
```javascript
|
||||
beforeDestroy() {
|
||||
// 清除所有自定义事件监听
|
||||
this.$off()
|
||||
// 清除事件总线监听
|
||||
EventBus.$off('data-updated', this.handler)
|
||||
}
|
||||
```
|
||||
|
||||
3. **异步事件处理**:
|
||||
```javascript
|
||||
// 返回Promise的事件处理
|
||||
async handleEvent() {
|
||||
try {
|
||||
await this.$nextTick()
|
||||
const result = await this.$emitAsync('async-event')
|
||||
// 处理结果
|
||||
} catch (error) {
|
||||
// 错误处理
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 六、与Vue3的对比
|
||||
|
||||
| 特性 | Vue2 | Vue3 |
|
||||
| -------- | -------------- | -------------------- |
|
||||
| 事件定义 | 隐式声明 | `emits` 选项显式声明 |
|
||||
| 原生事件 | 需要 `.native` | 自动识别 |
|
||||
| 移除API | - | 移除 `$on`, `$off` |
|
||||
| 性能 | 基于观察者 | 基于Proxy的响应式 |
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
<template>
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>学生姓名:{{ studenName }}</h3>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-primary" @click="onEmitStudentName">
|
||||
向父组件传递学生姓名
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "StudentInfo",
|
||||
data() {
|
||||
return {
|
||||
studenName: "Bunny",
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
onEmitStudentName() {
|
||||
this.$emit("getStudentName", this.studenName);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -0,0 +1,47 @@
|
|||
<template>
|
||||
<div class="container">
|
||||
<h4>
|
||||
学生姓名:
|
||||
<span class="badge text-bg-secondary"> {{ studenName }}</span>
|
||||
</h4>
|
||||
|
||||
<!-- 需要在前面加上native,否则会当成自定义事件 -->
|
||||
<StudentInfo ref="studenInfo" @click.native="showMessage" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import StudentInfo from "./components/SutdentInfo.vue";
|
||||
|
||||
export default {
|
||||
name: "Demo-9",
|
||||
components: { StudentInfo },
|
||||
data() {
|
||||
return {
|
||||
studenName: "",
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
/* 获取学生名 */
|
||||
getStudentName(name, ...params) {
|
||||
console.log(name, params);
|
||||
|
||||
this.studenName = name;
|
||||
},
|
||||
|
||||
/* 点击子组件显示消息 */
|
||||
showMessage() {
|
||||
alert("点击子组件触发点击事件。。。");
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.$refs.studenInfo.$on("getStudentName", this.getStudentName);
|
||||
|
||||
// 绑定事件,只能写箭头函数,否则当前this是Vue被绑定的组件
|
||||
// this.$refs.studenInfo.$on("getStudentName", (name, ...params) => {
|
||||
// console.log(name, params);
|
||||
// this.studenName = name;
|
||||
// });
|
||||
},
|
||||
};
|
||||
</script>
|
Loading…
Reference in New Issue