vue3 系列:Vue3 官方文档速通

发布时间:2024年01月17日

前言:参考Vue 官方文档,本文档主要学习组合式 API。

一 开始

1. 简介

1.1 什么是 Vue?

一款 JS 框架,并有两个核心功能:声明式渲染、响应性。

?1.2 渐进式框架

根据不同的需求场景,使用不同方式的 Vue,比如:

????????无需构建步骤,直接引入 vuejs。

????????在任何页面中作为 Web Components 嵌入

????????使用构建步骤,单页应用 (SPA)

????????全栈 / 服务端渲染 (SSR)

????????Jamstack / 静态站点生成 (SSG)

????????开发桌面端、移动端、WebGL,甚至是命令行终端中的界面

Vue 为什么可以称为“渐进式框架”:它是一个可以与你共同成长、适应你不同需求的框架。

?1.3 单文件组件

单文件组件是 Vue 的标志性功能。*.vue、SFC 就是单文件组件:将一个组件的逻辑 (JS),模板 (HTML) 和样式 (CSS) 封装在同一个文件里。

?1.4 API 风格

选项式 API(Options API):包含多个选项的对象来描述组件的逻辑,例如 data、methods 和 mounted。

组合式 API(Composition API):使用导入的 API 函数来描述组件逻辑。通常会与 <script setup> 搭配使用。

综上:两种 API 是同一个底层系统构建的。选项式 API 是在组合式 API 的基础上实现的!

?2. 快速上手

2.1 创建一个 Vue 应用

// 安装并执行 create-vue,它是 Vue 官方的项目脚手架工具。
npm create vue@latest

2.2 通过 CDN 使用 Vue

可以用于增强静态的 HTML 或与后端框架集成。但将无法使用单文件组件 (SFC) 语法:

<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>

<div id="app">{{ message }}</div>

<script>
  const { createApp, ref } = Vue;

  createApp({
    setup() {
      const message = ref("Hello vue!");
      return {
        message,
      };
    },
  }).mount("#app");
</script>

?通过 CDN 以及原生 ES 模块使用 Vue:

<div id="app">{{ message }}</div>

<script type="module">
  import {
    createApp,
    ref,
  } from "https://unpkg.com/vue@3/dist/vue.esm-browser.js";

  createApp({
    setup() {
      const message = ref("Hello Vue!");
      return {
        message,
      };
    },
  }).mount("#app");
</script>

使用导入映射表 (Import Maps) 来告诉浏览器如何定位到导入的 vue:

<script type="importmap">
  {
    "imports": {
      "vue": "https://unpkg.com/vue@3/dist/vue.esm-browser.js"
    }
  }
</script>

<div id="app">{{ message }}</div>

<script type="module">
  import { createApp, ref } from "vue";

  createApp({
    setup() {
      const message = ref("Hello Vue!");
      return {
        message,
      };
    },
  }).mount("#app");
</script>

分割代码成单独的 JS 文件,以便管理。

<!-- index.html -->
<div id="app"></div>

<script type="module">
  import { createApp } from "vue";
  import MyComponent from "./my-component.js";

  createApp(MyComponent).mount("#app");
</script>
// my-component.js
import { ref } from "vue";
export default {
  setup() {
    const count = ref(0);
    return { count };
  },
  template: `<div>count is {{ count }}</div>`,
};

注意:直接点击 index.html,会抛错误,因为 ES 模块不能通过 file:// 协议工作。只能通过 http:// 协议工作。需要启动一个本地的 HTTP 服务器,通过命令行在 HTML 文件所在文件夹下运行 npx serve。

?二 基础

1. 创建一个 Vue 应用

1.1 应用实例

通过 createApp 函数创建 Vue 应用实例:

import { createApp } from "vue";

const app = createApp({
  /* 根组件选项 */
});

1.2 根组件

createApp 需要传入一个根组件,其他组件将作为其子组件:

import { createApp } from "vue";
// 从一个单文件组件中导入根组件
import App from "./App.vue";

const app = createApp(App);

1.3 挂载应用

调用 .mount() 方法,传入一个 DOM 元素或是 CSS 选择器。它的返回值是根组件实例而非应用实例:

<div id="app"></div>
import { createApp } from "vue";
// 从一个单文件组件中导入根组件
import App from "./App.vue";

const app = createApp(App);
app.mount("#app");

1.4 应用配置

注意:确保在挂载应用实例之前完成所有应用配置!

import { createApp } from "vue";
// 从一个单文件组件中导入根组件
import App from "./App.vue";

const app = createApp(App);

// 应用实例的 .config 对象可以进行一些配置,例如配置错误处理器:用来捕获所有子组件上的错误:
app.config.errorHandler = (err) => {
  /* 处理错误 */
};

// 全局挂载组件
app.component("TodoDeleteButton", TodoDeleteButton);

// 全局属性的对象。
app.config.globalProperties.msg = "hello";

app.mount("#app");

1.5 多个应用实例

每个应用都拥有自己的用于配置和全局资源的作用域:

const app1 = createApp({
  /* ... */
});
app1.mount("#container-1");

const app2 = createApp({
  /* ... */
});
app2.mount("#container-2");

2. 模板语法

2.1 文本插值

最基本的数据绑定是文本插值,使用“Mustache”语法 (即双大括号):

<span>Message: {{ msg }}</span>

2.2 原始 HTML

双大括号会将数据解释为纯文本,若想插入 HTML,需要使用 v-html 指令。

安全警告:动态渲染 HTML 是很危险的,容易造成 XSS 漏洞。仅在内容安全可信时再使用 v-html,并且永远不要使用用户提供的 HTML 内容。

<p>Using text interpolation: {{ rawHtml }}</p>
<p>Using v-html directive: <span v-html="rawHtml"></span></p>

2.3 Attribute 绑定

绑定 attribute,使用 v-bind 指令:

<div v-bind:id="dynamicId"></div>
<!-- 简写 -->
<div :id="dynamicId"></div>

绑定多个值,通过不带参数的 v-bind。

const objectOfAttrs = {
  id: "container",
  class: "wrapper",
};
<div v-bind="objectOfAttrs"></div>

2.4 使用 JS 表达式

数据绑定都支持完整的 JS 表达式,也就是一段能够被求值的 JS 代码。一个简单的判断方法是是否可以合法地写在 return 后面:

{{ number + 1 }}

{{ ok ? 'YES' : 'NO' }}

{{ message.split('').reverse().join('') }}

<div :id="`list-${id}`"></div>

可以在绑定的表达式中使用一个组件暴露的方法:

<time :title="toTitleDate(date)" :datetime="date">
  {{ formatDate(date) }}
</time>

2.5 指令 Directives

指 v- 前缀的特殊 attribute。它的值为 JS 表达式(v-for、v-on、v-slot 除外),指令值变化时响应式的更新 DOM,比如 v-if:

<p v-if="seen">Now you see me</p>

带参数的指令,用':'隔开:

<a v-bind:href="url"> ... </a>
<!-- 简写 -->
<a :href="url"> ... </a>

指令的参数,也可以动态绑定,用 '[ ]' 包裹:

<a v-bind:[attributeName]="url"> ... </a>
<!-- 简写 -->
<a :[attributeName]="url"> ... </a>

带修饰符的指令,用 '.' 隔开:

<!-- 触发的事件调用 event.preventDefault() -->
<form @submit.prevent="onSubmit">...</form>

3. 响应式基础

3.1 声明响应式状态

官方推荐使用 ref() 函数来声明响应式状态:

import { ref } from "vue";

const count = ref(0);

ref() 接收参数,并返回一个带有 .value 属性的 ref 对象:

const count = ref(0);

console.log(count); // { value: 0 }
console.log(count.value); // 0

count.value++;
console.log(count.value); // 1

在模板中使用 ref 变量,不需要添加 .value。ref 会自动解包。也可以直接在事件监听器中改变一个 ref:

<button @click="count++">{{ count }}</button>

通过单文件组件(SFC),使用 <script setup> 来大幅度地简化代码:

<script setup>
import { ref } from "vue";

const count = ref(0);

function increment() {
  count.value++;
}
</script>

<template>
  <button @click="increment">
    {{ count }}
  </button>
</template>

为什么使用 ref,而不是普通的变量。这是因为 Vue 需要通过.value 属性来实现状态响应性。基础原理是在 getter 中追踪,在 setter 中触发:

// 伪代码,不是真正的实现
const myRef = {
  _value: 0,
  get value() {
    track();
    return this._value;
  },
  set value(newValue) {
    this._value = newValue;
    trigger();
  },
};

要等待 DOM 更新完成后再执行额外的代码,可以使用 nextTick() 全局 API:

import { nextTick } from "vue";

async function increment() {
  count.value++;
  await nextTick();
  // 现在 DOM 已经更新了
}

3.2 reactive()

reactive(),参数只能是对象类型,返回的是一个原始对象的 Proxy,它和原始对象是不相等的:

const raw = {};
const proxy = reactive(raw);

// 代理对象和原始对象不是全等的
console.log(proxy === raw); // false

reactive() 局限性包括:只能用于对象类型(对象,数组,Map,Set)、不能替换整个对象、对结构操作不友好:

let state = reactive({ count: 0 });
// 上面的 ({ count: 0 }) 引用将不再被追踪
// (响应性连接已丢失!)
state = reactive({ count: 1 });

// 对解构不友好
const state = reactive({ count: 0 });
// 当解构时,count 已经与 state.count 断开连接
let { count } = state;
// 不会影响原始的 state
count++;
// 该函数接收到的是一个普通的数字
// 并且无法追踪 state.count 的变化
// 我们必须传入整个对象以保持响应性
callSomeFunction(state.count);

reactive() API 有一些局限性,官方建议使用 ref() 作为声明响应式状态的主要 API。博主个人还是喜欢 ref,reactive 混着用,注意那些局限性就可以了。

?3.3 额外的 ref 解包细节

一个 ref 会在作为响应式对象的属性被访问或修改时自动解包:

const count = ref(0);
const state = reactive({
  count,
});

console.log(state.count); // 0

state.count = 1;
console.log(count.value); // 1

?4. 计算属性

4.1 基础示例

computed() 方法期望接收一个 getter 函数,返回为一个计算属性 ref:

<script setup>
import { reactive, computed } from "vue";

const author = reactive({
  name: "John Doe",
  books: [
    "Vue 2 - Advanced Guide",
    "Vue 3 - Basic Guide",
    "Vue 4 - The Mystery",
  ],
});

// 一个计算属性 ref
const publishedBooksMessage = computed(() => {
  return author.books.length > 0 ? "Yes" : "No";
});
</script>

<template>
  <p>Has published books:</p>
  <span>{{ publishedBooksMessage }}</span>
</template>

4.2 计算属性缓存 vs 方法

计算属性值会基于其响应式依赖被缓存。方法调用则总会在重新渲染时再次执行。

?4.3 可写计算属性

计算属性默认是只读的。但可以通过设置 get 和 set 函数变成可读可写:

<script setup>
import { ref, computed } from "vue";

const firstName = ref("John");
const lastName = ref("Doe");

const fullName = computed({
  // getter
  get() {
    return firstName.value + " " + lastName.value;
  },
  // setter
  set(newValue) {
    // 注意:我们这里使用的是解构赋值语法
    [firstName.value, lastName.value] = newValue.split(" ");
  },
});
</script>

4.4 最佳实践

使用计算属性,不要在里面做异步请求和修改 DOM。并且尽量保持只读。

5. 类与样式绑定

5.1 绑定 HTML class

通过对象来动态切换 class:

<div :class="{ active: isActive }"></div>

可以直接绑定一个对象:

const classObject = reactive({
  active: true,
  "text-danger": false,
});
<div :class="classObject"></div>

通过数组渲染多个 class:

const activeClass = ref("active");
const errorClass = ref("text-danger");
<div :class="[activeClass, errorClass]"></div>

数组中也可以使用 JS 表达式:

<div :class="[isActive ? activeClass : '', errorClass]"></div>
<!-- 等于 -->
<div :class="[{ activeClass: isActive }, errorClass]"></div>

如果组件有多个根元素,透传的 class 需要通过组件的 $attrs 属性来实现指定:

<MyComponent class="baz" />
<!-- MyComponent 模板使用 $attrs 时 -->
<p :class="$attrs.class">Hi!</p>
<span>This is a child component</span>
<p class="baz">Hi!</p>
<span>This is a child component</span>

5.2 绑定内联样式

值为对象,对应的是 style 属性:

const activeColor = ref("red");
const fontSize = ref(30);
<div :style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>

直接绑定一个样式对象使模板更加简洁:

const styleObject = reactive({
  color: "red",
  fontSize: "13px",
});
<div :style="styleObject"></div>

还可以绑定一个包含多个样式对象的数组:

<div :style="[baseStyles, overridingStyles]"></div>

6. 条件渲染

6.1 v-if、v-else、v-else-if

v-if 指令用于条件性地渲染内容。当值为真时才被渲染:

<h1 v-if="awesome">Vue is awesome!</h1>

v-else 为 v-if 添加一个“else 区块”。并且必须跟在一个 v-if 或者 v-else-if 元素后面:

<button @click="awesome = !awesome">Toggle</button>

<h1 v-if="awesome">Vue is awesome!</h1>
<h1 v-else>Oh no 😢</h1>

v-else-if 提供的是相应于 v-if 的“else if 区块”。可以连续多次使用。并且必须跟在一个 v-if 或者 v-else-if 元素后面:

<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> 包裹想要一起切换的元素块,渲染的结果并不会包含这个 <template> 元素。v-else、v-else-if 同理:

<template v-if="ok">
  <h1>Title</h1>
  <p>Paragraph 1</p>
  <p>Paragraph 2</p>
