跳到主要内容

核心知识点

1. Vue 3 简介

1.1 什么是 Vue 3?

Vue (发音为 /vjuː/,类似于 view) 是一套用于构建用户界面的渐进式框架。与单片框架不同,Vue 被设计为可以自底向上逐层应用。Vue 的核心库只关注视图层,不仅易于上手,还便于与第三方库或既有项目整合。另一方面,当与现代化的工具链以及各种支持类库结合使用时,Vue 也完全能够为复杂的单页应用 (SPA) 提供驱动。

Vue 3 是 Vue.js 的最新主版本,它在 Vue 2 的基础上进行了诸多改进和重构,带来了更好的性能、更小的包体积、更优秀的 TypeScript 支持、新的 API(如 Composition API)以及一些新功能(如 Teleport, Suspense)。

1.2 Vue 3 的主要优势与改进

  • 性能提升: 通过更优化的虚拟 DOM (Virtual DOM) 算法、编译时优化(静态节点提升、补丁标志等)实现更快的渲染和更新。
  • 更小的体积: 通过摇树优化 (Tree-shaking) 移除未使用的代码,核心库体积更小。
  • Composition API: 一种新的、基于函数的逻辑组织方式,更利于代码复用和组织复杂组件逻辑,与 Options API 并存。
  • 更好的 TypeScript 支持: 源码使用 TypeScript 重写,提供一流的类型推导和支持。
  • 新特性: 如 Teleport (瞬移组件到 DOM 其他位置)、Suspense (处理异步组件加载状态)、Fragments (模板允许多个根节点)。
  • 改进的响应式系统: 基于 ES6 Proxy 实现,性能更好,能检测到更多类型的变化(如属性添加/删除、数组索引修改)。

2. 创建 Vue 应用

2.1 使用 Vite 创建 (推荐)

Vite 是 Vue 官方推荐的现代前端构建工具,提供极速的冷启动和热模块替换 (HMR)。

# 使用 npm
npm create vue@latest

# 使用 yarn
# yarn create vue

# 使用 pnpm
# pnpm create vue

# 根据提示选择项目配置 (TypeScript, JSX, Router, Pinia 等)
cd <your-project-name>
npm install
npm run dev

2.2 应用实例与根组件

createApp 用于创建一个新的 Vue 应用实例。它接受一个根组件作为参数。

// main.js (或 main.ts)
import { createApp } from "vue";
import App from "./App.vue"; // 根组件
import "./style.css"; // 全局样式 (可选)

// 创建应用实例
const app = createApp(App);

// 可以在这里挂载插件、全局组件等
// app.use(...)
// app.component(...)
// app.directive(...)

// 挂载应用到 DOM 元素
app.mount("#app");
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vue 3 App</title>
</head>
<body>
<!-- 应用将挂载到这里 -->
<div id="app"></div>
<!-- 引入入口 JS 文件 -->
<script type="module" src="/src/main.js"></script>
</body>
</html>
<!-- src/App.vue (根组件示例) -->
<template>
<h1>{{ message }}</h1>
<p>Welcome to your Vue 3 App!</p>
</template>

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

const message = ref("Hello Vue 3!");
</script>

<style scoped>
h1 {
color: #42b983;
}
</style>

3. 模板语法 (Template Syntax)

Vue 使用一种基于 HTML 的模板语法,允许开发者声明式地将 DOM 绑定至底层 Vue 实例的数据。

3.1 文本插值

使用 "Mustache" 语法(双大括号 {{ }})进行数据绑定。

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

<script setup>
import { ref } from "vue";
const msg = ref("This is a message.");
</script>

3.2 Raw HTML (v-html)

双大括号会将数据解释为普通文本,而非 HTML 代码。为了输出真正的 HTML,需要使用 v-html 指令。注意:在网站上动态渲染任意 HTML 是非常危险的,容易导致 XSS 攻击。请只对可信内容使用 v-html,永远不要用在用户提交的内容上。

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

<script setup>
import { ref } from "vue";
const rawHtml = ref('<span style="color: red;">This should be red.</span>');
</script>

3.3 Attribute 绑定 (v-bind:)

Mustache 语法不能作用在 HTML attribute 上,需要使用 v-bind 指令或其简写 :

<template>
<!-- 绑定 id -->
<div v-bind:id="dynamicId">Div with dynamic ID</div>
<!-- 简写 -->
<button :disabled="isButtonDisabled">Click Me</button>
<!-- 动态绑定多个 attribute -->
<div v-bind="objectOfAttrs">Div with multiple attributes</div>
</template>

<script setup>
import { ref, reactive } from "vue";
const dynamicId = ref("my-element-id");
const isButtonDisabled = ref(true);
const objectOfAttrs = reactive({
id: "my-div",
class: "container",
"data-custom": "value",
});

// Example: Toggle button disabled state after 2 seconds
setTimeout(() => {
isButtonDisabled.value = false;
}, 2000);
</script>

3.4 使用 JavaScript 表达式

{{ }}v-bind 中可以支持完整的 JavaScript 表达式,但仅限于单个表达式

<template>
<p>{{ number + 1 }}</p>
<p>{{ ok ? "YES" : "NO" }}</p>
<p>{{ message.split("").reverse().join("") }}</p>
<div :id="'list-' + id">ID: {{ id }}</div>
<!-- 不能使用语句,如 if/else 或 for 循环 -->
<!-- {{ var a = 1 }} -->
<!-- {{ if (ok) { return message } }} -->
</template>

<script setup>
import { ref } from "vue";
const number = ref(5);
const ok = ref(true);
const message = ref("hello");
const id = ref(123);
</script>

3.5 指令 (Directives)

指令是带有 v- 前缀的特殊 attribute。指令 attribute 的值预期是单个 JavaScript 表达式

3.5.1 条件渲染 (v-if, v-else-if, v-else, v-show)

  • v-if, v-else-if, v-else: 根据表达式的真假值来条件性地渲染一块内容。元素及内部指令会被销毁和重建。
  • v-show: 根据表达式的真假值来切换元素的 CSS display 属性。元素始终会被渲染并保留在 DOM 中。v-show 不支持 <template> 元素,也不能和 v-else 一起使用。
<template>
<!-- v-if -->
<button @click="toggleVif">Toggle v-if</button>
<h2 v-if="awesome">Vue is awesome!</h2>
<h2 v-else>Oh no 😢</h2>

<!-- v-if with v-else-if -->
<div v-if="type === 'A'">Type A</div>
<div v-else-if="type === 'B'">Type B</div>
<div v-else-if="type === 'C'">Type C</div>
<div v-else>Not A/B/C</div>

