Vue 3 零基础入门

这是一篇写给前端新手的长文。目标只有一个:读完它能自己动手跑起一个 Vue 3 项目,并独立完成一个功能完整的待办清单应用。

写在前面

很多人学完 HTML、CSS、JavaScript 三件套之后会卡在同一个地方:知识点都懂,但一让他们做真实项目就无从下手。Vue 就是为解决这个问题而生的——它不要求你换掉已有知识,而是在其上叠了一层"组织代码的方式"。

这篇文章是我学习和使用 Vue 3 过程中整理的入门笔记,覆盖从环境搭建、核心语法、组件通信到一个完整 TodoList 实战的全过程,最后附上新手最常踩的几个坑。

一、为什么是 Vue

先回答一个根本问题:不用框架行不行?行,但会很痛苦。

用原生 JS 或 jQuery 写页面,你的大部分代码都在做同一件事:找到某个 DOM 元素,改它的内容、样式或结构。数据一变,你就得手动把所有相关的地方都更新一遍——漏掉一处就是 bug。

Vue 的核心思路正好相反:你只管维护数据,页面是数据的投影。 数据变了,Vue 帮你把 DOM 更新好。可以类比 Excel:你改一个单元格的数值,引用它的图表自动跟着变,你不需要手动去重画图表。

对比项 原生 JS / jQuery Vue
更新页面的方式 手动查找并修改 DOM 修改数据,视图自动同步
代码风格 命令式,一步步告诉浏览器怎么做 声明式,描述页面应该长什么样
代码复用 靠复制粘贴 组件化,天然复用
项目规模 越大越难维护 结构清晰,规模友好

至于"为什么选 Vue 而不是其他框架":

  • 国内使用率极高,招聘要求里出现频率最高,社区中文资源丰富;
  • 渐进式设计,可以在旧项目里局部使用,也可以全家桶整体上;
  • 对新手友好,模板语法接近原生 HTML,上手曲线平缓。

后台管理系统的典型场景就是 Vue 的主场:大量表格、表单、弹窗、权限切换,用数据驱动的方式写起来最省力。

二、先记住五个词

正式写代码前,把这五个词的字面意思搞清楚,后面的学习会顺畅很多:

概念 一句话解释
响应式 数据和页面保持联动的机制,改数据等于改页面
指令 模板里 v- 开头的特殊属性,给 HTML 附加行为
组件 把一段模板 + 逻辑 + 样式封装成可复用的独立单元
模板语法 在 HTML 里写 JS 表达式的方式,比如 {{ }}
生命周期 组件从创建、挂载、更新到卸载的各个阶段

三、搭建环境

3.1 安装 Node.js

Vue 的开发和构建工具链跑在 Node.js 上,先去官网装 LTS 版本:https://nodejs.org/

装完打开终端验证:

node -v
npm -v

两个命令都能输出版本号即可。

3.2 创建项目

Vue 官方脚手架是 create-vue,基于 Vite,一条命令搞定:

npm create vue@latest my-vue-app

命令行会依次询问可选项,新手阶段建议这样选:

TypeScript?               → No    (先专注 Vue 本身,TS 以后再加)
JSX Support?               → No
Vue Router?                → Yes   (单页应用路由,迟早要用)
Pinia?                     → Yes   (状态管理,迟早要用)
Vitest (单元测试)?          → No
End-to-End Testing?        → No
ESLint?                    → Yes
Prettier?                  → Yes

然后进入目录、装依赖、启动:

cd my-vue-app
npm install
npm run dev

终端输出类似这样就成功了:

  VITE v5.x.x  ready in xxx ms

  ➜  Local:   http://localhost:5173/

浏览器打开 http://localhost:5173/,看到欢迎页即环境就绪。

3.3 认识项目结构

