Vue3

Vue3

这是我第一遍学 Vue 时记的全景笔记: 把创建项目、响应式、组件、路由、Pinia、组件库整条链过一遍, 每个主题只到”能跑通、知道它是干嘛的”为止。
学到这里的前提: 三件套和 JS 基础过关, 工具链篇的 import/npm 概念清楚, 00 篇环境装好了。每节末尾给了对应深入篇的链接, 二刷时按需跳。

这篇示例全用纯 JS。脚手架默认生成的项目带 TypeScript, 代码里会多 lang="ts" 和类型标注, 从 01 篇开始我再跟着用。

一、Vue 是什么, 项目怎么跑起来

Vue 是构建网页界面的 JS 框架。在完整示例篇里我手写过 Todo: 每次改数据都得自己调 render 重画页面。Vue 解决的就是这个: 我只负责改数据和声明”页面长什么样”, 数据到页面的同步它全包。这套”数据驱动”是理解后面一切的总纲。

创建项目 (每个词的含义在 00 篇拆过):

代码块BASH · 4 行收起展开
npm create vue@latest   # 运行官方脚手架 create-vue, 问答式生成项目
cd 项目目录
npm install             # 按 package.json 装依赖进 node_modules
npm run dev             # 启动开发服务器, 浏览器开终端给的 localhost 地址

脚手架问的那串问题 (TypeScript / Router / Pinia / ESLint…), 第一次学勾 Router 和 Pinia 就够, 别的以后随时能加。

生成的目录, 记住几个常客就行 (每个文件的细节在文件架构篇):

Vue 脚手架项目目录结构

单文件组件 (SFC)

Vue 的组件写在 .vue 文件里, 一个文件三块, 正好对应三件套:

代码块VUE · 15 行收起展开
<template>
  <main class="page">{{ message }}</main>
</template>

<script setup>
// .vue 文件浏览器不认识, 是 Vite 把它编译成普通 JS 的
const message = 'Hello Vue3'
</script>

<style scoped>
/* scoped: 样式只作用于本组件, 不会污染别的组件 */
.page {
  padding: 24px;
}
</style>

<script setup> 这个写法是怎么进化来的 (以前要写 export default + setup() 手动 return), 专门写了一篇两种写法对比, 看完它再往下效果最好。

二、响应式初体验

ref: 会自动同步到页面的变量

代码块VUE · 15 行收起展开
<script setup>
import { ref } from 'vue'
// ref 是 vue 包 (npm 装的那个) 导出的函数, 把普通值包成响应式的

const salary = ref(15000)

function addSalary() {
  salary.value += 1000    // JS 里读写要 .value, 模板里不用
}
</script>

<template>
  <p>{{ salary }}</p>
  <button @click="addSalary">加薪</button>
</template>

点按钮, 页面数字自己变, 没有任何 DOM 操作。为什么非要 .value、背后的 Proxy 原理, 01 篇细讲, 现在先记规则: 脚本里 .value, 模板里直接写。

v-model: 表单双向绑定

代码块VUE · 10 行收起展开
<script setup>
import { ref } from 'vue'

const userName = ref('王一')
</script>

<template>
  <input v-model="userName" />
  <p>当前姓名: {{ userName }}</p>
</template>

输入框打字, 下面的字实时跟着变; 代码改 userName.value, 输入框内容也变。两个方向都通, 所以叫双向绑定。

reactive 和解构的坑

对象形态的状态可以用 reactive, 属性访问不用 .value; 但直接解构会丢响应式, 要用 toRefs:

代码块JS · 4 行收起展开
import { reactive, toRefs } from 'vue'

const state = reactive({ name: '王一', salary: 15000 })
const { salary } = toRefs(state)   // 直接 const { salary } = state 就断联了

第一遍先记结论, 拿不准就全用 ref

逻辑抽取: composable

一组相关的状态和函数可以抽成 useXxx 函数复用 (Vue 世界的”工具类”), 02 篇细讲, 先混个脸熟:

代码块JS · 8 行收起展开
// composables/useSalary.js
import { ref } from 'vue'

export function useSalary() {
  const salary = ref(15000)
  function addSalary() { salary.value += 1000 }
  return { salary, addSalary }
}

三、组件通信初体验

页面拆成组件后, 第一个问题就是父子之间怎么传数据。规则一句话: 父传子用 props, 子通知父用 emits。

<!-- 子组件 SalaryCard.vue -->
<script setup>
// defineProps/defineEmits 是 <script setup> 里的内置宏, 不用 import
const props = defineProps({
  userName: String,
  salary: Number
})

const emit = defineEmits(['increase'])
</script>

<template>
  <p>{{ props.userName }}: {{ props.salary }}</p>
  <button @click="emit('increase', 1000)">申请加薪</button>
</template>
<!-- 父组件 -->
<script setup>
import { ref } from 'vue'
import SalaryCard from './SalaryCard.vue'   // import 进来直接在模板当标签用

const salary = ref(15000)
</script>

<template>
  <SalaryCard user-name="王一" :salary="salary" @increase="salary += $event" />
</template>

props 是只读的, 子组件不许直接改, 要改就发事件让父组件动手。插槽、跨层通信、组件 v-model 这些进阶玩法在 02 篇