</template>

6.2 v-show

v-show 仅切换了元素上 display 的 CSS 属性。且不支持在 <template> 元素上使用,也不能和 v-else 搭配使用。

6.3 v-if vs v-show

v-if 切换的组件都会被销毁与重建。但是如果初始条件为 false,则不会做任何事,有更高的切换开销。

v-show 切换的组件只有 display 属性被修改,但初始化都会渲染。有更高的渲染开销。

综上:如果切换频繁用 v-show,反之用 v-if。

6.4 v-if 和 v-for

v-if 和 v-for 不推荐同时使用,因为这样二者的优先级不明显。如果二者同时存在一个元素上,v-if 优先执行。

7. 列表渲染

7.1 v-for

v-for 指令基于一个数组来渲染一个列表:

const parentMessage = ref("Parent");
const items = ref([{ message: "Foo" }, { message: "Bar" }]);
<li v-for="(item, index) in items">
  {{ parentMessage }} - {{ index }} - {{ item.message }}
</li>

可以使用 of 替代 in:

<div v-for="item of items"></div>

7.2 v-for 与对象

v-for 可以遍历对象属性:

const myObject = reactive({
  title: "How to do lists in Vue",
  author: "Jane Doe",
  publishedAt: "2016-04-10",
});
<!-- 第二个参数表示属性名,第三个参数表示位置索引 -->
<li v-for="(value, key, index) in myObject">
  {{ index }}. {{ key }}: {{ value }}
</li>

7.3 在 v-for 里使用范围值

v-for 可以接受一个整数。从 1~n 开始遍历:

<span v-for="n in 10">{{ n }}</span>

7.4 <template> 上的 v-for

<ul>
  <template v-for="item in items">
    <li>{{ item.msg }}</li>
    <li class="divider" role="presentation"></li>
  </template>
</ul>

7.5 v-for 与 v-if

在同一节点上,v-if 比 v-for 优先级更高。意味着 v-if 的条件无法访问到 v-for 中的变量:

<!-- 会抛出一个错误,因为v-if访问不到属性 todo -->
<li v-for="todo in todos" v-if="!todo.isComplete">{{ todo.name }}</li>

<!-- 解决方法:包裹一层template -->
<template v-for="todo in todos">
  <li v-if="!todo.isComplete">{{ todo.name }}</li>
</template>

7.6 通过 key 管理状态

Vue 默认的 “就地更新” 策略是高效的,但当数据源的顺序改变时,Vue 不会随之移动 DOM 顺序,而是就地更新每个元素。解决这一问题需要给每个元素添加一个 Key 值,官方推荐使用 v-for 都添加 key 值:

<div v-for="item in items" :key="item.id">
  <!-- 内容 -->
</div>

7.7 数组变化侦测

Vue 能侦听响应式数组的变化。改变原数组的方法:push(),pop(),shift(),unshift(),splice(),sort(),reverse()。不改变元素组:filter(),concat(),slice()

7.8 展示过滤或排序后的结果

使用 computed,在不修改数据源的前提下,展示过滤或排序后的数据:

const numbers = ref([1, 2, 3, 4, 5]);

const evenNumbers = computed(() => {
  return numbers.value.filter((n) => n % 2 === 0);
});
<li v-for="n in evenNumbers">{{ n }}</li>

8. 事件处理

8.1 监听事件

使用 v-on 指令 (简写 @) 来监听 DOM 事件

8.2 内联事件处理器

const count = ref(0);
<button @click="count++">Add 1</button>

8.3 方法事件处理器

const name = ref("Vue.js");

function greet(event) {
  alert(`Hello ${name.value}!`);
  // `event` 是 DOM 原生事件
  if (event) {
    alert(event.target.tagName);
  }
}
<!-- `greet` 是上面定义过的方法名 -->
<button @click="greet">Greet</button>

8.4 事件传参

function say(message) {
  alert(message);
}
<button @click="say('hello')">Say hello</button>
<button @click="say('bye')">Say bye</button>

8.5 在方法中访问原生 DOM

通过 $event 变量访问原生 DOM,或使用内联箭头函数中的 event 形参:

<!-- 使用特殊的 $event 变量 -->
<button @click="warn('Form cannot be submitted yet.', $event)">Submit</button>

<!-- 使用内联箭头函数 -->
<button @click="(event) => warn('Form cannot be submitted yet.', event)">
  Submit
</button>
function warn(message, event) {
  // 这里可以访问原生事件
  if (event) {
    event.preventDefault();
  }
  alert(message);
}

8.6 事件修饰符

修饰符是用 . 表示的指令后缀,包含:.stop,.prevent,.self,.capture,.once,.passive:

<!-- 单击事件将停止传递 -->
<a @click.stop="doThis"></a>

<!-- 提交事件将不再重新加载页面 -->
<form @submit.prevent="onSubmit"></form>

<!-- 修饰语可以使用链式书写 -->
<a @click.stop.prevent="doThat"></a>

<!-- 也可以只有修饰符 -->
<form @submit.prevent></form>

<!-- 仅当 event.target 是元素本身时才会触发事件处理器 -->
<!-- 例如:事件处理器不来自子元素 -->
<div @click.self="doThat">...</div>

.capture、.once 和 .passive 修饰符与原生 addEventListener 事件相对应:

<!-- 添加事件监听器时,使用 `capture` 捕获模式 -->
<!-- 例如:指向内部元素的事件,在被内部元素处理前,先被外部处理 -->
<div @click.capture="doThis">...</div>

<!-- 点击事件最多被触发一次 -->
<a @click.once="doThis"></a>

<!-- 滚动事件的默认行为 (scrolling) 将立即发生而非等待 `onScroll` 完成 -->
<!-- 以防其中包含 `event.preventDefault()` -->
<!-- .passive 修饰符一般用于触摸事件的监听器,可以用来改善移动端设备的滚屏性能。 -->
<div @scroll.passive="onScroll">...</div>

8.7 按键修饰符

.enter,.tab,.delete (捕获“Delete”和“Backspace”两个按键),.esc,.space,.up,.down,.left,.right

<!-- 仅在 `key` 为 `Enter` 时调用 `submit` -->
<input @keyup.enter="submit" />

9. ?表单输入绑定

v-model 数据双向绑定

<input v-model="text" />
<!-- 等价于 -->
<input :value="text" @input="event => text = event.target.value" />

9.1 基本用法

<!-- 文本 -->
<input v-model="message" placeholder="edit me" />

<!-- 多行文本 -->
<textarea v-model="message" placeholder="add multiple lines"></textarea>

<!-- 复选框 -->
<input type="checkbox" id="checkbox" v-model="checked" />

<!-- 单选按钮 -->
<input type="radio" id="one" value="One" v-model="picked" />

<!-- 选择器 -->
<select v-model="selected">
  <option disabled value="">Please select one</option>
  <option>A</option>
  <option>B</option>
  <option>C</option>
