Vue3

基础

app.use()

注册插件,有点像 Express 中的 use;所谓的插件,即具备某些功能的一段代码,这段代码用于添加全局功能;

插件可以是一个对象,也可以是一个函数;

如果是一个对象,需要有一个 install 方法,以便调用;该 install 函数的第一个参数是 app,第二个参数是 options

1
2
3
4
5
6
7
8
9
import { createApp } from 'vue'

const app = createApp({})

app.use(myPlugin, {
greetings: {
hello: "Bonjour!"
}
});
1
2
3
4
5
6
7
8
9
10
11
12
13
// plugins/i18n.js
export default {
install: (app, options) => {
// 注入一个全局可用的 $translate() 方法
app.config.globalProperties.$translate = (key) => {
// 获取 `options` 对象的深层属性
// 使用 `key` 作为索引
return key.split('.').reduce((o, i) => {
if (o) return o[i]
}, options)
}
}
}
1
<h1>{{ $translate('greetings.hello') }}</h1>

插件的几种使用场景:

  • 添加一些全局属性和方法;
  • 添加一个全局资源;
  • 添加一个全局组件
  • 添加自定义指令;

app.config.isCustomElement

有些元素是从外部引入的,并没有在 vue 中编写,此时需要备注一下哪些元素是自定义的,以免在编译时报错找不到;

1
app.config.isCustomElement = tag => /^x-/.test(tag);

app.mount

将 app 关联到 HTML 文件中的 Tag

1
2
3
4
5
6
7
8
9
10
11
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>TodoMVC built with Vue Composition Api and Vuex</title>
</head>
<body>
<app-root></app-root>
<script type="module" src="./main.js"></script>
</body>
</html>

reactive

reactive 可用来创建一个对象,这个对象可以被多个组件引入,共享使用;

对象可以有自己的方法,通过调用该方法,改变对象的状态;这个改变会在所有的组件上同时更新;

1
2
3
4
5
6
7
8
import { reactive } from "vue";

export const store = reactive({
count: 0,
increment() {
this.count++
}
});
1
2
3
4
5
<template>
<button @click="store.increment()">
{{ store.count }}
</button>
</template>

除了用 reactive 来创建全局对象外,其实 ref 或者函数也可以实现该功能;

函数之所以可以,主要是利用了闭包的特性;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { ref } from "vue"

// 保存状态的全局对象
const globalCount = ref(1);


export function useCount() {
// 保存状态的局部变量
const localCount = ref(1);

return {
globalCount,
localCount,
}
}

问:reactive 和 ref 有什么区别?

答:有以下一些区别:

  • reactive 只能处理对象,不能处理原始类型;ref 的底层实现其实最终也有调用 reactive;
  • ref 可以通过 .value 重新赋值,reactive 不行,因此 reactive 在处理新的 array 时,不如 ref 重新赋值方便;
  • 不过 reactive 修改对象的属性时,无须使用 .value,写起来会简单一些;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// reactive 很适合管理一个拥有多个原始类型属性的对象;

const person = reactive({
name: "John",
age: 37,
isTall: true,
});

// 以上写法比使用多个 ref 来得方便
const name = ref("Albert");
const age = ref(37);
const isTall = ref(true);

// 但 ref 其实也可以写成下面这样
const person = ref({
name: "John",
age: 37,
isTall: true,
});

computed() 其实也是返回一个 ref

computed

当值 B 依赖于值 A 时,通过 computed 可以实现当 A 变动时,B 实现实时更新;

computed 接收一个函数做为参数,返回的是一个 ref

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<script setup>
import { reacitve, computed } from "vue"

const author = reactive({
name: "john",
books: [
"vue1",
"vue2",
"vue3",
]
});

// computed 接收一个函数做为参数,返回的是一个 ref
const message = computed(() => {
return author.books.length > 0 ? "yes" : "no";
});
</script>

<template>
<p>Has Published books:</p>
<span>{{ message }}</span>
</template>

computed 的好处是有缓存,也就是说,如果所依赖的值没变的话,它是不会重新计算的;

实际上 message 也可以定义成一个函数,结果一样,示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<script setup>
const author = reactive({
name: "john",
books: [
"vue1",
"vue2",
"vue3",
]
});
function message() {
return author.books.length > 0 ? "yes" : "no";
}
</script>

<template>
<p>Has Published books:</p>
<span>{{ message() }}</span>
</template>

状态管理器

vue2 的状态管理器,在 vue3 中使用 Pinia

类与样式绑定

有多种写法可用来绑定样式

