学了React,再学个VUE试试
VUE简介
采用mvvm架构设计的前端框架
View:视图层(UI
ViewModel :业务逻辑层(一切JS
Model:数据层(存储数据及对数据处理
渐进式 学习框架,即不仅支持使用VUE进行项目构建,还支持某一功能的VUE构建,甚至某一HTML页面的VUE构建
VUE的使用方式有如下几种
无需构建步骤,渐进式增强静态HTML
在任何页面中作为Web Components嵌入
单页应用(SPA)
全栈/服务端渲染(SSR)
Jamstack/静态站点生成(SSG)
开发桌面端、移动端、WebGL,甚至是命令行终端中的界面
Vue2 VS Vue3
Vue2采用OptionsAPI,代码较为分散
Vue3采用CompositionAPI,使得代码分明
Vue3新特性
重写了双向数据绑定
2中使用Object.defineProperty
实现
3中使用ES6标准中的Proxy劫持
因为Proxy能够更好的处理数组
VDOM性能瓶颈得到提升
使用patch flag做静态标记,使得对比时不会进行全量对比,从而提升性能
支持Fragments
template
中可以写多个节点了
支持TSX和JSX的写法
增加Suspense和teleport
增加多v-model
支持Tree-Shaking
Composition API
环境配置
nodejs
nvm(nodejs环境管理工具)
vite
官方文档
https://cn.vuejs.org/
VSCode支持
volar
Vue Language Features
TypeScript Vue Plugin
需要注意的是Vue2的代码提示Vetur与Volar存在冲突,需要禁用
Vue
Vue API风格
选项式 API(Vue2)
组合式 API(Vue3)
选项式API
使用包含多个选项的对象来描述组件的逻辑。
例如data
,methods
,mounted
.选项所定义的属性都会暴露在函数内部的this
上,它会指向当前的组件实例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <script setip> export default { data() { return { count: 0 } }, methods: { increment() { this.count++ } }, mounted() { console.log(`The initial count is ${count.value}.`) } } </script> <templare> <button @click="increment"> Count is : {{ count }} </button> </templare>
组合式API
通过组合式API,可以使用导入的API函数来描述组件逻辑
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <script setup> import { ref, onMounted } from 'vue' const count = ref(0) function increment() { count.value++ } onMounted(() => { console.log(`The initial count is ${count.value}.`) }) </script> <templare> <button @click="increment"> Count is : {{ count }} </button> </templare>
API风格选择
当不需要使用构建工具,或者打算在低复杂度的场景 使用Vue时,例如渐进增强的应用场景,采用选项式API
当打算用Vue构建完整的单页应用 时,推荐采用组合式API +单文件组件
Vue项目结构
使用如下命令创建Vue项目时可以很方便的选择为自己的项目添加哪些组件:
例如添加ESLint、Vue Router、Prettier、对TS的支持、对TSX、JSX的支持等等
上述命令将会安装并执行create-vue
命令
随后创建的目录包含以下结构:
1 2 3 4 5 6 7 8 9 10 11 --public //用于存放静态资源,例如浏览器图标,且不会被Vite编译 --src //同样用于存放静态资源 --components //存放组件 App.vue //Vue的全局入口 main.ts //全局TS文件 style.css //脚手架自带样式文件 vite-env.d.ts //声明文件扩充,例如为TS做.vue的文件声明扩充 index.html //vite与webpack不同,使用html作为入口文件,使用es module的形式 package.json //项目描述文件,项目名,版本信息,依赖版本,开发环境版本依赖 tsconfig.json //ts配置文件 vite.vonfig.ts //vite配置文件
使用npm install
命令安装好依赖之后,使用npm run dev
运行即可。
运行命令会首先再当前目录的node_module中寻找是否安装了相应命令软件,如果没有回去全局node_module中继续寻找、如果还是没有则会继续去环境变量中寻找,如果再没有则会报错。
Vue模板语法
Vue使用基于HTML的模板语法,弄个声明式地将其组件实例的数据绑定到呈现的DOM上。所有的Vue模板都是语法层面合法的HTML ,可以呗符合规范的浏览器和HTML解析器解析
文本插值
最基本的数据绑定形式,使用“Mustache”语法(双大括号)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <template> <p> {{ msg }} </p> </template> <script> export default { data() { return { msg:"神奇海螺" } } } </script>
使用JS表达式
每个绑定仅支持单一表达式 ,即能够被求值的JS代码,一个简单的判断方法是是否可以合法地写在return
后面。
需要注意的是,不符合if…else这样的多行表达式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 <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, msg:"神奇海螺" } } } </script>
原始HTML
如果想要使用这样的方式直接插入可被渲染的HTML ,则需要使用v-html
指令
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <template> <p> 纯文本: {{ rawHTML }} </p> <p> 属性: <span v-html="rawHTML"></span> </p> </template> <script> export default { data() { return { rawHTML: "<a href='https://baidu.com'> 百度</a>" } } } </script>
属性绑定
属性需要使用v-bind
指令
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <template> <div v-bind:id="dynamicId" v-bind:class="msg"> 测试 </div> </template> <script> export default { data() { return { msg:"active", dynamicId:"appID" } } } </script>
如果绑定的元素值为null
或者undefined
,那么该属性将从渲染的元素上移除
此外由于v-bind
比较常用,Vue提供了:
语法糖,上面的v-bind
可以直接省略v-bind
写作:
1 <div :id="dynamicId" :class="msg">测试</div>
动态绑定多个属性
可以以直接使用JS对象对属性进行数据绑定:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <template> <div v-bind="objectOfAttrs"> 测试 </div> </template> <script> export default { data() { return { objectOfAttrs: { class:"active", id:"appID" } } } } </script>
条件渲染
类似JS中的条件语句:
v-if
v-else
v-else-if
v-show
v-if
会在指令表达式返回为true时渲染
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <template> <div v-if="flag"> 你能看见我么 </div> </template> <script> export default { data() { return { flg:true, } } } </script>
v-else
可以为v-if
添加一个else区块
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <template> <div v-if="flag"> 你能看见我么 </div> <div v-else> 那你还是看看我吧 </div> </template> <script> export default { data() { return { flg:true, } } } </script>
v-else-if
可连续多次重复使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 <template> <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> </template> <script> export default { data() { return { type:"D", } } } </script>
v-show
用法与v-if
基本相同
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <template> <div v-show="flag"> 你能看见我么 </div> </template> <script> export default { data() { return { flg:true, } } } </script>
v-show与v-if
v-if
确保了在切换状态时,条件块内的时间监听器和子组件都会被销毁与重建 ,同时它也是惰性的,即如果在出自渲染时条件值为false,则不会做任何事,只有当条件首次变为true时才会被渲染。
v-show
元素无论初始条件如何,始终会被渲染,只有CSSdisplay
属性会被切换
总的来说v-if
有更高的切换开销 ,而v-show
有更高的初始渲染开 销。因此,如果需要频繁切换则使用v-show
,如果运行时条件很少改变,则v-if
更合适
列表渲染
使用v-for
指令基于一个数组来渲染一个列表。v-for
指令需要使用item in items
形式的特殊语法,其中items
是源数据的数组,而item
是迭代项的别名
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <template> <div> <p v-for="item in names"> {{ item }} </p> </div> </template> <script> export default { data() { return { names:["A", "B", "C"] } } } </script>
复杂数据
例如请求得到的Json数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 <template> <div v-show="item in result"> <p> {{ item.title }} </p> <img :src="item.avator" alt=""> </div> </template> <script> export default { data() { return { result:[{ "id": 10010, "title": 中国电信 "avator": "https://pic.dianxin.com/avator/10010" },{ "id": 10086, "title": 中国移动 "avator": "https://pic.dianxin.com/avator/10010" }] } } } </script>
v-for
也支持使用可选的位置索引
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <template> <div> <p v-for="(item, index) in names"> {{ item }}:{{ index }} </p> </div> </template> <script> export default { data() { return { names:["A", "B", "C"] } } } </script>
xxxxxxxxxx3 1input::placeholder {2 color: red3}css
v-for
还可以用于遍历对象的所有属性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <template> <div> <p v-for="(value, key, index) in userInfo"> {{ value }}:{{ key }}:{{ index }} </p> </div> </template> <script> export default { data() { return { userInfo:{ name:"iwen", age:20 } } } } </script>
通过key管理状态
Vue默认采用就地更新的策略。
也就是说当渲染一组数据,例如[1, 2, 3]
后,如果我们将数据修改为[1, 3, 2]
,那么Vue会将整个列表重新渲染一遍,而不是移动已经渲染的DOM元素。
但可以通过key
属性为数据提供标识,从而重用或重排现有元素。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <template> <div> <p v-for="(item, index) in names" :key="index"> {{ value }} </p> </div> </template> <script> export default { data() { return { names:["A", "B", "C"] } } } </script>
注意key
在绑定时需要使用一个基础类型的值,例如string或number
推荐为每个item都加上key
并且在工作场景中,不推荐使用index作为key,因为我们无法确定新插入的数据是否会改变原有元素的index-item对应关系(比如在头部插入新数据)
事件处理
可以使用v-on
(简写为@
)来监听DOM事件,并在事件出发时执行对应的JS
事件处理器的值可以是:
内敛事件处理器 :事件被出发时执行的内敛JS语句(与onclick类似)
方法事件处理器 :一个指向组件上定义的方法的属性名或是路径
内联事件处理器
用于简单场景
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <template> <div> <button @click="count++"> Add 1 </button> <p> Count is : {{ count }} </p> </div> </template> <script> export default { data() { return { count:0 } } } </script>
方法事件处理器
项目中常用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 <template> <div> <button @click="addCount()"> Add 1 </button> <p> Count is : {{ count }} </p> </div> </template> <script> export default { data() { return { count:0 } }, methods: { addCount() { this.count+=1 } } } </script>
事件参数
事件参数可以获取event
对象和通过事件传递数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 <template> <div> <button @click="addCount()"> Add 1 </button> <p> Count is : {{ count }} </p> </div> </template> <script> export default { data() { return { count:0 } }, methods: { addCount(e) { console.log(e); this.count+=1 } } } </script>
传递参数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 <template> <div> <button @click="count('hello')"> Add 1 </button> <p> Count is : {{ count }} </p> </div> </template> <script> export default { data() { return { count:0 } }, methods: { addCount(msg) { console.log(msg); this.count+=1 } } } </script>
传递参数的同时可能传递event
,但需要增加$event$
作为参数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 <template> <div> <button @click="count('hello', $event)"> Add 1 </button> <p> Count is : {{ count }} </p> </div> </template> <script> export default { data() { return { count:0 } }, methods: { addCount(msg, e) { console.log(e); console.log(msg); this.count+=1; } } } </script>
事件修饰符
调用事件时通常会调用event.preventDefault()
或event.stopPropagation()
来组织事件冒泡。
如果可以更专注于数据逻辑而不用去处理DOM事件的细节会更方便
为了解决这一问题,Vue为v-on
提供了时间修饰符 ,常用的有以下几种:
.stop
.prevent
.onece
.enter
…
阻止默认事件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 <template> <div> <a @click.prevent="clickHandle" href="https://baidu.com">百度</a> <div @click="clickDiv"> <p @click.stop="clickP"> 测试冒泡 </p> </div> </div> </template> <script> export default { data() { return { count:0 } }, methods: { clickHandle(e) { // 使用.prevent修饰后相当于调用了如下代码 // e.preventDefault(); console.log("点击了"); }, clickDiv() { console.log("点击了Div"); }, clickP(e) { // 使用.stop修饰后相当于调用了如下代码 // e.stopPropagation(); console.log("点击了P"); } } } </script>
动态事件
vue中支持动态事件绑定:
使用[]
将用于管理事件的变量包裹即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <template> <div> <button @[event]="addCount"> Add 1 </button> <p> Count is : {{ count }} </p> </div> </template> <script setup lang='ts'> const event = 'click' let count = 0 const addCount = () => { count++ } </script>
数组变化的侦测
变更方法
Vue能够监听响应式数组的变更方式,并在它们贝调用时出发相关的更新。这些变更方法包括:
push()
pop()
shift()
unshift()
splice
sort()
reverse()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 <template> <div> <p v-for="(item, index) in names" :key="index"> {{ item }} </p> <button @click="clickAddNamesHandle"> 增加数据 </button> </div> </template> <script> export default { data() { return { names: [ "iwen", "ime", "frank" ] } }, methods: { clickAddNamesHandle() { this.names.push("sakura") } } } </script>
替换一个数组
除了使用会对原数组进行更改的变更方法外,有一些不可变(immutable)方法,例如fliter()
,concat()
和slice()
,这些都不会对原数组进行修改,而总是返回一个新数组 。这种情况需要使用新数组替换旧数组
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 <template> <div> <h3> 数组1 </h3> <p v-for="(item, index) in nums1" :key="index"> {{ item }} </p> <h3> 数组2 </h3> <p v-for="(item, index) in nums2" :key="index"> {{ item }} </p> <button @click="filterHandle"> 合并数据 </button> </div> </template> <script> export default { data() { return { nums1:[1,2,3,4,5], nums2:[6,7,8,9,10] } }, methods: { filterHandle() { this.nums1 = this.nums1.concat(this.nums2) } } } </script>
计算属性
在template
中虽然可以使用JS表达式进行一些计算,但在其中屑很多逻辑代码会使template
变得臃肿,难以维护。因此Vue提出计算属性 来解决这一问题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 <template> <div> <h3> {{ baidu.name }} </h3> <p> {{ baidu.content.length > 0 ? "Yes" : "No"}} </p> </div> </template> <script> export default { data() { return { baidu: { name: "百度", content: ["前端", "Java", "python"] } } } } </script>
上述代码在模板中使用了逻辑表达式,一旦出现bug,上述逻辑代码由于与逻辑块分散,难以定位。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 <template> <div> <h3> {{ baidu.name }} </h3> <p> {{ baiduContent }} </p> </div> </template> <script> export default { data() { return { baidu: { name: "百度", content: ["前端", "Java", "python"] } } }, // 计算属性 computed: { baiduContent() { return this.baidu.content.length > 0 ? "Yes" : "No" } } } </script>
计算属性缓存 VS 方法
可以注意到计算属性中仍然使用的时方法来进行表达式运算。
因此在methods
中设计一个函数,然后再模板中调用函数也能达到一样的效果
那么两者有什么区别呢?
重点区别:
计算属性:计算属性值会基于其响应式依赖被缓存。一个计算属性仅会在其响应式依赖更新时才重新计算
方法:方法调用总是 会在重渲染发生时再次执行函数
Class绑定
数据绑定的一个常见需求场景时操纵元素的CSS class列表,因为calss
也是属性,因此可以使用v-bind
将它们和动态的字符串绑定。但是,在处理比较复杂的绑定时,通过拼接生成字符串是麻烦且易错的。因此Vue专门为class
的v-bind
用法提供了特殊的功能增强。除了字符串外,class绑定的值也可以是对象和数组
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <template> <div :class="{ 'active': isActive, 'text-danger': hasError}"> isActive </div> </template> <script> export default { data() { return { isActive: true, hasError: true } } } </script>
多个对象绑定
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <template> <div :class="classObject"> Class </div> </template> <script> export default { data() { return { classObject: { 'active': true, 'text-danger': true } } } } </script>
绑定数组
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <template> <div :class="[activeClass, errorClass]"> isActive </div> </template> <script> export default { data() { return { activeClass: 'active', errorClass: 'text-danger' } } } </script>
数组中也支持使用三目运算符
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <template> <div :class="[isactive ? 'active text-danger' : '']"> isActive </div> </template> <script> export default { data() { return { isActive: true, } } } </script>
数组和对象也能混合使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <template> <div :class="[{'active': isActive}, errorClass]"> </div> </template> <script> export default { data() { return { isActive: true, errorClass: "text-danger" } } } </script>
注意事项
数组和对象嵌套过程中,只能是数组嵌套对象 ,反过来不行
style绑定
数据绑定的一个常见需求场景是操纵元素的CSS style列表,style也可以使用v-bind
进行绑定,但与Class一样,style绑定同样支持对象和数组
需要注意的是和React一样,style中的属性需要使用驼峰命名法 。否则需要使用‘’
包裹
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <template> <div :style="{ color: activeColor, fontSize: fontSize + 'px'}"> Style绑定 </div> </template> <script> export default { data() { return { activeColor: 'red', fontSize: 30 } } } </script>
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <template> <div :style="styleObject"> Style绑定 </div> </template> <script> export default { data() { return { styleObject: { color: 'red', fontSize: '30px' } } } } </script>
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <template> <div :style="[styleObject]"> Style绑定 </div> </template> <script> export default { data() { return { styleObject: { color: 'green', fontSize: '30px' } } } } </script>
不推荐使用数组形式
侦听器
可以通过watch
选项在每次响应式属性发生变化时触发一个函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 <template> <div> <h3> 侦听器 </h3> <P> {{ message }} </P> <button @click="updateHandle"> 修改数据 </button> </div> </template> <script> export default { data() { return { message: "Hello"; } }, methods: { updateHandle() { this.message = World; } }, watch: { message(newValue, oldValue) { console.log(newValue); console.log(oldValue); } } } </script>
注意事项:
watch函数需要与被监听的响应式数据同名
表单输入绑定v-model
前端处理表单时,常需要将表单输入框的内容同步给JS中相应的变量。手动连接值绑定和更改事件侦听器会很麻烦,v-model
简化了这一步骤
输入框
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <template> <input type="text" v-model="message"> <p> {{ message }} </p> </template> <script> export default { data() { return { message: "" } } } </script>
复选框
1 2 3 4 5 6 7 8 9 10 11 12 13 <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
事件后更新数据
例如input
标签默认会在失焦后出发change
事件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <template> <input type="text" v-model.lazy="message"> <p> {{ message }} </p> </template> <script> export default { data() { return { message: "" } } } </script>
Vue3
在Vue3中,v-module具有许多新特性:
支持多个v-module绑定
支持自定义修饰符Modifiers
自定义组件的v-model发生了变化:
接收方式prop由value变为了modelValue
更新事件emit由input变为了update:modelValue
v-bind的.sync修饰符和组件的model选项被移除
父组件给A组件绑定多个v-model并且包含一个自定义修饰符:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 <script setup lang="ts"> import A from './components/A.vue'; const showA = ref<boolean>(true) const dialogText = ref<string>("Ender") </script> <template> <div> <h1>我是父组件</h1> <div>showA: {{ showA }}</div> <hr> <div>dialogText: {{ dialogText }}</div> <div><button @click="showA = !showA">开关</button></div> <hr> <!-- 多v-model时需要使用:指定名称 --> <!-- 使用自定义修饰符isMagic --> <A v-model:textValue.isMagic="dialogText" v-model="showA"></A> </div> </template> <style scoped> </style>
A组件通过props接收绑定的值,并利用emit实现双向绑定:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 <script setup lang="ts"> const props = defineProps<{ // vue3默认使用modelValue接收,vue2中使用value modelValue: boolean, textValue: string, // 获取自定义修饰符的命名是固定的,即“被修饰属性” + “Modifiers” textValueModifiers?: { isMagic: boolean }, }>() // 使用emit的方式操作v-model绑定的变量。命名规定要以update:开头 const emit = defineEmits(['update:modelValue', 'update:textValue']) const closeDialog = () => { // 直接将要修改的值作为参数传递 emit('update:modelValue', false) } const changeInputValue = (e:Event) => { const target = e.target as HTMLInputElement emit('update:textValue', props?.textValueModifiers?.isMagic ? target.value + " Magic" : target.value) } </script> <template> <div v-if="modelValue" class="model"> <div class="close"><button @click="closeDialog">关闭</button></div> <h3>我是子组件model-dialog</h3> <div>内容:<input @input="changeInputValue" :value="textValue" type="text"></div> </div> </template> <style> .model { width: 500px; border: 5px solid #ccc; padding: 10px; } </style>
模板引用(读取DOM)
虽然Vue的声明性渲染模型抽象掉了大部分对DOM的直接操作,但在某些情况下,我们仍然需要直接访问底层DOM元素。此时需要使用ref
属性
挂载结束后引用都会被暴露在this.$refs
之上
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <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.innerHTML = "哈哈哈哈"); } } } </script>
注意事项:
非特殊情况不要操作DOM
因为直接操作DOM很消耗性能
组件基础
由三部分组成:
script
template
style
一般会将Vue组件定义为一个单独的.vue
文件中,这被称为单文件组件(SFC)
单文件组件sfc
1 2 3 4 5 6 7 8 9 10 11 <template> <div> 承载标签 </div> </template> <script> export default {} </script> <!-- scoped: 让当前样式只在当前组件中生效 --> <style scoped> </style>
组件引用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <template> <!-- 显示组件 --> <MyComponen /> <!-- 显示组件时还能使用如下命名方式 --> <my-componen /> </template> <script> // 引入组件 import MyComponent from './component/MyComponent.vue' // 注入组件 export default { components: { MyComponent } } </script> <style scoped> </style>
单文件组件中只能出现一个template
,且setup形式的script
标签块只能有一个
每个组件必须抱恨一个template
组件嵌套关系
组件尝被层层嵌套为树形结构
创建组件及引用关系
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <template> <h3> Header </h3> </template> <script> export default { } </script> <style scoped> h3{ width: 100%; height: 100%; border: 5px solid #999; text-align: center; line-height: 100px; box-sizing: border-box; } </style>
Main
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 <template> <div class="main"> <h3> Main </h3> <Article /> <Article /> </div> </template> <script> import Article from './Article.vue' export default { components: { Article } } </script> <style scoped> .main{ float: left; width: 70%; height: 600px; border: 5px solid #999; box-sizing: border-box; } </style>
Asid
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <template> <div class="aside"> <h3> Aside </h3> </div> </template> <script> export default { } </script> <style scoped> .aside{ float: right; width: 30%; height: 600px; border: 5px solid #999; box-sizing: border-box; } </style>
Article
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 <template> <h3> Article </h3> <Item /> <Item /> <Item /> </template> <script> import Item from "./Item.vue" export default { components: { Item } } </script> <style scoped> h3{ width: 80%; margin: 0 auto; text-align: center; line-height: 100px; box-sizing: border-box; margin-top: 50px; background: #999; } </style>
Item
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <template> <h3> Item </h3> </template> <script> export default { } </script> <style scoped> h3{ width: 80%; margin: 0 auto; text-align: center; line-height: 100px; box-sizing: border-box; margin-top: 10px; background: #999; } </style>
组件注册方式
Vue组件使用前需要先被注册,注册组件有两种方式:
全局注册
在最外层注册一次后,所有子组件中都能引用
1 2 3 4 5 6 7 8 9 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' );
1 2 3 4 5 <template> <h3> 全局应用组件 </h3> </template>
局部注册
全局注册虽然方便,但存在一下问题:
全局注册时,没有被使用的组件也会被打包到项目中去
全局注册在大型项目的使用中,依赖关系会变得不明确,不利于观察父子关系,可维护性低
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <template> <GlobalComponent /> </template> <script> import Item from "./GlobalComponent.vue" export default { GlobalComponent } </script> <style scoped> *{ margin: 0; padding: 0; } </style>
组件参数传递
Vue使用Props
处理组件间通信
Parent
在父组件使用属性的形式,为子组件添加数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 <template> <h3> Parent </h3> <Child title="Parent数据" /> <!-- 此外还可以使用v-bind传递动态数据 --> <Child :activeTitle="message" :names="names" :userInfo="userInfo" /> </template> <script> import Child from "./Child.vue" export default { data() { return{ childMessage: 20, names: ["iwen", "ime", "frank"], userInfo: { name: "iwen"m age: 20 } } }, components: { Child } } </script> <style scoped> </style>
Child
在子组件中使用props
接受这些属性名
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 <template> <h3> Child </h3> <p> {{ title }} </p> <ul> <li v-for="(item, index) of names" :key="index">{{ item }}</li> </ul> <p> {{ userInfo.name }} </p> <p> {{ userInfo.age }} </p> </template> <script> export default { data() { }, props: ["title", "activeTitle", "names", "userInfo"] } </script> <style scoped> </style>
注意事项:
props
传递数据,只能父组件传递给子组件
props类型校验
parent
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <template> <h3> Parent </h3> <Child :title="childMessage" /> </template> <script> import Child from "./Child.vue" export default { data() { return{ childMessage: 20, } }, components: { Child } } </script> <style scoped> </style>
child
子组件中使用type
进行校验,支持多类型
还可以使用default
设置默认值,当没有传递该参数时,直接使用该默认值
如果希望某个参数必须传值,那么可以使用required
将其设置为必选项
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 <template> <h3> Child </h3> <p> {{ title }} </p> </template> <script> export default { data() { }, props: { title: { // 多类校验 type: [String, Number], // 必选项 required: true }, age: { type: Number, // 默认值 default: 0 }, names: { type: Array, // 工厂函数设置默认值 defalut() { return ["Empty"] } } } } </script> <style scoped> </style>
注意事项:
数字和字符串可以直接使用default作为默认值,但数组和对象,必须通过工厂函数返回默认值,即使用default的函数形式进行返回。
其次props
是只读的
组件事件
组件数据传递
透传Attributes(属性继承)
插槽Slots
组件除了数据,还能接受模板内容,某些时候可能需要为子组件传递一些模板片段,让子组件在它们的组件中渲染这些片段
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 <template> <h3> ComponentA </h3> <ComponentB> <template> <h3> 插槽传递试图内容 </h3> </template> </ComponentB> </template> <script> import ComponentB from "./ConponentB.vue" export default { data() { }, components: { ComponentB } } </script> <style scoped> </style>
在子组件中可以使用slot
来显示插入的模板内容
slot
元素是一个插槽出口(slot outlet) ,标示了父元素提供的**插槽内容(slot content)**将在哪里渲染
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <template> <h3> ComponentB </h3> <slot></slot> </template> <script> export default { data() { } } </script> <style scoped> </style>
渲染作用域
插槽内容可以访问到父组件的数据作用域,因为插槽内容本身实在父组件模板中定义的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 <template> <h3> ComponentA </h3> <ComponentB> <template v-slot> <h3> {{ message }} </h3> </template> </ComponentB> </template> <script> import ComponentB from "./ConponentB.vue" export default { data() { return { message: "message在父级" } }, components: { ComponentB } } </script> <style scoped> </style>
插槽默认内容
在外部没有提供任何内容的情况下,可以为插槽指定默认内容
1 2 3 4 5 6 <template> <h3> ComponentB </h3> <slot>插槽默认值</slot> </template>
具名插槽
当我们想要设置多个不同的插槽 时,需要指定名字
在父组件中使用v-slot
来指定插槽名字,也可以使用简写#
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 <template> <h3> ComponentA </h3> <ComponentB> <template v-slot:header> <h3> {{ message }} </h3> </template> <template #main> <h3> {{ message }} </h3> </template> </ComponentB> </template> <script> import ComponentB from "./ConponentB.vue" export default { data() { return { message: "message在父级" } }, components: { ComponentB } } </script> <style scoped> </style>
在子组件中使用name
为插槽命名:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <template> <h3> ComponentB </h3> <slot name="header"></slot> <hr> <slot name="main"></slot> </template> <script> export default { data() { } } </script> <style scoped> </style>
插槽混合数据
slot
除了显示父组件的数据以外,实际上是可以通过其他方式显示子组件的数据的
slot
支持使用类似props
传递属性的方式,向插槽的出口传递数据
父组件可以使用v-slot
中的对象对子组件中传递的数据进行显示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 <template> <h3> ComponentA </h3> <ComponentB> <template v-slot="{ slotProps }"> <h3> {{ message }} - {{ slotProps.msg }} </h3> </template> </ComponentB> </template> <script> import ComponentB from "./ConponentB.vue" export default { data() { return { message: "message在父级" } }, components: { ComponentB } } </script> <style scoped> </style>
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <template> <h3> ComponentB </h3> <slot :msg="childMessage"></slot> </template> <script> export default { data() { return { childMessage: "子组件数据" } } } </script> <style scoped> </style>
对于具名组件,在父组件中=
可以写在名称后进行数据接收:
1 2 3 4 5 6 7 8 9 <template> <ComponentB> <template #header="slotProps"> <h3> {{ message }} - {{ slotProps.msg }} </h3> </template> </ComponentB> </template>
动态插槽
插槽的名称还可以是一个变量,允许我们动态的修改插槽内容插入的位置,例如:
插槽子组件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <template> <h3> ComponentB </h3> <slot name="header"></slot> <hr> <slot name="main"></slot> </template> <script> export default { data() { } } </script> <style scoped> </style>
使用[]
可以放入一个响应式对象来根据其中的值确定插槽的名称
插槽内容父组件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <template> <h3> ComponentA </h3> <ComponentB> <template #[name]> <h3> 插槽传递内容 </h3> </template> </ComponentB> </template> <script setup lang="ts"> import ComponentB from "./ConponentB.vue" import { reactive, ref } from 'vue' let name = ref('header') </script> <style scoped> </style>
此时动态的修改name
中的值即可改变插入的位置。
组件生命周期
其中有如下生命周期比较重要:
在组件的完整生命周期中,可以在这些自动执行的钩子函数中进行一些操作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 <template> <h3> 组件生命周期 </h3> <p> {{ message }} </p> <button @click="updateHandle"> 更新数据 </button> </template> <script> export default { data() { return { message: "更新之前" } }, components: { ComponentB }, methods: { updateHandle() { this.message = "更新之后" } }, beforeCreate() { console.log("组件创建之前") }, create() { console.log("组件创建完成") }, beforeMount() { console.log("组件挂载之前") }, Mounted() { console.log("组件挂载完成") }, beforeUpdate() { console.log("组件更新之前") }, upadte() { console.log("组件更新完成") }, beforeUnmount() { console.log("组件销毁之前") }, unmounted() { console.log("组件销毁完成") } } </script> <style scoped> </style>
例如我们可以在挂载完成之后获取到某个节点的DOM结构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <template> <h3> 生命周期应用 </h3> <p ref = "name"> 阿巴阿巴 </p> </template> <script> export default { beforeMount() { console.log(this.$ref.name); // undefined }, mounted() { console.log(this.$ref.name); } } </script>
可以在组件创建后从网络请求中获取数据,以为创建完成后,data
才会变为可用的状态
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 <template> <h3> 生命周期应用 </h3> <ul> <li v-for="(item, index) of banner" :key="item.key"> <h3> {{ item.title }} </h3> <p> {{ item.content }} </p> </li> </ul> </template> <script> export default { data() { return { banner: [] } }, mounted() { this.banner = [ { "title": "bibubibu", "content": "wahahah", "key": "1" } ] }, mounted() { console.log(this.$ref.name); } } </script>
注意事项
但通常情况下,带有网络请求的页面中,认为结构 比数据 更重要,因此通常网络请求会被放在mounted
中,来达到先渲染结构再获取数据的效果
动态组件
有些场景需要几个组件间来回切换,比如Tab界面
A组件
1 2 3 4 5 6 7 <template> <h3> ComponentA </h3> </template> <script> </script>
B组件
1 2 3 4 5 6 7 <template> <h3> ComponentB </h3> </template> <script> </script>
容器组件可以使用vue提供的component
标签处理多组件切换
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 <template> <h3> 容器 </h3> <component :is="tabComponent"></component> <button @click="changeHandle"> 切换组件 </button> </template> <script> import ComponentA from "./ComponentA.vue" import ComponentB from "./ComponentB.vue" export default { data() { return { tabComponent: "ComponentA" } }, methods: { changeHandle(){ this.tabComponent = this.tabComponent == "ComponentA" ? "ComponentB" : "ComponentA" } } components: { ComponentA, ComponentB } } </script>
组合式API的写法
使用组合式API中的steup
语法糖可以省略选项式API中的注册组件步骤,但要将组件变为响应式对象:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 <template> <div style="display:flex"> <div @click="switchCom(item, index)" :class="[active == index ? 'active' : '']" class="tab" v-for="(item, index) in data" :key="index" > <div> {{ item.name }} </div> </div> </div> <component :is="comId"></component> <button @click="changeHandle"> 切换组件 </button> </template> <script setup lang='ts'> import { ref, reactive, markRaw, shallowRef } from 'vue' import ComponentA from "./ComponentA.vue" import ComponentB from "./ComponentB.vue" const comId = shallowRef(ComponentA) const active = ref(0) const data = reactive([ { name: 'A组件', com: markRaw(ComponentA) }, { name: 'B组件', com: markRaw(ComponentB) } ]) const switchCom = (item, index) => { comId.value = item.com active.value = index } </script> <style scoped lang="less"> .active{ background: skyblue; } .tabs{ border: 1px solid #ccc; padding: 5px 10px; margin: 5px; } </style>
注意事项:
此处comId
需要使用shallowRef
注册为响应式对象而不使用reactive, ref
是因为避免由于组件内部的改变导致该组件的切换模块更新
此处reactive
注册的data
list中需要使用markRaw
来避免其中组件内部的改变而触发data
list的更新
对于组件的注册还可以使用类似选项式API的做法,使用字符串代替组件名:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 <template> <div style="display:flex"> <div @click="switchCom(item, index)" :class="[active == index ? 'active' : '']" class="tab" v-for="(item, index) in data" :key="index" > <div> {{ item.name }} </div> </div> </div> <component :is="comId"></component> <button @click="changeHandle"> 切换组件 </button> </template> <script setup lang='ts'> import { ref, reactive } from 'vue' const comId = ref('ComponentA') const active = ref(0) const data = reactive([ { name: 'A组件', com: 'ComponentA' }, { name: 'B组件', com: 'ComponentB' } ]) const switchCom = (item, index) => { comId.value = item.com active.value = index } </script> <script lang='ts'> import ComponentA from "./ComponentA.vue" import ComponentB from "./ComponentB.vue" export default{ components: { ComponentA, ComponentB } } </script> <style scoped lang="less"> .active{ background: skyblue; } .tabs{ border: 1px solid #ccc; padding: 5px 10px; margin: 5px; } </style>
组件保持存活
在直接使用<component :is="tabComponent"></component>
时,被切换掉的组件会被卸载,可以通过<keep-alive>
组件强制将其保持“存活”状态
该组件包含一个include
属性:
1 2 3 4 <keep-alive :include="['A']"> <A v-if="flag"></A> <B v-else></B> </keep-alive>
可以使用组件文件名字符串的形式来指定开启哪个组件的缓存,这样除了被开启的组件以外,其他组件都将在切换时被卸载
包含一个exclude
属性:
包含一个max
属性:
注意事项:
开启keep-alive的组件会多两个生命周期:
onActivated
onDeactivated
组件被卸载
A组件
组件被切换之后,出发组件的卸载相关钩子函数,控制台会显示组件卸载的信息
需要注意的是,组件卸载后重新挂载时,数据信息会变为初始值。因此切换前将老数据
变为新数据
后,切换后仍然显示老数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 <template> <h3> ComponentA </h3> <p> {{ message }} </p> <button @click="updateHandle"> 更新数据 </button> </template> <script> export default { beforeUnmount() { console.log("组件卸载之前"); }, unmounted() { console.log("组件卸载完成") }, methods: { updateHandle() { this.message = "新数据" } } } </script>
B组件
1 2 3 4 5 6 7 <template> <h3> ComponentB </h3> </template> <script> </script>
容器组件
当我们使用keep-alive
组件将component
组件包裹时,切换后组件则不会进入卸载生命周期
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 <template> <h3> 容器 </h3> <keep-live> <component :is="tabComponent"></component> </keep-live> <button @click="changeHandle"> 切换组件 </button> </template> <script> import ComponentA from "./ComponentA.vue" import ComponentB from "./ComponentB.vue" export default { data() { return { tabComponent: "ComponentA" } }, methods: { changeHandle(){ this.tabComponent = this.tabComponent == "ComponentA" ? "ComponentB" : "ComponentA" } } components: { ComponentA, ComponentB } } </script>
异步组件
在大型项目中,可能需要拆分应用为更小的块,这会导致项目包含很多很多组件,因此应付实现在需要的时候从服务器加载相关组件,以避免同时加载许多组件导致响应过慢。Vue提供了defineAsyncComponent
方法来实现这样的异步加载的组件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 <template> <h3> 组件切换 </h3> <keep-live> <component :is="tabComponent"></component> </keep-live> <button @click="changeHandle"> 切换组件 </button> </template> <script> import { defineAsyncComponent } from "vue" import ComponentA from "./ComponentA.vue" // import ComponentB from "./ComponentB.vue" // 异步加载组件 const ComponentB = defineAsyncComponent(() => { import("./ComponentB.vue") }) export default { data() { return { tabComponent: "ComponentA" } }, methods: { changeHandle(){ this.tabComponent = this.tabComponent == "ComponentA" ? "ComponentB" : "ComponentA" } } components: { ComponentA, ComponentB } } </script>
Vue3提供了一个更方便的异步组件渲染方式Suspense
,该组件包含两个插槽,一个是确认要渲染的组件#default
,另一个是临时渲染占位的组件#fallback
注意此处的defineAsyncComponent
中需要使用import的函数形式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 <template> <div class="ender-content"> <div class="ender-content__item"> <waterFallVue :list="list"></waterFallVue> </div> <div class="ender-content__item"> <Suspense> <template #default> <syncVue></syncVue> </template> <template #fallback> <Skeleton></Skeleton> </template> </Suspense> </div> </div> </template> <script setup lang="ts"> import { ref, reactive, defineAsyncComponent } from 'vue' import waterFallVue from '../../components/WaterFall/water-fall.vue' import Skeleton from '../../components/Skeleton/skeleton.vue' const syncVue = defineAsyncComponent(() => import('../../components/SyncCom/index.vue')) const list = [ { height: 300, background: 'red' }, { height: 400, background: 'pink' }, { height: 500, background: 'blue' } ] </script> <style scoped lang="scss"> @include b(content) { flex: 1; background-color: #fff; overflow: auto; overflow-y: hidden; padding: 0 10px; display: flex; flex-direction: column; @include e(item) { flex: 1; overflow: auto; border-bottom: 1px solid #ccc; padding: 10px; } } </style>
此外defineAsyncComponent
除了上述的回调函数写法,还有另外的一种对象写法:
1 2 3 4 5 const syncVue = defineAsyncComponent ({ loadingComponent :, errorComponent :, timeout : })
注意事项:
使用异步组件的好处在于:
不使用异步组件时,使用npm run build打包后会讲所有代码打包为一个js文件
使用异步组件后,打包会讲异步组件单独打包为另一个js
这样讲缩小主js文件的体积,缩短加载的时间,而异步组件对应的js文件只有在使用时才会被加载,优化用户体验。
依赖注入
VUE通过原型链 的方式实现依赖注入,angular则是使用IOC和DI的方式实现
依赖注入用于解决,某一深层的子孙组件需要一个来自距离自己较远的祖先组件的数据。
vue种使用provide
和inject
来解决这一问题,一个父组件对于其所有后代组件,作为依赖提供者 ,所有后代组件,都可以注入 由父组件给整条链路的依赖
需要注意的是如果要传递动态数据,则provide
要使用函数的形式
祖先组件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 <template> <h3> 祖先 </h3> <Parent /> </template> <script> import Parent from "./components/Parent.vue" export default { data() { return{ activeMessage: "祖先的财产" } }, provide: { message: "祖先财产" }, provide() { return { message: this.activeMessage } }, components: { Parent } } </script>
父组件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <template> <h3> Parent </h3> <Child /> </template> <script> import Child from "./components/Child.vue" export default { data() { return{ } }, components: { Child } } </script>
子孙组件
子孙组件如果要将注入的数据作为动态数据,则直接使用this
访问并赋值即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <template> <h3> Child </h3> <p> {{ message }} </p> <p> {{ fullMessage }} </p> </template> <script> export default { inject: ["message"], data() { return{ fullMessage: this.message } } } </script>
注意事项:
依赖注入与props
一样是单向的
传递动态数据
在setup语法糖模式下可以使用provide
的函数形式,可以传递响应式的数据,并且子孙组件对数据的修改也会引起父组件的更新,可以使用readonly
加以限制
父组件:
1 2 3 4 5 <script setup lang="ts"> import { ref, reactive, provide } from 'vue' const message = ref<string>('red'); provide('msg', message) </script>
子组件
1 2 3 4 5 6 7 8 9 <script setup lang="ts"> import { ref, reactive, inject } from 'vue' import type { Ref } from 'vue' const message = inject<Ref<string>>('msg') const change = () => { // 使用非空断言访问 message!.value = 'yellow' } </script>
全局注入
可以在main.js
种直接提供全局数据
min.js
1 2 3 4 5 6 7 8 import { createApp } from 'vue' import App from './App.vue' const app = creare (App )app.provide ("globalData" , "全局数据" ) app.mount ('#app' )
子组件依然使用inject: ["globalData"]
接受即可
Vue应用
应用实例
每个Vue应用通过createApp
函数来创建一个Vue应用实例,使用脚手架创建的项目目录下的main.js
即有如下写法
在一个项目种,有且仅有一个Vue实例对象
1 2 3 4 5 6 import { createApp } from 'vue' const app = creareApp ({ })
根组件
上述代码传入createApp
的对象实际上是一个组件,每个项目都需要一个根组件,其他组件均为其子组件。
1 2 3 4 5 import { createApp } from 'vue' import App from './App,vue' const app = createApp (App )
挂载应用
应用实例必须调用.mount
方法后才能渲染,该方法接受一个容器参数,可以是一个实际的DOM元素或一个CSS选择器字符串
公共资源文件夹
在src
下会有一个assets
文件夹,用来存放公共资源
Vue Router
Vue是组件式的开发,那么与React将面临同样的问题,就是URL如何映射到单个组件上。
因为我们知道浏览器认识的语言只有HTML、CSS、JS,那么路由的作用就是告诉浏览器什么时候该渲染那个组件。
我们先从一个例子来感受一下Vue Router的作用
入门
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <script src ="https://unpkg.com/vue@3" > </script > <script src ="https://unpkg.com/vue-router@4" > </script > <div id ="app" > <h1 > Hello App!</h1 > <p > <router-link to ="/" > Go to Home</router-link > <router-link to ="/about" > Go to About</router-link > </p > <router-view > </router-view > </div >
router-link
此处并没有使用a
标签来处理跳转,原因是Vue Router中的router-link
可以在不重新加载页面的情况下更改URL
router-view
此处显示与url对应的组件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 const Home = { template : '<div>Home</div>' }const About = { template : '<div>About</div>' }const routes = [ { path : '/' , component : Home }, { path : '/about' , component : About }, ] const router = VueRouter .createRouter ({ history : VueRouter .createWebHashHistory (), routes, }) const app = Vue .createApp ({})app.use (router) app.mount ('#app' )
调用app.use(router)
触发第一次导航并且可以在任意组件 中以this.$router
的形式访问它,并且以this.$router
的形式访问当前路由:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 export default { computed : { username ( ) { return this .$route .params .username }, }, methods : { goToDashboard ( ) { if (isAuthenticated) { this .$router .push ('/dashboard' ) } else { this .$router .push ('/login' ) } }, }, }
要在 setup
函数中访问路由,请调用 useRouter
或 useRoute
函数。我们将在 Composition API 中了解更多信息。
在整个文档中,我们会经常使用 router
实例,请记住,this.$router
与直接使用通过 createRouter
创建的 router
实例完全相同。我们使用 this.$router
的原因是,我们不想在每个需要操作路由的组件中都导入路由。
脚手架抽离
事实上在脚手架中配置路由时,我们希望家路由从业务逻辑中抽离,使得维护和扩展时变得方便,例如新建一个Router目录后创建index.js
文件,将路由全部写在此处后,将router抛出:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router' import HomeView from '../views/HomeView.vue' const routes :Array <RouteRecordRaw > = [ { path : '/' , name : 'home' , component : HomeView }, { path : '/about/:id' , name : 'about' , component : () => import ('../views/AboutView.vue' ) } ] const router = createRouter ({ history : createWebHistory (import .meta .env .BASE_URL ), routes }) export default router
带参数的动态路由
有时我们希望路由前往的页面根据不同情况显示不同的效果,例如可能有一个User
组件,它需要对所有用户进行渲染,但用户ID不同,需要显示的ID内容也不同,Vue Router
中可以在路径中使用一个动态字段来实现,称之为路径参数
1 2 3 4 5 6 7 8 9 const User = { template : '<div>User</div>' , } const routes = [ { path : '/users/:id' , component : User }, ]
那么现在如果有两个用户:johnny
、jolyne
那么以下两个路径将会映射到同一个组件:
/users/johnny
/users/jolyne
路径参数使用:
表示,当一个路由被匹配时,params
值将在每个组件中以this.$router.params
的形式暴露出来,因此在User组件的模板中我们可以这样显示:
1 2 3 const User = { template : '<div>User {{ $route.params.id }}</div>' , }
路径与params的对应关系如下:
模式串
路径
$router.params
/users/:username
/users/eduardo
{username: 'eduardo'}
/users/:username/posts/:postId
/users/eduardo/posts/123
{ username: 'eduardo', postId: '123' }
响应路由参数变化
如果使用上述方式渲染同一组件,那么当发生用户切换时,由于两个路径映射了同一组件,Vue Router使用的策略是直接复用这一组件,而不是销毁后再创建。
那么这也一位置,URL更新了,但是组件的生命周期钩子函数并不会被触发。
因此需要使用侦听器 来对$route.params
来监听其变化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 watch : { "$route.params" : { handler (newValue, oldValue ) { console .log ("previous: " , oldValue) console .log ("to: " , newValue) } } } watch (() => this .$route .params , (toParams, previousParams ) => { })
或者可以使用beforeRouteUpdate
导航守卫,可以取消导航:
1 2 3 4 5 6 7 const User = { template : '...' , async beforeRouteUpdate (to, from ) { this .userData = await fetchUser (to.params .id ) }, }
捕获所有路由或 404 Not found 路由
常规参数只匹配 url 片段之间的字符,用 /
分隔。如果我们想匹配任意路径 ,我们可以使用自定义的 路径参数 正则表达式,在 路径参数 后面的括号中加入 正则表达式 :
1 2 3 4 5 6 const routes = [ { path : '/:pathMatch(.*)*' , name : 'NotFound' , component : NotFound }, { path : '/user-:afterUser(.*)' , component : UserGeneric }, ]
在这个特定的场景中,我们在括号之间使用了自定义正则表达式 ,并将pathMatch
参数标记为可选可重复 。这样做是为了让我们在需要的时候,可以通过将 path
拆分成一个数组,直接导航到路由:
1 2 3 4 5 6 7 8 this .$router .push ({ name : 'NotFound' , params : { pathMatch : this .$route .path .substring (1 ).split ('/' ) }, query : this .$route .query , hash : this .$route .hash , })
高级匹配模式
Vue Router 使用自己的路径匹配语法,其灵感来自于 express
,因此它支持许多高级匹配模式,如可选的参数,零或多个 /
一个或多个,
甚至自定义的正则匹配规则。请查看高级匹配 文档来探索它们。!
路由传参数
使用编程时导航时可以利用query
和parms
两个参数进行传参
两者的区别:
query
传参配置的是 path
,而 params
传参配置的是name
,在 params
中配置 path
无效
query
在路由配置不需要设置参数,而 params
必须设置
query
传递的参数会显示在地址栏中
params传参 刷新会无效,但是 query 会保存传递过来的值,刷新不变 ;
路由配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 router.push ({ path : "/reg" , query : { name : "jj" } }) import {useRoute} from "vue-router" const route = useRoute ()route.query .name router.push ({ name : "reg" , parms : { name : "jj" } })
路由匹配语法
大多数情况下会使用/about
自定义正则
当定义像 :userId
这样的参数时,内部可以使用([^/])
来从URL中提取参数
例如我们有两个路由:/:orderId
和/:productName
,如果两者恰好匹配了相同的URL,同时我们并不像使用额外的静态路由来区分两个URL,那我们就可以通过两个字段的特点来进行区分:
例如对于orderId
而言,他的取值总是一个数字,而productName
的取值可以是任何形式,所以Vue Router支持在括号中为参数指定一个自定义的正则:
1 2 3 4 5 6 const routes = [ { path : '/:orderOd(\\d+)' }, { path : '/:productName' }, ]
routes中的顺序并不会影响匹配
注意事项:
确保转义反斜杠( \
) ,就像我们对 \d
(变成\\d
)所做的那样,在 JavaScript 中实际传递字符串中的反斜杠字符。
可重复参数
匹配例如/one/two/three
这样的多个序列的地址
1 2 3 4 5 6 const routes = [ { path : '/:chapters+' }, { path : '/:chapters*' }, ]
接受时则需要使用一个数组来接收,并且在使用命名路由时也需要传递一个数组:
1 2 3 4 5 6 7 8 9 router.resolve ({ name : 'chapters' , params : { chapters : [] } }).href router.resolve ({ name : 'chapters' , params : { chapters : ['a' , 'b' ] } }).href router.resolve ({ name : 'chapters' , params : { chapters : [] } }).href
此外也可以与自定义正则 相结合:
1 2 3 4 5 6 7 const routes = [ { path : '/:chapters(\\d+)+' }, { path : '/:chapters(\\d+)*' }, ]
sensitive和strict路由配置
默认情况下路由和标准URL一致不区分大小写,但可以通过strict
和sensitive
来切换,它们可以既可以应用在整个全局路由上,又可以应用于当前路由上:
1 2 3 4 5 6 7 8 9 10 11 12 const router = createRouter ({ history : createWebHistory (), routes : [ { path : '/users/:id' , sensitive : true }, { path : '/users/:id?' }, ], strict : true , })
可修改参数
可以使用?
修饰符将一个参数标记为可选项:
1 2 3 4 5 6 const routes = [ { path : '/users/:userId?' }, { path : '/users/:userId(\\d+)?' }, ]
嵌套路由
UI通常是由多层嵌套的组件构成例如:
1 2 3 4 5 6 7 8 /user/johnny/profile /user/johnny/posts +------------------+ +-----------------+ | User | | User | | +--------------+ | | +-------------+ | | | Profile | | +------------> | | Posts | | | | | | | | | | | +--------------+ | | +-------------+ | +------------------+ +-----------------+
Vue Router
可以使用嵌套路由来表达这种关系
例如上文提到的结构:
1 2 3 <div id="app"> <router-view></router-view> </div>
1 2 3 4 5 6 const User = { template : '<div>User {{ $route.params.id }}</div>' , } const routes = [{ path : '/user/:id' , component : User }]
此处 <router-view></router-view>
将作为一个顶层router-view
。接着我们再向其中嵌套一层router-view
,例如在User组件的模板中添加:
1 2 3 4 5 6 7 8 const User = { template : ` <div class="user"> <h2>User {{ $route.params.id }}</h2> <router-view></router-view> </div> ` ,}
接着需要在路由中配置children
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 const routes = [ { path : '/user/:id' , component : User , children : [ { path : 'profile' , component : UserProfile , }, { path : 'posts' , component : UserPosts , }, ], }, ]
注意,以 /
开头的嵌套路径将被视为根路径。这允许你利用组件嵌套,而不必使用嵌套的 URL。
需要注意的是,使用如上配置后,当我们访问/user/eduardo
时,页面并不能找到对应的组件,因为并没有这一路径的匹配方式。如果希望在父子组件之间显示一些内容的话需要添加一个空的嵌套路径 :
1 2 3 4 5 6 7 8 9 10 11 12 13 const routes = [ { path : '/user/:id' , component : User , children : [ { path : '' , component : UserHome }, ], }, ]
嵌套命名路由
处理命名路由 时,通常回给子路由命名:
1 2 3 4 5 6 7 8 const routes = [ { path : '/user/:id' , component : User , children : [{ path : '' , name : 'user' , component : UserHome }], }, ]
这将确保导航到 /user/:id
时始终显示嵌套路由。
在一些场景中,你可能希望导航到命名路由而不导航到嵌套路由。例如,你想导航 /user/:id
而不显示嵌套路由。那样的话,你还可以命名父路由 ,但请注意重新加载页面将始终显示嵌套的子路由 ,因为它被视为指向路径/users/:id
的导航,而不是命名路由:
1 2 3 4 5 6 7 8 const routes = [ { path : '/user/:id' , name : 'user-parent' , component : User , children : [{ path : '' , name : 'user' , component : UserHome }], }, ]
编程式导航
除了使用 <router-link>
创建 a 标签来定义导航链接,我们还可以借助 router 的实例方法,通过编写代码来实现。
想要导航到不同的 URL,可以使用 router.push
方法。这个方法会向 history 栈添加一个新的记录,所以,当用户点击浏览器后退按钮时,会回到之前的 URL。
当你点击 <router-link>
时,内部会调用这个方法,所以点击 <router-link :to="...">
相当于调用 router.push(...)
:
声明式
编程式
<router-link :to="...">
router.push(...)
该方法的使用方法如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 router.push ('/users/eduardo' ) router.push ({ path : '/users/eduardo' }) router.push ({ name : 'user' , params : { username : 'eduardo' } }) router.push ({ path : '/register' , query : { plan : 'private' } }) router.push ({ path : '/about' , hash : '#team' })
注意 :如果提供了 path
,params
会被忽略,上述例子中的 query
并不属于这种情况。取而代之的是下面例子的做法,你需要提供路由的 name
或手写完整的带有参数的 path
:
1 2 3 4 5 6 7 8 9 10 11 12 const username = 'eduardo' router.push (`/user/${username} ` ) router.push ({ path : `/user/${username} ` }) router.push ({ name : 'user' , params : { username } }) router.push ({ path : '/user' , params : { username } })
当指定 params
时,可提供 string
或 number
参数(或者对于可重复的参数 可提供一个数组)。任何其他类型(如 undefined
、false
等)都将被自动字符串化 。对于可选参数 ,你可以提供一个空字符串(""
)来跳过它。
to
和router.push
接受的对象种类相同,两者规则也相同
router.push
和所有其他导航方法都会返回一个 Promise ,让我们可以等到导航完成后才知道是成功还是失败。
替代当前位置
它的作用类似于 router.push
,唯一不同的是,它在导航时不会向 history 添加新记录,正如它的名字所暗示的那样——它取代了当前的条目。
声明式
编程式
<router-link :to="..." replace>
router.replace(...)
也可以直接在传递给 router.push
的 routeLocation
中增加一个属性 replace: true
:
1 2 3 router.push ({ path : '/home' , replace : true }) router.replace ({ path : '/home' })
使用replace进行跳转将不会留下历史记录(即浏览器无法进行前进后退
横跨历史
该方法采用一个整数作为参数,表示在历史堆栈中前进或后退多少步,类似于 window.history.go(n)
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 router.go (1 ) router.go (-1 ) router.back (1 ) router.go (3 ) router.go (-100 ) router.back (100 ) router.go (100 )
篡改历史
router.push
、router.replace
、router.go
实际上是windows.history
API的模仿
例如使用<router-link replace to="/"></router-link>
标签进行的跳转不会留下历史记录
因此其中很多与Browser History APIs
类似的操作
值得一提的是,无论在创建路由器实例时传递什么样的 history
配置 ,Vue Router 的导航方法( push
、replace
、go
)都能始终正常工作。
命名路由
除了使用path
进行路由以外,还可以使用name
进行路由。
该方式具有如下优点:
没有硬编码的URL
params
的自动编码/解码
防止在url中出现打错字
绕过路径排序(如显示一个)
1 2 3 4 5 6 7 const routes = [ { path : '/user/:username' , name : 'user' , component : User , }, ]
要链接到一个命名的路由,可以向 router-link
组件的 to
属性传递一个对象:
1 2 3 <router-link :to ="{ name: 'user', params: { username: 'erina' }}" > User </router-link >
对应的编程式导航写法如下:
1 2 router.push ({ name : 'user' , params : { username : 'erina' } })
命名视图
有时候想同时 (同级) 展示多个视图,而不是嵌套展示,例如创建一个布局,有 sidebar
(侧导航) 和 main
(主内容) 两个视图,这个时候命名视图就派上用场了。你可以在界面中拥有多个单独命名的视图,而不是只有一个单独的出口。如果 router-view
没有设置名字,那么默认为 default
1 2 3 <router-view class ="view left-sidebar" name ="LeftSidebar" > </router-view > <router-view class ="view main-content" > </router-view > <router-view class ="view right-sidebar" name ="RightSidebar" > </router-view >
一个视图使用一个组件渲染,因此对于同个路由,多个视图就需要多个组件。确保正确使用 components
配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 const router = createRouter ({ history : createWebHashHistory (), routes : [ { path : '/' , components : { default : Home , LeftSidebar , RightSidebar , }, }, ], })
嵌套命名试图
有时候对于更复杂的页面,可能需要使用命名试图创建嵌套视图的复杂布局。例如如下页面:
1 2 3 4 5 6 7 8 9 /settings/emails /settings/profile +-----------------------------------+ +------------------------------+ | UserSettings | | UserSettings | | +-----+-------------------------+ | | +-----+--------------------+ | | | Nav | UserEmailsSubscriptions | | +------------> | | Nav | UserProfile | | | | +-------------------------+ | | | +--------------------+ | | | | | | | | | UserProfilePreview | | | +-----+-------------------------+ | | +-----+--------------------+ | +-----------------------------------+ +------------------------------+
Nav
只是一个常规组件。
UserSettings
是一个视图组件。
UserEmailsSubscriptions
、UserProfile
、UserProfilePreview
是嵌套的视图组件。
其中UserSettings
组件的template
结构如下:
1 2 3 4 5 6 7 <div > <h1 > User Settings</h1 > <NavBar /> <router-view /> <router-view name ="helper" /> </div >
那么接下来可以通过如下的路由配置事项上述布局:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 { path : '/settings' , component : UserSettings , children : [{ path : 'emails' , component : UserEmailsSubscriptions }, { path : 'profile' , components : { default : UserProfile , helper : UserProfilePreview } }] }
重定向与别名
重定向
重定向可以使用routes
配置来完成,例如将/home
重定向到/
:
1 const routes = [{ path : '/home' , redirect : '/' }]
重定向的目标也可以是一个命名的路由:
1 const routes = [{ path : '/home' , redirect : { name : 'homepage' } }]
甚至是一个方法,动态返回重定向目标:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 const routes = [ { path : '/search/:searchText' , redirect : to => { return { path : '/search' , query : { q : to.params .searchText } } }, }, { path : '/search' , }, ]
请注意,导航守卫 并没有应用在跳转路由上,而仅仅应用在其目标上 。在上面的例子中,在 /home
路由中添加 beforeEnter
守卫不会有任何效果。
在写 redirect
的时候,可以省略 component
配置,因为它从来没有被直接访问过,所以没有组件要渲染。唯一的例外是嵌套路由 :如果一个路由记录有 children
和 redirect
属性,它也应该有 component
属性。
相对重定向
也可以重定向到相对位置:
1 2 3 4 5 6 7 8 9 10 11 12 const routes = [ { path : '/users/:id/posts' , redirect : to => { return 'profile' }, }, ]
别名
重定向是指当用户访问 /home
时,URL 会被 /
替换,然后匹配成 /
。那么什么是别名呢?
将 /
别名为 /home
,意味着当用户访问 /home
时,URL 仍然是 /home
,但会被匹配为用户正在访问 /
。
上面对应的路由配置为:
1 const routes = [{ path : '/' , component : Homepage , alias : '/home' }]
通过别名,你可以自由地将 UI 结构映射到一个任意的 URL,而不受配置的嵌套结构的限制。使别名以 /
开头,以使嵌套路径中的路径成为绝对路径。你甚至可以将两者结合起来,用一个数组提供多个别名:
1 2 3 4 5 6 7 8 9 10 11 12 13 const routes = [ { path : '/users' , component : UsersLayout , children : [ { path : '' , component : UserList , alias : ['/people' , 'list' ] }, ], }, ]
如果你的路由有参数,请确保在任何绝对别名中包含它们:
1 2 3 4 5 6 7 8 9 10 11 12 13 const routes = [ { path : '/users/:id' , component : UsersByIdLayout , children : [ { path : 'profile' , component : UserDetails , alias : ['/:id' , '' ] }, ], }, ]
SEO
关于 SEO 的注意事项 : 使用别名时,一定要定义规范链接 .
将props传递给路由组件
在你的组件中使用 $route
会与路由紧密耦合,这限制了组件的灵活性,因为它只能用于特定的 URL。虽然这不一定是件坏事,但我们可以通过 props
配置来解除这种行为:
我们可以将下面的代码
1 2 3 4 const User = { template : '<div>User {{ $route.params.id }}</div>' } const routes = [{ path : '/user/:id' , component : User }]
替换为:
1 2 3 4 5 6 const User = { props : ['id' ], template : '<div>User {{ id }}</div>' } const routes = [{ path : '/user/:id' , component : User , props : true }]
布尔模式
当 props
设置为 true
时,route.params
将被设置为组件的 props
。
命名视图
对于有命名视图的路由,你必须为每个命名视图定义 props
配置:
1 2 3 4 5 6 7 const routes = [ { path : '/user/:id' , components : { default : User , sidebar : Sidebar }, props : { default : true , sidebar : false } } ]
对象模式
当 props
是一个对象时,它将原样设置为组件 props。当 props 是静态的时候很有用。
1 2 3 4 5 6 7 const routes = [ { path : '/promotion/from-newsletter' , component : Promotion , props : { newsletterPopup : false } } ]
函数模式
你可以创建一个返回 props 的函数。这允许你将参数转换为其他类型,将静态值与基于路由的值相结合等等。
1 2 3 4 5 6 7 const routes = [ { path : '/search' , component : SearchUser , props : route => ({ query : route.query .q }) } ]
URL /search?q=vue
将传递 {query: 'vue'}
作为 props 传给 SearchUser
组件。
请尽可能保持 props
函数为无状态的,因为它只会在路由发生变化时起作用。如果你需要状态来定义 props,请使用包装组件,这样 vue 才可以对状态变化做出反应。
不同历史模式
在创建路由器实例时,history
配置允许我们在不同的历史模式中进行选择。
Hash模式
hash 模式是用 createWebHashHistory()
创建的:
1 2 3 4 5 6 7 8 import { createRouter, createWebHashHistory } from 'vue-router' const router = createRouter ({ history : createWebHashHistory (), routes : [ ], })
它在内部传递的实际 URL 之前使用了一个哈希字符(#
)。由于这部分 URL 从未被发送到服务器,所以它不需要在服务器层面上进行任何特殊处理。不过,它在 SEO 中确实有不好的影响 。如果你担心这个问题,可以使用 HTML5 模式。
该模式是通过locations
中的方法实现跳转的:
而浏览器的前进后退事件则是通过:
1 2 3 windows.addEventListener ("hashchange" , (e ) => { console .log (e) })
事件进行监听
HTML5 模式
用 createWebHistory()
创建 HTML5 模式,推荐使用这个模式:
1 2 3 4 5 6 7 8 import { createRouter, createWebHistory } from 'vue-router' const router = createRouter ({ history : createWebHistory (), routes : [ ], })
当使用这种历史模式时,URL 会看起来很 “正常”,例如 https://example.com/user/id
不过,问题来了。由于我们的应用是一个单页的客户端应用,如果没有适当的服务器配置,用户在浏览器中直接访问 https://example.com/user/id
,就会得到一个 404 错误。这就尴尬了。要解决这个问题,你需要做的就是在你的服务器上添加一个简单的回退路由。如果 URL 不匹配任何静态资源,它应提供与你的应用程序中的 index.html
相同的页面。
该模式通过h5新增的对象history
实现路由切换
1 history.pushState ({state : 1 }, "" , "/reg" )
但直接使用该方法进行页面跳转不会触发popstate
事件
监听前进后退事件则是通过popstate
事件进行:
1 2 3 windows.addEventListener ("popstate" , (e ) => { console .log (e) })
服务器配置示例
注意 :以下示例假定你正在从根目录提供服务。如果你部署到子目录,你应该使用Vue CLI 的 publicPath
配置 和相关的路由器的 base
属性 。你还需要调整下面的例子,以使用子目录而不是根目录(例如,将RewriteBase/
替换为 RewriteBase/name-of-your-subfolder/
)。
Apache
1 2 3 4 5 6 7 8 9 10 11 12 <IfModule mod_negotiation.c > Options -MultiViews </IfModule > <IfModule mod_rewrite.c > RewriteEngine On RewriteBase / RewriteRule ^index\.html$ - [L] RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule . /index.html [L] </IfModule >
也可以使用 FallbackResource
代替 mod_rewrite
。
nginx
1 2 3 location / { try_files $uri $uri / /index.html; }
原生Node.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 const http = require ('http' )const fs = require ('fs' )const httpPort = 80 http .createServer ((req, res ) => { fs.readFile ('index.html' , 'utf-8' , (err, content ) => { if (err) { console .log ('We cannot open "index.html" file.' ) } res.writeHead (200 , { 'Content-Type' : 'text/html; charset=utf-8' , }) res.end (content) }) }) .listen (httpPort, () => { console .log ('Server listening on: http://localhost:%s' , httpPort) })
…
Caveat
这有一个注意事项。你的服务器将不再报告 404 错误,因为现在所有未找到的路径都会显示你的 index.html
文件。为了解决这个问题,你应该在你的 Vue 应用程序中实现一个万能的路由来显示 404 页面。
1 2 3 4 const router = createRouter ({ history : createWebHistory (), routes : [{ path : '/:pathMatch(.*)' , component : NotFoundComponent }], })
另外,如果你使用的是 Node.js 服务器,你可以通过在服务器端使用路由器来匹配传入的 URL,如果没有匹配到路由,则用 404 来响应,从而实现回退。查看 Vue 服务器端渲染文档 了解更多信息。
路由守卫
守卫可以在任何位置使用router
进行添加
前置守卫
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 const router = useRouter ()const whiteList = ['/' ]router.forEach ((to, from , next ) => { if (whiteList.includes (to) || localStorage .getItem ("token" )) { next () } else { next ("/" ) } })
后置守卫
后置守卫会在跳转完成之后执行,我们可以利用前置和后置两个守卫来实现一个加载进度条的功能:
先准备好进进度条组件:
loadingBar.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 <template> <div class="wraps"> <div ref="bar" class="bar"></div> </div> </template> <script setup lang='ts'> import { ref, onMounted } from 'vue' let speed = ref<number>(1) let bar = ref<HTMLElement>() let timer = ref<number>(0) const startLoading = () => { let dom = bar.value as HTMLElement; speed.value = 1 // 使用Windows.requestAnimationFrame不会触发多次回流与重绘,浏览器会收集这些操作,并最最后触发一次重绘,并且会以60帧显示 // 如果使用interval每一次调用都会触发回流与重绘 timer.value = window.requestAnimationFrame(function fn() { if (speed.value < 90) { speed.value += 1; dom.style.width = speed.value + '%' timer.value = window.requestAnimationFrame(fn) } else { speed.value = 1; // 使用cancleAnimationFrame取消事件绑定 window.cancelAnimationFrame(timer.value) } }) } const endLoading = () => { let dom = bar.value as HTMLElement; setTimeout(() => { window.requestAnimationFrame(() => { speed.value = 100; dom.style.width = speed.value + '%' }) }, 500) } // 向外暴露两个方法用于开启和结束进度条 defineExpose({ startLoading, endLoading }) </script> <style scoped lang="less"> .wraps { position: fixed; top: 0; width: 100%; height: 2px; .bar { height: inherit; width: 0; background: blue; } } </style>
main.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import loadingBar from './components/loadingBar.vue' import {createVNode, render} from "vue" const router = useRouter ()const vNode = createVNode (loadingBar)render (vNode, document .body )router.beforeEach ((to, from , next ) => { vNode.component ?.exposed ?.startLoading () }) router.afterEach ((to, from ) => { vNode.component ?.exposed ?.endLoading () })
路由原信息
通过路由记录的 meta
属性可以定义路由的元信息 。使用路由元信息可以在路由中附加自定义的数据,例如:
权限校验标识。
路由组件的过渡名称。
路由组件持久化缓存 (keep-alive) 的相关配置。
标题名称
我们可以在导航守卫 或者是路由对象 中访问路由的元信息数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 declare module 'vue-router' { interface RouteMeta { title?: string } } const router = createRouter ({ history : createWebHistory (import .meta .env .BASE_URL ), routes : [ { path : '/' , component : () => import ('@/views/Login.vue' ), meta : { title : "登录" } }, { path : '/index' , component : () => import ('@/views/Index.vue' ), meta : { title : "首页" , } } ]
路由跳转动效
可以利用meta
信息为组件添加一些过度动效,直接将动效的名称放在meta中,这里使用的时Animate.css动效库:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 declare module 'vue-router' { interface RouteMeta { title :string , transition :string , } } const router = createRouter ({ history : createWebHistory (import .meta .env .BASE_URL ), routes : [ { path : '/' , component : () => import ('@/views/Login.vue' ), meta :{ title :"登录页面" , transition :"animate__fadeInUp" , } }, { path : '/index' , component : () => import ('@/views/Index.vue' ), meta :{ title :"首页!!!" , transition :"animate__bounceIn" , } } ] })
然后router4.X的router-view
组件支持插槽,可以在插槽中引入动效标签:
1 2 3 4 5 6 7 <!-- 插槽接收两个返回值 分别代表目前页面的路由信息以及当前显示的组件 --> <router-view #default="{route,Component}"> <!-- 将路由信息中的meta.transition动效名称拼接放到animate中即可 --> <transition :enter-active-class="`animate__animated ${route.meta.transition}`"> <component :is="Component"></component> </transition> </router-view>