</select>

9.2 值绑定

<!-- 复选框 -->
<!-- true-value 和 false-value 是 Vue 特有的 attributes,仅支持和 v-model 配套使用。 -->
<input type="checkbox" v-model="toggle" true-value="yes" false-value="no" />

<!-- 单选按钮 -->
<!-- pick 会在第一个按钮选中时被设为 first,在第二个按钮选中时被设为 second。 -->
<input type="radio" v-model="pick" :value="first" />
<input type="radio" v-model="pick" :value="second" />

<!-- 选择器选项 -->
<!-- v-model 同样也支持非字符串类型的值绑定!
在上面这个例子中,当某个选项被选中,selected 会被设为该对象字面量值 { number: 123 }。 -->
<select v-model="selected">
  <!-- 内联对象字面量 -->
  <option :value="{ number: 123 }">123</option>
</select>

9.3 修饰符

<!-- .lazy -->
<!-- 在 "change" 事件后同步更新而不是 "input" -->
<input v-model.lazy="msg" />

<!-- .number -->
<!-- 让用户输入自动转换为数字 -->
<input v-model.number="age" />

<!-- .trim -->
<!-- 默认自动去除用户输入内容中两端的空格 -->
<input v-model.trim="msg" />

10. 生命周期

Vue 组件实例的创建到销毁,一些列的生命周期钩子函数,可以让我们特定阶段运行自己的代码。最常用的是 onMounted、onUpdated 和 onUnmounted:

<script setup>
  import { onMounted, onUpdated, onUnmounted } from "vue";

  // 初始渲染、创建 DOM 节点后
  onMounted(() => {
    console.log(`the component is now mounted.`);
  });

  // 组件更新 DOM 树之后。
  onUpdated(() => {
    // 文本内容应该与当前的 `count.value` 一致
    console.log(document.getElementById("count").textContent);
  });

  // 组件实例被卸载之后。
  let intervalId;
  onMounted(() => {
    intervalId = setInterval(() => {
      // ...
    });
  });
  onUnmounted(() => clearInterval(intervalId));
</script>

11. 侦听器

11.1 基本示例

<script setup>
  import { ref, watch } from "vue";

  const question = ref("");
  const answer = ref("Questions usually contain a question mark. ;-)");
  const loading = ref(false);

  // 可以直接侦听一个 ref
  watch(question, async (newQuestion, oldQuestion) => {
    if (newQuestion.includes("?")) {
      loading.value = true;
      answer.value = "Thinking...";
      try {
        const res = await fetch("https://yesno.wtf/api");
        answer.value = (await res.json()).answer;
      } catch (error) {
        answer.value = "Error! Could not reach the API. " + error;
      } finally {
        loading.value = false;
      }
    }
  });
</script>

<template>
  <p>
    Ask a yes/no question:
    <input v-model="question" :disabled="loading" />
  </p>
  <p>{{ answer }}</p>
</template>

watch 的第一个参数可以是不同形式的“数据源”:它可以是一个 ref (包括计算属性)、一个响应式对象、一个 getter 函数、或多个数据源组成的数组:

const x = ref(0);
const y = ref(0);

// 单个 ref
watch(x, (newX) => {
  console.log(`x is ${newX}`);
});

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

// 多个来源组成的数组
watch([x, () => y.value], ([newX, newY]) => {
  console.log(`x is ${newX} and y is ${newY}`);
});

不能直接监听对象的属性:

const obj = reactive({ count: 0 });
// 错误,因为 watch() 得到的参数是一个 number
watch(obj.count, (count) => {
  console.log(`count is: ${count}`);
});

// 解决:提供一个 getter 函数
watch(
  () => obj.count,
  (count) => {
    console.log(`count is: ${count}`);
  }
);

11.2 深层侦听器

给 watch() 传入一个响应式对象,会监听对象的所有属性:

const obj = reactive({ count: 0 });

watch(obj, (newValue, oldValue) => {
  // 在嵌套的属性变更时触发
  // 注意:`newValue` 此处和 `oldValue` 是相等的
  // 因为它们是同一个对象!
});

obj.count++;

11.3 即时回调的侦听器

watch 默认是懒执行的:仅当数据源变化时,才会执行回调。可以通过设置 immediate:true,立即执行一遍回调:

watch(
  source,
  (newValue, oldValue) => {
    // 立即执行,且当 `source` 改变时再次执行
  },
  { immediate: true }
);

11.4 watchEffect()

侦听器的回调与源完全相同的响应式状态是很常见的。可以用 watchEffect 函数 来简化:

const todoId = ref(1);
const data = ref(null);

watch(
  todoId,
  async () => {
    const response = await fetch(
      `https://jsonplaceholder.typicode.com/todos/${todoId.value}`
    );
    data.value = await response.json();
  },
  { immediate: true }
);

// 简化
watchEffect(async () => {
  const response = await fetch(
    `https://jsonplaceholder.typicode.com/todos/${todoId.value}`
  );
  data.value = await response.json();
});

11.5 回调的触发时机

修改了监听的响应状态后,默认先触发监听函数,后更新组件。意味着在监听函数中访问的 DOM 是更新前的状态,如果要访问更新后的 DOM,可以通过 fulsh:'post'配置:

watch(source, callback, {
  flush: "post",
});
watchEffect(callback, {
  flush: "post",
});

// 或者
import { watchPostEffect } from "vue";
watchPostEffect(() => {
  /* 在 Vue 更新后执行 */
});

12. 模板引用

使用 ref attribute 访问 DOM 元素:

<input ref="input" />

12.1 访问模板引用

声明一个同名的 ref:

<script setup>
  import { ref, onMounted } from "vue";

  // 声明一个 ref 来存放该元素的引用
  // 必须和模板里的 ref 同名
  const input = ref(null);

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

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

侦听模板引用 ref 的变化,需要考虑值为 null 的情况:

watchEffect(() => {
  if (input.value) {
    input.value.focus();
  } else {
    // 此时还未挂载,或此元素已经被卸载(例如通过 v-if 控制)
  }
});

12.2 v-for 中的模板引用

v-for 使用模板引用时,对应的 ref 包含的是一个数组:

<script setup>
  import { ref, onMounted } from "vue";

  const list = ref([
    /* ... */
  ]);

  const itemRefs = ref([]);

  onMounted(() => console.log(itemRefs.value));
</script>

<template>
  <ul>
    <li v-for="item in list" ref="itemRefs">{{ item }}</li>
  </ul>
</template>

12.3 函数模板引用

ref attribute 还可以绑定为一个函数,每次组件更新时都被调用。收到的第一个参数是元素引用:

<input :ref="(el) => { /* 将 el 赋值给一个数据属性或 ref 变量 */ }" />

13. 组件基础

组件允许我们将 UI 划分为独立的、可重用的部分,并且对每个部分进行单独的思考。

13.1 定义一个组件

.vue 文件 (简称 SFC):

