目录
- 基础
- vue开发前的准备
- vue项目目录结构
- 模板语法
- 属性绑定
- 条件渲染
- 列表渲染
- 通过key管理状态
- 事件处理
- 事件传参
- 事件修饰符
- 数组变化侦测
- 计算属性
- Class绑定
- style绑定
- 侦听器
- 表单输入绑定
- 模板引用
- 组件组成
- 组件嵌套关系
- 组件注册方式
- 组件传递数据Props(父传子)
- 组件传递多种数据类型
- 组件传递Props效验
- 组件事件(emit子传父)
- 组件数据传递(props子传父)
- 透传 Attributes
- 插槽Slot
- 组件生命周期
- 动态组件
- 组件保持存活
- 异步组件
- 依赖注入
- vue应用
- 组合式API
- 选项式API和组合式API
- 响应式
- 计算属性
- 事件处理
- 侦听器
- 生命周期
- 模板引用
- Props
- 事件
- 自定义指令基础
- 全局与局部自定义指令
- 自定义指令钩子函数
- 内置组件
- Transition过渡与动画效果
- Transition使用animation
- TransitionGroup
- keepAlive
- Teleport
- 网络请求
- Axios网络请求_Get
- Axios网络请求_Post
- Axios网络请求——并发请求
- 全局配置
- Axios封装
- 跨域配置
- 创建服务器提供数据
- Vue路由
- Vue路由概念
- 项目引入路由
- 集成Vue路由
- 编程式导航
- 带参数的动态路由匹配{
- 嵌套路由
- 重定向和别名
- 命名路由
- 命名视图
- 不同的历史模式
- 导航守卫
- 路由元信息
- 过渡动效
- 滚动行为
- 路由懒加载
- 动态路由
- 路由高亮
- Vuex状态管理
- Vuex是什么?
- 全局属性管理
- 项目中引入Vuex
- 核心概念-State
- 核心概念-Getter
- 核心概念-Mutation
- 核心概念-Action
- 核心概念-Module
- vuex项目结构
- Vuex插件
- Vuex严格模式
- Vuex表单处理
- Pinia状态管理
- 为什么要使用Pinia?
- 项目中引入Pinia
- 组合式API风格
- 核心概念-state
- 核心概念-修改状态
- 核心概念-Getters
- 核心概念-Actions
- 解构赋值响应式
- Pinia热更新
- 核心概念-插件
- Pinia的数据持久化插件
- UI组件库
- 常见UI组件库
- Vue3加载Element-plus
- Vue3加载Element-plus的字体图标
基础
vue开发前的准备
安装vscode:安装地址
①vscode安装插件
Live Server
Auto Rename Tag
volar
②自动格式化:点击settings
,然后输入format
,然后勾选上Format On Save
。
安装Node.js:安装地址
创建vue项目
在vscode终端中:
npm init vue@latest
安装依赖
npm install
启动开发服务器
npm run dev
vue项目目录结构
模板语法
Vue 使用一种基于 HTML 的模板语法,使我们能够声明式地将其组件实例的数据绑定到呈现的 DOM 上。所有
的 Vue 模板都是语法层面合法的 HTML,可以被符合规范的浏览器和 HTML 解析器解析。
文本差值
最基本的数据绑定形式是文本插值,它使用的是“Mustache”语法(即双大括号):
<template><p>{{ msg }}</p>
</template><script>
export default{data(){return{msg:"神奇的魔法"}}
}
</script>
使用JavaScript表达式
每个绑定仅支持单一表达式,也就是一段能够被求值的JavaScript 代码。一个简单的判断方法是是否可以合法地写在 retum 后面
<template><p>{{ number + 1 }}</p><p>{{ ok ? 'YES' : 'NO'}}</p><p>{{ message.split('').reverse().join('') }}</p>
</template><script>
export default{data(){return{number:10,ok:true,message:"大家好"}}
}
</script>
无效
<!--这是一个语句,而非表达式-->
<p>{{ var a = 1 }}</p>
<!--条件控制也不支持,请使用三元表达式-->
<p>{{ if (ok) {return message} }}</p>
原始HTML
双大括号将会将数据插值为纯文本,而不是 HTML。若想插入 HTML,你需要使用 v-html
指令
<template><p>纯文本:{{ rawHtml }}</p><p>属性:<span v-html="rawHtml"></span></p>
</template><script>
export default{data(){return{rawHtml:"<a href='www.baiducom'>百度</a>"}}
}
</script>
属性绑定
双大括号不能在 HTML attributes 中使用。想要响应式地绑定一个 attribute,应该使用 v-bind
指令
<template><div v-bind:id="dynamicId" v-bind:class="dynamicClass">AppID</div>
</template><script>
export default{data(){return{dynamicId:"appid",dynamicClass:"appclass"}}
}
</script>
v-bind
指令指示 Vue 将元素的 Id
attribute 与组件的 dynamicId 属性保持一致。如果绑定的是 null
或者 undefined
,那么该 attribute 将会从渲染的元素上移除。
简写
因为 v-bind
非常常用,我们提供了特定的简写语法
<div :id="dynamicId" :class="dynamicClass">AppID</div>
布尔型Attribute
布尔型 attribute 依据 true / false 值来决定 attribute 是否应该存在于该元素上, disabled
就是最常见的例子之一
<template><button :disabled="isButtonDisabled">Button</button>
</template><script>
export default{data(){return{isButtonDisabled:true}}
}
</script>
动态绑定多个值
如果你有像这样的一个包含多个 attribute 的JavaScript 对象
<template><div v-bind="objectOfAttrs"></div>
</template><script>
export default{data(){return{objectOfAttrs:{id:'container',class:'wrapper'}}}
}
</script>
条件渲染
在Vue中,提供了条件渲染,这类似于 Javascript 中的条件语句
v-if
v-else
v-else-if
v-show
<template><div v-if="flag">你好</div><div v-else>不好</div><div v-if="type === 'A'">A</div><div v-else-if="type==='B'">B</div><div v-else-if="type==='C'">C</div><div v-else>Not A B C</div><div v-show="flag">flag为true时显示</div>
</template><script>
export default{data(){return{flag:true,type:"D"}}
}
</script>
v-if和v-show
v-if
是"真实的”按条件渲染,因为它确保了在切换时,条件区块内的事件监听器和子组件都会被销毁与重建。v-if
也是情性的:如果在初次染时条件值为 false,则不会做任何事。条件区块只有当条件首次变为 true 时才被渲染。
相比之下,v-show
简单许多,元素无论初始条件如何,始终会被染,只有 CSS display
属性会被切换.
总的来说,v-if
有更高的切换开销,而 v-show
有更高的初始染开销。因此,如果需要频繁切换,则使用
v-show
较好: 如果在运行时绑定条件很少改变,则 v-if
会更合适
列表渲染
我们可以使用 v-for
指令基于一个数组来染一个列表。v-for
指令的值需要使用 item in items
形式的特殊语法,其中 items
是源数据的数组,而 item
是选项的别名
<template><div v-for="item in names">{{ item }}</div>
</template><script>
export default{data(){return{names:["chx","123","abc"]}}
}
</script>
v-for
也支持使用可选的第二个参数表示当前项的位置索引
<template><div v-for="(item,index) in names">{{ item }}:{{ index }}</div>
</template><script>
export default{data(){return{names:["chx","123","abc"]}}
}
</script>
也可以使用of
作为分隔符来替代in
,这更接近JavaScript的迭代器语法
v-for与对象
也可以使用 v-for 来遍历一个对象的所有属性
<template><div v-for="(value,key,index) in userInfo">{{ value }}:{{ key }}:{{ index }}</div>
</template><script>
export default{data(){return{userInfo:{name:"iwen",age:20}}}
}
</script>
通过key管理状态
Vue 默认按照"就地更新”的策略来更新通过 v-for
渲染的元素列表。当数据项的顺序改变时,Vue 不会随之移动 DOM 元素的顺序,而是就地更新每个元素,确保它们在原本指定的索引位置上渲染。
为了给 Vue 一个提示,以便它可以跟踪每个节点的标识,从而重用和重新排序现有的元素,你需要为每个元素对应的块提供一个唯一的 key
attribute:
<template><div v-for="(item,index) in names" :key="index">{{ item }}:{{ index }}</div>
</template><script>
export default{
data(){return{names:["chx","123","abc"]}
}
}
</script>
key
在这里是一个通过 v-bind
绑定的特殊 attribute
推荐在任何可行的时候为 v-for
提供一个 key
attribute
key
绑定的值期望是一个基础类型的值,例如字符串或 number 类型
key的来源
请不要使用 index
作为 key
的值,我们要确保每一条数据的唯一索引不会发生变化
事件处理
我们可以使用 v-on
指令(简写为 @
)来监听 DOM 事件,并在事件触发时执行对应的JavaScript。用法:
v-on:click="methodName”
或@click="handler"
事件处理器的值可以是
内联事件处理器: 事件被触发时执行的内联JavaScript 语句(与 onclick
类似),内联事件处理器通常用于简单场景。
<template><button @click="count++">Add 1</button><p>count is:{{ count }}</p>
</template><script>
export default{
data(){return{count:0}
}
}
</script>
方法事件处理器: 一个指向组件上定义的方法的属性名或是路径
<template><button @click="addCount">Add 1</button><p>count is:{{ count }}</p>
</template><script>
export default{
data(){return{count:0}
},
methods:{addCount(){this.count+=1}
}
}
</script>
事件传参
事件参数可以获取event
对象和通过事件传递参数
获取event对象
<template><button @click="addCount">Add 1</button><p>count is:{{ count }}</p>
</template><script>
export default{
data(){return{count:0}
},
methods:{addCount(e){console.log(e);this.count+=1}
}
}
</script>
传递参数
<template><button @click="addCount('hello')">Add 1</button><p>count is:{{ count }}</p>
</template><script>
export default{
data(){return{count:0}
},
methods:{addCount(message){console.log(message);this.count+=1}
}
}
</script>
传递参数过程中获取event
变成$event
<template><button @click="addCount('hello',$event)">Add 1</button><p>count is:{{ count }}</p>
</template><script>
export default{
data(){return{count:0}
},
methods:{addCount(message,e){console.log(message,e);this.count+=1}
}
}
</script>
事件修饰符
在处理事件时调用 event.preventDefault0
或 evet.stopPropagation0
是很常见的。尽管我们可以直接在方法内调用,但
如果方法能更专注于数据逻辑而不用去处理 DOM 事件的细节会更好
为解决这一问题,Vue 为 v-on
提供了事件修饰符,常用有以下几个:
.stop
.prevent
.once
.enter
阻止默认事件
<template><a @click.prevent="clickHandle" href="www.baidu.com">Add 1</a>
</template><script>
export default{
data(){return{}
},
methods:{clickHandle(){console.log(111);}
}
}
</script>
阻止事件冒泡
<template><div @click="clickDiv"><p @click.stop="clickP">测试冒泡</p></div>
</template><script>
export default{
data(){return{}
},
methods:{clickDiv(){console.log("div");},clickP(){console.log("p");}
}
}
</script>
数组变化侦测
变更方法
Vue 能够侦听响应式数组的变更方法,并在它们被调用时触发相关的更新。这些变更方法包括:
push()
pop()
shift()
unshift()
splice()
sort()
reverse()
<template><button @click="addListHandle">添加数据</button><ul><li v-for="(item,index) of names" :key="index">{{ item }}</li></ul>
</template><script>
export default{
data(){return{names:["chx","123","abc"]}
},
methods:{addListHandle(){this.names.push("sakura")}
}
}
</script>
替换一个数组
变更方法,顾名思义,就是会对调用它们的原数组进行变更。相对地,也有一些不可变(immutable) 方法,例
如filter()
,concat()
和 slice()
,这些都不会更改原数组,而总是返回一个新数组。当遇到的是非变更方法时,我们需要将旧的数组替换为新的
<template><button @click="addListHandle">添加数据</button><ul><li v-for="(item,index) of names" :key="index">{{ item }}</li></ul>
</template><script>
export default{
data(){return{names:["chx","123","abc"]}
},
methods:{addListHandle(){// this.names.push("sakura")this.names = this.names.concat(["sakura"])//将值赋值给原数组使得ui发生变化}
}
}
</script>
计算属性
模板中的表达式虽然方便,但也只能用来做简单的操作。如果在模板中写大多逻辑,会让模板变得臃肿,难以维护。因此我们推荐使用计算属性来描述依赖响应式状态的复杂逻辑
<template><p>{{ pepoleContent }}</p>
</template><script>
export default{
data(){return{pepole:{name:"chx",content:["java",'python','c++']}}
},
computed:{pepoleContent(){return this.pepole.content.length > 0 ? 'Yes' : 'No'}
}
}
</script>
计算属性和方法的区别
计算属性: 计算属性值会基于其响应式依赖被缓存。 一个计算属性仅会在其响应式依赖更新时才重新计算
方法: 方法调用总是会在重渲染发生时再次执行函数
Class绑定
数据绑定的一个常见需求场景是操纵元素的 CSS class 列表,因为 class
是 attribute,我们可以和其他attribute 一样使用 v-bind
将它们和动态的字符串绑定。但是,在处理比较复杂的绑定时,通过拼接生成字符串是麻烦且易出错的。因此,Vue 专门为 class 的 v-bind 用法提供了特殊的功能增强。除了字符串外,表达式的值也可以是对象或数组
绑定对象
<template><div :class="{'active': isActive,'text-danger':hasError}">isActive</div>
</template><script>
export default{
data(){return{isActive:true,hasError:true}
}
}
</script>
多个对象绑定
<template><div :class="classObject">isActive</div>
</template><script>
export default{
data(){return{classObject:{active:true,'text-danger':true}}
}
}
</script>
绑定数组
<template><div :class="[activeClass,errorClass]">isActive</div>
</template><script>
export default{
data(){return{classObject:{activeClass:'active',errorClass:'text-danger'}}
}
}
</script>
若想有条件地渲染某个class,也可以使用三元表达式
<template><div :class="[isActive ? 'active' : '']">isActive</div>
</template><script>
export default{
data(){return{isActive:true}
}
}
</script>
数组和对象
数组和对象嵌套过程中,只能是数组嵌套对象。
<template><div :class="[{'active':isActive},errorClass]">isActive</div>
</template><script>
export default{
data(){return{isActive:true,errorClass:"text-danger"}
}
}
</script>
style绑定
数据绑定的一个常见需求场景是操纵元素的 CSS style列表,因为 stye 是 attribute,我们可以和其他 attribute 一样使用 v-bind
将它们和动态的字符串绑定。但是,在处理比较复杂的绑定时,通过拼接生成字符串是麻烦且易出错的。因此,Vue 专门为 stye 的 v-bind
用法提供了特殊的功能增强。除了字符串外,表达式的值也可以是对象或数组
绑定对象
<template><div :style="{color:activeColor,fontSize:fontSize + 'px'}"></div>
</template><script>
export default{
data(){return{activeColor:'red',fontSize:30}
}
}
</script>
绑定数组
绑定一个包含多个样式对象的数组
<template><div :style="[baseStyles]"></div>
</template><script>
export default{
data(){return{baseStyles:{color:"green",fontSize:"30px"}}
}
}
</script>
侦听器
使用watch
选项在每次响应式属性发生变化时触发一个函数
<template><p>{{ message }}</p><button @click="updateHandle">修改数据</button>
</template><script>
export default{
data(){return{message:"Hello"}
},
methods:{updateHandle(){this.message = "World"}
},
watch:{message(newValue,oldValue){console.log(newValue,oldValue);}
}
}
</script>
表单输入绑定
在前端处理表单时,我们常常需要将表单输入框的内容同步给 javaScript 中相应的变量。手动连接值绑定和更改事件监听器可能会很麻烦, v-model
指令帮我们简化了这一步骤
<template><input type="text" v-model="message" /><p>{{ message }}</p>
</template><script>
export default{
data(){return{message:""}
},
}
</script>
复选框
单一的复选框,绑定布尔类型值
<template><input type="checkbox" id="checkbox" v-model="checked"/><label for="checkbox">{{ checked }}</label>
</template><script>
export default{
data(){return{checked:true}
},
}
</script>
修饰符
v-model
也提供了修饰符: .lazy
、.number
、
.trim
、.lazy
默认情况下,v-model
会在每次 input
事件后更新数据。你可以添加 lazy
修饰符来改为在每次change
事件后更新数据
<template><input type="text" v-model.lazy="message"><p>{{ message }}</p>
</template><script>
export default{
data(){return{message:""}
},
}
</script>
模板引用
虽然 Vue 的声明性渲染模型为你抽象了大部分对 DOM 的直接操作,但在某些情况下,我们仍然需要直接访问底层 DOM 元素。要实现这一点,我们可以使用特殊的 ref
attribute
挂载结束后引用都会被暴露在 this.$refs
之上
<template><div ref="container" class="container">{{ content }}</div><button @click="getElementHandle">获取元素</button>
</template><script>
export default{
data(){return{content:"内容"}
},
methods:{getElementHandle(){console.log(this.$refs.container);//可以获取div标签}
}
}
</script>
组件组成
组件最大的优势就是可复用性
当使用构建步骤时,我们一般会将 Vue 组件定义在一个单独的 .vue
文件中,这被叫做单文件组件(简称 SFC)
组件组成结构
<template><div>承载标签</div>
</template><script>
export default{}
</script>
<style scoped>
</style>
组件引用
<template><!-- 第三步:显示组件 --><MyComponent />
</template><script>
// 第一步:引入组件
import MyComponent from "./components/MyComponent.vue"
export default{//第二步:注入组件components:{MyComponent}
}
</script>
<style scoped>
</style>
scoped
让当前样式只在当前组件中生效
组件嵌套关系
组件允许我们将 UI划分为独立的、可重用的部分,并且可以对每个部分进行单独的思考,在实际应用中,组件常常被组织成层层嵌套的树状结构
这和我们嵌套 HTML 元素的方式类似,Vue 实现了自己的组件模型,使我们可以在每个组件内封装自定义内容与逻辑
组件注册方式
一个Vue 组件在使用前需要先被“注册”,这样 Vue 才能在渲染模板时找到其对应的实现。组件注册有两种方式:全局注册和局部注册
全局注册
import { createApp } from 'vue'
import App from './App.vue'
import GlobalComponent from "./components/GlobalComponent.vue"const app = CreateApp(App);
app.component("GlobalComponent",GlobalComponent)
app.mount('#app')
局部注册
全局注册虽然很方便,但有以下几个问题:
①全局注册,但并没有被使用的组件无法在生产打包时被自动移除(也叫“tree-shaking”)。如果你全局注册了一个组件,即使它并没有被实际使用,它仍然会出现在打包后的JS 文件中
②全局注册在大型项目中使项目的依赖关系变得不那么明确。在父组件中使用子组件时,不太容易定位子组件的实现。和使用过多的全局变量一样,这可能会影响应用长期的可维护性
局部注册需要使用 components
选项
组件传递数据Props(父传子)
组件与组件之间不是完全独立的,而是有交集的,那就是组件与组件之间是可以传递数据的传递数据的。解决方案就是 props
Parent
<template><h3>Parent</h3><Child title="Parent数据"/>
</template><script>
import Child from "./Child.vue"
export default{data(){return{}},components:{Child}
}
</script>
Child
<template><h3>Child</h3><p>{{ title }}</p>
</template><script>
export default{data(){return{}},props:["title"]
}
</script>
动态数据传递
<template><h3>Parent</h3><Child :title="message"/>
</template><script>
import Child from "./Child.vue"
export default{data(){return{message:"动态数据"}},components:{Child}
}
</script>
props
传递数据,只能从父级传递到子级。
组件传递多种数据类型
通过 props
传递数据,不仅可以传递字符串类型的数据,还可以是其他类型,例如: 数字、对象、数组等
但实际上任何类型的值都可以作为 props 的值被传递
组件传递Props效验
Vue 组件可以更细致地声明对传入的 props 的校验要求
componentA
<template><h3>ComponentA</h3><ComponentB :title="title"/>
</template><script>
import ComponentB from "./ComponentB.vue"
export default{data(){return{title:"标题",names:["iwen","ime"]}},components:{ComponentB}
}
</script>
componentB
<template><h3>ComponentB</h3><p>{{ title }}</p><p>{{ age }}</p><p v-for="(item,index) of names" :key="index">{{ item }}</p>
</template><script>
export default{data(){},props:{title:{type:String,//必选项required:true},age:{type:Number,default:0},//数字和字符串可以直接default,但是如果是数组和对象,必须通过工厂函数返回默认值names:{type:Array,default(){return ["空"]}}}
}
</script>
props是只读的。
组件事件(emit子传父)
在组件的模板表达式中,可以直接使用 $emit 方法触发自定义事件
触发自定义事件的目的是组件之间传递数据
Parent
<template><h3>Parent</h3><Child @someEvent="getHandle"/>
</template><script>
import Child from "./Child.vue"
export default{components:{Child},methods:{getHandle(){console.log("触发了");}}
}
</script>
Child
<template><h3>Child</h3><button @click="clickEventHandle">传递数据</button>
</template><script>
export default{methods:{clickEventHandle(){this.$emit("someEvent");}}
}
</script>
组件数据传递(props子传父)
我们之前讲解过了组件之间的数据传递,props
和自定义事件两种方式
props
:父传子
自定义事件: 子传父
除了上述的方案,props
也可以实现子传父
Parent
<template><h3>Parent</h3><Child title="标题" :onEvent="dataFn"/>
</template><script>
import Child from "./Child.vue"
export default{components:{Child},methods:{dataFn(data){console.log(data);}}
}
</script>
Child
<template><h3>Child</h3><p>{{ onEvent('传递数据') }}</p>
</template><script>
export default {data() {return{}},props:{title:String,onEvent:Function}
}
</script>
透传 Attributes
“透传 attribute”指的是传递给一个组件,却没有被该组件声明为 props或emits的attribute或者v-on事件监听器。最常见的例子就是 class 、 style 和 id
当一个组件以单个元素为根作渲染时,透传的 attribute 会自动被添加到根元素上
App
<template><AttrComponents class="attr-container"/>
</template><script>
import AttrComponents from "./components/AttrComponents.vue"
export default{components:{AttrComponents}
}
</script>
<style scoped>
</style>
AttrComponents
<template><!-- 必须是唯一根元素 --><h3>透传属性</h3>
</template><style>
.attr-container{color:red;
}
</style>
禁传属性
<template><!-- 必须是唯一根元素 --><h3>透传属性</h3>
</template>
<script>
export default{inheritAttrs:false
}
</script>
<style>
.attr-container{color:red;
}
</style>
插槽Slot
我们已经了解到组件能够接收意类型的 JavaScript 值作为 props,但组件要如何接收模板内容呢? 在某些场景中,我们可能想要为子组件传递一些模板片段,让子组件在它们的组件中渲染这些片段
App
<template><SlotsBase><!-- 将如下标签的内容显示在SlotsBase中 --><div><h3>插槽标题</h3><P>插槽内容</P></div></SlotsBase>
</template><script>
import SlotsBase from "./components/SlotsBase.vue"
export default{components:{SlotsBase}
}
</script>
<style scoped>
</style>
SlotsBase
<template><slot></slot>
</template>
<slot>
元素是一个插槽出口,标示了父元素提供的插槽内容将在哪里被渲染
渲染作用域
插槽内容可以访问到父组件的数据作用域,因为插槽内容本身是在父组件模板中定义的
父组件的message可以在子组件中显示
<template><SlotsBase><h3>{{ message }}</h3></SlotsBase>
</template><script>
import SlotsBase from "./components/SlotsBase.vue"
export default{data(){return{message:"你好"}},components:{SlotsBase}
}
</script>
<style scoped>
</style>
默认内容
在外部没有提供任何内容的情况下,可以为插槽指定默认内容
<template><slot>插槽默认值</slot>
</template>
具名插槽
App
<template><SlotsBase><template v-slot:header><h3>标题</h3></template><template v-slot:main><p>内容</p></template></SlotsBase>
</template><script>
import SlotsBase from "./components/SlotsBase.vue"
export default{data(){return{message:"你好"}},components:{SlotsBase}
}
</script>
<style scoped>
</style>
SlotsBase
<template><slot name="header">插槽默认值</slot><hr><slot name="main">插槽默认值</slot>
</template>
v-slot
有对应的简写#
,因此<template v-slot:header>
可以简写为 <template #header>
。其意思就是"将这部分模板片段传入子组件的 header 插槽中"
在某些场景下插槽的内容可能想要同时使用父组件域内和子组件域内的数据。要做到这一点,我们需要一种方法来让子组件在渲染时将一部分数据提供给插槽
我们也确实有办法这么做!可以像对组件传递 props 那样,向一个插槽的出口上传递 attributes
app
<template><!-- 拿到子组件传递过来的数据 --><SlotsAttr v-slot="slotProps"><!-- 将数据在子组件插槽中展示 --><h3>{{ currentTest }}-{{ slotProps.msg }}</h3></SlotsAttr>
</template><script>import SlotsAttr from "./components/SlotsAttr.vue"
export default{data(){return{currentTest:"测试内容"}},components:{SlotsAttr}
}
</script>
<style scoped>
</style>
SlotsAttr
<template><h3>Slots</h3><!-- 将childMessage传递给父组件 --><slot :msg="childMessage"></slot>
</template><script>
export default{data(){return{childMessage:"子组件数据"}}
}
</script>
具名插槽传递数据
app
<template><!-- 拿到子组件传递过来的数据 --><SlotsAttr><!-- 将数据在子组件插槽中展示 --><template #header="slotProps"><h3>{{ currentTest }}-{{ slotProps.msg }}</h3></template><template #main="slotProps"><p>{{ slotProps.job }}</p></template></SlotsAttr>
</template><script>import SlotsAttr from "./components/SlotsAttr.vue"
export default{data(){return{currentTest:"测试内容"}},components:{SlotsAttr}
}
</script>
<style scoped>
</style>
SlotsAttr
<template><h3>Slots</h3><!-- 将childMessage传递给父组件 --><slot name="header" :msg="childMessage"></slot><slot name="main" :job="jobMessage"></slot>
</template><script>
export default{data(){return{childMessage:"子组件数据",jobMessage:"chx"}}
}
</script>
组件生命周期
每个 Vue 组件实例在创建时都需要经历一系列的初始化步骤,比如设置好数据侦听,编译模板,挂载实例到DOM,以及在数据改变时更新 DOM。在此过程中,它也会运行被称为生命周期钩子的函数,让开发者有机会在特定阶段运行自己的代码
生命周期示意图
生命周期函数
- 创建期:
beforeCreate
created
(组件创建之前,组件创建之后) - 挂载期:
beforeMounte
mounted
(组件渲染之前,组件渲染之后) - 更新期:
beforeUpdate
updated
(组件更新之前,组件更新之后) - 销毁期:
beforeUnmount
unmounted
(组件销毁之前,组件销毁之后)
动态组件
有些场景会需要在两个组件之间来回切换,比如Tab界面
app
<template><component :is="tabComponent"></component><button @click="changeHandle">切换组件</button>
</template><script>
import ComponentA from "./components/componentA.vue"
import ComponentB from "./components/componentB.vue"
export default{data(){return{tabComponent:ComponentA}},components:{ComponentA,ComponentB},methods:{changeHandle(){this.tabComponent = this.tabComponent == "ComponentA" ? "ComponentB" : "ComponentA"}}
}
</script>
<style scoped>
</style>
组件保持存活
当使用<component :is="...">
来在多个组件间作切换时,被切换掉的组件会被卸载。我们可以通过 <keep-alive>
组件
强制被切换掉的组件仍然保持“存活”的状态
<template><KeepAlive><component :is="tabComponent"></component></KeepAlive><button @click="changeHandle">切换组件</button>
</template><script>
import ComponentA from "./components/componentA.vue"
import ComponentB from "./components/componentB.vue"
export default{data(){return{tabComponent:ComponentA}},components:{ComponentA,ComponentB},methods:{changeHandle(){this.tabComponent = this.tabComponent == "ComponentA" ? "ComponentB" : "ComponentA"}}
}
</script>
<style scoped>
</style>
异步组件
在大型项目中,我们可能需要拆分应用为更小的块,并仅在需要时再从服务器加载相关组件。Vue提供了定义defineAsyncComponent
方法来实现此功能
<template><KeepAlive><component :is="tabComponent"></component></KeepAlive><button @click="changeHandle">切换组件</button>
</template><script>
import ComponentA from "./components/componentA.vue"
//异步加载组件
import { defineAsyncComponent } from "vue";
const ComponentB = defineAsyncComponent(() =>import("./components/componentB.vue")
)
export default{data(){return{tabComponent:ComponentA}},components:{ComponentA,ComponentB},methods:{changeHandle(){this.tabComponent = this.tabComponent == "ComponentA" ? "ComponentB" : "ComponentA"}}
}
</script>
<style scoped>
</style>
依赖注入
通常情况下,当我们需要从父组件向子组件传递数据时,会使用 props
。想象一下这样的结构:有一些多层级嵌套的组件,形成了一颗巨大的组件树,而某个深层的子组件需要一个较远的祖先组件中的部分数据。在这种情况下,如果仅使用 props 则必须将其沿着组件链逐级传递下去,这会非常麻烦
这一问题被称为"prop 逐级透传”
provide
和 inject
可以帮助我们解决这一问题。 一个父组件相对于其所有的后代组件,会作为依赖提供者。任何后代的组件树,无论层级有多深,都可以注入由父组件提供给整条链路的依赖
Provide提供
要为组件后代提供数据,需要使用provide
选项
export default{provide:{message:"爷爷的财产"}
}
也可以读取data
中的数据
export default{data(){return{message:'hello'}},provide(){//使用函数的形式可以访问到`this`return{message:this.message}}
}
除了在一个组件中提供依赖,我们还可以在整个应用层面提供依赖
import { createApp } from 'vue'
const app = createApp({})
app.provide(/*注入名*/'message',/*值*/'hello')
inject注入
要注入上层组件提供的数据,需使用inject
选项来声明
export default{inject:["message"]
}
注入会在组件自身的状态之前被解析,因此可以在data()
中访问到注入的属性
export default{inject:["message"],data(){return{//基于注入值的初始数据fullMessage:this.message}}
}
provide和inject只能由上到下的传递,不能反向传递
vue应用
应用实例
每个Vue应用都是通过createApp函数创建一个新的应用实例
import { createApp } from 'vue'
const app = createApp({//根组件选项
})
根组件
我们传入 ceateApp
的对象实际上是一个组件,每个应用都需要一个“根组件”,其他组件将作为其子组件。
import { createApp } from 'vue'
//从一个单文件组件中导入根组件
import App from './App.vue'
const app = createApp(App)
挂载应用
应用实例必须在调用了 .mount()
方法后才会渲染出来。该方法接收一个“容器"参数,可以是一个实际的 DOM
元素或是一个 CSS 选择器字符串
app.mount('#app')
<div id="app"></div>
公共资源
在 src
目录下的 assets
文件夹的作用就是存放公共资源,例如:图片、公共CSS或者字体图标等
组合式API
选项式API和组合式API
Vue支持两种代码风格,选项式API和组合式API,当然两种代码风格都可以完成一样的功能,不同的是书写风格上的差异
选项式API
使用选项式 API,我们可以用包含多个选项的对象来描述组件的逻辑,例如 data
、methods
和mounted
。选项所定义的属性都会暴露在函数内部的 this上,它会指向当前的组件实例
组合式API
通过组合式 API,我们可以使用导入的 API函数来描述组件逻辑。
<template><button @click="increment"></button>
</template><script>
import {ref,onMounted} from 'vue'
const count = ref(0)
function increment(){count.value++
}onMounted(()=>{console.log(count.value)
})
</script>
两种风格区别
选项式API
①在 vue2.x项目中使用的就是 选项式API写法
②优点:易于学习和使用,写代码的位置已经约定好了
③缺点:代码组织性差,相似的逻辑代码不便于复用,逻辑复杂代码多了不好阅读
组合式API
①在 vue3 中使用的就是 组合API写法
②优点:功能逻辑复杂繁多情况下,各个功能逻辑代码组织再一起,便于阅读和维护
③缺点:需要有良好的代码组织能力和拆分逻辑能力
响应式
选项式API_响应式
<template><p>{{ message }}</p>
</template><script>
export default{data(){return{message:"选项式API 绑定数据"}}
}
</script>
组合式API_响应式
<template><p>{{ message }}</p><p>{{ userInfo.name }}</p>
</template><script>
import {ref,reactive} from 'vue'
export default{setup(){const message = ref("组合式API 绑定数据")const userInfo = reactive({name:"chx"})//外部想访问时,必须return出去才可以return{message,userInfo}}
}
</script>
ref
和reactive
的区别
ref
用于创建基本类型响应数据
reactive
用于创建引用类型响应数据
简约组合式API(setup语法糖)
<template><p>{{ message }}</p><p>{{ userInfo.name }}</p>
</template><script setup>
import {ref,reactive} from 'vue'
const message = ref("组合式API 绑定数据")
const userInfo = reactive({name:"chx"
})
</script>
计算属性
使用选项式API的时候,所有的计算属性都必须放在computed
中,这样如果有很多计算属性就显得很臃肿,但如何使用组合式API,这个问题就迎刃而解了
选项式API
<template><p>{{ message }}</p><p>{{ reverse }}</p>
</template><script>
export default{data(){return{message:"选项式API 绑定数据"}},computed:{reverse(){return this.message.split("").reverse().join("")}}
}
</script>
组合式API
<template><p>{{ message }}</p><p>{{ reverse }}</p>
</template><script setup>
import {ref,computed} from 'vue'const message = ref("组合式API 绑定数据")
const reverse = computed(() =>{//在读取ref数据时,需要添加.valuereturn message.value.split("").reverse.join("")
})
</script>
事件处理
在组合式API中,事件的实现相对比较为简单,与在原生javascript 中有些相似
选项式API
<template><p>{{ count }}</p><button @click="addCountHandle">增加</button>
</template><script>
export default{data(){return{count:0}},methods:{addCountHandle(){this.count++}}
}
</script>
组合式API
<template><p>{{ count }}</p><button @click="addCountHandle">增加</button>
</template><script setup>
import {ref} from 'vue'const count = ref(0)
function addCountHandle(){count.value++
}
</script>
侦听器
选项式API
<template><p>{{ count }}</p><button @click="addCountHandle">增加</button>
</template><script>
export default{data(){return{count:0}},methods:{addCountHandle(){this.count++}},watch:{count(newValue,oldValue){console.log(newValue,oldValue);}}
}
</script>
组合式API
<template><p>{{ count }}</p><button @click="addCountHandle">增加</button>
</template><script setup>
import {ref,watch} from 'vue'const count = ref(0)
function addCountHandle(){count.value++
}
//参数1:代表侦听的数据
watch(count,(newValue,oldValue) =>{console.log(newValue,oldValue);
})
</script>
提取到独立文件
import {watch} from 'vue'export function countUil(count){watch(count,async(newValue,oldValue) =>{console.log(newValue,oldValue);})
}
生命周期
组合式API
<template><p>{{ count }}</p><button @click="addCountHandle">增加</button>
</template><script setup>
import {ref,watch,onMounted} from 'vue'const count = ref(0)
function addCountHandle(){count.value++
}
//参数1:代表侦听的数据
watch(count,(newValue,oldValue) =>{console.log(newValue,oldValue);
})onMounted(()=>{console.log("组件渲染之后");
})
</script>
每个生命周期函数方法,都可以独立的处理一个业务
模板引用
选项式API
<template><p ref="message">组合式API</p>
</template><script setup>
import {onMounted, ref} from 'vue'
//声明一个ref来存放该元素的引用,必须和模板里的ref同名
const message = ref(null)
//不能放在最外层,因为在DOM还没有渲染的时候,这里的代码就已经执行
//必须保证DOM渲染完成之后在去读取DOM
onMounted(()=>{console.log(message.value)message.value.innerHTML = "组合式修改"
})
</script>
Props
Parent
<template><h3>Parent</h3><Child title="传递数据"/>
</template><script setup>
import Child from "./Child.vue"
</script>
Child
<template><h3>Child</h3><p>{{ title }}</p>
</template><script setup>
const props = defineProps({title:{type:String,default:""}
})
</script>
事件
Parent
<template><h3>Parent</h3><Child @onSomeEvent="getMessageHandler"/><p>{{ message }}</p>
</template><script setup>
import Child from "./Child.vue"
import { ref } from "vue"const message = ref("")
function getMessageHandler(data){message.value = data.value
}
</script>
Child
<template><h3>Child</h3><button @click="sendMessageHandler">传递数据</button>
</template><script setup>
import {ref} from "vue"
const emit = defineEmits(["onSomeEvent"])
const message = ref("自定义事件")function sendMessageHandler(){emit("onSomeEvent",message)
}
</script>
自定义指令基础
除了 Vue 内置的一系列指令 (比如 v-model
或 v-show
)之外,Vue 还允许你注册自定义的指令(CustomDirectives)
<template><p v-author>文本信息</p>
</template><script setup>
const vAuthor = {mounted:(element) => {element.innerHTML = element.innerHTML + "chx"}
}
</script>
全局与局部自定义指令
自定义指令是区分全局和局部注册,在全局注册,可以在任意组件中使用,局部注册,只在当前组件中使用
main.js
import './assets/main.css'import { createApp } from 'vue'
//从一个单文件组件中导入根组件
import App from './App.vue'const app = createApp(App)
app.directive("red",{mounted(element){element.style.color = 'red'}
})app.mount('#app')
<template><p v-red>文本信息</p>
</template>
自定义指令钩子函数
自定义指令有很多钩子函数,我们可以理解为是自定义指令的生命周期函数,在不同的情况下会自动调用
<template><p v-red v-if="flag">{{ message }}</p><button @click="updateHandler">修改内容</button><button @click="delHandler">删除元素</button>
</template><script setup>
import {ref} from 'vue'
const message = ref("红色效果")
const flag = ref(true)function updateHandler(){message.value = "修改 红色效果"
}function delHandler(){flag.value = false
}const vRed = {created(){console.log("created");},beforeMount(){console.log("beforeMount");},mounted(){console.log("mount");},beforeUpdate(){console.log("beforeUpdate");},updated(){console.log("update");},beforeUnmount(){console.log("beforeUnmount");},unmounted(){console.log("unmount");}
}
</script>
参数
<template><p v-myShow="flag">{{ message }}</p><button @click="updateHandler">显隐开关</button>
</template><script setup>
import {ref} from 'vue'
const message = ref("模拟v-show指令")
const flag = ref(true)function updateHandler(){flag.value = flag.value === true ? flag.value=false : flag.value=true
}const vMyShow = {updated(el,binding){binding.value === true ? el.style.display='block': el.style.display='none'}
}
</script>
内置组件
Transition过渡与动画效果
Vue 提供了内置组件,,可以帮助你制作基于状态变化的过渡和动画
<Transition>
会在一个元素或组件进入和离开 DOM 时应用动画
<Transiion>
是一个内置组件,这意味着它在任意别的组件中都可以被使用,无需注册
进入或离开可以由以下的条件之一触发:
由 v-if
所触发的切换
由 v-show
所触发的切换
<template><button @click="show = !show">开关</button><Transition><p v-if="show">hello</p></Transition>
</template><script setup>
import {ref} from 'vue'
const show = ref(true)
</script><style scoped>
.v-enter-active,.v-leave-active{transition:opacity 0.5s ease;
}
.v-enter-from,.v-leave-to{opacity: 0;
}
.v-enter-to,.v-leave-from{opacity: 1;
}
</style>
CSS过渡
一共有6个应用于进入与离开过渡效果的CSS class
为过渡效果命名:
<template><button @click="show = !show">开关</button><Transition name="box"><p v-if="show">hello</p></Transition>
</template><script setup>
import {ref} from 'vue'
const show = ref(true)
</script><style scoped>
.box-enter-active,.box-leave-active{transition:opacity 0.5s ease;
}
.box-enter-from,.box-leave-to{opacity: 0;
}
.box-enter-to,.box-leave-from{opacity: 1;
}
</style>
Transition使用animation
<template><button @click="show = !show">开关</button><Transition name="box"><div v-if="show" class="box"></div></Transition>
</template><script setup>
import {ref} from 'vue'
const show = ref(true)
</script><style scoped>
.box{width:200px;height: 200px;background-color: red;
}
.box-enter-active{animation:bounce-in 1s;
}.box-leave-active{animation:bounce-in 1s reverse;
}
/* 实现动画 */
@keyframes bounce-in{0%{transform:scale(0);}50%{transform:scale(1.25);}100%{transform:scale(1.5);}
}
</style>
TransitionGroup
<Transitioncroup>
是一个内置组件,用于对 v-for
列表中的元素或组件的插入、移除和顺序改变添加动画效果
<template><button @click="addHandler">增加</button><TransitionGroup name="list" tag="ul"><li v-for="item in items" :key="item">{{ item }}</li></TransitionGroup>
</template><script setup>
import {reactive} from 'vue'
const items = reactive(["chx","123","ABC"])function addHandler(){items.push("sakura")
}
</script><style scoped>
.list-enter-active,.list-leave-active{transition: all 0.5s ease;
}.list-enter-from,.list-leave-to{opacity:0;transform:translateX(30px);
}
.list-enter-to,.list-leave-from{opacity: 1;
}
</style>
keepAlive
在切换时创建新的组件实例通常是有意义的,但在这个例子中,我们的确想要组件能在被“切走”的时候保留它们的状态。要解决这个问题,我们可以用<KeepAlive>
内置组件将这些动态组件包装起来
包含/排除
<KeepAlive>
默认会缓存内部的所有组件实例,但我们可以通过 incude
和 exclude
prop 来定制该行为
<template><button @click="changeHandler">切换</button><KeepAlive include="ComponentB"><component :is="activeComponent"></component></KeepAlive>
</template>
缓存实例的生命周期
<template><button @click="addCount">countA:{{ count }}</button>
</template><script setup>
import {onActivated, onDeactivated, ref} from "vue"
const count = ref(0)function addCount(){count.value++
}onActivated(()=>{//调用时机为首次挂载//以及每次从缓存中被重新插入时console.log("显示了");
})onDeactivated(()=>{//在从DOM上移除、进入缓存//以及组件卸载时调用console.log("隐藏了")
})
</script>
Teleport
<Teleport>
是一个内置组件,它可以将一个组件内部的一部分模板“传送"到该组件的 DOM 结构外层的位置去。有时我们可能会遇到这样的场景:一个组件模板的一部分在逻辑上从属于该组件,但从整个应用视图的角度来看,它在 DOM 中应该被渲染在整个 Vue 应用外部的其他地方
<Teleport>
提供了一个更简单的方式来解决此类问题,让我们不需要再顾虑 DOM 结构的问题
<template><button @click="open = true">Open Model</button><Teleport to="body"><div v-if="open" class="model"><p>hello</p></div></Teleport>
</template><script setup>
import {ref} from "vue"
const open = ref(false)
</script><style scoped>
.model{position:fixed;z-index:999;top:20%;left:50%;width: 300px;margin-left:-150px;background:#999;padding:20px;text-align:center;
}
</style>
网络请求
Axios网络请求_Get
Axios 是一个基于 promise 的 HTTP 库,可以用在浏览器和 node.js 中
安装Axios
npm install --save axios
Get请求
<template><ul><li v-for="item in bannerObj.banner" :key="item"><h3>{{ item.title }}</h3><p>{{ item.content }}</p></li></ul>
</template><script setup>
import { reactive,onMounted} from 'vue';
import axios from "axios"const bannerObj = reactive({banner:[]
})onMounted(()=>{axios.get("http://iwenwiki.com/api/blueberrypai/getIndexBanner.php").then(res=>{bannerObj.banner = res.data.banner}).catch(error=>{console.log(error)})
})
</script>
带参数的get请求
<template><h3>{{ music.songInfo.song_title }}</h3>
</template><script setup>
import { reactive,onMounted} from 'vue';
import axios from "axios"
const music = reactive({songInfo:{}
})
onMounted(()=>{axios.get("http://iwenwiki.com/api/blueberrypai/getSongInfo.php?song=mo").then(res=>{console.log(res.data);music.songInfo = res.data.songInfo}).catch(error=>{console.log(error)})
})
</script>
<template><h3>{{ music.songInfo.song_title }}</h3>
</template><script setup>
import { reactive,onMounted} from 'vue';
import axios from "axios"
const music = reactive({songInfo:{}
})
onMounted(()=>{axios.get("http://iwenwiki.com/api/blueberrypai/getSongInfo.php",{params:{song:"mo"}}).then(res=>{console.log(res.data);music.songInfo = res.data.songInfo}).catch(error=>{console.log(error)})
})
</script>
Axios网络请求_Post
安装依赖
axios
POST接收的网络请求参数需要进行格式转化
npm install --save querystring
<template><h3>网络请求—POST</h3>
</template><script setup>
import { onMounted} from 'vue';
import axios from "axios"
import qs from "querystring"onMounted(()=>{axios.post("http://iwenwiki.com/api/blueberrypai/login.php",qs.stringify({user_id:"iwen@qq.com",password:"iwen123", verification_code:"crfvw"})).then(res=>{console.log(res.data);}).catch(error=>{console.log(error);})
})
</script>
Axios网络请求——并发请求
在真实场景中,有可能会需要一次性获取多个网络请求的结果处理并发请求的助手函数
axios.all(iterable)
axios.spread(callback)
<template><h3>网络请求—并发请求</h3>
</template><script setup>
import { onMounted} from 'vue';
import axios from "axios"function getBanner(){returnaxios.get("http://iwenwiki.com/api/blueberrypai/getIndexBanner.php")
}function getChating(){returnaxios.get("http://iwenwiki.com/api/blueberrypai/getIndexChating.php")
}
onMounted(()=>{axios.all([getBanner(),getChating()]).then(axios.spread((banner,chating)=>{console.log(banner.data,chating.data);}))
})
</script>
全局配置
全局引用方案一
vue的全局处理方案 app.config.globalPropertis
import './assets/main.css'import { createApp } from 'vue'
//从一个单文件组件中导入根组件
import App from './App.vue'
import axios from "axios"const app = createApp(App)
app.config.globalProperties.$axios = axios
app.mount('#app')
<template><h3>网络请求—并发请求</h3>
</template><script setup>
import { onMounted,getCurrentInstance} from 'vue';
const {proxy} = getCurrentInstance()function getBanner(){returnproxy.$axios.get("http://iwenwiki.com/api/blueberrypai/getIndexBanner.php")
}function getChating(){returnproxy.$axios.get("http://iwenwiki.com/api/blueberrypai/getIndexChating.php")
}
onMounted(()=>{proxy.$axios.all([getBanner(),getChating()]).then(proxy.$axios.spread((banner,chating)=>{console.log(banner.data,chating.data);}))
})
</script>
全局引用方案二
利用provide
和inject
import './assets/main.css'import { createApp } from 'vue'
//从一个单文件组件中导入根组件
import App from './App.vue'
import axios from "axios"const app = createApp(App)
app.provide("axios",axios)
app.mount('#app')
<template><h3>网络请求—并发请求</h3>
</template><script setup>
import { onMounted,getCurrentInstance,inject} from 'vue';
const axios = inject("axios")function getBanner(){returnaxios.get("http://iwenwiki.com/api/blueberrypai/getIndexBanner.php")
}function getChating(){returnaxios.get("http://iwenwiki.com/api/blueberrypai/getIndexChating.php")
}
onMounted(()=>{axios.all([getBanner(),getChating()]).then(axios.spread((banner,chating)=>{console.log(banner.data,chating.data);}))
})
</script>
Axios封装
封装 axios 之后,在使用上会更优雅,我们知道面向对象编程(OOP)遵循,低耦合高内聚。我们把网络请求放在一起更利于后期维护
创建utils
文件夹,创建request.js
文件
import axios from "axios"
import exp from "constants";
import qs from "querystring"
/*** 错误处理* 根据状态和具体的错误信息给出开发者更明确的错误信息* 状态码:2x 3x 4x 5x*/
const errorHandler = (status,info)=>{switch(status){case 400:console.log("语义错误");break;case 401:console.log("服务器认证失败");break;case 403:console.log("服务器请求拒绝执行");break;case 404:console.log("请检查网络请求地址");break;case 500:console.log("服务器发生意外");break;case 502:console.log("服务器无响应");break;default:console.log(info);break;}
}
/*** 创建axios对象*/
const instance = axios.create({//公共配置baseURL:"http://www.baidu.com",timeout:5000
})/*** 拦截器* 发送请求和响应结果之前都可以拦截*/
instance.interceptors.request.use(//成功函数config=>{//config:请求信息//所有的post请求都需要增加一个参数的格式化 querystring.stringify()if(config.method === "post"){config.data = qs.stringify(config.data)}return config;},//失败函数error=>Promise.reject(error)
)instance.interceptors.response.use(response => response.status === 200 ? Promise.resolve(response) : Promise.reject(response),error=>{const { response } = error;if(response){errorHandler(response.status,response.info)}else{console.log("断网了");}}
)export default instance
在api
文件夹中创建base.js
/*** 公共地址和接口路径*/const base = {baseUrl:"http://iwenwiki.com", //公共地址banner:"/api/blueberrypai/getIndexBanner.php",//bannerlogin:"/api/blueberrypai/login.php"
}export default base
index.js
/*** 所有的网络请求的方法都放在这里*/import axios from "../utils/request"
import base from "./base"const api = {getBanner(params){axios.get(base.banner,{params})},getLogin(params){axios.post(base.login,params)}
}export default api
跨域配置
在vite.config.js
中添加如下代码,修改配置文件后要重启服务器:
server:{proxy:{'^/api':{target: 'http://iwenwiki.com',//后端服务实际地址changeOrigin:true,//开启代理rewrite:(path)=>path.replace(/^\/api/,'')}}}
此跨域配置是开发环境下的跨域配置,生产环境是不能用的,生产还依赖后台的配置
创建服务器提供数据
在真实开发场景中,很多项目是前后端同时开发
那么前端就不会第一时间拿到接口数据
但是为了前端开发期间的测试,我们还是需要自己创建服务器的
待编写
Vue路由
Vue路由概念
Vue Router 是 Vue.is 的官方路由。它与 Vue.is 核心深度集成,让用 Vue.is 构建单页应用变得轻而易举
单页面应用也称为SPA(Single Page Application),它主要是网页的界面渲染在一个静态的页面上,当用户要从当前界面跳到另一个界面的时候,在这个过程中,不需要重新加载整个页面,所以页面之间的切换十分快速
多页面在进行页面切换时十分缓慢,在路由进行跳转的时候会加载所有的资源,而且页面重复代码多
对比之下,单页面原理是|avaScript动态修改内容而已,资源只需要局部的刷新,因此SPA具有极高的流畅度,有利于提升用户体验
单页面vs多页面
路由作用
页面与页面之前的切换,是通过不同的地址访问的,地址对应着要显示的组件。而实现这个页面切换显示的功
能就是路由的作用
项目引入路由
用 Vue+Vue Router 创建单页应用非常简单:通过 Vue.is,我们已经用组件组成了我们的应用。当加入 VueRouter 时,我们需要做的就是将我们的组件映射到路由上,让 Vue Router 知道在哪里渲染它们
安装路由
npm install vue-router@4
main.js
import './assets/main.css'import { createApp } from 'vue'
import App from './App.vue'
import { createRouter, createWebHashHistory } from 'vue-router'import Home from "./views/Home.vue"
import About from "./views/About.vue"//2.引入要加载的路由配置routes
//放入页面配置const routes = [{//每个页面对应的路由(URL)path:"/",component:Home},{path:"/about",component:About}
]
//1.创建路由对象
const router = createRouter({routes,//内部提供了history模式的实现。为了简单起见,我们这里使用hash模式history:createWebHashHistory()
})//3.将路由对象挂载到vue中const app = createApp(App)//3.将路由对象挂载到vue中
app.use(router)
app.mount('#app')
app
<template><!-- 导航 --><router-link to="/">首页</router-link> |<router-link to="/about">关于</router-link><!-- 路由对应显示的位置 --><router-view></router-view>
</template><script setup>
</script>
router-view
router-view
将显示与 ur 对应的组件。你可以把它放在任何地方,以适应你的布局
router-link
请注意,我们没有使用常规的a
标签,而是使用一个自定义组件 router-link
来创建链接这使得 Vue Router 可以在不重新加载页面的情况下更改 URL,处理 URL 的生成以及编码
集成Vue路由
如果文件全部放在主入口文件中是不可合理的,所以我们需要提取成为独立的路由文件新项目可以直接选择集成路由在其中
router
文件夹中创建index.js
import { createRouter, createWebHashHistory } from 'vue-router'import Home from "../views/Home.vue"
import About from "../views/About.vue"//2.引入要加载的路由配置routes
//放入页面配置const routes = [{//每个页面对应的路由(URL)path:"/",component:Home},{path:"/about",component:About}
]
//1.创建路由对象
const router = createRouter({routes,//内部提供了history模式的实现。为了简单起见,我们这里使用hash模式history:createWebHashHistory()
})export default router
编程式导航
除了使用 <router-link>
创建a标签来定义导航链接,我们还可以借助 router 的实例方法,通过编写代码来实现在 Vue 实例中,可以通过 $router
访问路由实例。因此你可以调用 this.$router.push
Home.vue
<template><h3>首页</h3><button @click="clickHandler">跳转到about</button>
</template><script setup>
import { useRouter } from "vue-router"const router = useRouter()
function clickHandler() {router.push("/about")
}</script>
带参数的动态路由匹配{
//1.动态增加字段 keypath:"/details/:name",component:Details
}
很多的候,我们需要将给定匹配模式的路由映射到同一个组件。例如,我们可能有一个 User
组件,它应该对所有用户进行渲染,但用户ID 不同。在 Vue Router 中,我们可以在路径中使用一个动态字段来实现,我们称之为 路径参数
router
路由
{//1.动态增加字段 keypath:"/details/:name",component:Details
}
List.vue
<template><h3>列表</h3><ul><li v-for="(item,index) in user.names" :key="index"><!-- 2.增加参数内容 --><router-link :to="'/details/' + item">{{ item }}</router-link></li></ul>
</template><script setup>
import { reactive } from "vue"const user = reactive({names:["chx","abc","123"]
})
</script>
Details.vue
获取参数内容
<template><h3>详情</h3><p>{{ name }}</p>
</template>
<script setup>import { useRoute } from "vue-router"
import { ref } from "vue"const route = useRoute()
const name = ref("")name.value = route.params.name
</script>
可选参数
{//1.动态增加字段 keypath:"/details/:name?",component:Details
}
嵌套路由
一些应用程序的 UI由多层嵌套的组件组成,在这种情况下的片段通常对应于特定的嵌套组件结构
{path:"/news",component:News,children:[{path:"sport",component:SportNews},{path:"yule",component:YuleNews}]}
重定向和别名
默认跳转某个界面
{path:"/news",component:News,redirect:"/newsView/sport",children:[{path:"sport",component:SportNews},{path:"yule",component:YuleNews}]}
别名
{alias:"yl",path:"yule",component:YuleNews
}
命名路由
除了path
之外,还可以为任何路由提供name
。这有以下优点
- 没有硬编码的URL
params
的自动编码/解码- 防止你在url中出现打字错误
{name:"About",path:"/about",component:About
}
<script setup>
import { useRouter } from "vue-router"const router = useRouter()
function clickHandler() {router.push("About")
}</script>
传递参数
<router-link :to="{name:'Details',params:{name:item}}">{{ item }}</router-link>
命名视图
有时候想同时(同级)展示多个视图,而不是嵌套展示,例如创建一个布局,有 sidebar
(侧导航)和 main
(主内容)两个视图,这个时候命名视图就派上用场了。你可以在界面中拥有多个单独命名的视图,而不是只有一个单独的出口。如果router-view
没有设置名字,那么默认为 default
{//每个页面对应的路由(URL)path:"/",component:{default:Home,AD}}
<router-view name="AD"></router-view>
不同的历史模式
在创建路由器实例时,history
配置允许我们在不同的历史模式中进行选择
Hash模式
hash模式是用createWebHashHistory()
创建的
路由形式为:/#/
HTML5模式
用createWebHistory()
创建HTML5模式,推荐使用这个模式
路由形式为:/
当使用这种历史模式时,URL会看起来很"正常",例如 htps://example.com/user/id 。漂亮!
不过,问题来了。由于我们的应用是一个单页的客户端应用,如果没有适当的服务器配置,用户在浏览器中直接访问 https://example.com/user/id ,就会得到一个 404 错误。这就丑了。
不用担心:要解决这个问题,你需要做的就是在你的服务器上添加一个简单的回退路由。如果 URL不匹配任何静态资源,它应提供与你的应用程序中的 index.html 相同的页面。漂亮依旧!
导航守卫
正如其名,vue-router 提供的导航守卫主要用来通过跳转或取消的方式守卫导航。这里有很多方式植入路由导航中:全局的,单个路由独享的,或者组件级的
全局前置守卫
你可以使用 router.beforeEach
注册一个全局前置守卫,当一个导航触发时,就会触发全局前置守卫
router.beforeEach((to,form,next)=>{//next():是否允许跳转next()
})
全局解析守卫
你可以用router.beforeResolve
注册一个全局守卫。这和router.beforeEach
类似
router.beforeResolve((to,from,next)=>{next()
})
全局后置钩子
你可以直接在路由配置上定义beforeEnter
守卫,在导航结束之后触发
router.afterEach((to,from)=>{
})
路由独享的守卫
你可以直接在路由配置上定义beforeEnter
守卫,beforeEnter
守卫只在进入路由时触发
{name:"List",path:"/list",component:List,beforeEnter:(to,from)=>{return true;}
}
组件内的守卫
你可以在路由组件内直接定义路由导航守卫
beforeRouteEnter
beforeRouteUpdate
beforeRouteLeave
<template><h3>首页</h3>
</template><script setup>
import { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router';beforeRouteEnter(to,from){//在渲染该组件的对应路由被验证前调用//不能获取组件实例`this`//因为当守卫执行时,组件实例还没被创建console.log(to,from)}beforeRouteUpdate(to,from){//在当前路由改变,但是该组件被复用时调用//举例来说,对于一个带有动态参数的路径`/user/:id`,在`/user/1`和`/user/2`//之间跳转的时候//由于会渲染同样的`UserDetails`组件,因此组件实例会被复用。而这个钩子//就会在这个情况下被调用。//因为在这种情况发生的时候,组件已经挂载好了,导航守卫可以访问组件实例`this`}beforeRouteLeave(to,from){//在导航离开渲染该组件的对应路由时调用//与 `beforeRouteUpdate`一样,它可以访问组件实例`this`}</script>
完整的导航解析流程
①导航被触发。
②在失活的组件里调用beforeRouteLeave
守 卫。
③调用全局的 beforeEach
守卫。
④在重用的组件里调用 beforeRouteupdate
守卫(2.2+)。
⑤ 在路由配置里调用 beforeEnter
。
⑥解析异步路由组件。
⑦ 在被激活的组件里调用 beforeRouteEnter
⑧调用全局的 beforeResolve
守卫(2.5+)。
⑨导航被确认。
⑩ 调用全局的 afterEach
钩子。
一手课微信:61048621
11. 触发 DOM 更新。
12.调用 beforeRouteEnter
守卫中传给 next
的回调函数,创建好的组件实例会作为回调函数的参数传入
路由元信息
有时,你可能希望将任意信息附加到路由上,如过渡名称、谁可以访问路由等,这些事情可以通过接收属性对象的 meta
属性来实现
定义路由的时候你可以配置 meta
字段,这个 meta
就是路由元信息
过渡动效
想要在你的路径组件上使用转场,并对导航进行动画处理,你需要使用v-slot
API和 Transition
API
<router-view v-slot="{ Component }"><Transition name="fade"><component :is="Component"></component></Transition>
</router-view>
滚动行为
使用前端路由,当切换到新路由时,想要页面滚到顶部,或者是保持原先的滚动位置,就像重新加载页面那样。vue-router能做到,而且更好它让你可以自定义路由切换时页面如何滚动
这个功能只在支持history.pushState的浏览器中可用
const router = createRouter({history: createWebHistory(import.meta.env.BASE_URL),routes:[{path:'/',name:'home',component:HomeView}],scrollBehavior(to,from,savedPosition){if(savedPosition){return savedPosition}else{return {top: 0}}}
})
延迟滚动
scrollBehavior(to,from,savedPosition){return new Promise((resolve,reject)=>{setTimeout(() => {resolve({left:0,top:400})},500)})}
路由懒加载
当打包构建应用时,javaScript包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就会更加高效
/*** 将* import LoginView from "../views/LoginView.vue"* 替换成*/const LoginView = () => import("../views/LoginView.vue")
或
{path:'/login',name:'login',component:()=>import("../views/LoginView.vue")
}
动态路由
对路由的添加通常是通过 routes
选项来完成的,但是在某些情况下,你可能想在应用程序已经运行的时候添加或删除路由
添加路由
用router.addRoute()
新增加路由配置
router.addRoute({path:"/news",name:"News",component:News
})
添加多个路由
const currentRouter = [{paht:"/news",name:"News",component:News},{path:'/about',name:'about',component:About}
]
for(let i = 0;i < currentRouter.length;i++){router.addRoute(currentRouter[i])
}
删除路由
通过路由名字进行删除
router.removeRoute("about")
添加嵌套路由
要将嵌套路由添加到现有的路由中,可以将路由的name作为第一个参数
router.addRoute("News",{path:"yule",component:Yule
})
路由高亮
在实现导航的时候,我们需要给导航添加高亮
active-class
链接激活时,应用于渲染的<a>
的class
<RouterLink active-class="active" to="/">Home</RouterLink> |
<RouterLink active-class="active" to="/about">about</RouterLink>
linkActiveClass
全局配置
const router = createRouter({linkActiveClass:"active"
})
exact-active-class
链接精准激活时,应用于渲染的<a>
的class
<RouterLink exact-active-class="active" to="/">Home</RouterLink>
<RouterLink exact-active-class="active" to="/about">about</RouterLink>
linkExactActiveClass
全局配置
const router = createRouter({linkExactActiveClass:"active"
})
Vuex状态管理
Vuex是什么?
Vuex 是一个专为 Vue.is 应用程序开发的状态管理模式+库。它采用集中式存储管理应用的所有组件的状态并以相应的规则保证状态以一种可预测的方式发生变化
Vuex可以帮我们管理全局的属性,并且是是响应式的,状态的变化是可以跟踪的
什么是状态管理模式?
这个状态自管理应用包含以下几个部分
- 状态,驱动应用的数据源;
- 视图,以声明方式将状态映射到视图
- 操作,响应在视图上的用户输入导致的状态变化
但是,当应用遇到多个组件共享状态时,单向数据流的简洁性很容易被破坏 - 多个视图依赖于同一状态。
- 来自不同视图的行为需要变更同一状态
问题一,传参的方法对于多层嵌套的组件将会非常繁琐,并且对于兄弟组件间的状态传递无能为力。
问题二,我们经常会采用父子组件直接引用或者通过事件来变更和同步状态的多份拷贝。以上的这些模式非常脆弱,通常会导致无法维护的代码
因此,我们为什么不把组件的共享状态抽取出来,以一个全局单例模式管理呢?在这种模式下,我们的组件树构成了一个巨大的“视图”,不管在树的哪个位置,任何组件都能获取状态或者触发行为!
什么情况下应该使用Vuex?
Vuex 可以帮助我们管理共享状态,并附带了更多的概念和框架。这需要对短期和长期效益进行权衡
如果您不打算开发大型单页应用,使用 Vuex 可能是繁琐冗余的。确实是如此–如果您的应用够简单,您最好不要使用 Vuex。一个简单的 store 模式就足够您所需了。但是,如果您需要构建一个中大型单页应用,您很可能会考虑如何更好地在组件外部管理状态,Vuex将会成为自然而然的选择。引用 Redux的作者 DanAbramov 的话说就是:您自会知道什么时候需要它.
全局属性管理
一个用于注册能够被应用内所有组件实例访问到的全局属性的对象
main.js
import './assets/main.css'import { createApp } from 'vue'
import App from './App.vue'const app = createApp(App)
//全局属性
app.config.globalProperties.$num = 10;
app.mount('#app')
<template><p>{{ proxy.$num }}</p>
</template><script setup>
import { getCurrentInstance } from "vue"
const { proxy } = getCurrentInstance()
</script>
Vuex 和单纯的全局对象有以下不同:
全局对象可以更方便的在任意组件读取数据,但是无法达到响应式
Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新
项目中引入Vuex
安装
npm install vuex@next --save
创建Vuex
import './assets/main.css'import { createApp } from 'vue'
import App from './App.vue'
import { createStore } from 'vuex'
const app = createApp(App)
const store = createStore({state:{count:10}
})
app.use(store)
app.mount('#app')
<template><p>{{ $store.state.count }}</p>
</template><script setup>
</script>
核心概念-State
Vuex 使用单一状态树,用一个对象就包含了全部的应用层级状态。至此它便作为一个“唯一数据源"而存在。言这也意味着,每个应用将仅仅包含一个 store 实例
import { createStore } from 'vuex'const store = createStore({state:{count:10}
})export default store
<template><p>{{ $store.state.count }}</p><p>{{ count }}</p>
</template><script setup>
import { computed } from 'vue'
import {useStore} from 'vuex'
const store = useStore()
const count = computed(()=>{return store.state.count
})
</script>
mapState
<template><h3>count</h3><p>{{ $store.state.count }}</p><p>{{ count }}</p>
</template><script>
import { mapState } from 'vuex'
export default{computed:{...mapState(["count"])}
}
</script>
核心概念-Getter
Vuex 允许我们在 store 中定义"getter
可以认为是 store 的计算属性)
import { createStore } from 'vuex'const store = createStore({state:{count:10},getters:{getMyCount(state){return "Count的值为:" + state.count}}
})export default store
<template><p>{{ $store.getters.getMyCount }}</p><p>{{ getCount }}</p>
</template><script setup>
import { computed } from 'vue'
import { useStore } from 'vuex'const store = useStore()const getCount = computed(() =>{return store.getters.getMyCount
})
</script>
mapGetters
<template><p>{{ $store.getters.getMyCount }}</p><p>{{ getCount }}</p>
</template><script>
import { mapGetters } from 'vuex'
export default{computed:{...mapGetters(["getMyCount"]),getCount(){return this.$store.getters.getMyCount}}
}
</script>
核心概念-Mutation
更改 Vuex 的 store 中的状态的唯一方法是提交 mutation。Vuex 中的 mutation 非常类似于事件:每个mutation 都有一个字符串的事件类型(type)和一个回调函数(handler)。这个回调函数就是我们实际进行状态更改的地方
import { createStore } from 'vuex'const store = createStore({state:{count:10},getters:{getMyCount(state){return "Count的值为:" + state.count}},mutations:{increment(state){state.count++},decrement(state){state.count--}}
})export default store
<template><p>{{ $store.getters.getMyCount }}</p><p>{{ getCount }}</p><button @click="addHandler">增加</button><button @click="minHandler">减少</button>
</template><script setup>
import { computed } from 'vue'
import { useStore } from 'vuex'const store = useStore()const getCount = computed(() =>{return store.getters.getMyCount
})function addHandler() {store.commit("increment");
}function minHandler(){store.commit("decrement");
}
</script>
携带参数
你可以向 store.commit
传入额外的参数,即 mutation 的载荷(payload)
mutations:{increment(state,num){state.count += Number(num)},decrement(state,num){state.count -= Number(num)}}
function addHandler() {store.commit("increment",num.value);
}function minHandler(){store.commit("decrement",num.value);
}
对象风格的提交方式
store.commit({type:"increment",num:num.value});
当使用对象风格的提交方式,整个对象都作为载荷传给 mutation 函数
increment(state,{num}){state.count += Number(num)
}
Mutation必须是同步函数
条重要的原则就是要记住 mutation 必须是同步函数
现在想象,我们正在 debug,一个 app 并且观察 devtool 中的 mutation 日志。每一条 mutation 被记录devtools 都需要捕捉到前一状态和后一状态的快照。然而,在上面的例子中 mutation 中的异步函数中的回调让这不可能完成:因为当 mutation 触发的时候,回调函数还没有被调用,devtools 不知道什么时候回调函数实际上被调用一-实质上任何在回调函数中进行的状态的改变都是不可追踪的
辅助函数
你可以在组件中使用 this.sstore.commit('xxx')
提交 mutation,或者使用 mapmutations
辅助函数将组件中的 methods映射为 store.commit
调用(需要在根节点注入 store
)
methods:{...mapMutations(["increment","decrement"]),addHandler(){this.increment({num:this.num})}},minHandler(){this.decrement(this.num)}
核心概念-Action
Action 类似于 mutation,不同在于:
- Action 提交的是 mutation,而不是直接变更状态。
- Action 可以包含任意异步操作。
actions:{asyncIncrement(context,num){context.commit("increment",num)},asyncDecrement(context,num){context.commit("decrement",num)}}
function addHandler() {store.dispatch("asyncIncrement",{num:num.value})
}function minHandler(){store.dispatch("asyncDecrement",num.value)
}
异步操作
①安装依赖
npm install --save axios
import { createStore } from 'vuex'const store = createStore({state:{count:10,banner:[]},getters:{getMyCount(state){return "Count的值为:" + state.count}},mutations:{increment(state,{num}){state.count += Number(num)},decrement(state,num){state.count -= Number(num)},setBanner(state,banner){state.banner = banner;}},actions:{asyncIncrement(context,num){context.commit("increment",num)},asyncDecrement(context,num){context.commit("decrement",num)},asyncSetBanner(context,url){axios.get(url).then(res=>{context.commit("setBanner",res.data.banner)})}}
})export default store
<template><h3>Banner</h3><button @click="clickHandler">获取数据</button><ul><li v-for="(item,index) in $store.state.banner" :key="index"><h3>{{ item.title }}</h3><p>{{ item.content }}</p></li></ul>
</template><script setup>
import {useStore} from 'vuex'const store = useStore()
function clickHandler(){store.dispatch("asyncSetBanner","http://iwenwiki.com/api/blueberrypai/getIndexBanner.php")
}
</script>
Action辅助函数
核心概念-Module
由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿
为了解决以上问题,Vuex 允许我们将 store 分割成模块(module)。每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块–从上至下进行同样方式的分割
文件结构
index.js
import { createStore } from "vuex"
import login from "./login"
import order from "./order"const store = createStore({modules:{login,order}
})export default store
login/index.js
export default{state:{token:"HSDIFHO2HOH2O3",}
}
order/index.js
export default{state:{order:[{id:1001,name:"牙膏"},{id:1002,name:"牙刷"}]}
}
login.vue
<template><h3>登录页面</h3><p>{{ token }}</p>
</template><script setup>
import {computed} from 'vue'
import {useStore} from 'vuex'const store = useStore();
const token = computed(()=>{return store.state.login.token
})
</script>
order.vue
<template><h3>订单页面</h3><ul><li v-for="(item,index) in orders"><p>{{ item.name }}</p></li></ul>
</template><script setup>
import { computed } from 'vue'
import { useStore } from 'vuex'const store = useStore();const orders = computed(()=>{return store.state.order.order
})
</script>
模块的局部状态
每个模块拥有自己的state mutation action getter
login/index.js
export default{state:{token:"HSDIFHO2HOH2O3",},getters:{getToken(state,getters,rootState){console.log(getters,rootState);return "当前token值为:" + state.token}},mutations:{setToken(state,token){state.token = token}},actions:{asyncSetToken({state,commit,rootState},token){console.log(state,rootState);commit("setToken",token)}}
}
login.vue
<template><h3>登录页面</h3><p>{{ token }}</p><p>{{ currentToken }}</p><button @click="updateHandler">修改token</button>
</template><script setup>
import {computed} from 'vue'
import {useStore} from 'vuex'const store = useStore();
const token = computed(()=>{return store.state.login.token
})const currentToken = computed(()=>{//state在读取的时候,需要加上module的名字,但是getters不需要return store.getters.getToken;
})function updateHandler() {//store.commit("setToken","WERQQWRQWRD")store.dispatch("asyncSetToken","QWERQRQWDDQE")
}</script>
命名空间
默认情况下,模块内部的 action 和 mutation 仍然是注册在全局命名空间的-----这样使得多个模块能够对同一个 action 或 mutation 作出响应。Getter 同样也默认注册在全局命名空间,但是目前这并非出于功能上的目的(仅仅是维持现状来避免非兼容性变更)。必须注意,不要在不同的、无命名空间的模块中定义两个相同的getter 从而导致错误
如果希望你的模块具有更高的封装度和复用性,你可以通过添加 namespaced:true 的方式使其成为带命名空间的模块。当模块被注册后,它的所有 getter、action 及 mutation 都会自动根据模块注册的路径调整命名
login/index.js
export default{namespaced:true,state:{token:"HSDIFHO2HOH2O3",},getters:{getToken(state,getters,rootState){console.log(getters,rootState);return "当前token值为:" + state.token}},mutations:{setToken(state,token){state.token = token}},actions:{asyncSetToken({state,commit,rootState},token){console.log(state,rootState);commit("setToken",token)}}
}
login.vue
<template><h3>登录页面</h3><p>{{ token }}</p><p>{{ currentToken }}</p><button @click="updateHandler">修改token</button>
</template><script setup>
import {computed} from 'vue'
import {useStore} from 'vuex'const store = useStore();
const token = computed(()=>{return store.state.login.token
})const currentToken = computed(()=>{//state在读取的时候,需要加上module的名字,但是getters不需要//return store.getters.getToken;return store.getters.["login/getToken"]
})function updateHandler() {//store.commit("setToken","WERQQWRQWRD")store.dispatch("asyncSetToken","QWERQRQWDDQE")
}</script>
vuex项目结构
Vuex 并不限制你的代码结构。但是,它规定了一些需要遵守的规则:
①应用层级的状态应该集中到单个 store 对象中
②提交 mutation 是更改状态的唯一方法,并且这个过程是同步的
③异步逻辑都应该封装到 action 里面
只要你遵守以上规则,如何组织代码随你便。如果你的store文件太大,只需将 action、mutation和 getter分割到单独的文件
对于大型应用,我们会希望把 Vuex 相关代码分割到模块中。下面是项目结构示例
Vuex插件
Vuex 的 store 接受 plugins 选项,这个选项暴露出每次 mutation 的钩子。Vuex 插件就是一个函数,它接收store 作为唯一参数
const myPlugin = (store) =>{//当store初始化后调用store.subscribe((mutation,state)=>{//每次mutation之后调用//mutation的格式为{type,payload}})
}
然后像这样使用
const store = createStore({plugins:[myPlugin]
})
开发模式与生产模式
const store = createStore({plugins:process.env.NODE_ENV !== 'production' ? [myPlugin] : []
})
内置Logger插件
Vuex自带一个日志插件用于一般的调试
import { createLogger } from 'vuex'const store = createStore({plugins: [createLogger()]
})
Vuex严格模式
开启严格模式,仅需在创建 store 的时候传入 strict: true
const store = createStore({strict: true
})
在严格模式下,无论何时发生了状态变更且不是由 mutation 函数引起的,将会抛出错误。这能保址所有的状态变更都能被调试工具跟踪到
开发环境与发布环境
不要在发布环境下启用严格模式!严格模式会深度监测状态树来检测不合规的状态变更–请确保在发布环境下关闭严格模式,以避免性能损失
const store = createStore({strict: process.env.NODE_ENV != 'production'
})
Vuex表单处理
如果Vuex中处理的是表单输入框的数据,并且需要双向数据绑定效果该如何实现呢
第一种
<template><input type="text" :value="message" @input="updateMessage"><p>{{ message }}</p>
</template><script setup>
import {computed} from "vue"
import {useStore} from "vuex"const store = useStore();
const message = computed(()=>{return store.state.message
})function updateMessage(e){store.commit("setMessage",e.target.value)
}
</script>
第二种
<template><input type="text" v-model="search"><p>{{ search }}</p>
</template><script setup>
import {computed} from "vue"
import {useStore} from "vuex"const store = useStore();
const search = computed(()=>{get(){return store.state.search},set(value){store.commit("setSearch",value)}
})</script>
Pinia状态管理
为什么要使用Pinia?
Pinia 是 Vue 的存储库,它允许您跨组件/页面共享状态
Pinia 最初是为了探索 Vuex 的下一次迭代会是什么样子,结合了 Vuex5 核心团队讨论中的许多想法。最终我们意识到 Pinia 已经实现了我们在 Vuex5 中想要的大部分内容,并决定实现它 取而代之的是新的建议
与 Vuex 相比,Pinia 提供了一个更简单的 API,具有更少的规范
项目中引入Pinia
在项目中引入Pinia
有两种方案
①在创建项目结构时选中pinia
②创建项目之后,手动下载安装
项目中安装
npm install pinia --save
创建store仓库
import { defineStore } from "pinia"export const useCountStore = defineStore("count",{state:()=>{return{count:10}}
})
加载store
import './assets/main.css'import { createApp } from 'vue'
import App from './App.vue'
import { createPinia } from 'pinia'createApp(App).use(createPinia()).mount('#app')
组件中使用
<template><p>Count:{{ count }}</p>
</template>
<script setup>
import { useCountStore } from './store';
const {count} = useCountStore();
</script>
组合式API风格
import { defineStore } from "pinia"
import {ref,reactive} from "vue"export const useUserInfoStore = defineStore("userinfo",()=>{const userInfo = reactive({name:"chx",age:23})return {userInfo}
})
<template><p>Count:{{ userInfo }}</p>
</template>
<script setup>
import { useUserInfoStore } from './store';
const {userInfo} = useUserInfoStore();
</script>
核心概念-state
大多数时候,state 是 store 的核心部分。我们通常从定义应用程序的状态开始。 在 Pinia 中,状态被定义为返回初始状态的函数
选项式API
如果您不使用 Composition APl,并且使用的是 computed
、methods
、…则可以使用mapStete()
帮助器将状态属性映射为只读计算属性
import { defineStore } from "pinia"export const useCountStore = defineStore("count",{state:()=>{return{count:10}}
})
<template><p>Count:{{ count }}</p>
</template>
<script>
import { computed } from 'vue';
import { useCountStore } from './store';
import {mapState } from "pinia"export default{computed:{...mapState(useCountStore,{count:store=>store.count,})}
}
const {count} = useCountStore();
</script>
核心概念-修改状态
在 pinia
中修改状态要比在 vuex
中修改状态简单的多,因为不用引入额外的概念,直接用 store.counter++
修改store
<template><p>Count:{{ store.count }}</p><button @click="updateClick">修改状态</button>
</template>
<script setup>
import { useCountStore } from './store';
const store = useCountStore();
function updateClick(){store.count++
}
</script>
核心概念-Getters
Getter 完全等同于 Store 状态的计算值
import { defineStore } from "pinia"export const useCountStore = defineStore("count",{state:()=>{return{count:10}},getters:{getCount:(state)=>"当前Count:" + state.count}
})
组合式API直接读取
<template><p>Count:{{ store.count }}</p><p>{{ store.getCount }}</p><button @click="updateClick">修改状态</button>
</template>
<script setup>
import { useCountStore } from './store';
const store = useCountStore();
function updateClick(){store.count++
}
</script>
访问其他getter
import { defineStore } from "pinia"export const useCountStore = defineStore("count",{state:()=>{return{count:10}},getters:{getCount:(state)=>"当前Count:" + state.count,doubleCount(state){return state.count + this.getCount}}
})
核心概念-Actions
Actions 相当于组件中的 methods。它们可以使用 definestore()
中的 actions
属性定义,并且它们非常适合定义业务逻辑
import { defineStore } from "pinia"export const useCountStore = defineStore("count",{state:()=>{return{count:10}},getters:{getCount:(state)=>"当前Count:" + state.count,doubleCount(state){return state.count + this.getCount}},actions:{increment(){this.count++},decrement(){this.count--}}
})
<template><p>Count:{{ store.count }}</p><p>{{ store.getCount }}</p><p>{{ store.doubleCount }}</p><button @click="updateClick">修改状态</button>
</template>
<script setup>
import { useCountStore } from './store';
const store = useCountStore();
function updateClick(){store.increment();
}
</script>
传递参数
actions:{increment(num){this.count += num},decrement(num){this.count -= num}
}
异步操作
import { defineStore } from "pinia"
import axios from "axios"
export const useCountStore = defineStore("count",{state:()=>{return{banner:[]}},actions:{setBanner(url){axios.get(url).then(res=>{this.banner = res.data.banner}).catch(error=>{console.log(error);})}}
})
解构赋值响应式
如果直接从pinia
中解构数据,会丢失响应式
我们可以用storeToRefs
来解决这个问题
<template><p>Count:{{ store.count }}</p><p>{{ store.getCount }}</p><p>{{ store.doubleCount }}</p><button @click="updateClick">修改状态</button>
</template>
<script setup>
import { storeToRefs } from 'pinia';
import { useCountStore } from './store';
const store = useCountStore();
const {count,getCount,doubleCount} = storeToRefs(store);
function updateClick(){store.increment();
}
</script>
Pinia热更新
pinia 支持热模块替换,因此你可以编辑store,并直接在您的应用程序中与它们交互,而无需重新加载页面,允许您保持现有的状态,添加,甚至删除state,action和getter
import { acceptHMRUpdate, defineStore } from "pinia"export const useCountStore = defineStore("count",{state:()=>{return{banner:[]}},getters:{getCount:(state)=>"当前Count:" + state.count,doubleCount(state){return state.count + this.getCount}}
})if(import.meta.hot){import.meta.hot.accept(acceptHMRUpdate(useCountStore,import.meta.hot))
}
核心概念-插件
由于是底层 API,Pinia store可以完全扩展,这一扩展就是插件
- 向 Store 添加新属性
- 定义 Store 时添加新选项
- 为 Store 添加新方法
- 包装现有方法
- 更改甚至取消操作
- 实现本地存储等副作用
- 仅适用于特定 Store
定义插件
export function piniaStoragePlugins({store}){console.log(store.count);store.$subscribe(()=>{console.log(store.count)})
}
使用插件
import './assets/main.css'import { createApp } from 'vue'
import App from './App.vue'
import { createPinia } from 'pinia'
import { piniaStoragePlugins } from './plugins/countResult'
const pinia = createPinia()
pinia.use(piniaStoragePlugins)
createApp(App).use(pinia).mount('#app')
<template><p>Count:{{ store.count }}</p>
</template>
<script setup>
import { useCountStore } from './store';
const store = useCountStore();</script>
本地存储
pinia 有个副作用,就是无法持久化,在浏览器刷新重置之后,会全部回复默认,这时候,我们可以利用插件实现本地持久化存储
仓库:
import { defineStore } from "pinia"export const useCountStore = defineStore("count",{state:()=>{return{count:10}},
})
引入仓库:
import './assets/main.css'import { createApp } from 'vue'
import App from './App.vue'
import { createPinia } from 'pinia'
import { piniaStoragePlugins } from './plugins/countResult'
const pinia = createPinia()
pinia.use(piniaStoragePlugins)
createApp(App).use(pinia).mount('#app')
本地存储:
/utils/storage.js
export function setStorage(key,value){localStorage.setItem(key,value)
}export function getStorage(){return localStorage.getItem(key)
}
实现插件:
import { getStorage,setStorage } from "@/utils/storage";
export function piniaStoragePlugins({store}){if(getStorage("count")){store.count = getStorage("count")}else{setStorage("count",store.count)}store.$subscribe(()=>{setStorage("count",store.count)})
}
Pinia的数据持久化插件
pinia-plugin-persist
安装
npm install --save pinia-plugin-persist
引入持久化插件
import './assets/main.css'import { createApp } from 'vue'
import App from './App.vue'
import { createPinia } from 'pinia'
import piniaPersist from 'pinia-plugin-persist'
const pinia = createPinia()
pinia.use(piniaPersist)
createApp(App).use(pinia).mount('#app')
使用持久化插件
import { defineStore } from "pinia"export const useCountStore = defineStore("count",{state:()=>{return{count:10}},persist:{enabled:true,strategies:[{key:'counts', //自定义key值storage:localStorage, //选择存储方式}]}
})
UI组件库
常见UI组件库
UI组件库为我们在开发中提供一整套的UI设计,不需要我们自己通过CSS一点点设计,这大大提升了开发体验。大部分的U组件库针对的是后台管理系统类型的项目,当然也会存在移动端的U组件库
Vue3加载Element-plus
Element,一套为开发者、设计师和产品经理准备的基于 vue2.0
的桌面端组件库
Element Plus 基于 vue3
,面向设计师和开发者的组件库
安装Element-Plus
npm install element-plus --save
完整引用
如果你对打包后的文件大小不是很在乎,那么使用完整导入会更方便
import './assets/main.css'import { createApp } from 'vue'
import App from './App.vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
const app = createApp(App)
app.use(ElementPlus)
app.mount('#app')
按需导入
按需导入才是我们的最爱,毕竟在真实的应用场景中并不是每个组件都会用到,这会造成不小的浪费
首先你需要安装 unplugin-vue-components
和 unplugin-auto-import
这两款插件
npm install -D unplugin-vue-components unplugin-auto-import
修改vite.config.js
配置文件
import { fileURLToPath, URL } from 'node:url'import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'// https://vitejs.dev/config/
export default defineConfig({plugins: [vue(),AutoImport({resolvers:[ElementPlusResolver()],}),Components({resolvers:[ElementPlusResolver()],}),],resolve: {alias: {'@': fileURLToPath(new URL('./src', import.meta.url))}}
})
Vue3加载Element-plus的字体图标
安装icons
字体图标
npm install @element-plus/icons-vue
全局注册
在项目根目录下,创建plugins
文件夹,在文件夹下创建文件icons.js
文件
import * as components from "@element-plus/icons-vue"export default{install:(app)=>{for(const key in components){const componentConfig = components[key];app.component(componentConfig.name,componentConfig);}},
};
引入文件
在main.js
中引入icons.js
文件
import elementIcon from "./plugins/icons"
app.use(elementIcon)
特别使用提示
在按钮或者菜单中使用字体图标,与上述不同,使用方式如下:
<el-button type="success" :icon="Check" circle></el-button>
import {Check,Edit,Search,Message,Star,Delete} from '@element-plus/icons-vue'