Vue3
Vue3
这是我第一遍学 Vue 时记的全景笔记: 把创建项目、响应式、组件、路由、Pinia、组件库整条链过一遍, 每个主题只到”能跑通、知道它是干嘛的”为止。
学到这里的前提: 三件套和 JS 基础过关, 工具链篇的 import/npm 概念清楚, 00 篇环境装好了。每节末尾给了对应深入篇的链接, 二刷时按需跳。
这篇示例全用纯 JS。脚手架默认生成的项目带 TypeScript, 代码里会多 lang="ts" 和类型标注, 从 01 篇开始我再跟着用。
一、Vue 是什么, 项目怎么跑起来
Vue 是构建网页界面的 JS 框架。在完整示例篇里我手写过 Todo: 每次改数据都得自己调 render 重画页面。Vue 解决的就是这个: 我只负责改数据和声明”页面长什么样”, 数据到页面的同步它全包。这套”数据驱动”是理解后面一切的总纲。
创建项目 (每个词的含义在 00 篇拆过):
代码块收起展开
npm create vue@latest # 运行官方脚手架 create-vue, 问答式生成项目
cd 项目目录
npm install # 按 package.json 装依赖进 node_modules
npm run dev # 启动开发服务器, 浏览器开终端给的 localhost 地址脚手架问的那串问题 (TypeScript / Router / Pinia / ESLint…), 第一次学勾 Router 和 Pinia 就够, 别的以后随时能加。
生成的目录, 记住几个常客就行 (每个文件的细节在文件架构篇):
单文件组件 (SFC)
Vue 的组件写在 .vue 文件里, 一个文件三块, 正好对应三件套:
代码块收起展开
<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: 会自动同步到页面的变量
代码块收起展开
<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: 表单双向绑定
代码块收起展开
<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:
代码块收起展开
import { reactive, toRefs } from 'vue'
const state = reactive({ name: '王一', salary: 15000 })
const { salary } = toRefs(state) // 直接 const { salary } = state 就断联了第一遍先记结论, 拿不准就全用 ref。
逻辑抽取: composable
一组相关的状态和函数可以抽成 useXxx 函数复用 (Vue 世界的”工具类”), 02 篇细讲, 先混个脸熟:
代码块收起展开
// 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 在各节点留了回调:
代码块收起展开
<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:
代码块收起展开
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 一起练。
八、常见易错点
第一遍学最容易栽的几个, 全是我真犯过的:
- 脚本里忘写
.value, 或者模板里画蛇添足写了.value。 - 直接解构
reactive或 store, 页面从此不更新 (toRefs / storeToRefs)。 - 子组件直接改 props, 控制台警告 (发事件让父组件改)。
- 组件名拼错: 是
RouterLink/RouterView, 没有route-link这种东西。 - history 模式部署后刷新 404, 是服务器没配回退, 别怀疑代码。
- 把 Vue2 的
beforeDestroy当 Vue3 钩子用。
九、快速复习表
| 需求 | 用什么 |
|---|---|
| 响应式变量 | ref (脚本里 .value) |
| 对象整体响应式 | reactive |
| 解构保响应式 | toRefs / storeToRefs |
| 父传子 / 子传父 | defineProps / defineEmits |
| 拿 DOM 或子组件 | 模板 ref + defineExpose |
| 挂载后干活 / 销毁前清理 | onMounted / onUnmounted |
| 页面切换 | Vue Router |
| 跨页面共享状态 | Pinia |
| 现成 UI 控件 | Element Plus |