<!-- v-show -->
<button @click="toggleVshow">Toggle v-show</button>
<h2 v-show="okVshow">Hello with v-show!</h2>
</template>

<script setup>
import { ref } from "vue";
const awesome = ref(true);
const type = ref("A");
const okVshow = ref(true);

function toggleVif() {
awesome.value = !awesome.value;
}
function toggleVshow() {
okVshow.value = !okVshow.value;
}

// Example to change type
setTimeout(() => {
type.value = "B";
}, 2000);
</script>

3.5.2 列表渲染 (v-for)

基于源数据多次渲染元素或模板块。

<template>
<h4>Array Iteration</h4>
<ul>
<!-- item in items -->
<li v-for="item in items" :key="item.id">
{{ item.message }}
</li>
<!-- (item, index) in items -->
<li v-for="(item, index) in items" :key="item.id">
{{ index }} - {{ item.message }}
</li>
</ul>

<h4>Object Iteration</h4>
<ul>
<!-- (value, key, index) in object -->
<li v-for="(value, key, index) in myObject" :key="key">
{{ index }}. {{ key }}: {{ value }}
</li>
</ul>

<h4>Range Iteration</h4>
<span v-for="n in 5" :key="n">{{ n }} </span>

<h4>v-for with v-if</h4>
<ul>
<template v-for="item in items" :key="item.id">
<li v-if="!item.isHidden">
{{ item.message }}
</li>
</template>
<!-- Note: Avoid using v-if and v-for on the same element due to precedence ambiguity.
Use a <template> tag with v-for, and v-if on the inner element. -->
</ul>
</template>

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

const items = ref([
{ id: 1, message: "Foo", isHidden: false },
{ id: 2, message: "Bar", isHidden: true },
{ id: 3, message: "Baz", isHidden: false },
]);

const myObject = reactive({
title: "How to do lists in Vue",
author: "Jane Doe",
publishedAt: "2023-01-01",
});
</script>

key: 使用 v-for 时,强烈建议提供一个 key attribute,其值必须是字符串或数字类型,并且在兄弟节点中必须唯一。key 帮助 Vue 跟踪每个节点的身份,从而重用和重新排序现有元素,提高性能。

3.5.3 事件处理 (v-on@)

监听 DOM 事件,并在触发时运行一些 JavaScript 代码。

<template>
<!-- Inline handler -->
<button v-on:click="count++">Add 1 (Inline)</button>
<p>Count is: {{ count }}</p>

<!-- Method handler -->
<button @click="greet">Greet</button>

<!-- Method with event argument -->
<button @click="say('hello')">Say hello</button>
<button @click="say('bye')">Say bye</button>

<!-- Event modifiers -->
<!-- .stop: 阻止事件冒泡 -->
<div @click="handleOuterClick">
<button @click.stop="handleInnerClick">Click Me (Stop Propagation)</button>
</div>
<!-- .prevent: 阻止默认事件 (例如表单提交) -->
<form @submit.prevent="onSubmit">
<button type="submit">Submit (Prevent Default)</button>
</form>
<!-- .once: 只触发一次 -->
<button @click.once="handleOnce">Click Once</button>
<!-- .self: 只在事件是从侦听器绑定的元素本身触发时才触发 -->
<div @click.self="handleSelf">Outer Div (Self)<span>Inner Span</span></div>
<!-- Key modifiers -->
<input @keyup.enter="submitOnEnter" placeholder="Press Enter" />
<input @keyup.alt.enter="clearInput" placeholder="Press Alt+Enter" />
</template>

<script setup>
import { ref } from "vue";
const count = ref(0);

function greet(event) {
alert("Hello!");
// `event` 是原生 DOM 事件
if (event) {
console.log(event.target.tagName); // BUTTON
}
}

function say(message) {
alert(message);
}

function handleOuterClick() {
console.log("Outer div clicked");
}
function handleInnerClick() {
console.log("Inner button clicked (propagation stopped)");
}
function onSubmit() {
console.log("Form submitted (default prevented)");
}
function handleOnce() {
console.log("Clicked once!");
}
function handleSelf() {
console.log("Outer div clicked directly (self)");
}
function submitOnEnter() {
console.log("Enter key pressed on input");
}
function clearInput(event) {
event.target.value = "";
console.log("Alt+Enter pressed, input cleared");
}
</script>

3.5.4 表单输入绑定 (v-model)

在表单 <input>, <textarea>, <select> 元素上创建双向数据绑定。它会根据控件类型自动选取正确的方法来更新元素。

<template>
<h4>Text Input</h4>
<input type="text" v-model="message" placeholder="edit me" />
<p>Message is: {{ message }}</p>

<h4>Textarea</h4>
<textarea
v-model="multiLineMessage"
placeholder="add multiple lines"
></textarea>
<p style="white-space: pre-line;">
Multiline message is:\n{{ multiLineMessage }}
</p>

<h4>Checkbox (single)</h4>
<input type="checkbox" id="checkbox" v-model="checked" />
<label for="checkbox">{{ checked }}</label>

<h4>Checkbox (multiple)</h4>
<div>Checked names: {{ checkedNames }}</div>
<input type="checkbox" id="jack" value="Jack" v-model="checkedNames" />
<label for="jack">Jack</label>
<input type="checkbox" id="john" value="John" v-model="checkedNames" />
<label for="john">John</label>
<input type="checkbox" id="mike" value="Mike" v-model="checkedNames" />
<label for="mike">Mike</label>

<h4>Radio</h4>
<div>Picked: {{ picked }}</div>
<input type="radio" id="one" value="One" v-model="picked" />
<label for="one">One</label>
<input type="radio" id="two" value="Two" v-model="picked" />
<label for="two">Two</label>

<h4>Select (single)</h4>
<div>Selected: {{ selected }}</div>
<select v-model="selected">
<option disabled value="">Please select one</option>
<option>A</option>
<option>B</option>
<option>C</option>
</select>

<h4>Select (multiple)</h4>
<div>Selected multiple: {{ multiSelected }}</div>
<select v-model="multiSelected" multiple style="width:100px;">
<option>A</option>
<option>B</option>
<option>C</option>
</select>

<h4>v-model Modifiers</h4>
<!-- .lazy: 在 change 事件而非 input 事件更新 -->
<input v-model.lazy="lazyMsg" placeholder="Lazy update" />
<p>Lazy: {{ lazyMsg }}</p>
<!-- .number: 输入值转为数值类型 -->
<input v-model.number="age" type="number" placeholder="Number" />
<p>Age (type): {{ typeof age }}</p>
<!-- .trim: 自动过滤输入首尾空格 -->
<input v-model.trim="trimmedMsg" placeholder="Trim whitespace" />
<p>Trimmed: '{{ trimmedMsg }}'</p>
</template>

