组件化
组件使用时候 当作标签名使用
组件按照使用方式不同分局部组件和全局组件
组件使用三部曲
1 创建 2 导入 3 注册
组件的分类
组件按照使用方式不同分局部组件和全局组件
局部组件
1 创建 2 导入 3 通过components选项注册
全局组件
1 创建 2 在main.js导入并通过Vue.component('组件名',组件)
组件按用途分视图组件(配合路由使用)和业务组件
业务组件
components
视图组件
views
实例
1 在App.vue中书写
<template>
<div>
<h1>组件化</h1>
<!-- 组件使用时候 当作标签名使用 -->
<header-comp/>
<main-comp />
</div>
</template>
<script>
import HeaderComp from './components/HeaderComp'
import MainComp from './components/MainComp'
// 必须默认导出一个对象->组件实例(vue实例)
export default {
components: {
HeaderComp,
MainComp
}
}
</script>
<style></style>
2 在main.js中书写
// 导入Vue
import Vue from 'vue'
// 导入App根组件
import App from './App.vue'
// 注册全局组件
import ButtonComp from './components/ButtonComp.vue'
Vue.component('ButtonComp', ButtonComp)
Vue.config.productionTip = false
new Vue({
render: h => h(App)
}).$mount('#app')
组件生命周期
含义
组件生命周期
阶段
创建前 创建后
挂载前 挂载后
更新前 更新后
销毁前 销毁后
vue给组件的某个阶段提供了特定的函数(钩子函数)来执行特定逻辑,当到了某个节点会自动调用
页面一打开执行的生命周期 beforeCreate->created->beforeMount->mounted
组件data必须是一个函数,函数每次执行时候得到新对象
组件是可以服用,用对象形式导致组件的数据相互影响
beforeCreate
beforeCreate拿不到数据
beforeCreate () {
console.log('beforeCreate()', this.info)
},
created
created发送网络请求
created () {
console.log('created ()', this.info)
},
beforeMount
beforeMount 拿不到渲染后的dom
beforeMount () {
console.log('beforeMount()', document.getElementById('root'))
},
mounted
mounted 拿到渲染后的dom
mounted () {
console.log('----', document.getElementById('root'))
console.log('mounted()', this.$el)
},
beforeUpdate
beforeUpdate 数据更新前
beforeUpdate () {
console.log('beforeUpdate()', this.info)
},
updated
updated 数据更新后
updated () {
console.log('updated()', this.info)
},
beforeDestroy
beforeDestroy 实例销毁之前调用 ,实例仍然完全可用。包括资源清理-定时器取消、事件解绑等
beforeDestroy () {
console.log('beforeDestroy()')
},
destroyed
destroyed 执行了一系列的销毁动作
destroyed () {
console.log('destroyed ()')
},
代码示例
<template>
<!-- 有且只有一个根元素-->
<div id="root">
<div class="header">
我是头部区域{{ info }}
</div>
<button @click="fn">按钮</button>
</div>
</template>
<script>
export default {
// 组件名称 方便调试
name: 'HeaderComp',
data () {
return {
info: '123'
}
},
beforeCreate () {
console.log('beforeCreate()', this.info)
},
created () {
console.log('created ()', this.info)
},
beforeMount () {
console.log('beforeMount()', document.getElementById('root'))
},
mounted () {
console.log('----', document.getElementById('root'))
console.log('mounted()', this.$el)
},
beforeUpdate () {
console.log('beforeUpdate()', this.info)
},
updated () {
console.log('updated()', this.info)
},
beforeDestroy () {
console.log('beforeDestroy()')
},
destroyed () {
console.log('destroyed ()')
},
methods: {
fn () {
console.log(this.info)
this.info = 999
this.$destroy()
}
}
}
</script>
<style lang="less" scoped>
.header {
height: 200px;
background-color: #f40;
}
</style>
父子通信
父传子
1 父组件内,在子组件标签上,写属性
<main-comp :money="money" :a="true" />
2 子组件内,通过props 属性接收
// 2 子组件通过props来接受
props: ['money', 'a'],
子传父
1 子组件内, 通过:$emit("自定义事件",参数1, 参数2)
methods: {
give () {
// 触发自定义事件
this.$emit('updateMoney', this.son_money)
}
}
2 父组件,子组件标签上, :@自定义事件名=“函数名”
<main-comp : @updateMoney="fn"/>
依赖注入
用于祖先组件给后代组件传递数据:
① 在祖先组件中配置 provide
,指定需要传递的数据(provide
用法类比 data
)
② 在后代组件中配置 inject
,并接收数据(inject
用法类比 props
)
provide的两种写法
对象式写法:只能传递静态信息,不能获取 this
函数式写法:可以获取并传递 this
的信息,this
指向当前组件实例
EventBus
非父子组件之间,进行简易的消息传递
- 创建一个都能访问到的事件总线
- 接收方用$on监听事件
- 发送方用$emit触发事件
v-model指令
可用在表单元素或组件中
用在表单元素上
1 文本框或密码框相当于 :value="数据"+@input="数据=$event.target.value"
2 复选框 :checked="数据"+@change="数据=$event.target.checked"
3 下拉列表 :selected="数据"+@change="数据=$event.target.selected"
用在组件实现父子数据双向绑定
:value="数据" + @input="数据=$event"
ref用法
ref 被用来给元素或子组件注册引用信息, 引用信息将会注册在父组件的 $refs 对象上,如果是在普通的DOM元素上使用,引用指向的就是 DOM 元素,如果是在子组件上,引用就指向组件的实例。获取DOM或者组件实例,可以实现组件通信
普通元素
ref 加在普通的元素上,用this.$refs.(ref值) 获取到的是dom元素
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
</head>
<body>
<div id="app">
<p @click="handClick" ref="pp" id="p">你好web2208</p>
</div>
<script src="vue.js"></script>
<script type="text/javascript">
const vm = new Vue({
el:"#app",
data(){
return{ }
},
methods:{
handClick(){
console.log(document.getElementById("p"));
console.log(this.$refs.pp);
console.log(document.getElementById("p").innerHTML);
console.log(this.$refs.pp.innerHTML);
}
}
})
</script>
</body>
</html>
子组件上
2、ref 加在子组件上,用this.$refs.name
获取到的是组件实例,可以使用组件的所有方法。
sync 修饰符
sync
修饰符就是为这种模式提供的一个缩写
传值方式:
1.属性名.sync=“属性名" 前一个名字是传递之后子组件接收时的名字,后一个是传递的参数的属性名
2.子组件通过$emit('update:show')触发事件,update后面接着绑定修的属性名
父组件
<template>
<div>
<child v-if="show" :show.sync="show" />
</div>
</template>
<script>
import child from './child.vue'
export default {
components: {
child
},
data() {
return {
show: false
};
}
};
子组件
<template>
<div v-if=show"">
这是子组件弹窗
</div>
</template>
<script>
import child from './child.vue'
export default {
props:['show'],
methods: {
closeDialog() {
this.$emit('update:show', false); //触发 update:show 事件,父组件监听获取发射出的值
}
}
};
nextTick
nextTick 可以在下一次DOM更新循环结束后立刻执行回调函数。这个函数可以用来解决一些异步更新视图的问题
DOM更新完毕后执行回调函数
原理 promise.then->MutationObserver->setImmdiate->setTimeout
<template>
<div class="app">
<!-- 是否在编辑状态 -->
<div v-if="isShowEdit">
<input ref= "inp" v-model="editValue" type="text" />
<button>确认</button>
</div>
<!-- 默认状态 -->
<div v-else>
<span>{{ title }}</span>
<button @click="handleEdit">编辑</button>
</div>
</div>
</template>
<script>
export default {
data() {
return {
title: "大标题",
editValue: '',
isShowEdit: false,
};
},
methods: {
handleEdit() {
// 显示输入框
this.isShowEdit = true;
// 让输入框获取焦点
this.$nextTick(() => {
this.$refs.inp.focus()
})
},
},
};
</script>
<style>
</style>
指令
全局指令
语法:Vue.directive(指令名称,配置对象)
最简单的写法是在main.js直接书写全局自定义指令是通过Vue.directive('第一个参数是指令的名称,第二个是对象,对象里面有钩子函数,钩子函数分别有
inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。
bind:只调用一次,指令第一次绑定到元素时调用。使用场景:在这里可以进行一次性的初始化设置。
update:所在组件的 VNode 更新时调用,等...
钩子函数的形参有el,vnode,binding,el是绑定的元素、vnode虚拟dom,binding是被赋值的结果,要用binding.value来接收调用指令时赋的值,对象里操作逻辑
// 注册全局指令
Vue.directive('focus', {
inserted (el) {
console.log(el)
el.focus()
}
})
局部指令
语法:directives:{指令名称,配置对象}
局部指令是定义在组件内部的,只能在当前组件中使用,directives后是一个对象,对象里面key是指令名字,值是一个对象,这个对象里面有钩子函数update,bind,inserted,参数是el,binding,vnode,钩子函数里面书写逻辑
<template>
<div>
<h3>自定义指令</h3>
<input ref="inp" type="text" v-focus>
</div>
</template>
<script>
export default {
directives: {
// 自定义一个局部指令
focus: {
inserted (el) {
el.focus()
}
}
}
}
</script>
动态组件
动态组件即动态切换组件,实现组件的动态切换可以使用 VueJS 提供的 component 元素,该元素拥有一个 is 属性。可以向 component 元素提供一个组件的名称来作为 is 属性的参数,接下来对应的组件将替代 components 元素。
写法:<component :is="组件名称" />
举例:
首先先写一个 BaseSelect.vue组件
<template>
<select name="" id="" :value="selectId" @change="change">
<option value="1">HTML5</option>
<option value="2">CSS3</option>
<option value="3">ES6</option>
<option value="4">DOM</option>
</select>
</template>
<script>
export default {
// 组件名称 方便调试
name: 'BaseSelect',
props: ['selectId'],
data () {
return {
// 选中项的value值,这个数据由使用该组件的地方传递过来
// selectId: '3'
a: 100
}
},
methods: {
change (e) {
this.$emit('update:selectId', e.target.value)
}
}
}
</script>
再写App.vue用来完成动态切换效果
<template>
<div>
<h1>组件化</h1>
<input type="text" ref="txtRef" v-focus v-bgcolor="'#f40'">
<div v-bgcolor="color">1111</div>
<!-- <child-comp/> -->
<!--
动态组件
<component :is="组件名称" />
-->
<component :is="compName" selectId="3"/>
<button @click="compName = 'ChildComp'">切换组件</button>
</div>
</template>
<script>
import BaseSelect from './components/BaseSelect.vue'
import ChildComp from './components/ChildComp.vue'
export default {
name: 'AppComp',
// 父组件的数据
data () {
return {
color: 'green',
compName: 'BaseSelect'
}
},
mounted () {
// this.$refs.txtRef.focus()
},
components: {
ChildComp,
BaseSelect
}
// 局部指令
/* directives: {
// 指令所在的dom元素插入到dom树中执行这个函数
// focus (el) { // el就是指令所在元素即文本框
// el.focus()
// }
focus: {
inserted (el) {
el.focus()
}
}
} */
}
</script>
<style></style>
插槽
分类
默认插槽(组件内定制一处结构)
具名插槽(组件内定制多处结构)
1 好处:组件的内容结构可定制 用slot插槽进行占位
2 语法
子组件中通过slot进行占位
父组件,在子组件标签嵌套的内容就会被渲染到slot地方
2-1 默认插槽
子组件 <slot>默认内容</slot>
父组件 <子组件标签><div></div></子组件标签>
2-2 具名插槽(多个插槽 通过name具名插槽进行区别)
子组件 <slot name="header">默认内容</slot>
<slot name="main">默认内容</slot>
<slot name="footer">默认内容</slot>
父组件
<base-layout>
<template #header>
<div>头部</div>
</template>
<template v-slot:main>
<div>主体</div>
</template>
<template #footer>
<div>尾部</div>
</template>
</base-layout>
示例:
1 子组件中通过slot进行占位
<template>
<div>
<header>
<!-- 组件的内容可定制 用slot插槽进行占位-->
<!-- 多个插槽 通过name具名插槽进行区别-->
<slot name="header">header</slot>
</header>
<main>
<slot name="main">main</slot>
</main>
<footer name="footer">
<slot>footer</slot>
</footer>
</div>
</template>
<script>
export default {
}
</script>
<style lang="less" scoped>
header {
height: 200px;
background-color: skyblue;
}
main {
height: 500px;
background-color: teal;
}
footer {
height: 200px;
background-color: orange;
}
</style>
2 父组件,在子组件标签嵌套的内容就会被渲染到slot地方
<template>
<div>
<h1>组件化</h1>
<base-layout>
<template #header>
<div>头部</div>
</template>
<template #main>
<div>主体</div>
</template>
<template #footer>
<div>尾部</div>
</template>
</base-layout>
</div>
</template>
<script>
import BaseLayout from './components/BaseLayout.vue'
export default {
name: 'AppComp',
// 父组件的数据
data () {
return {
}
},
mounted () {
},
components: {
BaseLayout
}
}
</script>
作用域插槽
组件外用组件内的数据(插槽的一个传参语法)作用域插槽是带数据的插槽,子组件提供给父组件的参数,父组件根据子组件传过来的插槽数据来进行不同的展现和填充内容
示例:
父组件代码
<template>
<div>
<h1>组件化</h1>
<my-table :list="list">
<!--
组件外用组件内的数据-作用域插槽
obj获得是默认插槽身上所有属性组成的对象
<template #default="obj">
</template>
-->
<template #default="{item: {id}}">
<button @click="del(id)">删除</button>
</template>
</my-table>
<my-table :list="list">
<button>编辑</button>
</my-table>
<!-- <my-table :list="list2"></my-table> -->
</div>
</template>
<script>
import MyTable from './components/MyTable.vue'
export default {
name: 'AppComp',
// 父组件的数据
data () {
return {
list: [
{
id: 10001,
name: '周芷若',
score: 90
},
{
id: 10002,
name: '赵敏',
score: 92
},
{
id: 10003,
name: '小朱',
score: 88
}
]
}
},
mounted () {
},
methods: {
del (id) {
console.log('del', id)
this.list = this.list.filter(item => item.id !== id)
}
},
components: {
MyTable
}
}
</script>
子组件代码:
<template>
<table>
<thead>
<tr>
<th>学号</th>
<th>姓名</th>
<th>成绩</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="item in list" :key="item.id">
<td>{{ item.id }}</td>
<td>{{ item.name }}</td>
<td>{{ item.score }}</td>
<td>
<!--
通过slot属性存储数据
-->
<slot :item="item"></slot>
</td>
</tr>
</tbody>
</table>
</template>
<script>
export default {
props: {
list: {
type: Array,
required: true,
},
},
};
</script>
<style lang="less" scoped>
table {
width: 400px;
}
thead {
background-color: skyblue;
}
table,
thead,
tbody,
td,
th {
border: 1px solid #f40;
border-collapse: collapse;
text-align: center;
}
</style>
路由
单页应用(SPA)和多页应用(MPA)的区别
简介
单页面应用:SinglePage Web Application,简称 SPA
多页面应用:MultiPage Application,简称 MPA
区别(面试题)
结构:单页面应用是一个主页面 + 许多模块的组件
多页面应用是许多完整的页面
刷新方式:单页面应用是页面局部刷新
多页面应用是整页刷新
用户体验:单页面应用页面切换快,体验佳;当初次加载文件过多时,需要做相关的调优。
多页面应用页面切换慢,网速慢的时候,体验尤其不好
适用场景(SEO):单页面应用对体验度和流畅度有较高要求的应用,不利于 SEO
多页面应用适用于对 SEO 要求较高的应用
路由模式:单页面应用可以使用 hash ,也可以使用 history
多页面应用:普通链接跳转
什么是路由
在Vue中,路由是指通过URL路径来管理不同页面之间的交互跳转和状态切换的机制。Vue Router是Vue.js官方提供的路由管理器,可以用于构建单页应用(SPA)。
router:路由对象,管理路由,负责路由跳转
route:路由规则数组 路径和组件一一对应的关系
$router:在vue中$router其实就是router (Vue.prototype.$router=router) :路由对象,管理路由,负责路由跳转
$route:路由信息对象,负责存储路由信息:路径和参数等等
1.安装 npm install vue-router
2.在src目录下创建router目录,接着创建index.js文件
3.在index.js文件中引入Vue以及路由 import VueRouter from ‘vue-router’
// 1 导入Vue和VueRouter
import Vue from 'vue'
import VueRouter from 'vue-router'
4.在index.js文件中挂载路由 Vue.use(VueRouter)
// 2 注册
Vue.use(VueRouter) // 内部创建了router-view组件 router-link组件
5.在index.js文件中创建路由对象并定义路由规则
const router = new VueRouter({routes: [{定义路由规则}]})
// 3 创建路由实例
const router = new VueRouter({
// 定义路由规则
routes: [
{
path: '/login',
component: LoginComp
},
{
path: '/',
component: HomeComp
},
{
path: '/article/:id',
component: ArticleDetail
},
{
path: '/404',
component: NotFound
},
{
path: '*',
redirect: '/404' // 重定向
}
],
})
6.在index.js文件中导出路由实例
// 导出路由实例
export default router
7.在main.js文件路由实例中添加router
// 导入路由实例
import router from '@/router'
Vue.config.productionTip = false
new Vue({
router,
render: h => h(App),
}).$mount('#app')
7.在app.vue中预留路由出口
<!-- 路由出口-将来匹配的组件渲染地方 -->
<router-view></router-view>
路由导航
声明式导航
声明式导航是利用<router-link>
组件来创建链接,实现路由导航
<template>
<div id="app">
App组件
<router-link to="/">首页</router-link>
<router-link to="/login">登录</router-link>
<router-link to="/article/1">查看第一篇文章</router-link>
</div>
</template>
路由重定向
路由重定向是指在访问某个路径时,自动跳转到另一个路径
当访问根路径时会自动重定向到/home路径。可以看到,通过在路由规则中添加redirect属性来实现路由重定向。
const router = new VueRouter({
routes: [
{
path: '/',
redirect: '/home'
},
{
path: '/home',
component: Home
}
]
})
404页面
404页面是指在访问不存在的路径时显示的页面,通常用于提示用户当前页面不存在等信息。可以通过创建一个专门的路由规则来实现404页面
使用通配符匹配所有不存在的路径,并将其导航到NotFound组件。可以看到,通过在路由规则中添加path为的规则来实现404页面。
const router = new VueRouter({
routes: [
{
path: '/home',
component: Home
},
{
path: '*',
component: NotFound
}
]
})
需要注意的是,路由重定向和404页面都只能匹配一次,即只有第一个匹配的规则会被执行。因此,在定义路由规则时需要注意规则的顺序。
路由精确匹配和模糊匹配
/* router-link-active模糊匹配类名 */
/* router-link-exact-active精确匹配类名 */
router-link中要注意区分active-class(模糊匹配)和exact-active-class(精确匹配)
精确匹配必须要地址栏的地址和router-link中to属性值完全一样,此时会给对应的a增加一个类名router-link-exact-active
模糊匹配只要地址栏的地址和router-link中to属性值有对应的地方,此时会给对应的a增加一个类名router-link-active
动态路由
是一种通过用户输入的参数动态生成路由的方法。在Vue中定义路由规则时,可以通过在路由配置中使用冒号(:)来表示路由参数
{
path: '/article/:id',
component: ArticleDetail
},
这个路由表示访问/articles/:id时,会动态匹配任何以/articles/开头的路径,并将匹配到的参数传递给AtricleDetail组件。在AtricleDetail组件中,可以通过this.$route.params获取id参数的值。
动态路由的好处是能够更加灵活地匹配路由,使得应用程序可以处理各种不同的URL请求
子路由
路由是要实现什么效果呢,就是一个页面可以显示不同的界面,每个不同的界面就写在子路由里面
1.子路由的创建
首先我们创建三个组件,分别是hi父组件,hi1子组件一,hi2子组件二
<template>
<h2>我是大帅比{{msg}}</h2>
</template>
<script>
export default {
name:"hi",
data(){
return {
msg: "i am hi page"
}
}
}
</script>
<style>
</style>
<template>
<h2>{{msg}}</h2>
</template>
<script>
export default {
name:"hi1",
data(){
return {
msg: "i am hi1111 page"
}
}
}
</script>
<style>
</style>
<template>
<h2>{{msg}}</h2>
</template>
<script>
export default {
name:"hi2",
data(){
return {
msg: "i am hi2222 page"
}
}
}
</script>
<style>
</style>
为了能够显示不同的子路由界面(子界面),我们需要给hi添加一个标签
<template>
<h2>我是大帅比{{msg}}</h2>
<router-view> </router-view>
</template>
<script>
export default {
name:"hi",
data(){
return {
msg: "i am hi page"
}
}
}
</script>
<style>
</style>
router-view标签搭配router-link使用,它的具体作用是:router-view 当你的路由path 与访问的地址相符时,会将指定的组件替换该 router-view -->
接下来我们需要在router->index.js中设置子路由,这也是设置子路由的关键
import { createRouter, createWebHashHistory } from 'vue-router'
import hi from '@/components/hi.vue'
import hi1 from '@/components/hi1.vue'
import hi2 from '@/components/hi2.vue'
const routes = [
{
path:'/hi',
name:'hi',
component:hi,
children: [
{
path:'/',
name:"hi",
component:hi,
},
{
path:'/hi1',
name:"hi1",
component:hi1,
},
{
path:'/hi2',
name:"hi2",
component:hi2,
},
]
}//有子路由的话 name要设置在children里
]
const router = createRouter({
history: createWebHashHistory(),
routes
})
export default router
首先需要引入组件
import hi from '@/components/hi.vue'
import hi1 from '@/components/hi1.vue'
import hi2 from '@/components/hi2.vue'
绑定子-父关系:父界面是hi,所以
{
path:'/hi',
name:'hi',
component:hi,
}
子界面是hi1和hi2,用children数组表示
children: [
{
path:'/hi1',
name:"hi1",
component:hi1,
},
{
path:'/hi2',
name:"hi2",
component:hi2,
},
]
3.接下来再APP.vue中添加路径就好了
<template>
<nav>
<router-link to="/hi">hiPage</router-link>|
<router-link to="/hi1">hiPage1</router-link>|
<router-link to="/hi2">hiPage2</router-link>|
<router-view></router-view>
</template>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
}
nav {
padding: 30px;
}
nav a {
font-weight: bold;
color: #2c3e50;
}
nav a.router-link-exact-active {
color: #42b983;
}
</style>
路由传参
声明式导航传参
动态路由传参
* 在index.js中定义路由规则path -> /article/参数值
* 在App.vue中放入导航链接 <router-link to="/article/1">查看第一篇文章</router-link>
* 在组件获取参数: this.$route.params.参数名
查询参数传参
/路径?参数1=值&参数2=值
* 路由规则path -> /路径
* 导航链接 <router-link to="/article?id=1">查看第一篇文章</router-link>
* 组件获取参数 this.$route.query.参数名
编程式导航传参
* 按照path进行跳转
动态路由传参
* this.$router.push('/article/2')
* 组件获取参数: this.$route.params.id
查询参数传参数
* 按照命名路由进行跳转
* this.$router.push('/article?id=2&name=zs')
* 或写完整写法 this.$router.push({path: '路径', query: {参数1:值,参数2:值,...}})
* 路由规则path -> /路径
* 组件获取 this.$route.query.参数名
name 命名路由跳转
(适合 path 路径长的场景)
{ name: ‘路由名’,
path: ‘/path/xxx’, component: XXX },
this.$router.push({
name: ‘路由名’
})
举例:
在index.js
routes: [
{
path: '/login',
// 路由懒加载
component: () => import('@/views/LoginComp'),
// 命名路由
name: 'login'
}
在App.vue
methods: {
login () {
// 根据命名路由进行跳转
this.$router.push({
name: 'login' // 根据路由名称跳转
})
路由懒加载
将路由对应的组件打包成一个个的js代码块,只有在这个路由被访问的时候才加载对应的组件。
语法component: ()=> import ('component需要加载的组件地址')
import Home from '../components/Home'
import About from '../components/About'
Vue.use(VueRouter)
const routes = [
{
path: '/home',
component: () => import('../components/Home')
},
{
path: '/about',
component: () => import('../components/About')
}
]
路由模式
hash模式(默认)
地址上带#
优点 :兼容性好
缺点 :不美观
原理 :#后面地址改变不会引起页面刷新,路由会检测到地址变化(window.onhashchange),拿到最新的地址找到匹配的组件进行渲染
history模式
地址上不带#
优点 :美观
缺点 :兼容不好
原理 采用h5的history相关api(pushState, replaceState)
必须服务器支持,配合后台
在脚手架环境下,默认支持historyvuex
Vuex
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式 + 库。采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
对于组件间的通信方式而言,vuex也是一个可以进行任意组件通信的方法。
store文件夹的创建
概念:每一个Vuex应用的核心就是Store(仓库),我们可以说Store是一个容器,Store里面的状态与单纯的全局变量是不一样的,无法直接改变Store中的状态。想要改变状态需通过mutation去修改。
在 src根目录下创建文件夹store,并在其下index.js。需包含actions,mutations,state结构如下:
// 引入vue
import Vue from 'vue'
// 引入vuex
import Vuex from 'vuex'
// 应用vue插件
Vue.use(Vuex)
// actions响应组件中的动作
const actions = {
}
// mutations操作数据state
const mutations = {
}
// 准备state存储数据
const state = {
//状态对象
}
// 创建store并导出
const store = new Vuex.Store({
actions,
mutations,
state,
})
//默认导出store
export default store
引入store,在main.js中引入store,全局组件都可以使用vuex。
import store from './store'
new Vue({
render: h => h(App),
store,
}).$mount('#app')
各个状态的核心概念
state
在index.js中
// vuex代码
import Vue from 'vue'
import Vuex from 'vuex'
console.log(Vuex) // 对象
// 注册
Vue.use(Vuex)
// 创建store实例
const store = new Vuex.Store({
// strict: true, // 开启严格模式
// 1 state存储状态
state: {
count: 0,
uname: 'zs',
list: []
},
// 2 mutations修改状态
mutations: {
// 第一个参数state,第二个参数接受外界传来的参数
changeCount (state, num) {
// 同步逻辑
state.count += num
}
})
在App.vue中
<p>count: {{ $store.state.count }}</p>
在main.js
import store from '路径'
state是存储状态,可以通过this.$store.state来直接获取状态,也可以利用vuex提供的
mapState辅助函数将state映射到计算属性(computed)中去。
使用mapState辅助函数
注意:使用辅助函数的前提是要先引入
在App.vue中引入mapState
import {mapState,mapMutations,mapActions,mapGetters} from 'vuex';
// 辅助函数
import { mapMutations, mapState } from 'vuex'
export default {
computed: {
// count () {
// return this.$store.state.count
// }
...mapState(['count']) // {count() {return this.$store.state.count}}
}
}
mutations
更改 Vuex 的 store 中的修改状态的唯一方法是提交 mutation。Vuex 中的 mutation 非常类似于事件:每个 mutation 都有一个字符串的事件类型 (type)和一个回调函数 (handler)。这个回调函数就是我们实际进行状态更改的地方,并且它会接受 state 作为第一个参数。
在index.js中声明
// vuex代码
import Vue from 'vue'
import Vuex from 'vuex'
console.log(Vuex) // 对象
// 注册
Vue.use(Vuex)
// 创建store实例
const store = new Vuex.Store({
// strict: true, // 开启严格模式
// 1 state存储状态
state: {
count: 0,
uname: 'zs',
list: []
},
// 2 mutations修改状态
mutations: {
// 第一个参数state,第二个参数接受外界传来的参数
changeCount (state, num) {
// 同步逻辑
state.count += num
},
changeUname (state, name) {
state.uname = name
},
changeList (state, list) {
state.list = list
}
}
}
使用方式
this.$store.commit('mutationName', 参数)
在App.vue中使用methods方法,使用commit操作state中的数据
methods: {
changeCount () {
// 调用vuex中的mutation方法
this.$store.commit('changeCount', 5)
}
mapMutations辅助函数
mapMutations辅助函数的作用是把store中mutation内的方法映射到组件methods属性中,可以在组件中直接使用mutation里面的方法。
在App.vue中引入mapMutations
// 辅助函数
import { mapMutations, mapState } from 'vuex'
在App.vue中
<button @click="changeCount2(10)">修改count值</button>
<button @click="changeUname('lisi')">修改uname</button>
在App.vue中
methods: {
...mapMutations(['changeCount', 'changeUname'])
}
actions
存放异步逻辑
第一个参数context 上下文 提供了commit 、dispatch方法
在index.js中
// vuex代码
import Vue from 'vue'
import Vuex from 'vuex'
console.log(Vuex) // 对象
// 注册
Vue.use(Vuex)
// 创建store实例
const store = new Vuex.Store({
state: {
list: []
},
// 存放异步逻辑
actions: {
// 第一个参数context 上下文 提供了commit 、dispatch方法
getData (context, args) {
console.log(args[0], args[1])
// 模拟异步
setTimeout(() => {
// 调用mutation去改状态
const list = [{
id: 1,
name: 'zs',
gender: '男'
},
{
id: 2,
name: 'ls',
gender: '女'
}
]
context.commit('changeList', list)
}, 2000)
}
},
})
在App.vue中的methods方法中
methods: {
fn () {},
getData () {
// 调用vuex的action方法
this.$store.dispatch('getData', ['china', 20])
}
}
}
getters
类似于计算属性。getters
的第一个参数是state
, 第二个参数是getters
。每当state
中的值变化时,它也会自动更新
// vuex代码
import Vue from 'vue'
import Vuex from 'vuex'
console.log(Vuex) // 对象
// 注册
Vue.use(Vuex)
// 创建store实例
const store = new Vuex.Store({
// strict: true, // 开启严格模式
// 1 state存储状态
state: {
count: 0,
uname: 'zs',
list: []
},
// 类似于计算属性
getters: {
// 第一个参数 state 第二个参数getters
toUpperCase (state, getters) {
console.log(state, getters.a)
return state.uname.toUpperCase()
}
})
modules-模块
由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。
为了解决以上问题,Vuex 允许我们将 store 分割成模块(module)。每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块
相对来说 本意就是 能够在大型项目中,更加细化的我们的状态管理,使得结构更加的清晰,当然,也会稍微有点儿复杂
1 在store下创建modules/cart.js 每一个模块都有自己的state、mutations、actions、getters
2 组件中使用状态
{{ $store.state.模块名.状态 }}
或辅助函数
...mapState('模块名',[状态])
3 提交组件的mutation
this.$store.commit('模块名/模块里的mutation', 参数)
或辅助函数
...mapMutations('模块名', ['mutationName'])
4 派发组件的action
this.$store.dispatch('模块名/模块里的action', 参数)
或辅助函数
...mapActions('模块名', ['actionName'])
5 模块的getters
this.$store.getters['模块名/模块的计算属性']
或辅助函数
...mapGetters('模块名',['模块的计算属性'])
v-if 与 v-for 一起使?
不推荐同时使用 v-if 和 v-for 。
当 v-if 与 for 一起使用时, v-for 具有比 -f 更高的优先级。
路由守卫 面试题
1 全局路由守卫 router.beforeEach((to, from, next) => {}) router.afterEach(...)
to 去哪 from 从哪来 next进行拦截
2 路由独享守卫 在具体的路由规则配置 beforeEnter
3 组件内守卫 beforeRouteEnter
beforeRouteUpdate (2.2 新增)
beforeRouteLeave
axios拦截器
拦截器介绍
一般在使用axios时,会用到拦截器的功能,一般分为两种:请求拦截器、响应拦截器。
请求拦截器
在请求发送前进行必要操作处理,例如添加统一cookie、请求体加验证、设置请求头等,相当于是对每个接口里相同操作的一个封装;
响应拦截器
在请求得到响应之后,对响应体的一些处理,通常是数据统一处理等,也常来判断登录失效等。
每个拦截器都可以设置两个拦截函数,一个为成功拦截,一个为失败拦截。在调用axios.request()之后,请求的配置会先进入请求拦截器中,正常可以一直执行成功拦截函数,如果有异常会进入失败拦截函数,并不会发起请求;调起请求响应返回后,会根据响应信息进入响应成功拦截函数或者响应失败拦截函数。
因此,我们可以在拦截器中处理一些请求的统一处理。比如在请求拦截器中设置请求头,处理统一的请求数据,在响应拦截去中根据响应状态码做统一的提示信息,整理响应数据等。
权限
1主页鉴权(登录访问拦截)
(1)token存在(已登录),看是不是去登录页,去登录页,跳到主页,不是登录页直接放行
(2)token不存在(未登录) 但在白名单内 直接放行
(3)token不存在(未登录)且不在白名单 拦截到登录页
2 访问权限
访问权限:不同身份的人看到的页面不一样
RBAC-Role Based Access Control 基于角色的权限控制员工拥有角色(身份),角色分配权限(员工分配角色、角色分配权限)
(1)路由拆分
静态路由(任何人都能看的)+动态路由(根据权限筛选)
(2)根据权限标识筛选
根据用户资料的menus,筛选出动态路由
(3)通过addRoutes添加到动态路由表
(4)显示左侧菜单
把路由信息用vuex状态进行管理
(5)重置路由
不重置路由的话,路由信息还保存在内存中,防止后面的人还能访问上一个人访问的页面
3 操作权限:
根据用户有没有该按钮操作权限标识add-employee
<el-button v-if="?">添加员工</el-button>
echarts如何使用
步骤一:到ECharts官网下载一个合适版本的echarts 插件
步骤二:创建一个div容器初始化宽高来装我们的图
步骤三:使用echarts的init方法初始化echarts实例
步骤四:指定配置项和数据(option)
步骤五: 将配置项设置给echarts实例对象
如何实现行内编辑
给每一行数据添加一个编辑标记
给item增加一个属性isEdit,其值是false,通过this.$set增加的数据是响应式
数据发生改变,视图重新更新
问: 实际开发中有用过环境变量吗?
有用过的,我在公司里用过的场景主要是为了和后台对接口的时候切换根域名baseURL时用,因为公司有几个不同的环境嘛,一个环境对应前端一个环境变量文件,同样一个环境变量比如说VUE_APP_URL可以在不同的文件里配置不同的接口地址,再配合运行命令配置启动时指定一下运行的环境变量文件很容易就切过去了
问: axios有没有做过一些业务封装?
啊,有的,在项目里面我主要是封装过axios的拦截器部分我在请求拦截器里面做了注入全局token的事情,这个事儿是因为有很多接口都需要这个token来做数据鉴权,为了避免书写多次 统一配置了一下
响应拦截器的部分,我主要是做了一个自定义的成功错误判断 这个事儿,可以简单说一下,因为我们知道 拦截器的成功和失败判断本身是通过 http 状态码 200- 300 但是我们后端业务状态比较多一些 他们并没有采取这种依据作为判断条件,这样会有问题,导致前后端逻辑对不上 最重要的问题是 发生接口错误 感知不到 我做的事儿就是在成功回调里 自己做了判断 成功就返回数据 失败就手动return Promise.reject()
问: 工作里中的token是怎么管理的?
我们公司的token管理都是通过vuex配合本地存储来做的,使用vuex是因为token数据比较特殊,在很多模块中都可能会用到,vuex方便管理,配合本地存储比如localstorage,是因为vuex有一个不好的地方就是刷新就丢失,因为它是基于内存嘛,配合一下localStorage可以保持持久化
追问: 如果不用vuex只用本地存储能实现吗?
答: 效果上自然是没有问题的,不过我感觉俩种内存的优势融合一下岂不是更好吗
追问: proxy代理方案的原理是什么?
跨域的产生原因其实就是三要素不一致,加上浏览器同源策略导致的,如果我可以在前端和接口端之间架设一台之间服务器,保证它和前端三要素一致,和接口端因为是服务对服务不走浏览器 所以不跨域 就可以解决这个问题
追问: 那你了解跨域产生时,数据到底有没有发出去?
事实上数据是能发送到服务端的,那边也能接收到请求,只不过数据在返回客户端的时候被浏览器给拦截了
问: 怎么根据token的有无去控制路由的跳转?
vue这边的路由自带了路由前置守卫,我们可以在前置守卫里拿到token数据,然后根据需求做分支判断,要是token存在就使用next方法正常放行跳转,否则可以强制跳回到登录,让用户去获取token
问: token有一定的失效性,过期了该怎么做?
我这边采取的是被动失效,主要是后端失效之后会返回前端一个401的状态码,我们可以在axios的响应拦截器里,去获取这个状态,如果发现是401就清理一下过去的token,然后重新跳转到登录页,获取有效token
问: 有没有封装过一些自己的业务组件,简单说一下?
有的,不过我封装的基本都是和业务挂钩的组件,我挑一个比较简单的说一下吧,之前做业务的时候有遇到过 多个业务模块用到了类似的模板结构,就使用插槽封装了一些组件
大概的封装思路就是先把可复用的模板部分写好,然后把可变的需要自定义的部分用slot插槽占位 要是插槽个数比较多的话,可以区分一下默认插槽和具名插槽,尤其是功能区域比较明显的部分,用具名会更加清晰
问: 有做过权限相关的事情吗?
有做过,我们前端这边主要做的就是菜单权限控制和按钮显示控制
追问: 说说菜单权限怎么做的?
权限设计是一个需要前后端配合的事情,大概分成下面几个步骤
1.项目需要做权限设置的模块对应的路由单独放在动态路由中。路由规则中应该有个属性和后端返回的权限点是对应的(比如路由规则的name属性的值,或者在路由规则meta属性中新增个值都可以)
2.把后端返回的权限数据和前端本地的路由规则对应的那个值做对比(比如跟name属性对比),得到过滤之后的有资格显示的路由数组
3.调用路由核心方法router.addRoutes把路由数组加入到路由系统中(addRoutes添加的是有权限才能看到路由,所以即使用户记住了一些项目中的url也不怕)
4.如果需要显示到左侧菜单里,可以配合vuex做,vuex存一份相同的数据,渲染左侧的菜单
追问: 按钮的权限又怎么做呢?
按钮的权限其实就是控制显示隐藏
思路就是通过后端返回的权限标识和按钮自身的标识code做对比,如果有资格就显示,否则就隐藏
这里根据思路我们可以封装一个全局指令,通过指令可以实现复用,就可以在需要控制的按钮身上进行指令绑定,从而控制按钮的显示也隐藏