<script setup>
  import { ref } from "vue";

  const count = ref(0);
</script>

<template>
  <button @click="count++">You clicked me {{ count }} times.</button>
</template>

13.2 使用组件

官方推荐使用 PascalCase 标签名用于和原生 HTML 标签区分:

<script setup>
import ButtonCounter from "./ButtonCounter.vue";
</script>

<template>
  <h1>Here is a child component!</h1>
  <ButtonCounter />
</template>

13.3 传递 props

通过 defineProps 宏,声明可以向组件传递的状态:

<!-- BlogPost.vue -->
<script setup>
defineProps(["title"]);
</script>

<template>
  <h4>{{ title }}</h4>
</template>

13.4 监听事件

通过 defineEmits 宏,声明需要抛出的事件。子组件可以通过调用内置的 $emit 方法,通过传入事件名称来抛出一个事件:

<!-- BlogPost.vue -->
<script setup>
defineProps(["title"]);
const emit = defineEmits(["enlarge-text"]);

const handleClick = () => {
  emit("enlarge-text");
};
</script>

<template>
  <div class="blog-post">
    <h4>{{ title }}</h4>
    <button @click="handleClick">Enlarge text</button>
  </div>
</template>

13.5 通过插槽来分配内容

通过 <slot> 元素来实现:

<!-- AlertBox.vue -->
<template>
  <div class="alert-box">
    <strong>This is an Error for Demo Purposes</strong>
    <slot />
  </div>
</template>

<style scoped>
.alert-box {
  /* ... */
}
</style>
<AlertBox> Something bad happened. </AlertBox>

13.6 动态组件

通过 <component :is="..."> 来在多个组件间作切换,可以使用 <KeepAlive> 保持被切换的组件“存活”的状态:

<!-- currentTab 改变时组件也改变 -->
<component :is="tabs[currentTab]"></component>

三 深入组件

1. 注册

?1.1 全局注册

使用 .component() 方法,让组件在全局可用:

import { createApp } from "vue";

const app = createApp({});

app.component(
  // 注册的名字
  "MyComponent",
  // 组件的实现
  {
    /* ... */
  }
);

.component() 可以链式调用:

app
  .component("ComponentA", ComponentA)
  .component("ComponentB", ComponentB)
  .component("ComponentC", ComponentC);

注意:全局注册存在以下几个问题:1. 全局注册组件后,即使没有被实际使用,仍会被打包在 JS 文件中。2. 全局注册使依赖关系变得不那么明确。不容易定位子组件的实现。

1.2 局部注册

导入后可以直接使用:

<script setup>
import ComponentA from "./ComponentA.vue";
</script>

<template>
  <ComponentA />
</template>

1.3 组件名格式

官方推荐使用 PascalCase 作为组件名的注册格式。

2. Props

2.1 Props 声明

使用 defineProps() 宏来声明:

<script setup>
// defineProps传入字符串数组
const props = defineProps(["foo"]);
// 或 传入对象:可进行类型检测。
const props = defineProps({
  title: String,
  likes: Number,
});

console.log(props.foo);
</script>

2.2 传递 prop 的细节

如果 prop 的名字很长,官方建议使用 camelCase 形式:

defineProps({
  greetingMessage: String,
});

传递 props 时,官方推荐以 kebab-case 形式:

<MyComponent greeting-message="hello" />

传递不同类型的值:

<!-- 虽然 `42` 是个常量,我们还是需要使用 v-bind -->
<!-- 因为这是一个 JS 表达式而不是一个字符串 -->
<BlogPost :likes="42" />

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

<!-- 仅写上 prop 但不传值,会隐式转换为 `true` -->
<BlogPost is-published />

<!-- 虽然 `false` 是静态的值,我们还是需要使用 v-bind -->
<!-- 因为这是一个 JS 表达式而不是一个字符串 -->
<BlogPost :is-published="false" />

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

<!-- 虽然这个数组是个常量,我们还是需要使用 v-bind -->
<!-- 因为这是一个 JS 表达式而不是一个字符串 -->
<BlogPost :comment-ids="[234, 266, 273]" />

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

<!-- 虽然这个对象字面量是个常量,我们还是需要使用 v-bind -->
<!-- 因为这是一个 JS 表达式而不是一个字符串 -->
<!-- <BlogPost
  :author="{
    name: 'Veronica',
    company: 'Veridian Dynamics',
  }"
/> -->

一个对象绑定多个 prop:

const post = {
  id: 1,
  title: "My Journey with Vue",
};
<BlogPost v-bind="post" />
<!-- 等价于: -->
<BlogPost :id="post.id" :title="post.title" />

2.3 单向数据流

props 都遵循单向绑定原则,不允许在子组件中去更改一个 prop。但有两个场景可能想要修改 prop:1.prop 被用于传入初始值,之后想将其作为一个响应式属性。2. 需要对传入的 prop 值做进一步的转换。

2.4 Prop 校验

defineProps({
  // 基础类型检查
  // (给出 `null` 和 `undefined` 值则会跳过任何类型检查)
  propA: Number,
  // 多种可能的类型
  propB: [String, Number],
  // 必传,且为 String 类型
  propC: {
    type: String,
    required: true,
  },
  // Number 类型的默认值
  propD: {
    type: Number,
    default: 100,
  },
  // 对象类型的默认值
  propE: {
    type: Object,
    // 对象或数组的默认值
    // 必须从一个工厂函数返回。
    // 该函数接收组件所接收到的原始 prop 作为参数。
    default(rawProps) {
      return { message: "hello" };
    },
  },
  // 自定义类型校验函数
  propF: {
    validator(value) {
      // The value must match one of these strings
      return ["success", "warning", "danger"].includes(value);
    },
  },
  // 函数类型的默认值
  propG: {
    type: Function,
    // 不像对象或数组的默认,这不是一个
    // 工厂函数。这会是一个用来作为默认值的函数
    default() {
      return "Default function";
    },
  },
});

自定义类或构造函数去验证,校验是否 Person 类的实例:

class Person {
  constructor(firstName, lastName) {
    this.firstName = firstName;
    this.lastName = lastName;
  }
}

defineProps({
  author: Person,
});

2.5 Boolean 类型转换

<!-- 等同于传入 :disabled="true" -->
<MyComponent disabled />

<!-- 等同于传入 :disabled="false" -->
<MyComponent />

3. 事件

3.1 触发与监听事件

模板表达式可以直接使用 $emit 触发自定义事件:

<!-- MyComponent -->
<button @click="$emit('someEvent')">click me</button>

父组件通过@来监听事件:

<MyComponent @some-event="callback" />

3.2 事件参数

<button @click="$emit('increaseBy', 1)">
  Increase by 1
</button>
<MyButton @increase-by="(n) => (count += n)" />

3.3 声明触发的事件

通过 defineEmits() 宏来声明要触发的事件:

<script setup>
const emit = defineEmits(["inFocus", "submit"]);

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

3.4 事件校验