1
2
3
4
5
6
// 方式一: 使用单个 ref
<script setup>
const isAcitve = ref(true);
</script>

<div :class="{ acitve: isActive }"></div>
1
2
3
4
5
6
7
8
9
10
11
12
// 方式二:使用多个 ref
<script setup>
const isAcitve = ref(true);
const hasError = ref(false);
</script>

<template>
<div
class="static"
:class="{ active: isActive, 'text-danger': hasError }">
</div>
</template>
1
2
3
4
5
6
7
8
9
// 方式三:使用对象
<script setup>
const classObject = {
active: true,
'text-danger': false,
}
</script>

<div :class="classObject"></div>
1
2
3
4
5
6
7
8
9
10
11
12
// 方法四:使用数组
<script setup>
const activeClass = ref('active');
const errorClass = ref('text-danger');

const isActive = ref(true);
</script>

<div :class="[isActive ? activeClass : '', errorClass]"></div>

// 或者
<div :class="[{[activeClass]: isActive }, errorClass]"></div>

自定义组件上的 class 值,会传递到组件内部的 Tag 上面,示例如下:

1
2
// 组件 myComponent 内部的内容
<p class="foo bar"></p>
1
2
// 在调用 myComponent 组件时
<myComponent class="baz boo"></myComponent>
1
2
// 渲染结果为
<p class="foo bar baz boo"></p>

如果 myComponent 内部有多上根Tag,那么需要指定哪个根 Tag 接收外部传进来的 class 值,示例如下

1
2
3
// 内部有两个根元素 p 和 span,此处指定 p 接收 myComponent 的 class 值
<p :class="$attrs.class">hi</p>
<span>message</span>

适用于 class 的绑定规则,同样也适用于 style 的绑定,示例如下:

1
2
3
4
const styleObject = reactive({
color: 'red',
fontSize: '30px'
})
1
<div :style="styleObject"></div>

事件修饰符

当我们想阻止某个事件的冒泡时,可以在绑定的方法中调用 event.stopPropagation(),但 vue 还提供了一种更简便的方法,示例如下:

1
2
3
4
5
6
7
8
// 旧方法
function warn(message, event) {
// 这里可以访问原生事件
if (event) {
event.preventDefault()
}
alert(message)
}

以下是使用事件修饰符进行绑定的方式:

1
2
3
4
5
6
7
8
<!--新方法-->
<script setup>
function warn(message, event) {
alert(message)
}

</script>
<a @click.stop="warn"></a>

按键修饰符

按键修饰符可用于监听键盘上某个特定的键被按下的事件

1
2
3
4
5
6
<!--此处监听 enter 键-->
<input @keyup.enter="submit" />


<!--此处监听 pageDown 键-->
<input @keyup.page-down="onPageDown" />

鼠标修饰符

用来监听鼠标事件

  • .left 左键
  • .right 右键
  • .middle 中键

表单输入绑定

在处理表单输入时,是需要双向绑定的,即改动 data,会更新 html;而改动 input 时,也会更新 data

vue 使用 v-model 关键字来实现这种双向绑定

1
<input v-model="text">

多个复选框可以绑定到一个数组或集合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<script setup>
const checkedNames = ref([]);
</script>

<template>
<div>checked names: {{ checkedNames }}</div>

<input type="checkbot" id="jack" value="jack" v-model="checkedNames">
<label for="jack"></label>

<input type="checkbot" id="john" value="john" v-model="checkedNames">
<label for="john"></label>

<input type="checkbot" id="mike" value="mike" v-model="checkedNames">
<label for="mike"></label>
</template>

v-bind

v-bind 可用于标签的属性绑定

1
2
3
4
5
6
7
8
9
10
11
<div v-bind="{ id: 'blue'}"></div>

<!--等同于如下-->
<div id="blue"></div>

<!--简写如下-->
<script setup>
const id = ref("abc");
</script>

<div :id="id"></div>

v-model 修饰符

.lazy

默认情况下,v-model 的更新是实时的,但可使用 .lazy 修饰符,让更新不再实时,而是触发 change 事件后再更新

1
<input v-model.lazy="msg" />

.number

将输入的字符串自动转成数字

1
<input v-model.number="age" />

.trim

自动去除字符串首尾的空格

1
<input v-mdoel.trim="msg" />

生命周期

最常用的几个生命周期

  • onMounted
  • onUpdated
  • onUnmounted

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
const x = ref(0);
const y = ref(0);

// 监听单个 ref 对象
watch(x, (new_x) => {
// do something
});


// 监听 getter 函数
watch(
() => x.value + y.value,
(sum) => {
console.log("sum of x and y is: ", sum);
}
);