顺带一个小能力, 模板引用: 给标签写 ref="xxx" 配合同名 ref 变量, 能拿到真实 DOM (比如让输入框聚焦); 子组件想暴露方法给父组件调, 用 defineExpose。用得不多, 知道有这回事就行。

四、生命周期

组件从创建到销毁, Vue 在各节点留了回调:

代码块VUE · 6 行收起展开
<script setup>
import { onMounted, onUnmounted } from 'vue'

onMounted(() => console.log('挂载完成, DOM 可用, 适合发首屏请求'))
onUnmounted(() => console.log('销毁了, 清理定时器和监听'))
</script>

全套钩子有八个 (onBeforeMount / onUpdated 等), 日常九成场景就用上面这两个。Vue2 老代码里的 beforeDestroy/destroyed 对应 Vue3 的 onBeforeUnmount/onUnmounted, 读老项目时对个号。

五、Vue Router 初体验

路由是什么, 先一句话建立概念: SPA 只有一个 html 页面, “URL 变了显示哪个组件”这个映射关系就是前端路由 (从零的完整解释在 03 篇)。脚手架勾了 Router 的话这些都是现成的:

// src/router/index.js: 路由表, URL 和组件的对应关系
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '@/views/HomeView.vue'   // @ 是指向 src 的路径别名

const router = createRouter({
  history: createWebHistory(),
  routes: [
    { path: '/', redirect: '/home' },
    { path: '/home', name: 'home', component: HomeView },
    { path: '/news/:id', name: 'news-detail', component: () => import('@/views/NewsView.vue') }
  ]
})

export default router
<!-- App.vue: 链接 + 出口 -->
<template>
  <nav>
    <RouterLink to="/home">首页</RouterLink>   <!-- 导航链接, 点了不刷新页面 -->
  </nav>
  <RouterView />                               <!-- 当前 URL 匹配到的组件渲染在这 -->
</template>

代码里跳转用 useRouter, 读参数用 useRoute:

代码块JS · 8 行收起展开
import { useRoute, useRouter } from 'vue-router'

const router = useRouter()
router.push({ name: 'news-detail', params: { id: 2 } })   // 跳到 /news/2

const route = useRoute()
route.params.id      // 路径参数: /news/:id 里的 2
route.query.title    // 问号参数: ?title=xxx

两个当下就该知道的点: URL 里只传 ID 这类短标识, 详情数据拿 ID 去查; createWebHistory 模式的 URL 没有 # 但部署时服务器要配置回退, 刷新子页面 404 基本都是这个原因 (03 篇讲怎么配)。

六、Pinia 初体验

组件多了以后, “登录用户信息”这类好几个页面都要用的数据放谁身上? 层层 props 传太痛苦, Pinia 就是放共享状态的地方, 角色类似后端的全局单例 Bean。

// stores/user.js
import { defineStore } from 'pinia'
import { computed, ref } from 'vue'

// Setup Store 写法: 和组件里的组合式 API 一模一样
export const useUserStore = defineStore('user', () => {
  const username = ref('--')                                  // 状态
  const upperUsername = computed(() => username.value.toUpperCase())  // 派生值

  function changeUsername(value) {                            // 操作
    if (value && value.length < 10) username.value = value
  }

  return { username, upperUsername, changeUsername }
})
// 任何组件里
import { storeToRefs } from 'pinia'
import { useUserStore } from '@/stores/user'

const userStore = useUserStore()
const { username } = storeToRefs(userStore)   // 解构 state 必须走 storeToRefs, 直接解构丢响应式
const { changeUsername } = userStore          // 函数直接解构没事

storeToRefs 这个坑和 reactive 解构是同一个坑的换皮, 遇到”改了 store 页面不动”先想它。什么时候该用 store、什么时候普通 props 就够, 04 篇专门讨论。

七、Element Plus 初体验

真实后台项目不会从零画按钮表格, 都用组件库。Vue3 生态最常用的是 Element Plus:

npm install element-plus
// main.js 全量引入 (先图省事, 按需引入等 05 篇)
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'

createApp(App).use(ElementPlus).mount('#app')
<template>
  <el-button type="primary">Primary</el-button>   <!-- el- 开头的都是它的组件 -->
</template>

表单、表格、弹窗这些真实用法在 05 篇结合 CRUD 一起练。

八、常见易错点

Pasted image 20260722150056

第一遍学最容易栽的几个, 全是我真犯过的:

  1. 脚本里忘写 .value, 或者模板里画蛇添足写了 .value
  2. 直接解构 reactive 或 store, 页面从此不更新 (toRefs / storeToRefs)。
  3. 子组件直接改 props, 控制台警告 (发事件让父组件改)。
  4. 组件名拼错: 是 RouterLink/RouterView, 没有 route-link 这种东西。
  5. history 模式部署后刷新 404, 是服务器没配回退, 别怀疑代码。
  6. 把 Vue2 的 beforeDestroy 当 Vue3 钩子用。

九、快速复习表

需求用什么
响应式变量ref (脚本里 .value)
对象整体响应式reactive
解构保响应式toRefs / storeToRefs
父传子 / 子传父defineProps / defineEmits
拿 DOM 或子组件模板 ref + defineExpose
挂载后干活 / 销毁前清理onMounted / onUnmounted
页面切换Vue Router
跨页面共享状态Pinia
现成 UI 控件Element Plus

延伸阅读