<script setup>
import { ref } from "vue";
const message = ref("");
const multiLineMessage = ref("");
const checked = ref(true);
const checkedNames = ref(["Jack"]); // Needs to be an array for multiple checkboxes
const picked = ref("One");
const selected = ref("");
const multiSelected = ref(["A"]); // Needs to be an array for multiple select
const lazyMsg = ref("");
const age = ref(0);
const trimmedMsg = ref("");
</script>

3.5.5 Class 与 Style 绑定 (:class, :style)

动态地绑定 HTML classstyle

<template>
<h4>Class Bindings</h4>
<!-- Object syntax -->
<div :class="{ active: isActive, 'text-danger': hasError }">
Object Syntax Class
</div>
<!-- Array syntax -->
<div :class="[activeClass, errorClass]">Array Syntax Class</div>
<!-- With plain class -->
<div class="static" :class="{ active: isActive }">Static + Object Syntax</div>

<button @click="toggleClass">Toggle Classes</button>

<h4>Style Bindings</h4>
<!-- Object syntax -->
<div :style="{ color: activeColor, fontSize: fontSize + 'px' }">
Object Syntax Style
</div>
<!-- Use camelCase or kebab-case (in quotes) -->
<div :style="{ 'font-weight': 'bold', backgroundColor: bgColor }">
Object Syntax Style (mixed case)
</div>
<!-- Array syntax (multiple style objects) -->
<div :style="[baseStyles, overridingStyles]">Array Syntax Style</div>

<button @click="changeStyle">Change Style</button>
</template>

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

// Class binding refs
const isActive = ref(true);
const hasError = ref(false);
const activeClass = ref("active");
const errorClass = ref("text-danger");

function toggleClass() {
isActive.value = !isActive.value;
hasError.value = !hasError.value;
}

// Style binding refs
const activeColor = ref("red");
const fontSize = ref(20);
const bgColor = ref("lightyellow");

const baseStyles = reactive({
color: "blue",
fontSize: "16px",
});
const overridingStyles = reactive({
fontWeight: "bold",
border: "1px solid blue",
});

function changeStyle() {
activeColor.value = activeColor.value === "red" ? "green" : "red";
fontSize.value += 2;
}
</script>

<style>
.static {
font-style: italic;
}
.active {
font-weight: bold;
border: 1px solid green;
}
.text-danger {
color: red;
}
</style>

4. 响应式核心 (Reactivity Fundamentals)

Vue 的核心特性之一是其响应式系统。当数据变化时,视图会自动更新。Vue 3 使用 ES6 Proxy 实现响应式。

4.1 ref()

接受一个内部值并返回一个响应式且可变的 ref 对象。ref 对象仅有一个 .value property,指向该内部值。用于使基本类型值 (String, Number, Boolean 等) 或单个对象/数组引用具有响应性。

<template>
<p>Count: {{ count }}</p>
<button @click="increment">Increment</button>
</template>

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

// 创建一个 ref,初始值为 0
const count = ref(0);

console.log(count.value); // 0 (在 <script> 中访问需要 .value)

function increment() {
// 在 <script> 中修改需要 .value
count.value++;
}
// 在 <template> 中使用时,会自动解包 (unwrap),无需 .value
// 所以模板中可以直接用 {{ count }}
</script>

4.2 reactive()

返回一个对象的响应式代理。它深层地将对象内部的所有嵌套 property 都转换为响应式。主要用于使对象或数组具有响应性。注意: reactive() 不能处理基本类型值,并且解构 reactive 对象会使其失去响应性。

<template>
<p>User: {{ state.user.name }} ({{ state.user.age }})</p>
<p>Skills: {{ state.skills.join(", ") }}</p>
<button @click="updateUser">Update User</button>
<button @click="addSkill">Add Skill</button>
</template>

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

// 创建一个响应式对象
const state = reactive({
user: {
name: "Alice",
age: 30,
},
skills: ["Vue", "JavaScript"],
});

function updateUser() {
// 可以直接修改对象的属性
state.user.name = "Bob";
state.user.age++;
// 也可以添加新属性,它也是响应式的
state.user.city = "New York";
}

function addSkill() {
// 修改数组也是响应式的
state.skills.push("TypeScript");
}

// 错误示例:解构会失去响应性
// let { name, age } = state.user; // name 和 age 不再是响应式的
// name = 'Charlie'; // 不会更新视图

// 正确处理解构需要 toRefs()
// import { toRefs } from 'vue'
// const { user, skills } = toRefs(state);
// user.value.name = 'Charlie'; // 这样可以
</script>

4.3 computed()

用于声明计算属性。计算属性会基于其响应式依赖进行缓存。只有在相关响应式依赖发生改变时它们才会重新求值。

<template>
<p>First Name: <input v-model="firstName" /></p>
<p>Last Name: <input v-model="lastName" /></p>
<p>Full Name (Computed): {{ fullName }}</p>
<p>Full Name (Method): {{ getFullName() }}</p>
<button @click="changeFirstName">Change First Name</button>
</template>

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

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

// 定义计算属性 fullName
const fullName = computed(() => {
console.log("Computed fullName recalculated"); // 只在 firstName 或 lastName 改变时执行
return firstName.value + " " + lastName.value;
});

// 对比:使用方法
function getFullName() {
console.log("Method getFullName called"); // 每次模板渲染都会执行
return firstName.value + " " + lastName.value;
}

function changeFirstName() {
firstName.value = "Jane";
}

// 计算属性默认是 getter-only,也可以提供 setter
// const fullNameWithSetter = computed({
// get() {
// return firstName.value + ' ' + lastName.value;
// },
// set(newValue) {
// [firstName.value, lastName.value] = newValue.split(' ');
// }
// });
// fullNameWithSetter.value = 'Peter Jones'; // 会调用 setter
</script>

4.4 watch()watchEffect()

用于侦听响应式数据的变化并执行副作用 (Side Effects),如 API 调用、DOM 操作等。

  • watch():
    • 懒执行: 默认情况下,仅在侦听源发生变化时才执行回调。
    • 需要明确指定要侦听的响应式源 (ref, reactive 对象, getter 函数, 或包含这些的数组)。
    • 可以访问变化前后的值。
    • 提供更多配置选项 (如 immediate, deep)。
  • watchEffect():
    • 立即执行: 初始化时会立即执行一次回调函数,然后在其依赖项变化时重新运行。
    • 自动追踪依赖: 无需指定侦听源,它会自动追踪在回调函数中访问过的响应式依赖。
    • 无法访问变化前的值。