// 监听数组
watch(
[x, () => y.value],
([new_x, new_y]) => {
console.log(`new x is ${new_x} and new y is ${new_y}`);
}
);

watch 并非马上执行,而是当监听对象的值出现变化时,才会执行。因此,如果想让它立即执行,那么需要加个 { immediate: true } 参数;

默认情况下,如果在回调函数中访问监听对象,此时监听对象的值,是原始状态;如果未被回调函数改变前的状态;如果需要访问改变后的状态,则需要给 watch 传递 { flush: “post” } 选项;

watchEffect

watchEffect 有点像是 watch 的语法糖,在使用 watch 时,需要显示的指定某个监听对象;watchEffect 则不用,它可以自动从回调函数中判断需要监听的对象;而且是加载后,马上执行

1
2
3
4
5
6
7
const todoId = ref(1);
const data = ref(null);

watchEffect(async () => {
const res = await fetch(`https://example.com/${todoId.value}`)
data.value = await res.json();
})

访问 DOM

如果想直接访问 DOM,则可以给标签的 ref 属性设置名称,之后就可以在代码中引用它,示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
<script setup>
import { ref, onMounted } from "vue";

const myInput = ref(null); // 此处用同名变量,实现对 input 标签的引用

onMounted(() => {
myInput.value.focus();
})
</script>

<template>
<input ref="myInput">
</template>

当 ref 被用在子组件上时,此时引用的不再是标签,而是子组件实例

1
2
3
4
5
6
7
8
9
10
<script setup>
import { ref, onMounted } from "vue";
import Child from "./Child.vue";

const child = ref(null); // 此处引用的是 Child 实例
</script>

<template>
<Child ref="child"></Child>
</template>

默认情况下,子组件内部的属性和方法是私有的,父组件无法直接访问,除非子组件使用 defineExpose 进行暴露;

1
2
3
4
5
6
7
8
9
10
11
<script setup>
import { ref } from "vue";

const a = 1;
const b = ref(2);

defineExpose({
a,
b,
})
</script>

此时父组件可通过 ref 引用来访问子组件中的 a 和 b 变量

1
// 此时 ref 的值为 { a: number, b: number }

组件API

以下两种形式的 API 是等价的

1
2
3
4
5
6
7
8
9
<!-- 组合式 API -->
<script setup>
import { ref } from 'vue';
const count = ref(0);
</script>

<template>
<button @click="count++"></button>
</template>
1
2
3
4
5
6
7
8
9
10
11
12
<!-- 选项式 API -->
import { ref } from 'vue';

export default {
setup: () => {
const count = ref(0);
return { count };
},
template: `<button @click="count++"></button>`
// template 也可以引用一个模板
// template: '#my-template-element'
}

父组件可通过 props 向子组件传递数据;子组件可 emit 事件,父组件通过监听事件来获得子组件传递的数据;

slot 插槽

slot 的作用类似于占位符,可接收由父组件传进来的 HTML 内容,示例如下:

1
2
3
4
5
6
7
<!--AlertBox.vue-->
<template>
<div>
<strong>This is an Error box</strong>
<slot></slot>
</div>
</template>
1
2
<!--此处父组合的内容 Something bad happen 会出现中子组件的 slot 位置-->
<AlertBox>Something bad happen</AlertBox>

深入组件

注册

全局注册

组件需要注册后才能使用,通过 app.component 方法,可将某个组件注册为全局的组件,之后可以在任意文件中使用该全局组件;

1
2
3
4
5
6
import { createApp } from 'vue';
import MyComponent from "./App.vue";

const app = createApp({});

app.component('MyComponent', MyComponent); // 全局注册

局部注册

局部注册:仅在需要使用的位置,导入相应的组件

1
2
3
4
5
6
7
<script setup>
import ComponentA from './ComponentA.vue'
</script>

<template>
<ComponentA />
</template>

props

除了 attribute 外,考虑父组件还可通过 props 传递数据给子组件。因此,最好显式的声明 props,这样有利于 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
// props 使用对象,并写上属性值的类型,有助于尽早发现传错参数
defineProps({
title: String,
likes: Number
})

// 还可以添加校验规则
defineProps({
propA: {
type: String,
required: true,
default: "hello"
},
propB: {
validator(value, props) {
return ['success', 'warning', 'danger'].includes(value);
}
},
propC: {
type: Function, // 可以是函数类型
default() {
return 'Default Function'
}
}
})

