Skip to content
第 113 / 250 章前端⏱ 10 分钟阅读

第 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');

核心两步:

  1. createApp(App) - 创建 Vue 应用实例
  2. .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监听响应式数据

动手练习

  1. 计数器:实现一个完整的计数器组件,支持加减、步长设置、重置
  2. 待办列表:用 v-for 渲染待办列表,v-model 绑定输入框
  3. 条件渲染:实现一个登录/注册切换界面
  4. 响应式:用 ref 创建一个用户对象,展示在页面上

推荐阅读


下一章第 114 章:模板语法深入

本站基于 VitePress 构建 · 由 Codebook 团队维护