<template>
<div>
<p>Watch Source (Ref): <input v-model="watchSourceRef" /></p>
<p>
Watch Source (Reactive Prop):
<input v-model="watchSourceReactive.nested.value" />
</p>
<p>Watch Log: {{ watchLog }}</p>
<p>WatchEffect Log: {{ effectLog }}</p>
</div>
</template>

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

const watchSourceRef = ref("initial ref");
const watchSourceReactive = reactive({ nested: { value: "initial reactive" } });
const watchLog = ref("");
const effectLog = ref("");

// --- watch examples ---

// 1. Watching a ref
watch(watchSourceRef, (newValue, oldValue) => {
watchLog.value = `Ref changed from '${oldValue}' to '${newValue}'`;
console.log("Watch (ref):", newValue, oldValue);
});

// 2. Watching a getter function for a reactive property
watch(
() => watchSourceReactive.nested.value, // Use a getter for specific reactive property
(newValue, oldValue) => {
watchLog.value = `Reactive prop changed from '${oldValue}' to '${newValue}'`;
console.log("Watch (getter):", newValue, oldValue);
}
);

// 3. Watching a reactive object directly (requires deep: true for nested changes)
// watch(watchSourceReactive, (newValue, oldValue) => {
// // Note: newValue and oldValue will be the same object reference for reactive objects!
// console.log('Watch (reactive object - deep):', newValue, oldValue);
// watchLog.value = `Reactive object changed (nested value: ${newValue.nested.value})`;
// }, { deep: true }); // Deep watch needed for nested properties

// 4. Immediate watch
// watch(watchSourceRef, (newValue) => {
// console.log('Immediate Watch (ref):', newValue);
// }, { immediate: true });

// --- watchEffect example ---
watchEffect(() => {
// This runs immediately and whenever watchSourceRef or watchSourceReactive.nested.value changes
// because they are accessed inside the effect function.
const message = `Effect ran. Ref: '${watchSourceRef.value}', Reactive: '${watchSourceReactive.nested.value}'`;
effectLog.value = message;
console.log("watchEffect ran");
});

// Example change after 2 seconds
setTimeout(() => {
watchSourceRef.value = "updated ref";
watchSourceReactive.nested.value = "updated reactive";
}, 2000);
</script>

5. Composition API

Composition API 是一系列 API 的集合,允许我们使用导入的函数而不是声明选项 (Options API) 来编写 Vue 组件。它提供了更灵活、更可组合的代码组织方式。

5.1 setup() 函数 (Composition API 入口点 - 可选)

setup 函数是组件中使用 Composition API 的入口点。它在组件实例创建之前执行。

  • 参数: 接收 propscontext (包含 attrs, slots, emit)。
  • 返回值: 返回的对象或 ref 会暴露给模板和组件实例。
  • this: 在 setupthis 不可用 (为 undefined)。

注意: 随着 <script setup> 的引入,显式使用 setup() 函数的场景已经大大减少,但理解其工作原理仍然有帮助。

<!-- ComponentWithOptionsSetup.vue -->
<template>
<p>Message from setup: {{ message }}</p>
<p>Prop value: {{ myProp }}</p>
<button @click="increment">Increment from setup</button>
</template>

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

export default {
props: {
myProp: {
type: String,
required: true,
},
},
// Explicit setup function
setup(props, context) {
console.log("setup() executed");
// props is reactive, but destructuring it directly loses reactivity
// Use toRefs or access via props.myProp
const { myProp } = toRefs(props); // Maintain reactivity

// Define reactive state
const message = ref("Hello from setup!");
const count = ref(0);

// Define methods
function increment() {
count.value++;
message.value = `Count is ${count.value}`;
// Emit event using context.emit
context.emit("incremented", count.value);
}

// Watch props
watch(myProp, (newVal) => {
console.log("Prop changed in setup:", newVal);
});

// Expose state and methods to the template
return {
message,
increment,
// myProp is automatically available in the template via props
};
},

// Options API can still be used alongside setup()
data() {
return {
optionsData: "Data from Options API",
};
},
mounted() {
console.log("mounted() hook from Options API");
console.log("Accessing setup ref from options:", this.message); // Can access exposed refs
},
};
</script>

5.2 <script setup> (推荐语法)

setup() 函数的编译时语法糖。提供了更简洁、更符合人体工程学的 Composition API 用法。

  • <script setup> 中声明的顶层绑定(包括变量、函数声明、以及 import 引入的内容)都能在模板中直接使用。
  • 无需显式返回任何内容。
  • props, emit, attrs, slots 需要通过 defineProps, defineEmits, useAttrs, useSlots 等编译器宏来访问。
<!-- MyComponentScriptSetup.vue -->
<template>
<h2>Using &lt;script setup&gt;</h2>
<p>Message: {{ message }}</p>
<p>Prop value: {{ myProp }}</p>
<p>Double count: {{ doubleCount }}</p>
<button @click="increment">Increment</button>
</template>

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

// 1. defineProps to declare props
const props = defineProps({
myProp: {
type: String,
required: true,
default: "Default Value",
},
});

// 2. defineEmits to declare emitted events
const emit = defineEmits(["incremented"]);

// 3. Reactive state (refs, reactive, etc.) are automatically exposed
const count = ref(0);
const message = ref("Hello from <script setup>");

// 4. Computed properties
const doubleCount = computed(() => count.value * 2);

// 5. Functions are automatically exposed
function increment() {
count.value++;
message.value = `Count updated to ${count.value}`;
// Emit event using the emit function
emit("incremented", count.value);
}

// 6. Watchers
watch(
() => props.myProp,
(newVal) => {
console.log("<script setup> prop changed:", newVal);
}
);

watch(count, (newCount) => {
console.log("<script setup> count changed:", newCount);
});

// 7. Lifecycle hooks are imported and used directly
import { onMounted, onUnmounted } from "vue";

onMounted(() => {
console.log("<script setup> component mounted");
});

onUnmounted(() => {
console.log("<script setup> component unmounted");
});

// No return statement needed!
</script>

<style scoped>
/* Component-specific styles */
</style>

5.3 Lifecycle Hooks

Composition API 提供了 onX 形式的生命周期钩子函数,需要在 setup()<script setup> 内同步调用。

Options APIComposition API (setup or <script setup>)
beforeCreateUse setup() itself
createdUse setup() itself
beforeMountonBeforeMount
mountedonMounted
beforeUpdateonBeforeUpdate
updatedonUpdated
beforeUnmountonBeforeUnmount
unmountedonUnmounted
errorCapturedonErrorCaptured
renderTrackedonRenderTracked (Debug)
renderTriggeredonRenderTriggered (Debug)
activatedonActivated (for <KeepAlive>)
deactivatedonDeactivated (for <KeepAlive>)
serverPrefetchserverPrefetch (SSR only)
<script setup>
import { onMounted, onUpdated, onUnmounted, ref } from "vue";

