Vue3
VUE3基础
可以向下兼容,也可写为vue3+vue2型
setup
setup参数
setup
函数包含了两个入参:
参数 | 类型 | 含义 | 是否必传 |
---|---|---|---|
props | object | 由父组件传递下来的数据 | 否 |
context | object | 组件的执行上下文 | 否 |
props是响应式的context
只是一个普通的对象(可以用ES解构),它暴露三个组件的 Property :
属性 | 类型 | 作用 |
---|---|---|
attrs | 非响应式对象 | 未在 Props 里定义的属性都将变成 Attrs |
slots | 非响应式对象 | 组件插槽,用于接收父组件传递进来的模板内容 |
emit | 方法 | 触发父组件绑定下来的事件 |
defineComponent可用于简化对props和context的类型声明
用法为
1 | import { defineComponent } from 'vue' |
定义数据
Ref
let str=1
:死数据,无法响应let str=ref({a:1})
:响应数据- 获取数据通过
str.value.a
- 也可以通过
str.value.a
改数据 - 也可以通过
str.value
增加数据(例如str.value.b=2
)1
2// TypeScript 会推导 `msg.value` 是 `string` 类型
const msg = ref('Hello World')
- 获取数据通过
Reactive
let str=reactive({a:1})
- 无需使用value
- 只能写对象或者数组
reactive定义的可以通过toRefs改为响应式
1 | // 方式一:先定义接口再响应式 |
reactive在读取对象字段和修改的时候没有任何问题
但是在数组上需要注意1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25let uids: number[] = reactive([1, 2, 3])
/**
* 不推荐使用这种方式,会丢失响应性
* 异步添加数据后,模板不会响应更新
*/
uids = []
// 异步获取数据后,模板依然是空数组
setTimeout(() => {
uids.push(1)
}, 1000)
//--------------------------
const uids: number[] = reactive([1, 2, 3])
/**
* 推荐使用这种方式,不会破坏响应性
*/
uids.length = 0
// 异步获取数据后,模板可以正确的展示
setTimeout(() => {
uids.push(1)
}, 1000)
不要对reactive进行解构操作!否则失去响应性1
2
3const newUserInfo: Member = { ...userInfo }
const { name } = userInfo
//都无法实现响应
toRef
- 转换对象的某个属性
如果想转换name
这个字段为 Ref 变量,只需要这样操作:
1 | const name = toRef(userInfo, 'name') |
此后,修改 Ref 变量的值,reactive内部和ref两者同步更新
如果转换的属性是可选项(
?
)可以传入第三个参数代表默认值,在无值的时候会变以默认值代替
- 转换数组的某个元素
1 | // 这一次声明的是数组 |
如果转换的那个下标不确定是否存在(可能越界了),可以传入第三个参数代表默认值,在无值的时候会变以默认值代替
toRefs
这里比toref多了个s
对对象和数组都适用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
26interface Member {
id: number
name: string
}
// 声明一个 Reactive 变量
const userInfo: Member = reactive({
id: 1,
name: 'Petter',
})
// 传给 `toRefs` 作为入参
const userInfoRefs = toRefs(userInfo)
//----------
//当然可以新定义接口来实现类型。当然这没必要
// 新声明的类型每个字段都是一个 Ref 变量的类型
interface MemberRefs {
id: Ref<number>
name: Ref<string>
}
// 使用新的类型进行声明
const userInfoRefs: MemberRefs = toRefs(userInfo)
转换后的 Reactive 对象或数组支持 ES6 的解构,并且不会失去响应性
1 | // 为了提高开发效率,可以直接将 Ref 变量直接解构出来使用 |
Hooks 函数
1 | import { reactive, toRefs } from 'vue' |
组件
子组件接收
defineProps({ })
- 或者在setup函数中
setup(props){ }
:这里可以接收props
父组件接收
1 | // 子组件 |
生命周期
可以从下表直观地了解:
Vue 2 生命周期 | Vue 3 生命周期 | 执行时间说明 |
---|---|---|
beforeCreate | setup | 组件创建前执行 |
created | setup | 组件创建后执行 |
beforeMount | onBeforeMount | 组件挂载到节点上之前执行 |
mounted | onMounted | 组件挂载完成后执行 |
beforeUpdate | onBeforeUpdate | 组件更新之前执行 |
updated | onUpdated | 组件更新完成之后执行 |
beforeDestroy | onBeforeUnmount | 组件卸载之前执行 |
destroyed | onUnmounted | 组件卸载完成后执行 |
errorCaptured | onErrorCaptured | 当捕获一个来自子孙组件的异常时激活钩子函数 |
可以看到 Vue 2 生命周期里的 beforeCreate
和 created
,在 Vue 3 里已被 setup
替代。
用法:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18import { defineComponent, onBeforeMount, onMounted } from 'vue'
export default defineComponent({
setup() {
console.log(1)
onBeforeMount(() => {
console.log(2)
})
onMounted(() => {
console.log(3)
})
console.log(4)
},
})
// 输出顺序为:1、4、2、3
简单的hello world
1 | <!-- Template 代码和 Vue 2 一样 --> |
函数、侦听与计算
函数
在模板里通过 @click
、@change
等行为触发,和变量一样,需要把函数名在 setup
里进行 return
出去。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>
<p>{{ msg }}</p>
<!-- 在这里点击执行 `return` 出来的方法 -->
<button @click="updateMsg">修改MSG</button>
</template>
<script lang="ts">
import { defineComponent, onMounted, ref } from 'vue'
export default defineComponent({
setup() {
const msg = ref<string>('Hello World!')
// 这个要暴露给模板使用,必须 `return` 才可以使用
function updateMsg() {
msg.value = 'Hi World!'
}
// 这个要在页面载入时执行,无需 `return` 出去
const init = () => {
console.log('init')
}
onMounted(() => {
init()
})
return {
msg,
updateMsg,
}
},
})
</script>
侦听watch
1 | import { watch } from 'vue' |
options
是一个对象的形式传入,有以下几个选项:
选项 | 类型 | 默认值 | 可选值 | 作用 | ||
---|---|---|---|---|---|---|
deep | boolean | false | true \ | false | 是否进行深度侦听 | |
immediate | boolean | false | true \ | false | 是否立即执行侦听回调 | |
flush | string | ‘pre’ | ‘pre’ \ | ‘post’ \ | ‘sync’ | 控制侦听回调的调用时机 |
onTrack | (e) => void | 在数据源被追踪时调用 | ||||
onTrigger | (e) => void | 在侦听回调被触发时调用 |
如果是reactive,deep是强制开启的,无需手动开启,但是对于ref,如果侦听的是ref对象/数组,需要deep
如果侦听对象的某个属性,需要用类似getter的语法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
36export default defineComponent({
setup() {
// 定义一个响应式数据
const userInfo = reactive({
name: 'Petter',
age: 18,
})
// 2s后改变数据
setTimeout(() => {
userInfo.name = 'Tom'
}, 2000)
/**
* 可以直接侦听这个响应式对象
* callback 的参数如果不用可以不写
*/
watch(userInfo, () => {
console.log('侦听整个 userInfo ', userInfo.name)
})
/**
* 也可以侦听对象里面的某个值
* 此时数据源需要写成 getter 函数
*/
watch(
// 数据源,getter 形式
() => userInfo.name,
// 回调函数 callback
(newValue, oldValue) => {
console.log('只侦听 name 的变化 ', userInfo.name)
console.log('打印变化前后的值', { oldValue, newValue })
}
)
},
})
支持批量侦听(将watch的回调函数抽离)1
2
3
4
5
6
7
8
9
10
11// 抽离相同的处理行为为公共函数
const handleWatch = (
newValue: string | number,
oldValue: string | number
): void => {
console.log({ newValue, oldValue })
}
// 然后定义多个侦听操作,传入这个公共函数
watch(message, handleWatch)
watch(index, handleWatch)
数据源也可批量1
2
3
4
5
6
7
8
9watch(
// 数据源改成了数组
[message, index],
// 回调的入参也变成了数组,每个数组里面的顺序和数据源数组排序一致
([newMessage, newIndex], [oldMessage, oldIndex]) => {
console.log('message 的变化', { newMessage, oldMessage })
console.log('index 的变化', { newIndex, oldIndex })
}
)
watchEffect
简化版的watch,会响应式追踪其依赖
watcheffect不可访问变化前后的值
watcheffect默认执行一次,相当于开启了immediate
1 | import { defineComponent, ref, watchEffect } from 'vue' |
计算
1 | // 定义基本的数据 |
- 定义出来的
computed
变量,和 Ref 变量的用法一样,也是需要通过.value
才能拿到它的值- 但是区别在于,默认情况下
computed
的value
是只读的- 一般情况下,是不需要显式的去定义
computed
出来的变量类型的。
注意:计算更新仅限于其以来的响应式更新
1 | const nowTime = computed(() => new Date()) |
假设要获取当前的时间信息,因为不是响应式数据(new Date()不是响应式的),所以这种情况下就需要用普通的函数去获取返回值,才能拿到最新的时间。
setter
通过 computed 定义的变量默认都是只读的形式(只有一个 getter ),但是在必要的情况下,也可以使用其 setter 属性来更新数据。
1 | // 注意这里computed接收的入参已经不再是函数 |
举个例子——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// 还是这2个数据源
const firstName = ref<string>('Bill')
const lastName = ref<string>('Gates')
// 这里配合setter的需要,改成了另外一种写法
const fullName = computed({
// getter还是返回一个拼接起来的全名
get() {
return `${firstName.value} ${lastName.value}`
},
// setter这里改成只更新firstName,注意参数也定义TS类型
set(newFirstName: string) {
firstName.value = newFirstName
},
})
console.log(fullName.value) // 输出 Bill Gates
// 2s后更新一下数据
setTimeout(() => {
// 对fullName的赋值,其实更新的是firstName
fullName.value = 'Petter'
// 此时firstName已经得到了更新
console.log(firstName.value) // 会输出 Petter
// 当然,由于firstName变化了,所以fullName的getter也会得到更新
console.log(fullName.value) // 会输出 Petter Gates
}, 2000)
他是侦听set的变化!set中firstName
变化了。
指令、插槽
自定义指令
1 | <template> |
插槽
1 | //父组件 |
具名插槽
1 | //父组件 |
子组件如果不指定默认插槽,那么在具名插槽之外的内容将不会被渲染。
其他
返回两个相同的变量名
下面这种情况,会以单独的 name
为渲染数据:
哪个会生效,取决于谁排在后面,因为 return
出去的其实是一个对象,在对象里,如果存在相同的 key
,则后面的会覆盖前面的。
1 | return { |
路由
需要注意的是,与 Vue 3 配套的路由版本是 vue-router 4.x 以上才可以正确适配项目。
路由树
index.ts(代码中标注了*
的才是必选)
1 | // src/router/routes.ts |
- 公共基础路径:项目配置中的
base
一级路由
1
2
3
4
5
6
7
8import Home from '@views/home.vue'
const routes: Array<RouteRecordRaw> = [
{
path: '/',
name: 'home',
component: Home,//可以实现 路由懒加载 。
},//访问路径https://example.com/home
]多级路由:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24const routes: Array<RouteRecordRaw> = [
// 注意:这里是一级路由
{
path: '/lv1',
name: 'lv1',
component: () => import('@views/lv1.vue'),
// 注意:这里是二级路由,在 `path` 的前面没有 `/`
children: [
{
path: 'lv2',
name: 'lv2',
component: () => import('@views/lv2.vue'),
// 注意:这里是三级路由,在 `path` 的前面没有 `/`
children: [
{
path: 'lv3',
name: 'lv3',
component: () => import('@views/lv3.vue'),
},//访问地址:https://example.com/lv1/lv2/lv3
],
},
],
},
]
路由渲染
所有路由组件,要在访问后进行渲染,都必须在父级组件里带有 <router-view />
标签。
1 | <template> |
使用 route
获取路由信息
先导入路由 API :
1 | import { useRoute } from 'vue-router' |
再在 setup
里定义一个变量来获取当前路由:
1 | const route = useRoute() |
接下来就可以通过定义好的变量 route
去获取当前路由信息了。
如果要在 <template />
里使用路由,记得把 route
在 setup
里 return 出去。
1 | // 获取路由名称 |
操作路由
1 | // 跳转首页 |
router-link 标签跳转
router-link
是一个全局组件,可直接在 <template />
里直接使用1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 <!-- 不带参数 -->
<template>
<router-link to="/home">首页</router-link>
</template>
<!-- 带参数 -->
<router-link
class="link"
:to="{
name: 'article',
params: {
id: 123,
},
}"
>
这是文章的标题
</router-link>
默认会生成<a>
标签,如果希望转化为其他标签,可以使用(如下图为转化为到span)1
2
3<router-link to="/home" custom v-slot="{ navigate }">
<span class="link" @click="navigate"> 首页 </span>
</router-link>
参数解释——
custom
,一个布尔值,用于控制是否需要渲染为a
标签,当不包含custom
或者把custom
设置为false
时,则依然使用a
标签渲染。v-slot
是一个对象,用来决定标签的行为,它包含了:
字段 | 含义 |
---|---|
href | 解析后的 URL,将会作为一个 a 元素的 href 属性 |
route | 解析后的规范化的地址 |
navigate | 触发导航的函数,会在必要时自动阻止事件,和 router-link 同理 |
isActive | 如果需要应用激活的 class 则为 true ,允许应用一个任意的 class |
isExactActive | 如果需要应用精确激活的 class 则为 true ,允许应用一个任意的 class |
一般来说,
v-slot
必备的只有navigate
,用来绑定元素的点击事件,否则元素点击后不会有任何反应,其他的可以根据实际需求来添加。
路由元信息
在定义路由树的时候可以配置 meta
字段,比如下面就是包含了多种元信息的一个登录路由:
1 | const routes: Array<RouteRecordRaw> = [ |
可以配合路游拦截
导航守卫
路由里的全局钩子
在创建 router
的时候进行全局的配置,也就是说,只要配置了钩子,那么所有的路由在被访问到的时候,都会触发这些钩子函数。(一般在APP.vue
或者全局的header.vue
使用)
可用钩子 | 含义 | 触发时机 |
---|---|---|
beforeEach | 全局前置守卫 | 在路由跳转前触发 |
beforeResolve | 全局解析守卫 | 在导航被确认前,同时在组件内守卫和异步路由组件被解析后 |
afterEach | 全局后置守卫 | 在路由跳转完成后触发 |
beforeEach:
- to: 即将今日的对象
- from:来自哪里
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18//设置标题
router.beforeEach((to, from) => {
const { title } = to.meta
document.title = title || '默认标题'
})
//是否登录
router.beforeEach((to, from) => {
const { isNoLogin } = to.meta
if (!isNoLogin) return '/login'
})
// params丢失的时候返回首页
router.beforeEach((to) => {
if (to.name === 'article' && to.matched.length === 0) {
return '/'
}
})
beforeResolve,与befroeEach相同。
- 它通常会用在一些申请权限的环节,比如一些 H5 页面需要申请系统相机权限、一些微信活动需要申请微信的登录信息授权,获得权限之后才允许获取接口数据和给用户更多的操作,使用
beforeEach
时机太早,使用afterEach
又有点晚,那么这个钩子的时机就刚刚好。
- 它通常会用在一些申请权限的环节,比如一些 H5 页面需要申请系统相机权限、一些微信活动需要申请微信的登录信息授权,获得权限之后才允许获取接口数据和给用户更多的操作,使用
- afterEach
- 参数同befroeEach
- 如流量统计
在组件内使用全局钩子
1 | import { defineComponent } from 'vue' |
路由里的独享钩子
如果只是有个别路由要做处理,可以使用路由独享的守卫
写在router的index.ts里面
1 | const routes: Array<RouteRecordRaw> = [ |
顺序:beforeEach
(全局) > beforeEnter
(独享) > beforeResolve
(全局)。
重定向
1 | // 路由index.ts文件 |
redirect
字段可以接收三种类型的值:
类型 | 填写的值 |
---|---|
string | 另外一个路由的 path |
route | 另外一个路由(类似 router.push ) |
function | 可以判断不同情况的重定向目标,最终 return 一个 path 或者 route |
- 使用
string
:缺点也显而易见,只能针对那些不带参数的路由。 route
:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18const routes: Array<RouteRecordRaw> = [
// 重定向到 `/home` ,并带上一个 `query` 参数
{
path: '/',
redirect: {
name: 'home',
query: {
from: 'redirect',
},
},
},
// 真正的首页
{
path: '/home',
name: 'home',
component: () => import('@views/home.vue'),
},
]//最终访问的地址就是 `https://example.com/home?from=redirect`配置为
function
1 | const routes: Array<RouteRecordRaw> = [ |
路由别名
添加一个 alias
字段即可轻松实现:
1 | const routes: Array<RouteRecordRaw> = [ |
404
配置方法
1 | const routes: Array<RouteRecordRaw> = [ |
这样配置之后,只要访问到不存在的路由,就会显示为这个 404 模板。
新版的路由不再支持直接配置通配符
*
,而是必须使用带有自定义正则表达式的参数进行定义,详见官网 删除了 *(星标或通配符)路由 的说明。
路由侦听
1 | import { defineComponent, watchEffect } from 'vue' |