my-vue-app/
├── node_modules/     # 第三方依赖,不用手动管
├── public/           # 不参与构建的静态资源
├── src/
│   ├── assets/       # 图片、全局样式等资源
│   ├── components/   # 可复用组件
│   ├── router/       # 路由配置
│   ├── stores/       # Pinia 状态管理
│   ├── views/        # 页面级组件
│   ├── App.vue       # 根组件
│   └── main.js       # 应用入口
├── index.html        # 单页应用的 HTML 壳
├── package.json      # 依赖与脚本声明
└── vite.config.js    # Vite 配置

现阶段只需要关注四个:src/App.vue(根组件)、src/components/(放组件)、src/views/(放页面)、package.json(依赖清单)。

四、核心语法

4.1 模板插值:{{ }}

双大括号里可以放任何 JS 表达式,Vue 会把结果渲染进页面:

<template>
  <div>
    <h1>{{ title }}</h1>
    <p>总数:{{ list.length }}</p>
    <p>当前状态:{{ online ? '在线' : '离线' }}</p>
    <p>{{ title.toUpperCase() }}</p>
  </div>
</template>

<script setup>
import { ref } from 'vue'

const title = ref('Hello Vue 3')
const list = ref(['a', 'b', 'c'])
const online = ref(true)
</script>

注意花括号里是表达式(有返回值),不能写语句(比如 iffor)。

4.2 响应式:ref 与 reactive

Vue 3 提供两个 API 把普通数据变成响应式数据:

import { ref, reactive } from 'vue'

// ref:万能选项,基本类型、对象、数组都能包
const count = ref(0)
const user = ref({ name: '老王', age: 30 })

count.value++            // script 里读写要 .value
user.value.age = 31      // ref 包对象时,改属性也是 .value 一下

// reactive:只能包对象/数组,用起来像普通对象
const form = reactive({
  username: '',
  password: ''
})
form.username = 'admin'  // 不需要 .value

两者怎么选?我的建议是统一用 ref,规则只有一条(script 里加 .value),不容易出错。reactive 看懂别人的代码即可。

4.3 六个最常用的指令

① v-bind(简写 :)—— 绑定属性

<template>
  <!-- 把变量绑到 HTML 属性上 -->
  <img :src="avatarUrl" :alt="nickname">

  <!-- 动态 class:对象写法,键为类名,值为布尔 -->
  <button :class="{ active: selected }">按钮</button>

  <!-- 动态 style -->
  <p :style="{ color: danger ? 'red' : '#333' }">提示文字</p>
</template>

<script setup>
import { ref } from 'vue'
const avatarUrl = ref('/avatar.png')
const nickname = ref('我的头像')
const selected = ref(false)
const danger = ref(false)
</script>

② v-on(简写 @)—— 绑定事件

<template>
  <p>当前计数:{{ count }}</p>
  <button @click="count++">加一</button>
  <button @click="reset(0)">清零</button>

  <!-- 修饰符:阻止表单默认提交行为 -->
  <form @submit.prevent="onSubmit">
    <button type="submit">提交</button>
  </form>
</template>

<script setup>
import { ref } from 'vue'
const count = ref(0)
function reset(val) {
  count.value = val
}
function onSubmit() {
  console.log('提交,页面不会刷新')
}
</script>

常用修饰符:.prevent(阻止默认行为)、.stop(阻止冒泡)、.once(只触发一次)。

③ v-if / v-show —— 条件渲染

<template>
  <button @click="visible = !visible">切换</button>

  <!-- v-if:条件不满足时元素不存在于 DOM -->
  <p v-if="visible">v-if 控制的内容</p>

  <!-- v-show:条件不满足时只是 display:none,元素还在 -->
  <p v-show="visible">v-show 控制的内容</p>

  <!-- 多分支 -->
  <p v-if="score >= 90">优秀</p>
  <p v-else-if="score >= 60">及格</p>
  <p v-else>不及格</p>
</template>

<script setup>
import { ref } from 'vue'
const visible = ref(true)
const score = ref(72)
</script>

选择标准:切换频繁用 v-show(DOM 一直在,切换只是改样式),条件基本不变用 v-if(省掉不必要的 DOM)

④ v-for —— 列表渲染