const count = ref(0);

console.log("Component setup phase");

onMounted(() => {
console.log("Component has been mounted!");
// Perform setup tasks like fetching data, setting up timers/listeners
});

onUpdated(() => {
console.log("Component has been updated!");
// Called after the component's DOM has been updated due to reactive state changes
});

onUnmounted(() => {
console.log("Component is about to be unmounted!");
// Perform cleanup tasks like clearing timers, removing listeners
});
</script>

<template>
<p>Count: {{ count }}</p>
<button @click="count++">Update</button>
</template>

5.4 Dependency Injection (provide & inject)

允许一个祖先组件向其所有后代组件注入依赖,无论组件层次有多深。

  • provide(key, value): 在祖先组件中提供数据或方法。key 可以是字符串或 Symbol。
  • inject(key, defaultValue): 在后代组件中注入由祖先提供的数据或方法。可以提供一个默认值。
<!-- AncestorComponent.vue -->
<template>
<div>
<h2>Ancestor Component</h2>
<button @click="changeTheme">Change Theme</button>
<ChildComponent />
</div>
</template>

<script setup>
import { ref, provide } from "vue";
import ChildComponent from "./ChildComponent.vue";

// 1. Provide reactive data
const theme = ref("light");
provide("theme", theme); // Provide the ref itself for reactivity

// 2. Provide a method
function changeTheme() {
theme.value = theme.value === "light" ? "dark" : "light";
console.log("Theme changed in ancestor:", theme.value);
}
provide("changeThemeMethod", changeTheme);

// 3. Provide static data
provide("staticData", "This data is static");
</script>
<!-- ChildComponent.vue -->
<template>
<div class="child">
<h3>Child Component</h3>
<GrandChildComponent />
</div>
</template>

<script setup>
import GrandChildComponent from "./GrandChildComponent.vue";
// Child doesn't need to know about the provided data if it doesn't use it
</script>

<style scoped>
.child {
margin-left: 20px;
border-left: 1px solid #ccc;
padding-left: 10px;
}
</style>
<!-- GrandChildComponent.vue -->
<template>
<div class="grandchild">
<h4>GrandChild Component</h4>
<p>Injected Theme: {{ theme }}</p>
<p>Injected Static Data: {{ staticValue }}</p>
<p>Data with Default: {{ nonExistentData }}</p>
<button @click="callInjectedMethod">Change Theme from GrandChild</button>
</div>
</template>

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

// 1. Inject data provided by ancestor
const theme = inject("theme"); // Injects the reactive ref
const staticValue = inject("staticData");

// 2. Inject with a default value
const nonExistentData = inject("non-existent-key", "Default Value");

// 3. Inject the method
const callInjectedMethod = inject("changeThemeMethod");

console.log("Injected theme in grandchild:", theme.value);
</script>

<style scoped>
.grandchild {
margin-left: 20px;
border-left: 1px solid #eee;
padding-left: 10px;
}
</style>

6. 组件 (Components)

组件是 Vue 的核心概念,允许将 UI 划分为独立可复用的部分。

6.1 单文件组件 (Single-File Components - SFC)

使用 .vue 文件,将组件的模板 (<template>)、逻辑 (<script>) 和样式 (<style>) 封装在一起。这是开发 Vue 应用的标准方式。

<!-- src/components/MyButton.vue -->
<template>
<button class="my-button" @click="handleClick">
<slot></slot>
<!-- Default slot for button text -->
<span v-if="count > 0"> ({{ count }})</span>
</button>
</template>

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

// Define props
const props = defineProps({
initialCount: {
type: Number,
default: 0,
},
});

// Define emitted events
const emit = defineEmits(["button-click"]);

// Reactive state
const count = ref(props.initialCount);

// Method
function handleClick() {
count.value++;
emit("button-click", count.value); // Emit event with payload
}
</script>

<style scoped>
/* Scoped styles only apply to this component */
.my-button {
background-color: #42b983;
color: white;
padding: 8px 15px;
border: none;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.3s ease;
}
.my-button:hover {
background-color: #36a374;
}
</style>

6.2 组件注册

  • 全局注册: 使用 app.component()。全局注册的组件可以在应用的任何模板中使用。应谨慎使用,避免全局污染。
  • 局部注册: 在需要使用组件的父组件的 <script setup>components 选项中导入并注册。推荐方式。
// main.js (Global Registration - less common)
import { createApp } from "vue";
import App from "./App.vue";
import GlobalButton from "./components/GlobalButton.vue"; // Assume GlobalButton.vue exists

const app = createApp(App);
// Register GlobalButton globally
app.component("GlobalButton", GlobalButton);
app.mount("#app");
<!-- ParentComponent.vue (Local Registration - Recommended) -->
<template>
<div>
<h2>Parent Component</h2>
<!-- Use the locally registered component -->
<MyButton :initial-count="5" @button-click="onMyButtonClick">
Click Me Locally!
</MyButton>
<p>Button clicked {{ clickCount }} times.</p>

<!-- Global component can also be used here -->
<!-- <GlobalButton>Global</GlobalButton> -->
</div>
</template>

<script setup>
// Import the component locally
import MyButton from "./components/MyButton.vue"; // Adjust path as needed
import { ref } from "vue";

const clickCount = ref(0);

function onMyButtonClick(newCount) {
console.log("MyButton clicked in parent, new count:", newCount);
clickCount.value = newCount;
}
</script>

6.3 Props

Props 是父组件向子组件传递数据的方式。数据单向流动:从父到子。子组件应避免直接修改 prop。

  • 声明: 使用 defineProps 宏 (在 <script setup>) 或 props 选项。
  • 类型和验证: 可以指定 prop 的类型、是否必需、默认值以及自定义验证函数。
<!-- ChildComponentWithProps.vue -->
<template>
<div class="child-props">
<h3>Child Component</h3>
<p>Message Prop: {{ message }}</p>
<p>Count Prop: {{ count }} (Type: {{ typeof count }})</p>
<p>User Prop Name: {{ user?.name ?? "N/A" }}</p>
<!-- Optional chaining -->
<p>Is Active Prop: {{ isActive }}</p>
</div>
</template>

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

const props = defineProps({
// Basic type check (`null` and `undefined` values will allow any type)
message: String,
// Multiple possible types
count: [String, Number],
// Required string
id: {
type: String,
required: true,
},
// Number with a default value
level: {
type: Number,
default: 1,
},
// Object with a default value (use factory function)
user: {
type: Object,
// Default factory function for objects/arrays
default: () => ({ name: "Guest", age: 0 }),
},
// Boolean with default
isActive: {
type: Boolean,
default: false,
},
// Custom validator function
customProp: {
validator: (value) => {
// The value must match one of these strings
return ["success", "warning", "danger"].includes(value);
},
},
});