Vue 倾向在写 HTML atribute 时,使用传统的 kecal-case 枨,然后它还会自动映射 kebab-case 和 camelCase 格式,以便和传统的 javascript camelCase 保持一致;

个人感觉这种两边讨好的做法不是很好;缺少一致性,容易让人感到困惑;

1
<MyComponent greeting-message="hello"></MyComponent>

当使用 v-bind 时,引号中的内容,实际上是一个表达式,而不是字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!-- 因为使用 v-bind,所以此处的 42 其实是一个 Number 类型,  -->
<!-- 因为这是一个 JavaScript 表达式而不是一个字符串 -->
<BlogPost :likes="42" />

<!-- 根据一个变量的值动态传入 -->
<BlogPost :likes="post.likes" />

<!-- 同理,false 是一个布尔值 -->
<BlogPost :is-published="false" />

<!-- 表达式自然是支持数组的 -->
<BlogPost :commend-ids="[234, 245, 273]" />

<!--表达式也支持对象-->
<BlogPost :author="{ name: 'John', age: 47 }" />

可通过 v-bind=对象,批量绑定多个 prop

1
2
3
4
5
6
7
8
9
<script setup>
const post = {
id: 1,
title: "My Journey"
}
</script>

<!--同时绑定了 id 和 title 两个 prop -->
<BlogPost v-bind="post" />

注意:prop 是单向绑定,即数据由父组件传递给子组件,这意味着它是只读的,我们不能在子组件的代码中,修改 prop 的值

1
2
3
4
const props = defineProps(['foo']);

// 以下尝试修改 foo 的值是错误的
props.foo = "bar";

由于在 Javascript 中,对象类型的参数,实际上是一个引用,因此,虽然无法直接更改对象绑定的变量,但可以改变对象内部的属性。但这是一种不良做法,应该在实践中避免;如有需要修改,应使用 emit 事件的方式;由监听事件的父组件对 prop 进行修改;

事件

在组件的 template 模板中,可使用内置的 $emit 函数来触发事件

1
<button @click="$emit('someEvent')" />

事件支持携带参数

1
<button @click="$emit('someEvent', param)" />

通过使用 defineEmit() 宏显式的声明可触发的事件后,会返回一个 emit 函数,能够在代码中直接调用,它的效果跟 template 中的 $emit 是一样的;

1
2
3
4
5
6
7
<script setup>
const emit = defineEmits(['inFocus', 'submit']);

function buttonClick() {
emit("submit");
}
</script>

组件 v-model

通过在子组件上使用 v-model,可以实现父子组件之间数据的双向绑定;父子组件传统的通信方式是使用 prop 和 emit,事实上在组件上使用 v-model 只是一个语法糖,它的底层仍然还是 prop 和 emit,只是它由解释器完成补全;

1
2
3
4
5
6
<!--父组件-->
<script setup>
const myRef = ref();
</script>

<Child v-model="myRef" />
1
2
3
4
5
6
7
8
<!--子组件 Child.vue -->
<script setup>
const myRefVar = defineModel();
</script>

<template>
<input v-model='myRefVar' />
</template>

可以绑定多个 v-model

1
2
3
4
5
<!-- 父组件 -->
<UserName
v-model:firstName="first"
v-model:lastName="last"
/>
1
2
3
4
5
6
7
8
9
10
11
<!-- 子组件 -->
<script setup>
const firstName = defineModel("firstName");
const lastName = defineModel("lastName");
</script>

<template>
<input type="text" v-model="firstName" />
<input type="text" v-model="lastName" />
</template>

组件 v-model 同样支持修饰符,例如 v-model.capitalize,之后在 defineModel 中,可以基于传入的修饰符的值,自定义 set 函数,实现想要的处理;

1
2
<!-- 父组件 -->
<MyComponent v-model.capitalize='myText' />
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!-- 子组件 -->
<script setup>
const [model, modifiers] = defineModel();
console.log("modifiers") // { capitalize: true }
</script>

<template>
<input type='text' v-model="model" />
</template>


<!-- 或者可以针对 modifiers 自定义处理方法 -->
<script setup>
const [model, modifiers] = defineModel({
set(value) {
if (modifiers.capitalize) {
return value.charAt(0).toUppercase() + value.slice(1)
}
return value;
}
})
</script>

透传 Attributes

最常见的透传包括 class, style, id 等几个 HTML 标签的属性;但其实 v-on 监听器也会实现透传

1
2
<!-- 父组件 -->
<MyButton @click="onClick"></MyButton>
1
2
<!-- 子组件 -->
<button @click="onChildClick" />