<template>
  <ul>
    <li v-for="task in tasks" :key="task.id">
      {{ task.title }} —— {{ task.done ? '已完成' : '进行中' }}
    </li>
  </ul>
</template>

<script setup>
import { ref } from 'vue'
const tasks = ref([
  { id: 1, title: '装环境', done: true },
  { id: 2, title: '学指令', done: false },
  { id: 3, title: '写项目', done: false }
])
</script>

:key 必须加,它是 Vue 识别列表项身份的依据,优先用稳定唯一的 id,尽量不要用数组索引(索引在增删时会错位,导致渲染混乱)。

⑤ v-model —— 双向绑定

表单和数据的双向同步,一行搞定:

<template>
  <input v-model="keyword" placeholder="搜索...">
  <p>关键词:{{ keyword }}</p>

  <label><input type="checkbox" v-model="agreed"> 我已阅读协议</label>
  <p>{{ agreed }}</p>

  <select v-model="city">
    <option value="hz">杭州</option>
    <option value="sh">上海</option>
  </select>
  <p>{{ city }}</p>
</template>

<script setup>
import { ref } from 'vue'
const keyword = ref('')
const agreed = ref(false)
const city = ref('hz')
</script>

原理上它是 :value + @input 的语法糖:输入框打字会更新数据,程序改数据也会更新输入框。

⑥ v-html —— 渲染富文本

数据里包含 HTML 标签、需要真实渲染时使用:

<p v-html="richContent"></p>

注意: v-html 只能用于可信内容,绝不能直接渲染用户输入,否则会被利用做 XSS 注入攻击。

4.4 computed:有缓存的派生数据

从一个(或多个)响应式数据"算"出另一个数据时用 computed,它会缓存结果,依赖不变就不重算:

<template>
  <input v-model="price">
  <input v-model="count" type="number">
  <p>小计:¥{{ subtotal }}</p>
  <p>含税(6%):¥{{ taxed }}</p>
</template>

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

const price = ref(100)
const count = ref(2)

const subtotal = computed(() => price.value * count.value)
const taxed = computed(() => (subtotal.value * 1.06).toFixed(2))
</script>

computed 里也可以依赖另一个 computed(如上面的 taxed 依赖 subtotal)。

4.5 watch:监听变化执行副作用

需要在数据变化时做一些事(请求接口、打日志、弹提示)而不是算新值时,用 watch

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

const keyword = ref('')

watch(keyword, (newVal, oldVal) => {
  console.log(`关键词从 ${oldVal} 变为 ${newVal}`)
  // 实际项目里通常在这里做防抖搜索请求
})
</script>

两者分工一句话:要结果用 computed,要动作用 watch

五、组件化与通信

5.1 组件是什么

组件是自带模板、逻辑、样式的独立积木。一个页面通常是这样拆的:

页面
├── 页头组件
├── 侧边栏组件
└── 内容区
    ├── 搜索栏组件
    └── 列表组件(可复用到多个页面)

5.2 父传子:props

子组件用 defineProps 声明自己接收什么:

<!-- 子组件 TaskCard.vue -->
<template>
  <div class="card">
    <strong>{{ task.title }}</strong>
    <span>{{ task.done ? '✅' : '⏳' }}</span>
  </div>
</template>

<script setup>
defineProps({
  task: {
    type: Object,
    required: true
  }
})
</script>

<style scoped>
.card {
  padding: 12px;
  border: 1px solid #e2e2e2;
  border-radius: 8px;
}
</style>

父组件像传 HTML 属性一样传数据:

<!-- 父组件 -->
<template>
  <TaskCard v-for="t in tasks" :key="t.id" :task="t" />
</template>

<script setup>
import { ref } from 'vue'
import TaskCard from './components/TaskCard.vue'

const tasks = ref([
  { id: 1, title: '学 props', done: true },
  { id: 2, title: '学 emits', done: false }
])
</script>

props 是单向的:父传给子,子只能读不能改。

5.3 子传父:emits