onMounted(() => {
console.log("Props received:", props);
// console.log(props.message);
});
</script>

<style scoped>
.child-props {
border: 1px solid blue;
padding: 10px;
margin-top: 10px;
}
</style>
<!-- ParentUsingProps.vue -->
<template>
<div>
<h2>Parent Using Props</h2>
<ChildComponentWithProps
:id="'item-123'"
message="Hello from parent!"
:count="42"
:user="{ name: 'Alice', age: 30 }"
:is-active="true"
customProp="success"
/>
<ChildComponentWithProps
:id="'item-456'"
message="Another message"
:count="'55'"
/>
<!-- Other props use defaults -->
<!-- Missing required 'id' would cause a warning -->
<!-- Invalid customProp would cause a warning -->
<!-- <ChildComponentWithProps customProp="invalid"/> -->
</div>
</template>

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

6.4 自定义事件 ($emit / defineEmits)

子组件可以通过触发事件 (emit) 来与父组件通信,通常用于响应用户交互或内部状态变化。

  • 声明: 使用 defineEmits 宏 (在 <script setup>) 或 emits 选项。推荐显式声明,便于理解和类型检查。
  • 触发: 在子组件中使用 emit('eventName', ...payload)
  • 监听: 在父组件中使用 v-on@ 监听子组件触发的事件 (@eventName="handler")。
<!-- CustomInput.vue -->
<template>
<div>
<label :for="id">{{ label }}: </label>
<input
:id="id"
:value="modelValue"
@input="handleInput"
placeholder="Enter text..."
/>
</div>
</template>

<script setup>
import { defineProps, defineEmits } from "vue";

const props = defineProps({
modelValue: String, // Used for v-model compatibility
label: String,
id: {
type: String,
default: () => `input-${Math.random().toString(36).substr(2, 9)}`,
},
});

// Declare the 'update:modelValue' event for v-model support
// Declare a custom 'focused' event
const emit = defineEmits(["update:modelValue", "focused"]);

function handleInput(event) {
// Emit 'update:modelValue' for v-model
emit("update:modelValue", event.target.value);

// Emit custom event when focused (example)
if (event.type === "focus") {
// This example uses @input, add @focus if needed
emit("focused");
}
}
</script>
<!-- ParentUsingEvents.vue -->
<template>
<div>
<h2>Parent Listening to Events</h2>
<!-- Use v-model which translates to :modelValue and @update:modelValue -->
<CustomInput
v-model="inputValue"
label="My Custom Input"
@focused="logFocus"
/>
<p>Input Value in Parent: {{ inputValue }}</p>
</div>
</template>

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

const inputValue = ref("Initial value");

function logFocus() {
console.log("CustomInput received focus!");
}
</script>

6.5 插槽 (Slots)

允许父组件向子组件指定的位置插入内容。

  • 默认插槽: 子组件使用 <slot></slot> 标签定义内容插入点。父组件在使用子组件时,放在标签内的内容会插入到默认插槽。
  • 具名插槽: 子组件使用 <slot name="slotName"></slot> 定义。父组件使用 <template v-slot:slotName>#slotName 来向指定插槽插入内容。
  • 作用域插槽: 子组件可以在 <slot> 标签上绑定 attribute (props),父组件通过 v-slot:slotName="slotProps"#slotName="slotProps" 来接收这些数据,从而可以在父组件中定义使用子组件数据的模板。
<!-- FancyButton.vue (Child) -->
<template>
<button class="fancy-btn">
<!-- Default Slot -->
<slot>Default Button Text</slot>

<!-- Named Slot for an icon -->
<span class="icon">
<slot name="icon"></slot>
</span>

<!-- Scoped Slot passing data to parent -->
<div class="data-area">
<slot name="dataScope" :internalData="childData" :count="clickCount">
<!-- Fallback content if parent doesn't provide scoped slot -->
Fallback: {{ childData }} (Clicks: {{ clickCount }})
</slot>
</div>
<button @click="clickCount++">Inc Child Count</button>
</button>
</template>

<script setup>
import { ref } from "vue";
const childData = ref("Data from child");
const clickCount = ref(0);
</script>

<style scoped>
.fancy-btn {
/* ... styles ... */
padding: 10px;
border: 1px solid #ccc;
position: relative;
}
.icon {
margin-left: 5px;
}
.data-area {
margin-top: 5px;
font-size: 0.8em;
color: gray;
}
</style>
<!-- ParentUsingSlots.vue -->
<template>
<div>
<h2>Parent Using Slots</h2>

<!-- 1. Using Default Slot -->
<FancyButton>
Click Me!
<!-- This goes into the default slot -->
</FancyButton>

<hr />

<!-- 2. Using Named Slots -->
<FancyButton>
<!-- Content for default slot -->
Submit

<!-- Content for named slot 'icon' -->
<template v-slot:icon>
🚀
<!-- Rocket emoji -->
</template>
<!-- Shorthand #icon -->
<!-- <template #icon>🚀</template> -->
</FancyButton>

<hr />

<!-- 3. Using Scoped Slots -->
<FancyButton>
<template v-slot:dataScope="slotProps">
<!-- Use data passed from child via slotProps -->
Parent sees: {{ slotProps.internalData }} | Clicks:
{{ slotProps.count }}
</template>
<!-- Shorthand #dataScope="{ internalData, count }" -->
<!--
<template #dataScope="{ internalData, count }">
Parent sees: {{ internalData }} | Clicks: {{ count }}
</template>
-->
</FancyButton>
</div>
</template>

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

6.6 Fallthrough Attributes ($attrs)

指父组件传递给子组件,但没有被子组件通过 propsemits 声明接收的 attribute 或 v-on 事件监听器。默认情况下,这些 attribute 会被自动添加到子组件的根元素上。可以通过 inheritAttrs: false 禁用此行为,并通过 useAttrs() (Composition API) 或 $attrs (Options API) 访问它们。

<!-- BaseInput.vue -->
<template>
<label>
{{ label }}
<input
type="text"
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
v-bind="inputAttrs"
<!--
Bind
non-prop
attributes
to
the
input
--
/>
>
</label>
</template>

<script>
// Disabling inheritAttrs is common for components wrapping an input
// to avoid applying attributes like 'class' or listeners to the root label
// instead of the input itself.
export default {
inheritAttrs: false,
};
</script>

<script setup>
import { defineProps, defineEmits, useAttrs, computed } from "vue";

defineProps({
label: String,
modelValue: String,
});
defineEmits(["update:modelValue"]);

const attrs = useAttrs();