校验事件参数是否符合要求,返回布尔值表明事件是否符合:

<script setup>
const emit = defineEmits({
  // 没有校验
  click: null,

  // 校验 submit 事件
  submit: ({ email, password }) => {
    if (email && password) {
      return true;
    } else {
      console.warn("Invalid submit event payload!");
      return false;
    }
  },
});

function submitForm(email, password) {
  emit("submit", { email, password });
}
</script>

4. 组件 v-model

v-model 在 input 上的用法:

<input v-model="searchText" />
<!-- 展开 -->
<input :value="searchText" @input="searchText = $event.target.value" />

v-model 在组件上的用法,通过 modelValue,update:modelValue 设置:

<!-- CustomInput.vue -->
<script setup>
defineProps(["modelValue"]);
defineEmits(["update:modelValue"]);
</script>

<template>
  <input
    :value="modelValue"
    @input="$emit('update:modelValue', $event.target.value)"
  />
</template>
<CustomInput v-model="searchText" />
<!-- 展开 -->
<CustomInput
  :model-value="searchText"
  @update:model-value="newValue => searchText = newValue"
/>
<!-- CustomInput.vue -->
<script setup>
import { computed } from "vue";

const props = defineProps(["modelValue"]);
const emit = defineEmits(["update:modelValue"]);

const value = computed({
  get() {
    return props.modelValue;
  },
  set(value) {
    emit("update:modelValue", value);
  },
});
</script>

<template>
  <input v-model="value" />
</template>

4.1 v-model 的参数

<MyComponent v-model:title="bookTitle" />
<!-- MyComponent.vue -->
<script setup>
defineProps(["title"]);
defineEmits(["update:title"]);
</script>

<template>
  <input
    type="text"
    :value="title"
    @input="$emit('update:title', $event.target.value)"
  />
</template>

4.2 多个 v-model 的绑定

<UserName v-model:first-name="first" v-model:last-name="last" />
<script setup>
defineProps({
  firstName: String,
  lastName: String,
});

defineEmits(["update:firstName", "update:lastName"]);
</script>

<template>
  <input
    type="text"
    :value="firstName"
    @input="$emit('update:firstName', $event.target.value)"
  />
  <input
    type="text"
    :value="lastName"
    @input="$emit('update:lastName', $event.target.value)"
  />
</template>

4.3 处理 v-model 修饰符

<MyComponent v-model.capitalize="myText" />
<script setup>
const props = defineProps({
  modelValue: String,
  modelModifiers: { default: () => ({}) },
});

const emit = defineEmits(["update:modelValue"]);

function emitValue(e) {
  let value = e.target.value;
  if (props.modelModifiers.capitalize) {
    value = value.charAt(0).toUpperCase() + value.slice(1);
  }
  emit("update:modelValue", value);
}
</script>

<template>
  <input type="text" :value="modelValue" @input="emitValue" />
</template>

v-model 参数和修饰符一起绑定:

<MyComponent v-model:title.capitalize="myText"></MyComponent>
const props = defineProps(['title', 'titleModifiers'])
defineEmits(['update:title']) 

console.log(props.titleModifiers) // { capitalize: true }

5. 透传 Attributes

5.1 Attributes 继承

class、style、id 传递给组件,透传给组件的元素。如果只有一个根节点,默认透传给根节点上,如果有多个根节点,通过 v-bind='$attrs'来确定透传给哪个根节点。

5.2 禁用 Attributes 继承

应用场景:组件只有一个根节点,但透传的 attribute 需要传到其他元素上。通过设置 inheritAttrs 选项为 false,v-bind='$attrs',来完全控制透传:

<script setup>
defineOptions({
  inheritAttrs: false,
});
// ...setup 逻辑
</script>

<template>
  <div class="btn-wrapper">
    <button class="btn" v-bind="$attrs">click me</button>
  </div>
</template>

5.3 在 JS 中访问透传 Attributes

使用 useAttrs() API 来访问一个组件的所有透传 attribute:

<script setup>
import { useAttrs } from "vue";

const attrs = useAttrs();
</script>

6. 插槽

6.1 插槽内容与出口

通过 <slot> 确定插槽插入的位置:

<!-- FancyButton.vue -->
<button class="fancy-btn">
  <!-- 插槽出口 -->
  <slot></slot>
</button>
<FancyButton>
  <!-- 插槽内容 -->
  Click me!
</FancyButton>

6.2 渲染作用域

插槽内容可以访问父组件的状态,无法访问子组件的状态:

<span>{{ message }}</span>
<FancyButton>{{ message }}</FancyButton>

6.3 默认内容

外部没有传递内容,展示默认内容:

<button type="submit">
  <slot>
    <!-- 默认内容 -->
    Submit
  </slot>
</button>

6.4 具名插槽

通过 <slot name=''>来设定,name 默认值为 default:

<div class="container">
  <header>
    <slot name="header"></slot>
  </header>
  <main>
    <slot name="default"></slot>
  </main>
  <footer>
    <slot name="footer"></slot>
  </footer>
</div>

通过 v-slot 有对应的简写 # 来使用,因此 <template v-slot:header> 可以简写为 <template #header>:

<BaseLayout>
  <template #header>
    <!-- header 插槽的内容放这里 -->
  </template>
</BaseLayout>

6.5 动态插槽名

<BaseLayout>
  <template v-slot:[dynamicSlotName]> ... </template>

  <!-- 缩写为 -->
  <template #[dynamicSlotName]> ... </template>
</BaseLayout>

6.6 作用域插槽

插槽内容同时访问父组件和子组件的状态:

<!-- <MyComponent> 的模板 -->
<div>
  <slot :text="greetingMessage" :count="1"></slot>
</div>
<MyComponent v-slot="slotProps">
  {{ slotProps.text }} {{ slotProps.count }}
</MyComponent>

向具名插槽中传入 props:

<!-- MyComponent.vue -->
<slot name="header" message="hello"></slot>

具名插槽访问子组件的状态:

<MyComponent>
  <template #header="headerProps"> {{ headerProps }} </template>

  <template #default="defaultProps"> {{ defaultProps }} </template>

  <template #footer="footerProps"> {{ footerProps }} </template>
</MyComponent>

7. 依赖注入

7.1 Prop 逐级透传问题

解决多层级嵌套的组件,共享祖先状态。使用 provide 和 inject 可以帮助我们解决。

7.2 局部 Provide(提供)

<script setup>
import { provide } from "vue";

provide(/* 注入名 */ "message", /* 值 */ "hello!");
</script>

7.3 全局 Provide

import { createApp } from "vue";

const app = createApp({});

app.provide(/* 注入名 */ "message", /* 值 */ "hello!");

7.4 Inject(注入)

<script setup>
import { inject } from "vue";

const message = inject("message", "这是默认值");
</script>

7.5 和响应数据配合使用

官方建议将共享状态和修改共享状态的方法都 Provide 出去:

<!-- 在供给方组件内 -->
<script setup>
import { provide, ref } from "vue";