子组件要改变数据时,通过事件把"意图"抛给父组件,由父组件执行:

<!-- 子组件 DeleteButton.vue -->
<template>
  <button @click="onClick">删除</button>
</template>

<script setup>
const emit = defineEmits(['remove'])

function onClick() {
  emit('remove', 1)  // 把要删除的 id 抛出去
}
</script>
<!-- 父组件 -->
<template>
  <DeleteButton @remove="handleRemove" />
</template>

<script setup>
import DeleteButton from './components/DeleteButton.vue'

function handleRemove(id) {
  console.log('删除 id =', id)
}
</script>

口诀:props 向下传数据,emits 向上传事件

六、实战:TodoList

知识点凑齐了,做一个覆盖全部核心语法的项目。功能目标:

  • 输入回车添加待办
  • 勾选切换完成状态
  • 删除单条
  • 按全部 / 未完成 / 已完成筛选
  • 底部统计与一键清除已完成

6.1 完整组件代码

<!-- src/components/TodoList.vue -->
<template>
  <div class="todo">
    <h2>待办清单</h2>

    <!-- 输入区 -->
    <div class="row">
      <input
        v-model="draft"
        placeholder="输入后回车添加"
        @keyup.enter="add"
      >
      <button @click="add">添加</button>
    </div>

    <!-- 筛选区 -->
    <div class="row filters">
      <button
        v-for="opt in filterOptions"
        :key="opt.value"
        :class="{ on: filter === opt.value }"
        @click="filter = opt.value"
      >
        {{ opt.label }}
      </button>
    </div>

    <!-- 列表区 -->
    <ul>
      <li v-for="t in visibleTodos" :key="t.id" :class="{ done: t.done }">
        <input type="checkbox" v-model="t.done">
        <span class="text">{{ t.text }}</span>
        <button class="del" @click="remove(t.id)">删除</button>
      </li>
      <li v-if="visibleTodos.length === 0" class="empty">这里空空如也</li>
    </ul>

    <!-- 统计区 -->
    <div class="row stats">
      <span>未完成 {{ remaining }} 项 / 共 {{ todos.length }} 项</span>
      <button v-if="remaining < todos.length" @click="clearDone">
        清除已完成
      </button>
    </div>
  </div>
</template>

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

// ---------- 数据 ----------
const draft = ref('')
const todos = ref([
  { id: 1, text: '跑通第一个 Vue 项目', done: true },
  { id: 2, text: '学完核心语法', done: false }
])
let nextId = 3

const filter = ref('all')
const filterOptions = [
  { label: '全部', value: 'all' },
  { label: '未完成', value: 'active' },
  { label: '已完成', value: 'done' }
]

// ---------- 派生数据 ----------
const visibleTodos = computed(() => {
  if (filter.value === 'active') return todos.value.filter(t => !t.done)
  if (filter.value === 'done') return todos.value.filter(t => t.done)
  return todos.value
})

const remaining = computed(() => todos.value.filter(t => !t.done).length)

// ---------- 操作 ----------
function add() {
  const text = draft.value.trim()
  if (!text) return
  todos.value.push({ id: nextId++, text, done: false })
  draft.value = ''
}

function remove(id) {
  todos.value = todos.value.filter(t => t.id !== id)
}

function clearDone() {
  todos.value = todos.value.filter(t => !t.done)
}
</script>