// Example: Separate input-specific attributes from others
const inputAttrs = computed(() => {
const inputOnlyAttrs = {};
for (const key in attrs) {
// Example: only bind attributes like placeholder, maxlength etc. to input
if (
["placeholder", "maxlength", "required", "disabled", "readonly"].includes(
key
) ||
key.startsWith("data-")
) {
inputOnlyAttrs[key] = attrs[key];
}
// If there were listeners like @focus, @blur they would also be in attrs
}
return inputOnlyAttrs;
});

// Non-input attrs (like class, style) are not automatically applied now.
// If needed, they could be bound to the root <label> using v-bind="labelAttrs"
</script>
<!-- ParentUsingAttrs.vue -->
<template>
<BaseInput label="My Input" v-model="text" placeholder="Enter something..."
maxlength="50" required class="parent-class"
<!-- This class will NOT be applied to BaseInput's root label -->
data-test-id="my-base-input"
<!-- This data-* attr WILL be bound to the input -->
@focus="onFocus"
<!-- This listener will NOT be applied to the root label -->
/>
</template>

<script setup>
import { ref } from "vue";
import BaseInput from "./BaseInput.vue";
const text = ref("");
function onFocus() {
console.log("BaseInput focused (listener passed via attrs)");
}
</script>

7. Advanced Features

7.1 Custom Directives

除了核心指令 (v-model, v-show 等),你还可以注册自定义指令来封装可复用的 DOM 操作。

<!-- ComponentUsingCustomDirective.vue -->
<template>
<input v-focus placeholder="I should have focus on mount" />
<input v-color="'red'" placeholder="My text should be red" />
<p
v-demo-directive:[argument].modifier1.modifier2="{
color: 'blue',
text: 'hello!',
}"
>
Custom Directive with Args/Mods/Value
</p>
</template>

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

// --- Local Custom Directive ---

// Simple focus directive
const vFocus = {
// Called when the bound element's parent component is mounted
mounted: (el) => {
console.log("v-focus mounted on:", el);
el.focus();
},
};

// Directive with value binding
const vColor = {
mounted: (el, binding) => {
// binding.value contains the value passed to the directive
el.style.color = binding.value;
console.log("v-color applied with value:", binding.value);
},
updated: (el, binding) => {
el.style.color = binding.value; // Handle value updates
},
};

// Directive with arguments, modifiers, and value object
const vDemoDirective = {
mounted(el, binding) {
console.log("vDemoDirective Mounted");
console.log(" Argument:", binding.arg); // e.g., 'foo' from v-demo-directive:foo
console.log(" Modifiers:", binding.modifiers); // e.g., { modifier1: true, modifier2: true }
console.log(" Value:", binding.value); // e.g., { color: 'blue', text: 'hello!' }

el.style.color = binding.value.color || "black";
el.textContent = binding.value.text || "Default Text";
if (binding.modifiers.modifier1) {
el.style.border = "1px solid red";
}
},
};

const argument = ref("foo"); // Dynamic argument example
</script>

全局注册 (在 main.js)

// import { createApp } from 'vue'
// const app = createApp(App)
// app.directive('focus', { /* ... implementation ... */ })

7.2 Teleport

将组件模板的一部分渲染到 DOM 树中的另一个位置,例如将模态框或通知直接渲染到 <body> 下,避免 CSS z-indexoverflow 问题。

<template>
<button @click="showModal = true">Show Modal</button>

<!-- Teleport the modal content to the body -->
<Teleport to="body">
<div v-if="showModal" class="modal">
<h2>I'm a teleported modal!</h2>
<p>
My parent component is somewhere else, but I render directly under body.
</p>
<button @click="showModal = false">Close</button>
</div>
</Teleport>
</template>

<script setup>
import { ref } from "vue";
const showModal = ref(false);
</script>

<style scoped>
/* Styles for the modal */
.modal {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: white;
padding: 20px;
border: 1px solid #ccc;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
z-index: 1000; /* Ensure it's on top */
}
</style>

7.3 Suspense (Experimental)

用于协调异步依赖(如异步组件加载、setupawait)的加载状态,可以在等待异步内容时显示 fallback 内容。

<!-- ParentWithSuspense.vue -->
<template>
<h2>Handling Async Components</h2>
<Suspense>
<!-- Component with async setup or async component itself -->
<AsyncComponent />

<!-- Fallback content while loading -->
<template #fallback>
<div>Loading... Please wait.</div>
</template>
</Suspense>
</template>

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

// Define an async component (e.g., code-splitting)
const AsyncComponent = defineAsyncComponent(
() => import("./MyAsyncDataComponent.vue") // Assume this component fetches data in setup
);
</script>
<!-- MyAsyncDataComponent.vue -->
<template>
<div>
<h3>Async Data Component</h3>
<p v-if="error">Error loading data: {{ error }}</p>
<ul v-else-if="data">
<li v-for="item in data" :key="item.id">{{ item.name }}</li>
</ul>
<p v-else>No data loaded (should have shown loading in parent).</p>
</div>
</template>

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

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

// Simulate async data fetching in setup
async function fetchData() {
console.log("Async component setup started...");
return new Promise((resolve, reject) => {
setTimeout(() => {
if (Math.random() > 0.1) {
// Simulate success most of the time
console.log("Async data fetched.");
resolve([
{ id: 1, name: "Item A" },
{ id: 2, name: "Item B" },
]);
} else {
console.error("Async data fetching failed.");
reject(new Error("Failed to load data"));
}
}, 1500); // Simulate network delay
});
}

// Using top-level await in <script setup> makes the component async
try {
data.value = await fetchData();
} catch (e) {
error.value = e.message;
}
console.log("Async component setup finished.");
</script>

7.4 Composables (组合式函数)

利用 Composition API 的函数,用于封装和复用有状态逻辑。它们是普通的 JavaScript 函数,约定俗成以 use 开头,可以返回响应式状态、计算属性和方法。

// composables/useMousePosition.js
import { ref, onMounted, onUnmounted } from "vue";

// Convention: composable function names start with "use"
export function useMousePosition() {
// State encapsulated and managed by the composable
const x = ref(0);
const y = ref(0);

// A composable can update its managed state over time.
function update(event) {
x.value = event.pageX;
y.value = event.pageY;
}

// A composable can also hook into its owner component's
// lifecycle to set up and tear down side effects.
onMounted(() => {
console.log("useMousePosition mounted");
window.addEventListener("mousemove", update);
});

onUnmounted(() => {
console.log("useMousePosition unmounted");
window.removeEventListener("mousemove", update);
});

// Expose managed state as return value
return { x, y };
}
<!-- ComponentUsingComposable.vue -->
<template>
<div>
<h3>Mouse Position Composable</h3>
<p>Mouse X: {{ mouseX }}</p>
<p>Mouse Y: {{ mouseY }}</p>
</div>
</template>