const location = ref("North Pole");

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

provide("location", {
  location,
  updateLocation,
});
</script>

使用 readonly(),注入方就只读不可修改:

<script setup>
import { ref, provide, readonly } from "vue";

const count = ref(0);
provide("read-only-count", readonly(count));
</script>

8. 异步组件

8.1 基本用法

仅在需要页面渲染时加载组件。实现延迟加载。全局注册:

app.component(
  "MyComponent",
  defineAsyncComponent(() => import("./components/MyComponent.vue"))
);

局部注册:

<script setup>
import { defineAsyncComponent } from "vue";

const AdminPage = defineAsyncComponent(() =>
  import("./components/AdminPageComponent.vue")
);
</script>

<template>
  <AdminPage />
</template>

8.2 加载与错误状态

异步操作不可避免地会涉及到加载和错误状态:

const AsyncComp = defineAsyncComponent({
  // 加载函数
  loader: () => import("./Foo.vue"),

  // 加载中使用的组件
  loadingComponent: LoadingComponent,
  // 展示加载组件前的延迟时间,默认为 200ms
  delay: 200,

  // 加载失败后展示的组件
  errorComponent: ErrorComponent,
  // 如果提供了一个 timeout 时间限制,并超时了
  // 也会显示这里配置的报错组件,默认值是:Infinity
  timeout: 3000,
});

四 逻辑复用

1. 组合式函数

1.1 什么是 “组合式函数”

“组合式函数”(Composables) 是一个利用 Vue 的组合式 API 来封装和复用有状态逻辑的函数。

1.2 鼠标跟踪器示例

使用组合式 API 实现鼠标跟踪功能:

// 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 };
}

在组件中使用:

<script setup>
import { useMouse } from "./mouse.js";

const { x, y } = useMouse();
</script>

<template>Mouse position is at: {{ x }}, {{ y }}</template>

一个组合式函数可以调用一个或多个其他的组合式函数。这样就可以用多个较小且逻辑独立的单元来组合形成复杂的逻辑。举例来说,我们可以将添加和清除 DOM 事件监听器的逻辑也封装进一个组合式函数中:

// event.js
import { onMounted, onUnmounted } from "vue";

export function useEventListener(target, event, callback) {
  // 如果你想的话,
  // 也可以用字符串形式的 CSS 选择器来寻找目标 DOM 元素
  onMounted(() => target.addEventListener(event, callback));
  onUnmounted(() => target.removeEventListener(event, callback));
}
// mouse.js
import { ref } from "vue";
import { useEventListener } from "./event";

export function useMouse() {
  const x = ref(0);
  const y = ref(0);

  useEventListener(window, "mousemove", (event) => {
    x.value = event.pageX;
    y.value = event.pageY;
  });

  return { x, y };
}

1.3 异步状态示例

在做异步数据请求时,我们常常需要处理不同的状态:加载中、加载成功和加载失败。

1.4 约定和最佳实践

组合式函数约定用驼峰命名法命名,并以“use”作为开头:

import { toValue } from "vue";

function useFeature(maybeRefOrGetter) {
  // Todo
}

1.5 通过抽取组合式函数改变代码结构

组合式 API 会给予你足够的灵活性,让你可以基于逻辑问题将组件代码拆分成更小的函数

1.6 与其他模式的比较

和 Mixin 的对比,mixins 有三个主要的短板:不清晰的数据来源、命名空间冲突、隐式的跨 mixin 交流

和无渲染组件的对比:组合式函数不会产生额外的组件实例开销。

2. 自定义指令

2.1 介绍

自定义指令主要是为了重用涉及普通元素的底层 DOM 访问的逻辑。一个自定义指令由一个包含类似组件生命周期钩子的对象来定义。钩子函数会接收到指令所绑定元素作为其参数。任何以 v 开头的驼峰式命名的变量都可以被用作一个自定义指令:

<script setup>
// 在模板中启用 v-focus
const vFocus = {
  mounted: (el) => el.focus(),
};
</script>

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

全局注册自定义指令:

const app = createApp({});

// 使 v-focus 在所有组件中都可用
app.directive("focus", {
  /* ... */
});

2.2 指令钩子

指令钩子函数都是可选的:

const myDirective = {
  // 在绑定元素的 attribute 前
  // 或事件监听器应用前调用
  created(el, binding, vnode, prevVnode) {
    // 下面会介绍各个参数的细节
  },
  // 在元素被插入到 DOM 前调用
  beforeMount(el, binding, vnode, prevVnode) {},
  // 在绑定元素的父组件
  // 及他自己的所有子节点都挂载完成后调用
  mounted(el, binding, vnode, prevVnode) {},
  // 绑定元素的父组件更新前调用
  beforeUpdate(el, binding, vnode, prevVnode) {},
  // 在绑定元素的父组件
  // 及他自己的所有子节点都更新后调用
  updated(el, binding, vnode, prevVnode) {},
  // 绑定元素的父组件卸载前调用
  beforeUnmount(el, binding, vnode, prevVnode) {},
  // 绑定元素的父组件卸载后调用
  unmounted(el, binding, vnode, prevVnode) {},
};

指令的钩子会传递以下几种参数:

el:指令绑定到的元素。这可以用于直接操作 DOM。

binding:一个对象,包含以下属性。

????????value:传递给指令的值。例如在 v-my-directive="1 + 1" 中,值是 2。

????????oldValue:之前的值,仅在 beforeUpdate 和 updated 中可用。无论值是否更改,它都可用。

????????arg:传递给指令的参数 (如果有的话)。例如在 v-my-directive:foo 中,参数是 "foo"。

????????modifiers:一个包含修饰符的对象 (如果有的话)。例如在 v-my-directive.foo.bar 中,修饰符对象是 { foo: true, bar: true }。

????????instance:使用该指令的组件实例。

????????dir:指令的定义对象。

vnode:代表绑定元素的底层 VNode。

prevNode:代表之前的渲染中指令所绑定元素的 VNode。仅在 beforeUpdate 和 updated 钩子中可用。

例子如下:

<div v-example:foo.bar="baz"></div>
{
  arg: 'foo',
  modifiers: { bar: true },
  value: /* `baz` 的值 */,
  oldValue: /* 上一次更新时 `baz` 的值 */
}

2.3 简化形式

一般情况指令仅需要 mounted 和 updated 实现相同行为,可以直接用一个函数定义指令:

app.directive("color", (el, binding) => {
  // 这会在 `mounted` 和 `updated` 时都调用
  el.style.color = binding.value;
});

2.4 对象字面量

指令值可以接受一个 JS 对象字面量:

<div v-demo="{ color: 'white', text: 'hello!' }"></div>
app.directive("demo", (el, binding) => {
  console.log(binding.value.color); // => "white"
  console.log(binding.value.text); // => "hello!"
});

3. 插件

3.1 接受

插件 (Plugins) 是一种能为 Vue 添加全局功能的工具代码。下面是如何安装一个插件的示例:

import { createApp } from "vue";

const app = createApp({});

app.use(myPlugin, {
  /* 可选的选项 */
});

构建插件:拥有 install() 方法的对象,接受两个参数一个应用实例和一个传递给 app.use() 的额外参数:

const myPlugin = {
  install(app, options) {
    // 配置此应用
  },
};

3.2 编写一个插件

编写一个翻译函数,接收一个以 . 作为分隔符的 key 字符串。可以在全局使用,通过 app.config.globalProperties 实现:

// 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);
    };
  },
};

翻译字典对象应当作为 app.use() 的额外参数被传入:

import i18nPlugin from "./plugins/i18n";

app.use(i18nPlugin, {
  greetings: {
    hello: "Bonjour!",
  },
});

使用插件:

<h1>{{ $translate('greetings.hello') }}</h1>

五 内置组件

1. Transition

会在一个元素或组件进入和离开 DOM 时应用动画,如果内容是一个组件,这个组件必须仅有一个根元素。

1.1 <Transition> 组件

可以直接使用,无需注册。动画过渡触发条件:v-if、v-show、<component>切换组件、改变特殊的 key 属性:

<button @click="show = !show">Toggle</button>
<Transition>
  <p v-if="show">hello</p>
</Transition>
/* 进入,离开的过渡效果 */
.v-enter-active,
.v-leave-active {
  transition: opacity 0.5s ease;
}

/* 进入前,离开后的DOM状态 */
.v-enter-from,
.v-leave-to {
  opacity: 0;
}

1.2 基于 CSS 的过渡效果

有 6 个应用于进入和离开过渡效果的 CSS class:

v-enter-from:进入动画的起始状态。

v-enter-active:进入动画的生效状态。

v-enter-to:进入动画的结束状态。

v-leave-from:离开动画的起始状态。

v-leave-active:离开动画的生效状态。

v-leave-to:离开动画的结束状态。

使用 name prop 自定义过渡效果名:

<Transition name="fade">
  ...
</Transition>
.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.5s ease;
}

.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}

CSS 的 transition,显示样式是 DOM 的 css 的样式,只需定义进入前,离开后的 css 即可:

<Transition name="slide-fade">
  <p v-if="show">hello</p>
</Transition>
/*
  进入和离开动画可以使用不同
  持续时间和速度曲线。
*/
.slide-fade-enter-active {
  transition: all 0.3s ease-out;
}

.slide-fade-leave-active {
  transition: all 0.8s cubic-bezier(1, 0.5, 0.8, 1);
}

.slide-fade-enter-from,
.slide-fade-leave-to {
  transform: translateX(20px);
  opacity: 0;
}

CSS 的 animation:

<Transition name="bounce">
  <p v-if="show" style="text-align: center;">
    Hello here is some bouncy text!
  </p>
</Transition>
.bounce-enter-active {
  animation: bounce-in 0.5s;
}
.bounce-leave-active {
  animation: bounce-in 0.5s reverse;
}
@keyframes bounce-in {
  0% {
    transform: scale(0);
  }
  50% {
    transform: scale(1.25);
  }
  100% {
    transform: scale(1);
  }
}

可自定义过渡 class,在你想集成第三方 CSS 动画库时非常有用,比如 Animate.css:

enter-from-class

enter-active-class

enter-to-class

leave-from-class

leave-active-class

leave-to-class

<!-- 假设你已经在页面中引入了 Animate.css -->
<Transition
  name="custom-classes"
  enter-active-class="animate__animated animate__tada"
  leave-active-class="animate__animated animate__bounceOutRight"
>
  <p v-if="show">hello</p>
</Transition>

在深层级的元素上触发过渡效果:

<Transition name="nested">
  <div v-if="show" class="outer">
    <div class="inner">
      Hello
    </div>
  </div>
</Transition>
/* 应用于嵌套元素的规则 */
.nested-enter-active .inner,
.nested-leave-active .inner {
  transition: all 0.3s ease-in-out;
}

.nested-enter-from .inner,
.nested-leave-to .inner {
  transform: translateX(30px);
  opacity: 0;
}

/* 延迟嵌套元素的进入以获得交错效果 */
.nested-enter-active .inner {
  transition-delay: 0.25s;
}

/* ... 省略了其他必要的 CSS */

等待所有内部元素的过渡完成,传入 duration prop 来显式指定过渡的持续时间 (以毫秒为单位):

<Transition :duration="550">...</Transition>
<Transition :duration="{ enter: 500, leave: 800 }">...</Transition>

1.3 JS 钩子

<Transition
  @before-enter="onBeforeEnter"
  @enter="onEnter"
  @after-enter="onAfterEnter"
  @enter-cancelled="onEnterCancelled"
  @before-leave="onBeforeLeave"
  @leave="onLeave"
  @after-leave="onAfterLeave"
  @leave-cancelled="onLeaveCancelled"
>
  <!-- ... -->
</Transition>
// 在元素被插入到 DOM 之前被调用
// 用这个来设置元素的 "enter-from" 状态
function onBeforeEnter(el) {}

// 在元素被插入到 DOM 之后的下一帧被调用
// 用这个来开始进入动画
function onEnter(el, done) {
  // 调用回调函数 done 表示过渡结束
  // 如果与 CSS 结合使用,则这个回调是可选参数
  done();
}

// 当进入过渡完成时调用。
function onAfterEnter(el) {}

// 当进入过渡在完成之前被取消时调用
function onEnterCancelled(el) {}

// 在 leave 钩子之前调用
// 大多数时候,你应该只会用到 leave 钩子
function onBeforeLeave(el) {}

// 在离开过渡开始时调用
// 用这个来开始离开动画
function onLeave(el, done) {
  // 调用回调函数 done 表示过渡结束
  // 如果与 CSS 结合使用,则这个回调是可选参数
  done();
}

// 在离开过渡完成、
// 且元素已从 DOM 中移除时调用
function onAfterLeave(el) {}

// 仅在 v-show 过渡中可用
function onLeaveCancelled(el) {}

如果仅有 JS 控制过渡,最好手动加上:css='false',防止 CSS 规则意外干扰过渡:

<Transition ... :css="false">
  ...
</Transition>

1.4 可复用过渡效果

将 <Transition> 封装成组件:

<!-- MyTransition.vue -->
<script>
// JS 钩子逻辑...
</script>

<template>
  <!-- 包装内置的 Transition 组件 -->
  <Transition name="my-transition" @enter="onEnter" @leave="onLeave">
    <slot></slot>
    <!-- 向内传递插槽内容 -->
  </Transition>
</template>

<style>
/*
  必要的 CSS...
  注意:避免在这里使用 <style scoped>
  因为那不会应用到插槽内容上
*/
</style>
<MyTransition>
  <div v-if="show">Hello</div>
</MyTransition>

文章来源:https://blog.csdn.net/weixin_64684095/article/details/135640510
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。