第 113 章:第一个 Vue 应用
学习目标
- 掌握 Vue 3 项目的完整启动流程
- 理解
createApp和组件挂载机制 - 学会 SFC 单文件组件的写法
- 能在浏览器中验证应用正常运行
一、最小 Vue 3 应用
1.1 HTML 入口
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>My Vue App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>1.2 入口 main.ts
typescript
import { createApp } from 'vue';
import App from './App.vue';
createApp(App).mount('#app');核心两步:
createApp(App)- 创建 Vue 应用实例.mount('#app')- 挂载到 DOM 元素
1.3 App.vue
vue
<script setup lang="ts">
import { ref } from 'vue';
const message = ref('Hello Vue 3!');
</script>
<template>
<h1>{{ message }}</h1>
</template>二、SFC 单文件组件
SFC(Single File Component)是 Vue 的标志性特性,一个 .vue 文件 = 一个组件。
2.1 三段式结构
vue
<script setup lang="ts">
// 1. 逻辑层:TypeScript/JavaScript
import { ref } from 'vue';
const count = ref(0);
</script>
<template>
<!-- 2. 视图层:HTML 模板 -->
<div>{{ count }}</div>
</template>
<style scoped>
/* 3. 样式层:CSS/SCSS/Less */
div {
color: red;
}
</style>2.2 各层职责
| 层 | 作用 | 语言 |
|---|---|---|
<script> | 组件逻辑 | TS/JS |
<template> | 视图结构 | HTML |
<style> | 组件样式 | CSS/SCSS/Less |
2.3 <script setup> 语法糖
vue
<!-- ❌ 传统写法 -->
<script>
export default {
setup() {
const count = ref(0);
return { count };
}
};
</script>
<!-- ✅ setup 语法糖(推荐) -->
<script setup lang="ts">
import { ref } from 'vue';
const count = ref(0);
// 顶层变量自动暴露给模板
</script>推荐
Vue 3 官方推荐:所有新项目都用 <script setup>。
三、模板语法
3.1 文本插值
vue
<script setup>
const msg = 'hello';
const html = '<strong>加粗</strong>';
</script>
<template>
<!-- 文本插值 -->
<p>{{ msg }}</p>
<!-- 原始 HTML(v-html) -->
<p v-html="html"></p>
<!-- JS 表达式(只能是单个表达式) -->
<p>{{ msg.toUpperCase() }}</p>
<p>{{ msg.split('').reverse().join('') }}</p>
<p>{{ Math.random() }}</p>
</template>3.2 属性绑定 v-bind
vue
<script setup>
const url = 'https://vuejs.org';
const isDisabled = true;
const imageUrl = '/logo.png';
</script>
<template>
<!-- 完整写法 -->
<a v-bind:href="url">Vue</a>
<!-- 简写 -->
<a :href="url">Vue</a>
<!-- 布尔属性 -->
<button :disabled="isDisabled">按钮</button>
<!-- 动态参数 -->
<img :src="imageUrl" />
</template>3.3 条件渲染 v-if
vue
<script setup>
import { ref } from 'vue';
const show = ref(true);
const type = ref('A');
</script>
<template>
<!-- v-if / v-else-if / v-else -->
<div v-if="type === 'A'">A</div>
<div v-else-if="type === 'B'">B</div>
<div v-else>Other</div>
<!-- v-show(切换 display) -->
<p v-show="show">一直渲染,只是隐藏</p>
</template>v-if vs v-show:
| 指令 | 适用场景 | 渲染机制 |
|---|---|---|
v-if | 不常切换 | 真正销毁/重建 |
v-show | 频繁切换 | CSS display 切换 |
3.4 列表渲染 v-for
vue
<script setup>
const items = [
{ id: 1, name: 'Tom' },
{ id: 2, name: 'Jerry' }
];
const object = { a: 1, b: 2, c: 3 };
</script>
<template>
<!-- 数组 -->
<ul>
<li v-for="item in items" :key="item.id">
{{ item.name }}
</li>
</ul>
<!-- 索引 -->
<ul>
<li v-for="(item, index) in items" :key="item.id">
{{ index }} - {{ item.name }}
</li>
</ul>
<!-- 对象 -->
<ul>
<li v-for="(value, key) in object" :key="key">
{{ key }}: {{ value }}
</li>
</ul>
</template>注意
永远给 v-for 加 :key,且 key 应该是稳定的、唯一的(不要用 index)。
3.5 事件处理 v-on
vue
<script setup>
const count = ref(0);
const handleClick = (event: MouseEvent) => {
console.log(event.target);
};
const handleGreet = (name: string) => {
console.log('Hello,', name);
};
</script>
<template>
<!-- 完整写法 -->
<button v-on:click="handleClick">+</button>
<!-- 简写 -->
<button @click="handleClick">+</button>
<!-- 内联处理器 -->
<button @click="count++">count: {{ count }}</button>
<!-- 传参 -->
<button @click="handleGreet('Tom')">Greet</button>
<!-- 同时访问 event -->
<button @click="handleGreet($event, 'Tom')">Greet</button>
</template>3.6 表单输入 v-model
vue
<script setup>
import { ref } from 'vue';
const text = ref('');
const checked = ref(false);
const picked = ref('A');
const selected = ref([]);
</script>
<template>
<!-- 文本输入 -->
<input v-model="text" />
<p>{{ text }}</p>
<!-- 复选框 -->
<input type="checkbox" v-model="checked" />
<!-- 单选 -->
<input type="radio" v-model="picked" value="A" />
<input type="radio" v-model="picked" value="B" />
<!-- 下拉选择(多选) -->
<select v-model="selected" multiple>
<option value="1">一</option>
<option value="2">二</option>
</select>
</template>四、计算属性 computed
vue
<script setup>
import { ref, computed } from 'vue';
const firstName = ref('Tom');
const lastName = ref('Jerry');
const fullName = computed(() => {
return `${firstName.value} ${lastName.value}`;
});
</script>
<template>
<p>姓名:{{ fullName }}</p>
</template>五、监听器 watch
vue
<script setup>
import { ref, watch } from 'vue';
const count = ref(0);
// 监听 ref
watch(count, (newVal, oldVal) => {
console.log(`count: ${oldVal} -> ${newVal}`);
});
// 立即执行
watch(count, (newVal) => {
console.log(newVal);
}, { immediate: true });
</script>六、完整示例:计数器
vue
<script setup lang="ts">
import { ref, computed, watch } from 'vue';
// 状态
const count = ref(0);
const step = ref(1);
// 计算
const doubled = computed(() => count.value * 2);
const isEven = computed(() => count.value % 2 === 0);
// 方法
const increment = () => {
count.value += step.value;
};
const decrement = () => {
count.value -= step.value;
};
const reset = () => {
count.value = 0;
};
// 监听
watch(count, (newVal) => {
console.log(`count 变为 ${newVal}`);
});
</script>
<template>
<div class="counter">
<h2>计数器</h2>
<p>当前值: <strong>{{ count }}</strong></p>
<p>双倍: {{ doubled }}</p>
<p>是否偶数: {{ isEven ? '是' : '否' }}</p>
<div class="step">
<label>步长:</label>
<input v-model.number="step" type="number" min="1" />
</div>
<div class="actions">
<button @click="decrement">-{{ step }}</button>
<button @click="reset">重置</button>
<button @click="increment">+{{ step }}</button>
</div>
</div>
</template>
<style scoped>
.counter {
max-width: 400px;
margin: 50px auto;
padding: 24px;
border: 1px solid #eee;
border-radius: 8px;
text-align: center;
}
.actions {
display: flex;
gap: 8px;
justify-content: center;
margin-top: 16px;
}
button {
padding: 8px 16px;
cursor: pointer;
}
</style>七、组件化思维
7.1 拆分为组件
src/
├── App.vue # 根组件
└── components/
├── Counter.vue # 计数器组件
├── StepInput.vue # 步长输入
└── Result.vue # 结果展示7.2 App.vue
vue
<script setup lang="ts">
import { ref } from 'vue';
import Counter from './components/Counter.vue';
</script>
<template>
<main>
<Counter />
</main>
</template>八、调试技巧
8.1 Vue DevTools
安装 Vue DevTools 浏览器扩展,可以看到:
- 组件树
- 响应式数据
- 事件追踪
- Pinia stores
- 路由信息
8.2 console.log 调试
vue
<script setup>
import { ref, watch } from 'vue';
const count = ref(0);
watch(count, (val) => {
console.log('[count 变化]', val);
});
</script>8.3 VSCode 断点
Volar 插件支持在 .vue 文件的 <script> 中打断点。
九、常见错误
9.1 忘记 .value
typescript
// ❌ 错误
const count = ref(0);
console.log(count); // Ref 对象,不是 0
count++; // ❌ 不能直接 ++
// ✅ 正确
console.log(count.value); // 0
count.value++; // ✅9.2 v-for 不加 key
vue
<!-- ❌ 警告 -->
<li v-for="item in items">{{ item.name }}</li>
<!-- ✅ 加 key -->
<li v-for="item in items" :key="item.id">{{ item.name }}</li>9.3 模板里不能写语句
vue
<!-- ❌ 错误:模板里不能写 var/if/return -->
{{ var a = 1 }}
<!-- ✅ 只能写表达式 -->
{{ a + 1 }}十、本章小结
| 要点 | 关键 |
|---|---|
| 入口 | index.html + src/main.ts + src/App.vue |
| SFC | 单文件组件,<script> + <template> + <style> |
<script setup> | Vue 3 推荐语法 |
| 模板语法 | 插值、v-bind、v-if、v-for、v-on、v-model |
| computed | 计算属性,有缓存 |
| watch | 监听响应式数据 |
动手练习
- 计数器:实现一个完整的计数器组件,支持加减、步长设置、重置
- 待办列表:用 v-for 渲染待办列表,v-model 绑定输入框
- 条件渲染:实现一个登录/注册切换界面
- 响应式:用 ref 创建一个用户对象,展示在页面上
推荐阅读
- 📖 Vue 3 模板语法 — 官方模板文档
- 📖 Vue 3 SFC — 单文件组件
- 🌐 Vue 3 Examples — 官方示例
下一章:第 114 章:模板语法深入 →