1 vue
1.1 v-if
00.总结
a.v-once
定义它的元素或组件只染一次包括元素或组件的所有子节点首次渲染后不再随数据的变化重新渲染将被视为静态内容
b.v-cloak
这个指令保持在元素上直到关联实例结束编译,解决初始化慢导致页面闪动的最佳实践
c.v-bind
绑定属性,动态更新HTML元素上的属性,例如v-bind:class
d.v-on
用于监听DoM事件例如v-on:click v-on:keyup
e.v-html
赋值就是变量innerHTML,注意防止xss攻击
f.v-text
更新元素的textContent
g.v-model
在普通标签:变成value和input的语法糖,并且会处理拼音输入法的问题
在组件上面:处理为value和input语法糖
h.v-if/v-else/v-else-if
可以配合template来使用
在render函数里面就是三元表达式
i.v-show
使用指令来实现,最终会通过display来进行显隐
j.v-for
循环指令编译出来的结果是_l代表渲染列表
优先级比v-if高最好不要一起使用,尽量使用计算属性去解决
注意增加唯一key值,不要使用index作为key
k.v-pre
跳过这个元素以及子元素的编译过程,以此来加快整个项目的编译速度
01.v-if、v-show
a.编译
v-if 在编译过程中会被转化成三元表达式,条件不满足时不渲染此节点
v-show 会被编译成指令,条件不满足时控制样式将对应节点隐藏 (display:none)
b.场景
v-if 适用于在运行时很少改变条件,不需要频繁切换条件的场景
v-show 适用于需要非常频繁切换条件的场景
02.v-if与v-for为什么不建议一起使用
因为解析时先解析 v-for 再解析 v-if,如果遇到需要同时使用时可以考虑写成计算属性的方式
03.v-for为什么要加key
通过提供key,Vue.js可以在数组变化时更准确地识别、定位和更新元素
这样就可以正确地处理新元素的添加、元素的重新排序和元素的删除
04.v-for为什么不建议用 index 作为循环项的 key 呢?
如果我们以循环的索引作为key,这些节点的key 值也会随之发生变化,导致它们被识别为新创建的节点
而不是仅更新相应的内容。 这将导致整个列表的重新渲染,即使实际上只有一个节点发生了变化
因此,为了避免以上问题,我们不建议使用循环的索引作为节点的key
1.2 mvvm
01.回答
View的变化能实时让Model发生变化,而Model的变化也能实时更新View
02.说明
Vue数据双向绑定原理是通过 数据劫持结合发布者-订阅者模式 的方式来实现的
首先是通过 ES5 提供的 Object.defineProperty() 方法来劫持(监听)各属性的 getter、setter
并在当监听的属性发生变动时通知订阅者,是否需要更新,若更新就会执行对应的更新函数
03.工作原理
a.ViewModel层
做了两件事达到了数据的双向绑定 一是将【模型】转化成【视图】,即将后端传递的数据转化成所看到的页面
实现方式:数据绑定。二是将【视图】转化成【模型】,即将所看到的页面转化成后端的数据
实现方式:DOM 事件监听
b.MVVM与MVC最大的区别
它实现了 View 和 Model 的自动同步
也就是当 Model 的属性改变时,我们不用再自己手动操作 Dom 元素,来改变 View 的显示
而是改变属性后该属性对应 View 层显示会自动改变(对应Vue数据驱动的思想)
c.MVVM比MVC精简很多
不仅简化了业务与界面的依赖,还解决了数据频繁更新的问题,不用再用选择器操作 DOM 元素
因为在 MVVM 中,View 不知道 Model 的存在,Model 和 ViewModel 也观察不到 View
这种低耦合模式提高代码的可重用性
1.3 data函数
01.回答
组件中的data写成一个函数,数据以函数返回值形式定义,这样每复用一次组件,就会返回一份新的data
02.说明
类似于给每个组件实例创建一个私有的数据空间,让各个组件实例维护各自的数据。
而单纯的写成对象形式,就使得所有组件实例共用了一份 data,就会造成一个变了全都会变的结果
1.4 scoped原理
01.定义
Vue中的scoped是一种基于属性选择器的样式隔离方案,它并不是传统意义上的CSS模块化方案
它通过为组件生成唯一的属性选择器来实现样式的隔离,提高了样式的可维护性和组件的独立性
02.工作原理
a.说明
当在 <style> 标签上使用 scoped 属性时,Vue 会为当前组件的每个元素添加一个唯一的 data-v-xxxxxx 属性
并将样式规则中的选择器修改为包含该属性的形式
b.编译阶段
在编译 .vue 文件时,Vue 的编译器会处理 <style> 标签,具体步骤如下:
解析样式:使用 postcss 解析样式,生成 AST(抽象语法树)
添加属性选择器:遍历 AST,为每个选择器添加 [data-v-xxxxxx] 属性选择器
生成唯一属性:xxxxxx 是一个基于组件文件路径和内容的哈希值,确保唯一性
c.运行时
在运行时,Vue 会为组件的根元素和所有子元素添加 data-v-xxxxxx 属性
03.优点、缺点
a.优点
a.样式隔离
scoped可以有效地防止组件间的样式冲突,确保每个组件的样式都是独立的
b.提高可维护性
由于样式被限制在组件内部,因此当需要修改或调试样式时,可以更容易地定位到相关的组件和样式
b.缺点
a.性能影响
虽然scoped样式带来了很多好处,但由于需要为每个组件生成唯一的属性选择器和修改样式选择器
因此可能会对性能产生一定的影响,然而,在大多数情况下,这种影响是可以接受的
b.无法使用全局样式库
如果需要使用全局样式库(如Bootstrap),则需要在全局样式文件中引入,而不能在scoped样式中使用
这是因为scoped样式被限制在组件内部,无法应用到全局元素上
c.深度选择器
在某些情况下,你可能需要为scoped样式添加一个全局样式或修改子组件的样式
这时可以使用深度选择器(如Vue 3中的::v-deep或Vue 2中的/deep/、>>>)来“穿透”scoped限制
但需要注意的是,过度使用深度选择器可能会破坏样式的隔离性
04.示例
a.假设有以下组件
<template>
<div class="example">Hello World</div>
</template>
<style scoped>
.example {
color: red;
}
</style>
b.编译后生成的样式和模板如下
<div class="example" data-v-f3f3eg9>Hello World</div>
<style>
.example[data-v-f3f3eg9] {
color: red;
}
</style>
1.5 computed和watch
01.区别
computed:计算属性,依赖其他属性计算值,computed的值有缓存,只有当计算值变化才会返回内容,可以设置getter和setter
watch:监听到值的变化就会执行回调,在回调中可以进行一些逻辑操作
02.场景
computed:一般用在模板渲染中,某个值是依赖了其它的响应式对象甚至是计算属性计算而来
watch:适用于观测某个值的变化去完成一段复杂的业务逻辑
2 vue
2.1 生命周期
01.创建阶段
beforeCreate,created
02.挂载阶段
beforeMount,mounted
03.运行阶段
beforeUpdate,updated
04.销毁阶段
beforeDestroy,destroyed
2.2 状态管理
01.定义
vuex 是专门为 vue 提供的全局状态管理系统,用于多个组件中数据共享、数据缓存等
无法持久化、内部核心原理是通过创造一个全局实例 new Vue
02.状态
a.State
定义了应用状态的数据结构,可以在这里设置默认的初始状态
b.Getter
允许组件从 Store 中获取数据,mapGetters 辅助函数仅仅是将 store 中的 getter 映射到局部计算属性
c.Mutation
是唯一更改 store 中状态的方法,且必须是同步函数
d.Action
用于提交 mutation,而不是直接变更状态,可以包含任意异步操作
e.Module
允许将单一的 Store 拆分为多个 store 且同时保存在单一的状态树中
02.Vuex页面刷新数据丢失怎么解决
a.本地存储的方案来保存数据
自己设计存储方案 / 第三方插件
b.使用vuex-persist插件
为Vuex持久化存储而生的一个插件
不需要你手动存取storage,而是直接将状态保存至cookie或者localStorage中
03.Vuex为什么要分模块并且加命名空间
a.模块
由于使用单一状态树,应用的所有状态会集中到一个比较大的对象
当应用变得非常复杂时,store 对象就有可能变得相当臃肿
为了解决以上问题,Vuex 允许我们将 store 分割成模块(module)
每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块
b.命名空间
默认情况下,模块内部的 action、mutation 和 getter 是注册在全局命名空间
这样使得多个模块能够对同一 mutation 或 action 作出响应
如果希望你的模块具有更高的封装度和复用性,你可以通过添加 namespaced: true 的方式使其成为带命名空间的模块
当模块被注册后,它的所有 getter、action 及 mutation 都会自动根据模块注册的路径调整命名
2.3 父子组件:通讯4种
00.汇总
a.父子通信
defineProps
b.子父通信
发布订阅方式
defineExpose+ref
c.子父组件的双向数据绑定
v-model
d.子孙通信
provide/inject
01.父组件主动向子组件传递数据
a.defineProps
a.说明
父组件使用v-bind绑定需要传递的数据,子组件从vue中使用defineProps,实例化对象进行接受
b.父组件
<template>
<Child :msg="parentMsg" />
</template>
<script setup>
import Child from './child.vue';
import { ref } from 'vue';
const parentMsg = ref('im parent')
</script>
<style lang="css" scoped></style>
c.子组件
<template>
<div>
{{ props.msg }}
</div>
</template>
<script setup>
const props = defineProps({
msg: {
type: String
}
})
</script>
<style lang="scss" scoped></style>
02.子父通信
a.子组件主动向父组件传递数据
a.发布订阅方式
a.说明
采用了发布订阅的方式实现。
首先子组件定义好一个事件。
const emit = defineEmits(['addChild'])
定义好后,将该事件发布出去,我在这采用了一个点击事件触发发布时间的行为。
const add = () => {
emit('addChild', childMsg.value)
}
最后在父组件对该事件进行订阅,订阅的事件名一定要和子组件发布的事件名一致,里面会传递一个参数,该参数就是子组件传递的数据。
<Child @addChild="handle" />
b.child
<template>
<div>
<button @click="add">发送给父组件</button>
</div>
</template>
<script setup>
import { ref } from 'vue';
const childMsg = ref('im child')
const emit = defineEmits(['addChild'])
const add = () => {
emit('addChild', childMsg.value)
}
</script>
<style lang="scss" scoped></style>
c.parent
<template>
<Child @addChild="handle" />
{{ msg }}
</template>
<script setup>
import Child from './child.vue';
import { ref } from 'vue';
const msg = ref('')
const handle = (e) => {
msg.value = e
}
</script>
<style lang="css" scoped></style>
b.defineExpose+ref
a.说明
使用了defineExpose主动对外暴露,想要暴露的数据。
defineExpose({
msg
})
父组件通过ref定义子组件的dom元素,从而从dom元素中拿到挂载在该对象上的属性。注意由于这个过程是异步的第一次访问时这个值是不存在,所以需要在访问之前判断存不存否则会执行会出错。
<Child ref="refChild" />
{{ refChild?.msg }}
b.child
<template>
<div>
</div>
</template>
<script setup>
import { ref } from 'vue';
const msg = ref('im child')
defineExpose({
msg
})
</script>
<style lang="scss" scoped></style>
c.parent
<template>
<Child ref="refChild" />
{{ refChild?.msg }}
</template>
<script setup>
import Child from './child.vue';
import { ref } from 'vue';
const refChild = ref(null)
</script>
<style lang="css" scoped></style>
03.子父组件的双向数据绑定
a.双向数据绑定,实现vue3数据不再是单向流通的
a.v-model
a.说明
双向数据绑定主要使用了v-model。
首先在父组件上选择好绑定的数据。
<Child v-model:msg="msg" />
对于子组件来说分两步走:
第一,子组件得到来自父组件传递的数据,使用defineProps。
第二,子组件使用defineEmits,发布事件。
const props = defineProps({
msg: {
type: String,
required: true
}
})
const emits = defineEmits(['update:msg'])
b.parent
<template>
{{ msg }}
<Child v-model:msg="msg" />
</template>
<script setup>
import Child from './child.vue';
import { ref } from 'vue';
const msg = ref('im parent')
</script>
<style lang="css" scoped></style>
c.child
<template>
<div>
<input type="text" :value="props.msg" @input="changeMsg">
</div>
</template>
<script setup>
const props = defineProps({
msg: {
type: String,
required: true
}
})
const emits = defineEmits(['update:msg'])
const changeMsg = (e) => {
emits('update:msg', e.target.value)
}
</script>
<style lang="scss" scoped></style>
04.子孙通信
a.主要设计目的是用于祖先组件向后代组件单向传递数据,但如果有需求让后代组件向祖先组件传递数据
a.provide/inject
a.说明
首先在父组件中使用provide抛出想要传递的数据。
provide('parentMsg', msg)
在孙组件中使用inject接受即可。
const msg = inject('parentMsg')
b.parent
<template>
<Child />
</template>
<script setup>
import Child from './child.vue';
import { provide, ref } from 'vue';
const msg = ref('im parent')
provide('parentMsg', msg)
</script>
<style lang="css" scoped></style>
c.grandson
<template>
<div>
{{ msg }}
</div>
</template>
<script setup>
import { inject } from 'vue';
const msg = inject('parentMsg')
</script>
<style lang="scss" scoped></style>
2.4 父子组件:通讯7种
00.汇总
a.图示
方法 优点 缺点 推荐场景
Props + 事件 官方推荐,数据流清晰 层级深时复杂 简单父子组件通信
Pinia 专业状态管理,响应式完美 需要学习成本 大型应用
事件总线 完全解耦,灵活性强 需手动管理事件监听 临时事件通信
Provide/Inject 跨层级通信高效 数据流向不易追溯 深层嵌套组件
共享对象 实现简单 难以维护 小型项目或原型开发
组件实例引用 直接访问组件方法 破坏组件封装性 特殊场景需急需使用
b.对比
a.props、$emit
父组件向子组件传递数据是通过 prop 传递的,子组件传递数据给父组件是通过$emit 触发事件来做到的
b.$parent、$children
获取当前组件的父组件和当前组件的子组件
c.$attrs、$listeners A->B->C
Vue 2.4 开始提供了$attrs 和$listeners 来解决这个问题
d.父组件中通过 provide 来提供变量,然后在子组件中通过 inject 来注入变量
官方不推荐在实际业务中使用,但是写组件库时很常用
e.$refs
获取组件实例
f.eventBus兄弟组件数据传递
这种情况下可以使用事件总线的方式
g.vuex
状态管理
h.pinia
一般超过两个组件交互用pinia、父子props和$refs
01.父传子通信(Props)
a.概述
父传子通信是Vue中最基本也是最常用的通信方式。父组件通过属性(props)向子组件传递数据
b.示例代码
a.父组件(ParentComponent.vue)
<template>
<div>
<child-component :message="parentMessage" :userInfo="userInfo"></child-component>
</div>
</template>
<script setup>
import { ref, reactive } from 'vue';
import ChildComponent from './ChildComponent.vue';
const parentMessage = ref('Hello from Parent');
const userInfo = reactive({ name: 'John', age: 30 });
</script>
b.子组件(ChildComponent.vue)
<template>
<div>
<p>{{ message }}</p>
<p>{{ userInfo.name }}</p>
</div>
</template>
<script setup>
import { defineProps } from 'vue';
const props = defineProps({
message: String,
userInfo: Object,
});
</script>
c.说明
在父组件中,通过v-bind指令(简写为:)
将parentMessage和userInfo数据绑定到子组件的message和userInfo属性上
子组件通过defineProps函数接收这些属性,并在模板中使用
02.子传父通信(Emit)
a.概述
子组件可以通过触发事件的方式向父组件传递数据
Vue3中,子组件使用$emit方法触发事件,父组件通过监听这些事件来接收数据
b.示例代码
a.父组件(ParentComponent.vue)
<template>
<div>
<child-component @update="handleUpdate" @delete="handleDelete"></child-component>
</div>
</template>
<script setup>
import { ref } from 'vue';
import ChildComponent from './ChildComponent.vue';
const handleUpdate = (data) => {
console.log('Received from child:', data);
};
const handleDelete = () => {
// 处理删除逻辑
};
</script>
b.子组件(ChildComponent.vue)
<template>
<div>
<button @click="handleClick">更新父组件</button>
</div>
</template>
<script setup>
import { defineEmits } from 'vue';
const emit = defineEmits(['update', 'delete']);
const handleClick = () => {
emit('update', { id: 1, value: 'new value' });
};
</script>
c.说明
在子组件中,通过defineEmits函数定义可以触发的事件
然后,在按钮点击事件中调用emit方法触发update事件,并传递数据给父组件
父组件通过v-on指令(简写为@)监听update和delete事件,并定义相应的事件处理函数
03.兄弟组件通信
a.概述
兄弟组件之间的通信通常需要通过一个共同父组件来中转,或者使用事件总线(如mitt库)来实现
b.通过父组件中转
a.父组件(ParentComponent.vue)
<template>
<div>
<sibling-a @message="handleMessage" />
<sibling-b :message="message" />
</div>
</template>
<script setup>
import { ref } from 'vue';
import SiblingA from './SiblingA.vue';
import SiblingB from './SiblingB.vue';
const message = ref('');
const handleMessage = (value) => {
message.value = value;
};
</script>
b.兄弟组件A(SiblingA.vue)
<template>
<button @click="sendMessage">发送消息</button>
</template>
<script setup>
import { defineEmits } from 'vue';
const emit = defineEmits(['message']);
const sendMessage = () => {
emit('message', 'Hello from sibling A');
};
</script>
c.兄弟组件B(SiblingB.vue)
<template>
<p>{{ message }}</p>
</template>
<script setup>
import { defineProps } from 'vue';
const props = defineProps(['message']);
</script>
d.说明
在这个例子中,兄弟组件A通过触发message事件向父组件发送数据
父组件接收到数据后更新自己的状态,并将状态通过props传递给兄弟组件B
b.使用事件总线(mitt)
a.安装mitt库
npm install mitt
b.全局配置事件总线
import { createApp } from 'vue';
import mitt from 'mitt';
import App from './App.vue';
const app = createApp(App);
app.config.globalProperties.$bus = mitt();
app.mount('#app');
c.组件A(ComponentA.vue)
<template>
<button @click="sendMessage">发送消息</button>
</template>
<script setup>
import { getCurrentInstance } from 'vue';
const { proxy } = getCurrentInstance();
const sendMessage = () => {
proxy.$bus.emit('myEvent', 'Hello from ComponentA');
};
</script>
d.组件B(ComponentB.vue)
<script setup>
import { onMounted, getCurrentInstance } from 'vue';
const { proxy } = getCurrentInstance();
onMounted(() => {
proxy.$bus.on('myEvent', (message) => {
console.log(message); // 输出: "Hello from ComponentA"
});
});
</script>
e.说明
在这个例子中,组件A通过事件总线发送消息,组件B通过事件总线接收消息
这种方式不需要通过父组件中转,实现了兄弟组件之间的直接通信
c.全局状态管理(Pinia)
a.创建 Pinia Store
// stores/message.js
import { defineStore } from 'pinia';
export const useMessageStore = defineStore('message', {
state: () => ({
content: ''
}),
actions: {
setContent(newContent) {
this.content = newContent;
}
}
});
b.组件 A 修改 Store 中的状态
<!-- ComponentA.vue -->
<template>
<button @click="update">更新全局状态</button>
</template>
<script setup>
import { useMessageStore } from '@/stores/message';
const store = useMessageStore();
const update = () => {
store.setContent('全局消息内容');
};
</script>
c.组件 B 读取 Store 中的状态
<!-- ComponentB.vue -->
<template>
<div>{{ store.content }}</div>
</template>
<script setup>
import { useMessageStore } from '@/stores/message';
const store = useMessageStore();
</script>
d.Provide/Inject + 响应式数据
a.祖先组件使用 provide 提供数据
<!-- Ancestor.vue -->
<template>
<ComponentA />
<ComponentB />
</template>
<script setup>
import { provide, ref } from 'vue';
const sharedData = ref('初始数据');
const updateData = (newValue) => {
sharedData.value = newValue;
};
provide('sharedContext', {
sharedData,
updateData
});
</script>
b.兄弟组件使用 inject 注入数据
<!-- ComponentA.vue -->
<template>
<button @click="update">修改共享数据</button>
</template>
<script setup>
import { inject } from 'vue';
const { updateData } = inject('sharedContext');
const update = () => {
updateData('新数据');
};
</script>
<!-- ComponentB.vue -->
<template>
<div>{{ sharedData }}</div>
</template>
<script setup>
import { inject } from 'vue';
const { sharedData } = inject('sharedContext');
</script>
e.共享响应式对象
a.创建独立的响应式对象文件供组件导入
// sharedState.js
import { reactive } from 'vue';
export const state = reactive({
value: '',
setValue(newVal) {
this.value = newVal;
}
});
b.组件直接导入使用
<!-- ComponentA.vue -->
<template>
<input v-model="state.value" />
</template>
<script setup>
import { state } from './sharedState';
</script>
<!-- ComponentB.vue -->
<template>
<div>{{ state.value }}</div>
</template>
<script setup>
import { state } from './sharedState';
</script>
f.组件实例引用(ref)
a.通过 ref 直接访问组件实例方法
<!-- Parent.vue -->
<template>
<ComponentA ref="compA" />
<ComponentB :trigger="compA?.method" />
</template>
<script setup>
import { ref } from 'vue';
const compA = ref(null);
</script>
b.子组件暴露方法
<!-- ComponentA.vue -->
<script setup>
const method = () => {
console.log('组件A的方法被调用');
};
defineExpose({ method }); // 必须暴露方法
</script>
c.通过 ref 调用方法
<!-- ComponentB.vue -->
<template>
<button @click="trigger">调用方法</button>
</template>
<script setup>
defineProps(['trigger']);
</script>
2.5 父子组件:钩子执行顺序
01.加载渲染过程
父 beforeCreate->父 created->父 beforeMount->子 beforeCreate->子 created->子 beforeMount->子 mounted->父 mounted
02.子组件更新过程
父 beforeUpdate->子 beforeUpdate->子 updated->父 updated
03.父组件更新过程
父 beforeUpdate->父 updated
04.销毁过程
父 beforeDestroy->子 beforeDestroy->子 destroyed->父 destroyed
2.6 路由动态:vue-router
00.实现思路
a.动态路由
将拥有权限的路由单独从路由表系统中提出,视为动态路由,待后续根据用户权限来导入拥有权限的动态路由
b.权限与动态路由绑定
用户登录之后,需要获取该用户所拥有的权限,该权限前后端需要协调一致,需要对应前端拥有权限的动态路由
后端可以返回动态路由的name来做以区分,也可以在前端单独定义每个动态路由(权限)的标识进行权限的区分
c.获取权限筛选动态路由
获取到用户所拥有的权限之后,将后端返回的权限标识与动态路由中所定义的权限标识比对
筛选出该用户所拥有的权限(动态路由)
d.添加权限(动态路由)
使用router.addRoute()方法将拥有权限的动态路由添加到路由表系统中实现权限的控制
e.渲染菜单
获取到拥有权限的动态路由之后,在vuex/pinia中去组合需要渲染菜单的动态路由(+/静态路由)
菜单需要在路由中按需定义菜单的名称、图片、icon等等,最后将路由中组合好的菜单循环渲染展示
f.退出登录删除动态路由(权限)
用户退出登录时需要删除动态路由(权限),因为如果退出登录时不删除动态路由(权限)
紧接着登录其他权限不同的用户,该用户没有上一个用户所拥有的权限,所以无法查看上一个用户所看到的菜单以及页面
但是该用户可以使用浏览器的回退功能或者修改url地址栏的路径去访问上一个用户能访问的页面
这样该用户就可以访问到不属于自己权限的页面,所以在用户退出登录时需要删除动态路由(权限)
来保证权限的准确性。删除动态路由使用到router.removeRoute()
01.提取动态路由
a.说明
每个动态路由中的meta对象中定义了要渲染菜单的名称的title和图片img
permissionCode定义的标识为与后端确定的该用户是否具有该路由菜单的权限
b.代码
export const changeChildRouter = [
{
path: 'user',
name: 'SysUser',
component: () => import(/* webpackChunkName:"system" */'../../views/System/UserManage'),
meta: {
title: '用户管理',
permissionCode: 10004,
img: require('@/assets/images/Tree/setting-user.png')
}
},
{
path: 'department',
name: 'SysDepartment',
component: () => import(/* webpackChunkName:"system" */'../../views/System/DepartmentManage'),
meta: {
title: '部门管理',
permissionCode: 10404,
img: require('@/assets/images/Tree/setting-gis.png')
}
},
{
path: 'position',
name: 'SysPosition',
component: () => import(/* webpackChunkName:"system" */'../../views/System/PositionManage'),
meta: {
title: '岗位管理',
permissionCode: 10204,
img: require('@/assets/images/Tree/setting-user.png')
}
},
{
path: 'role',
name: 'SysRole',
component: () => import(/* webpackChunkName:"system" */'../../views/System/RoleManage'),
meta: {
title: '角色管理',
permissionCode: 10104,
img: require('@/assets/images/Tree/setting-user.png')
}
},
]
02.登录之后,获取该登录用户的权限,筛选动态路由
a.说明
a.第一种想法
当用户登录之后,紧接着在逻辑当中获取该用户的权限,将用户的权限存储到session当中
目的是为了防止用户刷新浏览器权限依然不会丢失
但是安全性不高,用户手动修改session中存储的权限就会导致权限丢失
或者非法获取其他用户的session权限放置未拥有该权限的用户的session中
导致该用户拥有了其他用户不属于自己的权限等等问题
b.第二种想法
当用户登录之后,在路由前置守卫router.beforeEach()中去获取权限
获取到权限之后,引入并筛选出该用户权限的动态路由
使用router.addRoute()将该用户的动态路由添加到路由表系统当中
将筛选出的动态路由(权限)存储到vuex当中
由于vuex做临时存储,所以原理上刷新页面之后vuex中的数据就会丢失
但是在router.beforeEach()路由前置守卫中获取权限就解决了该问题
每次刷新页面都会先执行router.beforeEach()
所以每次刷新页面都会先去获取该用户的权限
根据后端返回的权限标识筛选出的动态路由存储到vuex当中
但是想要实现刷新页面动态路由(权限页面)不丢失
不能将404页面定义为静态路由放置在路由表系统当中
需要随权限获取后,动态添加404页面
目的是为了解决刷新权限页面之后权限丢失,在没有重新获取到权限的时候
检测到路由表系统当中不存在该动态路由,所以直接进入404页面的问题
b.代码
import router from "@/router"
import store from '@/store'
import { changeChildRouter } from '@/router/system'
let routeFlag = false;
router.beforeEach(async (to, from, next) => {
let token = localStorage.getItem('token');
if (!token && !to.path.includes('/login')) {
routeFlag = false
next('/login')
} else if (to.path.includes('/login')) {
routeFlag = false
next()
} else {
if (routeFlag) return next()
routeFlag = true
const res = await store.dispatch('menu/MenuInfo')
const Liststr = res.map(item => item.roleId)
const systemRoutes = systemAsyncRouter(changeChildRouter, Liststr)
asyncRoutes(systemRoutes)
router.addRoute(
{
path: '/:pathMatch(.*)*',
name: '404',
component: () => import(/* common */'@/views/NotFound'),
meta: { title: '404' }
},
)
next({
...to,
replace: true
})
}
})
function systemAsyncRouter(routes, codes) {
const filterRoutes = routes.filter(item => codes.includes(item.meta?.permissionCode))
addAsyncRouter('系统功能', filterRoutes)
store.commit('menu/setMenuList', filterRoutes)
return filterRoutes
}
function addAsyncRouter(parentName, asyncRoutesList) {
asyncRoutesList.map(item => {
if (!item.component) return addAsyncRouter(parentName, item.children)
router.addRoute(parentName, item)
})
}
function asyncRoutes(...args) {
store.commit('menu/setPermissionRouteList', [].concat(...args))
}
03.添加动态路由之后
a.说明
需要在用户退出的时候调用以下方法删除动态路由以及保存的权限
b.代码
export function removeAsyncRouter() {
const permissionRouteList = store.state.menu.permissionRouteList
const removeRouteFn = (permissionRouteList) => {
if (permissionRouteList.length > 0) {
permissionRouteList.map(item => {
if (!item.component && item.children) return removeRouteFn(item.children)
router.removeRoute(item.name)
})
}
}
removeRouteFn(permissionRouteList)
store.commit('menu/setMenuList', [])
store.commit('menu/setPermissionCodeList', [])
}
04.在vuex当中将动态路由(+/静态路由)组合来渲染权限菜单展示
a.代码
import { loadAuthority } from '@/api/loaddata'
import { systemChildRouter } from '@/router/system'
const state = () => ({
permissionCodeList: [],
permissionRouteList: [],
menuList: [],
})
const mutations = {
setPermissionCodeList(state, permissionCodeList) {
state.permissionCodeList = permissionCodeList
},
setPermissionRouteList(state, permissionRouteList) {
state.permissionRouteList = permissionRouteList
},
setMenuList(state, permissionRouteList) {
const menuList = [...systemChildRouter, ...permissionRouteList]
},
}
const actions = {
async MenuInfo(ctx) {
const res = await loadAuthority.getUserAuthList({ userName: localStorage.getItem('userName') })
ctx.commit('setPermissionCodeList', res)
return res
}
}
export default {
namespaced: true,
state,
actions,
mutations
}
05.渲染菜单
a.说明
在vue组件计算属性当中获取vuex中组合好的渲染菜单的路由, 在模板中循环渲染展示菜单
b.代码
<template>
<a-layout-sider :width="220">
<a-menu mode="inline" v-model:selectedKeys="current">
<left-menu-item v-for="item in menuList" :key="item.path" :item="item">
</left-menu-item>
</a-menu>
</a-layout-sider>
</template>
<script>
import LeftMenuItem from './LeftMenuItem.vue'
export default {
name: "SystemMenu",
components: {
LeftMenuItem
},
data() {
return
current: ["help"],
};
},
computed: {
menuList() {
return this.$store.state.menu.menuList
}
},
};
</script>
<template>
<div>
<template v-if="!item.children">
<a-menu-item :key="item.path">
<template #icon>
<img :src="item.meta.img">
</template>
<router-link :to="item.path">{{ item.meta.title }}</router-link>
</a-menu-item>
</template>
<template v-else>
<a-sub-menu :key="item.path">
<template #icon>
<img :src="item.meta.img">
</template>
<template #title>{{ item.meta.title }}</template>
<left-menu-item v-for="item in item.children" :key="item.path" :item="item">
</left-menu-item>
</a-sub-menu>
</template>
</div>
</template>
<script>
export default {
name: "LeftMenuItem",
props: {
item: {
type: Object,
required: true
},
}
}
</script>
<style lang="scss" scoped></style>
c.递归自身组件
想要递归自身组件,需要在组件脚本中导出定义自身的name:LeftMenuItem
在组件内容即可调用自身的name来实现递归调用组件本身
这样就实现了菜单的权限控制,拥有不同权限的用户登录之后,就只会看到属于自己权限的页面!
2.7 路由方式:vue-router、hash、history
00.总结
a.项目需求
如果需要支持旧版浏览器或不想配置服务器,选择 Hash 模式
如果需要美观的 URL 和更好的 SEO 支持,选择 History 模式
b.服务器配置
使用 History 模式时,确保服务器正确配置以处理所有路由请求
01.vue-router路由
a.钩子函数种类
全局守卫、路由守卫、组件守卫
b.完整的导航解析流程
导航被触发
在失活的组件里调用 beforeRouteLeave 守卫
调用全局的 beforeEach 守卫
在重用的组件里调用 beforeRouteUpdate 守卫
在路由配置里调用 beforeEnter
解析异步路由组件
在被激活的组件里调用 beforeRouteEnter
调用全局的 beforeResolve 守卫
导航被确认
调用全局的 afterEach 钩子
触发 DOM 更新
调用 beforeRouteEnter 守卫中传给 next 的回调函数,创建好的组件实例会作为回调函数的参数传入
02.Hash模式
a.定义
Hash 模式使用 URL 的 hash(#)部分来模拟完整的 URL,从而在不重新加载页面的情况下实现路由切换
b.工作原理
URL 中的 hash 部分(#后面的内容)不会被发送到服务器
浏览器的 hashchange 事件用于监听 URL 的变化
c.优点
简单易用,兼容性好,支持所有浏览器,包括不支持 HTML5 History API 的旧浏览器
不需要服务器配置,因为 hash 部分不会被发送到服务器
d.缺点
URL 中包含 #,不够美观
对 SEO 不友好,因为搜索引擎通常不会索引 # 后面的内容
e.示例
const router = new VueRouter({
mode: 'hash',
routes: [
{ path: '/', component: Home },
{ path: '/about', component: About }
]
});
03.History模式
a.定义
History 模式依赖于 HTML5 History API 来实现 URL 路径的变化,而不需要使用 hash
b.工作原理
使用 pushState 和 replaceState 方法来操作浏览器的历史记录
允许使用正常的 URL 结构(不带 #)
c.优点
URL 美观,没有 #
更好的 SEO 支持,因为 URL 是完整的路径
d.缺点
需要服务器配置支持,将所有路由请求重定向到入口 HTML 文件
不支持 HTML5 History API 的浏览器无法使用
e.服务器配置示例(以 Nginx 为例)
location / {
try_files $uri $uri/ /index.html;
}
f.示例
const router = new VueRouter({
mode: 'history',
routes: [
{ path: '/', component: Home },
{ path: '/about', component: About }
]
});
3 前端管理
3.1 ref
$
this 作用域
3.2 url:路径
00.汇总
01.computed计算
02.methods使用时直接拼接
03.data初始值直接拼接逻辑
04.三种变式:纯data、纯setup、先setup+后data
01.computed计算
a.示例
data() {
return {
// 1. 定义公共基础URL
baseUrl: '/dev-api', // 切换环境时仅改这里
// 2. 定义两个URL的“专属路径”(无需带公共部分)
urlPath1: '/user/login',
urlPath2: '/order/list'
};
},
computed: {
// 计算完整URL:公共URL + 专属路径
fullUrl1() {
return this.baseUrl + this.urlPath1; // 结果:/dev-api/user/login
},
fullUrl2() {
return this.baseUrl + this.urlPath2; // 结果:/dev-api/order/list
}
},
methods: {
// 使用时直接调用computed里的完整URL
async fetchData() {
const res1 = await axios.post(this.fullUrl1, { username: 'test' });
const res2 = await axios.post(this.fullUrl2, { page: 1 });
}
}
02.methods使用时直接拼接
a.示例
data() {
return {
baseUrl: '/dev-api', // 公共部分
url1: '/user/login', // 第一个URL的专属路径
url2: '/order/list' // 第二个URL的专属路径
};
},
methods: {
async fetchData() {
// 拼接:公共URL + 专属路径
const fullUrl1 = this.baseUrl + this.url1;
const fullUrl2 = this.baseUrl + this.url2;
const res1 = await axios.post(fullUrl1, { username: 'test' });
const res2 = await axios.post(fullUrl2, { page: 1 });
}
}
03.data初始值直接拼接逻辑
a.示例
data() {
// 1. 先定义公共URL(切换环境仅改这里)
const baseUrl = '/dev-api';
return {
// 2. 两个完整URL:直接用“公共URL + 专属路径”拼接
fullUrl1: baseUrl + '/user/login', // 结果:/dev-api/user/login
fullUrl2: baseUrl + '/order/list' // 结果:/dev-api/order/list
};
},
methods: {
// 使用时直接拿data里的完整URL
async sendRequests() {
const res1 = await axios.post(this.fullUrl1, { username: 'admin' });
const res2 = await axios.post(this.fullUrl2, { page: 1, size: 10 });
}
}
04.三种变式:纯data、纯setup、先setup+后data
a.纯data
data() {
// 在 data 函数内部定义一个局部常量
const baseUrl = '/dev-api';
return {
// 通用-路径
apiPaths: {
// 请求路径:枚举
sysAttrList: baseUrl + '/onboard/attr/list', // GET请求
// 请求路径:主表
applicantList: baseUrl + '/onboard/evaluateApply/list', // POST请求
applicantDelete: baseUrl + '/onboard/evaluateApply/delete', // POST请求
applicantExport: baseUrl + '/onboard/applicant/person/export', // POST请求
// 请求路径:分配
allocateWithdrawAudit: baseUrl + '/onboard/evaluate/withdraw-audit', // POST请求
// 请求路径:审批流程痕迹
approvalTrailUrl: baseUrl + '/onboard/evaluateApply/getApprovalTrail', // POST请求
},
// ...
};
},
b.纯setup
import { reactive, ref } from 'vue';
setup() {
// 加载
const loading = ref(false);
// 定义基础 URL
const baseUrl = '/dev-api';
// 将所有 API 路径也在这里定义
const apiPaths = {
sysAttrList: baseUrl + '/onboard/attr/list',
applicantList: baseUrl + '/onboard/evaluateApply/list',
applicantDelete: baseUrl + '/onboard/evaluateApply/delete',
applicantExport: baseUrl + '/onboard/applicant/person/export',
allocateWithdrawAudit: baseUrl + '/onboard/evaluate/withdraw-audit',
approvalTrailUrl: baseUrl + '/onboard/evaluateApply/getApprovalTrail',
};
return {
loading,
apiPaths,
};
},
data() {
return {
attrLists: {
flowStatusOptions: [],
},
};
},
c.先setup+后data
setup() {
// 加载
const baseUrl = '/dev-api';
const loading = ref(false)
return {
// 加载
baseUrl,
loading,
};
},
data() {
return {
apiPaths: {
// 请求路径:枚举
sysAttrList: this.baseUrl + '/onboard/attr/list', // GET请求
// 请求路径:主表
applicantList: this.baseUrl + '/onboard/evaluateApply/list', // POST请求
applicantDelete: this.baseUrl + '/onboard/evaluateApply/delete', // POST请求
applicantExport: this.baseUrl + '/onboard/applicant/person/export', // POST请求
// 请求路径:分配
allocateWithdrawAudit: this.baseUrl + '/onboard/evaluate/withdraw-audit', // POST请求
// 请求路径:审批流程痕迹
approvalTrailUrl: this.baseUrl + '/onboard/evaluateApply/getApprovalTrail', // POST请求
},
}
}
-----------------------------------------------------------------------------------------------------
在 Vue 3 的生命周期中,setup() 函数是最先执行的。setup() 首先运行,并返回 baseUrl。然后 Vue 才开始处理 data()、methods 等其他选项。
当 data() 执行时,从 setup 返回的 baseUrl 已经存在于 this 上下文中了,所以 this.baseUrl 可以被正确读取。
3.3 import:引入
引入方式
import tab from './tab';
import { VueInstanceAuth } from './auth';
import cache from './cache';
import { VueInstanceDownload } from './download';
import { App } from 'vue';
import { VueInstanceModal } from './modal';
3.4 mixins
src/mixins
https://zhuanlan.zhihu.com/p/482735975
BaseMixins.ts
drawMixin.js
ListMixins.js
ListModalMixins.js
OldBaseMixins.ts
Standard.js
TableMixins.js
TreeListMixins.js
TreeListModalMixins.js
3.5 plugins
src/plugins
// 页签操作
app.config.globalProperties.$tab = tab;
// 认证对象
app.config.globalProperties.$auth = new VueInstanceAuth();
// 缓存对象
app.config.globalProperties.$cache = cache;
// 模态框对象
const modal = new VueInstanceModal();
app.config.globalProperties.$modal = modal;
// 下载文件
app.config.globalProperties.$download = new VueInstanceDownload();
app.config.globalProperties.file_viewer_url = 'http://172.18.248.205:8012/onlinePreview';
app.config.globalProperties.msgSuccess = modal.msgSuccess;
app.config.globalProperties.msgWarning = modal.msgWarning;
app.config.globalProperties.msgError = modal.msgError;
// 新增常用的方法 全局变量
app.config.globalProperties.getAction = getAction;
app.config.globalProperties.postAction = postAction;
app.config.globalProperties.deleteAction = deleteAction;
app.config.globalProperties.putAction = putAction;
app.config.globalProperties.validateNull = validateNull;
3.6 字典配置
this.getDictListData(["meterRegion"]);
getDictListData (strSn) { src/mixins/ListModalMixins.js:24
ListModalMixins.js created () { },
ListMixins.js
this.$nextTick(() => {
if (!this.$refs.modalForm) {
// this.proxy.$modal.msgWarning('请添加modalForm表单!')
// return
}
});
if (this.autoLoadList && this.url && !this.isFangKeCar) {
this.loadData();
}
if (this.autoLoadDict) {
//初始化字典配置 在自己页面定义
this.initDictConfig();
}
3.7 父子组件
src/views/performance/meteringManagement
mixins: [ListModalMixins, ListMixins],
components: {
treeMultiple,
personSelect,
},