<script setup>
// Import and use the composable
import { useMousePosition } from "../composables/useMousePosition"; // Adjust path

// Call the composable to get the reactive state
const { x: mouseX, y: mouseY } = useMousePosition(); // Destructure return value
</script>

8. 生态系统

8.1 Vue Router

官方的路由管理器,用于构建单页应用 (SPA)。

  • 安装: npm install vue-router@4
  • 配置: 创建路由实例,定义路由映射,将其挂载到 Vue 应用实例。
  • 核心组件: <router-link> (导航), <router-view> (路由出口)。
  • API: useRouter (访问路由器实例), useRoute (访问当前路由信息)。
// router/index.js
import { createRouter, createWebHistory } from "vue-router";
import HomeView from "../views/HomeView.vue";
import AboutView from "../views/AboutView.vue";

const routes = [
{
path: "/",
name: "home",
component: HomeView,
},
{
path: "/about",
name: "about",
component: AboutView,
// Example of lazy loading:
// component: () => import('../views/AboutView.vue')
},
{
path: "/user/:id", // Dynamic route segment
name: "user",
component: () => import("../views/UserProfile.vue"),
props: true, // Pass route params as props to the component
},
];

const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL), // HTML5 History mode
// history: createWebHashHistory(), // Hash mode (#)
routes,
});

export default router;
// main.js
import { createApp } from "vue";
import App from "./App.vue";
import router from "./router"; // Import the router

const app = createApp(App);

app.use(router); // Use the router plugin

app.mount("#app");
<!-- App.vue -->
<template>
<header>
<nav>
<router-link to="/">Home</router-link> |
<router-link to="/about">About</router-link> |
<router-link :to="{ name: 'user', params: { id: '123' } }"
>User 123</router-link
>
</nav>
</header>
<main>
<!-- Where routed components will be rendered -->
<router-view />
</main>
</template>

<script setup>
// No specific script needed here for basic routing setup
</script>

<style>
/* Basic styling */
nav a.router-link-exact-active {
color: #42b983;
font-weight: bold;
}
</style>
<!-- views/UserProfile.vue -->
<template>
<div>
<h2>User Profile</h2>
<p>User ID: {{ id }}</p>
<p>Current Path: {{ route.path }}</p>
<button @click="goToAbout">Go to About</button>
</div>
</template>

<script setup>
import { defineProps } from "vue";
import { useRoute, useRouter } from "vue-router";

// Access props passed from router (if props: true)
const props = defineProps({
id: String,
});

// Access current route information
const route = useRoute();
console.log("Current route params:", route.params); // { id: '123' }

// Access router instance for navigation
const router = useRouter();
function goToAbout() {
router.push({ name: "about" }); // Navigate programmatically
// router.push('/about'); // Alternative
}
</script>

8.2 Pinia

官方推荐的状态管理库,用于管理跨组件共享的状态。相比 Vuex,Pinia API 更简洁,对 TypeScript 支持更好,且更模块化。

  • 安装: npm install pinia
  • 配置: 创建 Pinia 实例并挂载到 Vue 应用。
  • 定义 Store: 使用 defineStore 创建一个 Store,包含 state (类似 data), getters (类似 computed), actions (类似 methods)。
  • 使用 Store: 在组件中导入并调用 store 函数。
// stores/counter.js
import { defineStore } from "pinia";
import { ref, computed } from "vue"; // Can use Composition API inside stores

// Option Store syntax (similar to Options API)
// export const useCounterStore = defineStore('counter', {
// state: () => ({
// count: 0,
// name: 'My Counter'
// }),
// getters: {
// doubleCount: (state) => state.count * 2,
// },
// actions: {
// increment(amount = 1) {
// this.count += amount; // 'this' refers to the store instance
// },
// reset() {
// this.count = 0;
// },
// },
// });

// Setup Store syntax (using Composition API)
export const useCounterStore = defineStore("counter", () => {
// State -> refs
const count = ref(0);
const name = ref("My Counter");

// Getters -> computeds
const doubleCount = computed(() => count.value * 2);

// Actions -> functions
function increment(amount = 1) {
count.value += amount;
}
function reset() {
count.value = 0;
}

// Return state, getters, and actions
return { count, name, doubleCount, increment, reset };
});
// main.js
import { createApp } from "vue";
import { createPinia } from "pinia"; // Import Pinia
import App from "./App.vue";

const app = createApp(App);
const pinia = createPinia(); // Create Pinia instance

app.use(pinia); // Use the Pinia plugin
app.mount("#app");
<!-- ComponentUsingPinia.vue -->
<template>
<div>
<h2>Pinia Counter Store</h2>
<p>Store Name: {{ counterStore.name }}</p>
<p>Count: {{ counterStore.count }}</p>
<p>Double Count: {{ counterStore.doubleCount }}</p>
<button @click="counterStore.increment()">Increment</button>
<button @click="counterStore.increment(5)">Increment by 5</button>
<button @click="counterStore.reset()">Reset</button>
<hr />
<p>Local Count (for comparison): {{ localCount }}</p>
<button @click="localCount++">Inc Local</button>
</div>
</template>

<script setup>
import { useCounterStore } from "../stores/counter"; // Import the store
import { ref } from "vue";
import { storeToRefs } from "pinia"; // Utility to keep reactivity when destructuring state/getters

// Use the store
const counterStore = useCounterStore();

// Option 1: Access directly via counterStore.property (always works)

// Option 2: Destructuring (loses reactivity for state/getters without storeToRefs)
// const { count, doubleCount } = counterStore; // WRONG: count and doubleCount are not reactive here
// const { increment, reset } = counterStore; // OK: Actions are just functions

// Option 3: Destructuring with storeToRefs (Recommended for state/getters)
// const { count, doubleCount, name } = storeToRefs(counterStore);
// const { increment, reset } = counterStore; // Actions can still be destructured directly

// Local state for comparison
const localCount = ref(0);
</script>

9. 工具

9.1 Vite

下一代前端构建工具,利用浏览器原生 ES 模块导入和 esbuild(Go 编写,极快)提供极速的冷启动和即时热模块更新 (HMR)。是 Vue 3 项目的默认和推荐构建工具。

9.2 Vue Devtools

浏览器扩展,用于检查 Vue 组件树、状态 (Props, Data, Computed)、事件、路由和 Pinia/Vuex 状态,是调试 Vue 应用的必备工具。

# Typically installed automatically when creating project with `create-vue`
# If not, install Vite: npm install -D vite @vitejs/plugin-vue
# Start dev server: npm run dev
# Build for production: npm run build