<style scoped>
.todo {
  max-width: 480px;
  margin: 40px auto;
  padding: 24px;
  border-radius: 12px;
  background: #fafafa;
}
.row {
  display: flex;
  gap: 8px;
  margin-bottom: 12px;
}
.row input[type='text'],
.row > input {
  flex: 1;
  padding: 8px 12px;
  border: 1px solid #ddd;
  border-radius: 6px;
}
button {
  padding: 8px 14px;
  border: none;
  border-radius: 6px;
  background: #4f6ef7;
  color: #fff;
  cursor: pointer;
}
.filters button {
  background: #fff;
  color: #666;
  border: 1px solid #ddd;
  border-radius: 999px;
  padding: 4px 14px;
}
.filters button.on {
  background: #4f6ef7;
  color: #fff;
  border-color: #4f6ef7;
}
ul {
  list-style: none;
  padding: 0;
  background: #fff;
  border-radius: 8px;
}
li {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 14px;
  border-bottom: 1px solid #f0f0f0;
}
li.done .text {
  text-decoration: line-through;
  color: #aaa;
}
.text {
  flex: 1;
}
.del {
  background: transparent;
  color: #e5484d;
  padding: 2px 8px;
}
.empty {
  justify-content: center;
  color: #bbb;
}
.stats {
  justify-content: space-between;
  font-size: 13px;
  color: #666;
}
.stats button {
  background: #fff;
  color: #666;
  border: 1px solid #ddd;
  padding: 4px 10px;
}
</style>

6.2 挂到页面上

<!-- src/App.vue -->
<template>
  <TodoList />
</template>

<script setup>
import TodoList from './components/TodoList.vue'
</script>

6.3 这个项目练到了什么

  • ref 定义响应式数据(drafttodosfilter
  • v-model 双向绑定(输入框、复选框)
  • v-for + :key 渲染列表
  • v-if / v-show 处理空状态和条件按钮
  • @click@keyup.enter 事件处理
  • computed 派生筛选结果和统计数
  • :class 动态样式(筛选项高亮、完成态删除线)

能把每一行代码解释清楚,Vue 入门这一关就算过了。

七、新手常见的六个坑

坑 1:script 里忘写 .value

const count = ref(0)
count = 1        //  直接赋值会报错
count.value = 1  // 

模板里不用加 .value(自动解包),只有 <script> 里需要。

坑 2:v-for 不加 key,或用索引当 key

增删列表项时索引会整体错位,Vue 就无法正确追踪每一项,出现渲染错乱。用稳定的 id 做 key。

坑 3:子组件直接改 props

props 对子组件是只读的,改了会收到警告。正确做法是 emit 事件交给父组件处理。

坑 4:解构 reactive 对象导致响应式丢失

const form = reactive({ name: '老王', age: 30 })
let { name } = form      // 这里会报错 name 变成普通字符串,不再响应

要解构请用 toRefs(form),或者干脆整个对象用,不拆开。

坑 5:v-if 和 v-for 写在同一个标签上

Vue 3 中 v-if 的优先级高于 v-for,如果 v-if 里引用了循环变量会直接报错(因为执行到 v-if 时循环变量还不存在)。两个版本都明确不建议这样写,需要过滤列表时用 computed 先算好。

坑 6:style 忘加 scoped

组件里的 <style> 默认是全局的,类名一撞样式就互相污染。养成习惯,每个组件都写 <style scoped>

八、速查表与下一步

核心速查

需求 用法
显示数据 {{ expr }}
定义响应式数据 ref() / reactive()
绑定属性 :attr="value"
绑定事件 @event="handler"
显示/隐藏 v-if / v-show
循环列表 v-for="(item, i) in list" :key="item.id"
表单双向绑定 v-model
派生数据(有缓存) computed(() => ...)
监听变化做副作用 watch(source, cb)
父传子 props + defineProps
子传父 defineEmits + emit

建议的学习路径

  1. 把本文的语法部分亲手敲一遍(不要复制粘贴);
  2. 独立复现 TodoList,再扩展两个功能练手:双击编辑内容、用 localStorage 持久化数据;
  3. 补上生命周期(onMounted 等)和 Vue Router(多页面跳转);
  4. 学 Pinia 处理跨组件共享状态;
  5. 找一个真实的小需求(个人主页、记账工具)完整做下来。

入门的标志不是"看懂了",而是"能独立写出 TodoList 并解释每一行"。

官方文档 & 资源

官方文档:https://cn.vuejs.org/guide/introduction.html

posted @ 2026-09-15 17:20  Star丶呢喃  阅读(5)  评论(0)    收藏  举报