当 button 触发点击事件时,onChildClick 和 onClick 两个函数都会被执行,事实上 button 标签绑定了来自父子组件的两个点击事件;

深层组件继承

如果子组件的根元素也是一个组件,那么父组件的 attributes 会持续向下一级透传;

如果不想要继承透传,可在组件选项中设置 inheritAttrs: false

1
2
3
4
5
<script setup>
defineOptions({
inheritAttrs: false,
})
</script>

透传的 attributes 可在 template 中使用 $attris 进行访问

1
<span>{{ $attrs }}</span>

@click 在透传后,子组件可使用 $attrus.onClick 进行访问;

如果子组件有多个根节点,那么需要显式指定由哪个根节点继承父组件透传的 attris,否则编译器会抛出警告;

如果想要在 js 代码中访问 attrus,则可以使用 useAttrs

1
2
3
4
5
<script setup>
import { useAttrs } from 'vue';

const attrs = useAttrs();
</script>

插槽

父组件可通过插横向子组件传递内容;插槽从某种意义上来说,有点像是一个形式参数。子组件本身只提供样式,内容则由参数来决定,这样可以提高子组件的通用性和灵活性;

1
2
3
4
5
<!-- 父组件 -->
<FancyButton>
<span style="color: red">Click me!</span>
<AwesomeIcon name="plus" />
</FancyButton>
1
2
3
4
<!-- 子组件 FancyButton.vue -->
<button class="fancy-btn">
<slot></slot> <!-- 插入的位置 -->
</button>
1
2
3
4
5
<!-- 最终渲染结果 -->
<button class="fancy-btn">
<span style="color: red">Click me!</span>
<AwesomeIcon name="plus" />
</button>

作用域:插槽内容可以访问父组件中定义的变量,但无法访问子组件中的数据;

默认内容:插槽允许指定默认内容,这样当父组件没有传入内容时,可显示默认内容;就像默认参数值一样;

1
2
3
4
5
<button type="submit">
<slot>
Submit <!-- 此处的 Submit 为默认内容 -->
</slot>
</button>

具名插槽

组件支持多个插槽,为了避免混淆,需要为每个插槽指定名称,这样传入内容的时候,才能够匹配;

1
2
3
4
5
6
7
8
9
10
11
12
<!-- 子组件 BaseLayout.vue -->
<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot> <!-- 没有名称,默认名称为 default -->
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
1
2
3
4
5
6
7
8
9
<!-- 父组件 -->
<BaseLayout>
<template v-slot="header">
<!-- 此处的内容将匹配到名称为 header 的插槽上 -->
</template>
<template #footer> <!-- v-slot 支持简写为 # -->
<!-- 此处的内容将匹配到名称为 footer 的插槽上 -->
</template>
</BaseLayout>

父组件的插槽名称必须和子组件中的插槽名称完全一样,如果不一样,会无法匹配,因此也无法渲染

插槽的名称可以是动态的

1
2
3
<base-layout>
<template v-slot:[dynamicSlotName]></template>
</base-layout>

反向传递

子组件可以将自己的数据,通过插槽,反向传递给父组件

无渲染组件

利用插槽机制,再加上 v-slot 让子组件能够向父组件传递数据,那么接下来便出现了一种有趣的用法,即子组件只封装了逻辑,但没有封装要渲染的内容。它在通过逻辑获得数据后,可以将数据传递给父组件,由父组件自行决定如何渲染;

依赖注入

当需要向深层次的组件时,使用 props 会导致逐级透传的问题

Vue 使用 provide/inject 机制来解决逐级透传的问题

1
2
3
4
5
6
7
<script setup>
import { provide } from 'vue';
provide({ 'message', 'hello'}) // 此处 message 是键,hello 是值;

const count = ref(0);
provide('count', count); // provide 支持多次调用,以便传入多个值
</script>

app 可以提供全局依赖/注入

1
2
3
4
5
import { createApp } from 'vue'

const app = createApp({});

app.provide("message", "hello");

在子组件中,使用 inject 来获得想要的数据

1
2
3
4
5
6
7
8
9
10
11
<!-- 子组件 -->
<script setup>
import { inject } from 'vue'

const message = inject("message"); // 使用 inject 获得想要的数据

const value = inject("count", "defaultValue") // inject 支持设置一个默认值

// 默认值也可以使用工厂函数来生成, 第三个参数 true 用来声明默认值是由一个函数生成
const value = inject("key", () => new DefautlValue(), true);
</script>

如果需要在子组件中更改注入的数据,那么 provide 最好提供一个方法,供子组件调用,而不是直接修改。这样有利于未来的维护;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<script setup>
import { ref, provide } from 'vue'

const location = ref("North Pole");

function updateLocation() {
location.value = 'South Pole';
}

provide("location", {
location,
updateLocation,
})
</script>
1
2
3
4
5
6
7
8
9
10
11
12
<!-- 子组件 -->
<script setup>
import { inject } from 'vue'

const { location, updateLocation } = inject("location"); // 可以解包
</script>

<template>
<button @click="updateLocation">
{{ location }}
</button>
</template>

如果提供方想保护自己的数据不能被修改,可以使用 readonly 将其装饰为只读的状态

1
2
3
4
5
6
7
<script setup>
import { ref, provide, readonly } from 'vue'

const count = ref(0)

provide('readOnlyCount', readonly(count)) // 使用 readonly 装饰
</script>

使用 Symbol 避免命名冲突

如果构建的应用很大,或者所编写的组件会被很多人调用,那么有可能产生命名冲突。解决办法就是将名称放在一个单独的文件中统一管理

1
2
// key.js
export const myInjectKey = Symbol(); // Symbol 会生成一个唯一值,以便作为标识符,避免重名
1
2
3
4
5
// 在 provide 组件中
import { provide } from 'vue'
import { myIndectKey } from "./key.js"

provide(myInjectKey, {/* something */})
1
2
3
4
5
// 在 inject 组件中
import { inject } from 'vue'
import { myInjectKey } from "./key.js"

const injected = inject("myInjectKey");

异步组件

当应用变得很大时,每次打开便加载所有组件将耗费很长的等待时间,更好的做法是懒加载,即等用到某个组件时,再去加载它;

1
2
3
4
5
6
7
8
9
// 普通加截组件的方法
import MyComponet from "./components/MyComponent.vue"

// 异步加载组件的方法
import { defineAsyncComponent } from 'vue'

const AsyncComp = defineAsyncComponent(() => {
import("./components/MyComponent.vue")
})

加载错误

异步加载因为是使用时再加载的,那么有可能因为网络信号不好,导致加载失败,此时可提供一个组件,来应对出错的情况,defineAsyncComponent 支持多个配置选项

1
2
3
4
5
6
7
const AsyncComp = defineAsyncComponent({
loader: () => import("../components/Foo.vue"),
loadingComponent: LoadingComponent, // 例如可显示加载动画
errComponent: ErrorComponent, // 例如在出错时,显示错误提示信息
delay: 200, // 设置延迟,有助于让画面过渡更加顺滑,以免加载太快,切换太快,像是页面闪烁
timeout: 3000, // 超时后报错
})

逻辑复用

组合式函数

当某个行为逻辑被很多个组件复用时,可以把它抽象到一个公式的函数中,然后由各组件引入使用;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 该函数实时读取鼠标的位置,现抽象到单独的 mouse.js 文件中
import { ref, onMounted, onUnmounted } from 'vue'

// 按照惯例,组合式函数名以“use”开头
export function useMouse() {
// 被组合式函数封装和管理的状态
const x = ref(0)
const y = ref(0)

// 组合式函数可以随时更改其状态。
function update(event) {
x.value = event.pageX
y.value = event.pageY
}

// 一个组合式函数也可以挂靠在所属组件的生命周期上
// 来启动和卸载副作用
onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))

// 通过返回值暴露所管理的状态
return { x, y }
}
1
2
3
4
5
<!-- 在组件中使用 mouse.js -->
<Script setup>
import { useMouse } from "./mouse.js
const { x, y } = useMouse(); // useMouse 会创建单独的实例,因此各个组件间的状态不会相互影响
</Script>

我们可以将动作拆分成更小的函数,然后不同的函数可以相互组合,这样可以尽可能实现复用;

例如从后端获取数据是一个很常见的动作,获取的过程涉及三个动作,显示正在获取中;如果成功,显示数据;如果失败,显示失败提示;由于该动作很常见,因此我们可以将它封装成一个单独的函数,以便各个组件可以复用该逻辑;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!-- 传统的方式 -->
<script setup>
import { ref } from 'vue'

const data = ref(null)
const error = ref(null)

fetch('...')
.then((res) => res.json())
.then((json) => (data.value = json))
.catch((err) => (error.value = err))
</script>

<template>
<div v-if="error">Oops! Error encountered: {{ error.message }}</div>
<div v-else-if="data">
Data loaded:
<pre>{{ data }}</pre>
</div>
<div v-else>Loading...</div>
</template>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 抽象成单独的函数
// fetch.js
import { ref } from 'vue'

export function useFetch(url) {
const data = ref(null)
const error = ref(null)

fetch(url)
.then((res) => res.json())
.then((json) => (data.value = json))
.catch((err) => (error.value = err))

return { data, error }
}
1
2
3
4
5
6
<!-- 使用封装后的函数 -->
<script setup>
import { useFetch } from './fetch.js'

const { data, error } = useFetch('...')
</script>

理论上也可以直接使用普通的函数,没有必要将函数封装组装,这种做的好处其实在于让它变成响应式的。因为普通的函数每次执行,都需要手动主动调用。而如果封装成了组件,同时参数为 ref 或者 getter 函数等动态类型,那么每当参数值发生变化时,组件就会自动运行。这是相对普通函数的好处;

1
2
3
4
5
6
const url = ref('/initial-url')

const { data, error } = useFetch(url)

// 这将会重新触发 fetch
url.value = '/new-url'

另外,也可以在函数式组件中使用 watchEffect 来监听参数变化;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// fetch.js
import { ref, watchEffect, toValue } from 'vue'

export function useFetch(url) {
const data = ref(null)
const error = ref(null)

const fetchData = () => {
// 每次运行前重置
data.value = null
error.value = null

fetch(toValue(url)) // toValue 的好处是让参数可以支持多种类型,更加灵活
.then((res) => res.json())
.then((json) => (data.value = json))
.catch((err) => (error.value = err))
}

watchEffect(() => { // 使用 watchEffect 来监听变化
fetchData()
})

return { data, error }
}

解构重命名

1
2
3
4
5
6
7
const obj = {
a: 1,
b: 2,
c: 3,
}

const {a : a1, b : b2, c: c3, d4 = 'default'} = obj;

自定义指令

Vue 有一些内置的指令,例如 v-model、v-show 等,这些指令从本质上来,其实是为了操作和控制 DOM;除了内置指令,Vue 也支持编写自定义的指令,这些指令可以在不同的组件上实现复用;

1
2
3
4
5
6
7
8
9
<script setup>
const vFocus = {
mounted: (el) => el.focus(), // 加载后,可自动对焦
}
</script>

<template>
<input v-focus />
</template>

vFocus 是一种强制的命名规范,以小写字母 v 开头;

指令支持多种钩子函数

1
2
3
4
5
6
7
8
9
10
11
12
13
const myDirective = {
created(el, binding, vnode){},
beforeMount(el, binding, vnode){},
mounted(el, binding, vnode){},
beforeUpdated(el, binding, vnode){},
updated(el, binding, vnode){},
beforeUnmounted(el, binding, vnode){}
unmounted(el, binding, vnode){}
}
// el 参数指要操作的元素
// binding 是一个对象,主要用来存放要传给指令的值;以便 el 可以读取这些值,进行相应的操作;
// vnode 代表绑定元素的底层 VNode
// prevVnode 代表之前绑定的底层 VNode

注:应避免有组件上面使用自定义指令,而是只在原生的 HTML 元素上使用,以避免冲突,产生预期外的效果;

插件

插件可用来给 Vue 添加全局功能;

1
2
3
4
5
import { createApp } from 'vue'

const app = createApp();

app.use(myPlugin), {...}; // 全局使用插件
1
2
3
4
5
6
7
8
// 定义插件示例

const myPlugin = {
install: (app, options) {...},
}

// 或者
const myPlugin = (app, options) => {}

编写插件示例

该插件给在 app 上注册一个全局可用的 $translate 函数,用来翻译指定字段

1
2
3
4
5
6
7
8
9
10
11
12
// plugins/i18n.js
export default {
install: (app, options) => {
app.config.globalProperties.$translate = (key) => {
return key.split(".").reduce((0, i) => {
if (o) {
return o[i];
}
}, options);
}
}
}
1
2
3
4
5
6
7
8
// 引入插件
import i18nPlugin from "./plugins/i18n";

app.use(i18nPlugin, {
greetings: {
hello: "Bonjour!",
}
})
1
2
<!-- 使用插件 -->
<h1>{{ $translate("greetings.hello")}}</h1>

插件中也可以引入 provide / inject,这样各个组件就可以直接读取插件提供的值了

1
2
3
4
5
export default {
install: (app, options) => {
app.provide('i18n', options),
}
}
1
2
3
4
5
6
<!-- 在组件中通过 inject 读取 options -->
<script setup>
import { inject } from 'vue';
const i18n = inject('i18n');
console.log(i18n.greetings.hello);
</script>

内置组件

Transition

内置的 Transition 组件,可用来给组件加载或卸载时提供动画效果;

1
2
3
4
<button @click="show =!show">Toggle</button>
<Transition>
<p v-if="show">hello</p>
</Transition>

动画效果可以自定义,并且可以命名,以方便管理多种不同的动画效果;

TransitionGroup 可用来设置列表的动画,当列表添加或删除元素时,呈现动画效果;

KeepAlive

KeepAlive 可用来缓存实例

Teleport

Teleport 有点像是一个传送门,用来将组件中的部分模板,传送到外部组件上面;之所以这么做,是为了能够更好的展示传送的内容,避免受到深层嵌套过程中的其他组件的布局影响;

1
2
3
4
5
6
7
8
<button @click="open = true">Open Modal</button>

<Teleport to="body">
<div v-if="open" class="model">
<p>Hello from the modal</p>
<button @click="open = false">Close</button>
</div>
</Teleport>

Teleport 会改变 DOM 的层级关系,但不会改组件之间的层级关系;

Suspence

在某个组件内部存在多个异步组件时,有可能这些异步组件都有自己的异步处理机制,例如显示加载图标。当这些子组件同时加载时,会导致页面上出现多个异步图标。Suspence 的目标是对这些异步状态统一管理,展示统一的加载状态;

应用规模化

单文件组件

一个 Vue 文件同时包含 js、html 和 css 三部分内容,即同时包含逻辑、模板和样式数据;

单文件组件是一种代码的组织方式,如果需要实现的功能非常小,例如只是给静态文件添加一些简单的交互,则可以考虑使用 petite-vue,只有 6k 大小;

路由

非常简单的页面,可用 computed 配合监听浏览器的 haschange 来切面页面;它的原理很简单,即 js 代码调用浏览器的接口,更新了 url,触发了 haschange 事件,从而调用监听函数,完成组件的更新,实现页面的切换;

正式的路由器则使用 Vue Router

状态管理

如果多个组件依赖同一份数据,那么使用 props 逐级透传的方式,会让代码变得臃肿。解决办法是将数据封装成一个全局的单例,供各个组件使用;

其中一个方案是使用 reactive、ref、computed 或者组合式函数,创建一个响应式对象,放在单独的文件中,供各个组件引用;

如果应用使用服务器渲染,则以上方案变得不太可行;此时需要使用单独的状态管理器,例如 Pinia 或者 Vuex;

测试

需要测试的东西:

  • 单元测试:确保函数正常
  • 组件测试:确保 component 的功能正常
  • 端到端测试:类似于集合测试,确保整个应用正常运行;常用框架:Cypress,Playwright,Nightwatch 等;

其中端到端是最重要的,因为它确保了应用程序的运行正常;

服务端渲染

有两种场景需要用到服务端渲染 SSR:

  • SEO 非常重要;
  • 首页加载速度非常重要;

最佳实践

生产部署

在生产服务器部署,那些提高开发效率的工具就不需要了,因此记得在打包代码是地,排除它们,以缩小文件的体积;

性能优化

Vue 本身包含了优化功能,在绝大部分场景下,vue 的性能都是够用的,除非遇到一些极端的场景,才需要手动优化;

两个常见的优化指标:

  • 页面加载速度
  • 页面更新速度

页面加载优化

常用的手段包括:

  • 服务端渲染
  • 减小包体积:例如构建工具使用 Tree Shaking,预编译等,避免引入太大的依赖;
  • 代码分割:实现懒加载;

页面更新优化

当 props 变更时,会触发组件的更新,因此,在设计组件时,应该确保它的 props 值尽量稳定,以减少不必要的更新触发;

v-once 指令可用来标识无需更新的组件,这样进行更新计算时,会跳过该组件;

v-memo 指令可用来设置更新的条件;

computed 的计算结果如果发生变化,也会触发更新。 如果是值还好说,可直接比较;如果比较的是对象,那么即使值没有变化,也会触发更新;此时可考虑引入自定义的比较函数;

通用优化

  • 大型列表的虚拟化;
  • 绕开深层级对象的深度检查;
  • 在大型列表中,减少不必要的组件抽象;

进阶

使用 Vue 的多种方式

  • 独立脚本,像引入 jQuery 一样轻量化使用;
  • 作为 Web Component 嵌入原有的旧应用;
  • 单页面应用:主流用法;
  • 全栈 / SSR:适用于 SEO 很重要的场景;
  • 静态 SSG:静态站点生成 JAMStack,作用静态文件部署;

Vue3
https://ccw1078.github.io/2023/08/19/Vue3/
作者
ccw
发布于
2023年8月19日
许可协议