Vue 3 笔记
JavaScript (ES5/ES6+)
下面这份按大型前后端分离项目速成 + 找工作面试来写。你先别陷入“把 Vue 文档从头背完”,重点是看懂项目里的页面怎么渲染、数据怎么流动、接口怎么调、权限怎么控制、状态怎么共享、工程怎么启动打包。
Vue3 核心定位
Vue3 是一个用于构建 Web 用户界面的前端框架,建立在 HTML、CSS、JavaScript 之上,核心特点是组件化、响应式、声明式渲染、生态完整。Vue 官方把它定位为 progressive framework,即可以只用来增强一个页面,也可以配合 Router、Pinia、Vite、TypeScript 做完整大型单页应用。Vue 的渲染系统是响应式驱动的,模板经过编译优化后生成渲染函数,数据变化时只更新相关视图。(Vue.js)
大型前后端分离项目里,Vue 一般负责:页面展示、表单交互、路由跳转、权限菜单、接口请求、状态管理、组件复用;后端负责:登录认证、业务接口、数据库、权限校验、文件上传下载等。前端不是直接操作数据库,而是通过 Axios / Fetch 调后端 REST API,拿 JSON 数据再渲染页面。
一、项目结构先看懂
典型 Vue3 + Vite 项目大概是这样:
project
├── package.json # 项目依赖、启动命令
├── vite.config.ts # Vite 配置、代理、别名、插件
├── index.html # 页面入口
├── src
│ ├── main.ts # Vue 应用入口,挂载 app、注册插件
│ ├── App.vue # 根组件,通常放 router-view
│ ├── router # 路由配置
│ ├── stores # Pinia 全局状态
│ ├── api # 接口封装
│ ├── views # 页面级组件
│ ├── components # 通用组件
│ ├── layout # 后台管理系统布局
│ ├── utils # 工具函数
│ ├── assets # 静态资源
│ └── styles # 全局样式
快速看项目时,不要从组件细节开始。先看 package.json 的启动命令,再看 main.ts 注册了哪些插件,然后看 router/index.ts 知道有哪些页面,再看 api 目录知道前端调哪些后端接口,最后看 views 里的页面业务逻辑。Vite 是现代前端常用构建工具,开发阶段提供基于原生 ES modules 的 dev server 和 HMR,生产阶段通过 build 输出可部署的静态资源包。(vitejs)
Vue 模板常见写法速查表
| 写法 | 全称 / 本质 | 作用 | 例子 | 快速记忆 |
|---|---|---|---|---|
class="xxx" |
普通 HTML 属性 | 加 CSS 类名 | class="register-form" |
写样式 |
id="xxx" |
普通 HTML 属性 | 设置元素 id | id="app" |
标识元素 |
:xxx="变量" |
v-bind:xxx |
把 JS 变量传给属性 / 组件 | :model="registerForm" |
冒号 = 传变量 |
@xxx="函数" |
v-on:xxx |
监听事件 | @click="login" |
@ = 触发函数 |
v-model="变量" |
双向绑定 | 输入框和变量同步 | v-model="form.username" |
表单输入专用 |
ref="名字" |
模板引用 | 在 JS 里拿到 DOM / 组件对象 | ref="formRef" |
拿组件实例 |
{{ 变量 }} |
插值表达式 | 把变量显示到页面 | {{ username }} |
页面显示变量 |
v-if="条件" |
条件渲染 | 条件成立才创建元素 | v-if="isLogin" |
不满足就不存在 |
v-show="条件" |
显示隐藏 | 条件不成立就隐藏 | v-show="visible" |
元素还在,只是隐藏 |
v-for="item in list" |
列表渲染 | 遍历数组生成多个元素 | v-for="item in users" |
循环渲染 |
:key="item.id" |
唯一标识 | 帮 Vue 区分列表项 | :key="user.id" |
v-for 基本都要写 |
| :rules | 就是给表单每个输入框定规矩<br>(比如必填、邮箱格式),违反规矩就显示错误提示 | |||
ref 绑定到哪个标签上,拿到的就是哪个东西 |
| 符号 | 作用 | 例子 | 记忆 |
|---|---|---|---|
: |
绑定变量 | :disabled="loading" |
JS 变量传进去 |
@ |
绑定事件 | @click="submit" |
用户操作后执行函数 |
v-model |
双向绑定 | v-model="form.name" |
输入框改变量,变量也改输入框 |
ref |
拿实例 | ref="formRef" |
JS 里调用组件方法 |
| 写法 | 作用 | 常见场景 |
|---|---|---|
.prevent |
阻止默认行为 | 表单提交不刷新页面 |
.stop |
阻止事件冒泡 | 点击子元素,不触发父元素点击 |
.enter |
按回车触发 | 输入框回车搜索 |
.once |
事件只触发一次 | 一次性按钮 |
.capture |
捕获阶段触发 | 较少用 |
常用命令:
npm install # 安装依赖
npm run dev # 本地开发启动
npm run build # 打包生产环境
npm run preview # 本地预览打包结果
package.json 里重点看:
{
"scripts": {
"dev": "vite",
"build": "vue-tsc && vite build"
},
"dependencies": {
"vue": "^3.x",
"vue-router": "^4.x",
"pinia": "^3.x",
"axios": "^1.x"
}
}
二、Vue 单文件组件 SFC
Vue 项目里最常见的是 .vue 文件,叫 Single-File Component。一个组件通常由 <template>、<script setup>、<style scoped> 三部分组成:模板写页面结构,脚本写状态和逻辑,样式写当前组件 CSS。Vue 官方 SFC 规范说明,.vue 文件是用 HTML-like 语法描述组件的自定义文件格式;scoped 样式会让 CSS 主要作用于当前组件,减少全局样式污染。(Vue.js)
<script setup lang="ts">
import { ref } from 'vue'
const count = ref(0)
function add() {
count.value++
}
</script>
<template>
<button @click="add">点击 {{ count }}</button>
</template>
<style scoped>
button {
padding: 8px 12px;
}
</style>
重点理解:template 里可以直接用 count,不用写 .value;但在 script setup 里访问 ref 数据要写 count.value。这是 Vue 模板自动解包造成的,不是 JavaScript 本身的语法。
三、声明式渲染:数据决定页面
传统 JS 是“找到 DOM,然后改 DOM”;Vue 是“声明数据和模板的关系,数据变了页面自动变”。比如:
<script setup>
import { ref } from 'vue'
const username = ref('jinbai')
const isLogin = ref(true)
</script>
<template>
<div v-if="isLogin">欢迎,{{ username }}</div>
<div v-else>请登录</div>
</template>
{{ }} 是文本插值,v-if 是条件渲染,v-for 是列表渲染,v-bind 绑定属性,v-on 绑定事件,v-model 做表单双向绑定。Vue 官方文档里 v-if 用于根据表达式真假决定是否渲染某个块。(Vue.js)
常用模板语法:
<template>
<img :src="avatarUrl" />
<button @click="submit">提交</button>
<input v-model="form.name" placeholder="用户名" />
<ul>
<li v-for="item in list" :key="item.id">
{{ item.name }}
</li>
</ul>
<div v-show="visible">只是显示隐藏,不销毁 DOM</div>
</template>
v-if 是真正创建 / 销毁 DOM,适合条件不频繁变化;v-show 是 CSS display 切换,适合频繁显示隐藏;v-for 必须写稳定的 key,不要随便用数组下标,否则列表更新时可能出现状态错乱。
四、响应式原理:ref、reactive、computed、watch
Vue3 的响应式核心可以理解成:数据被包装后,读取数据时收集依赖,修改数据时触发依赖更新。Vue 官方在响应式深入文档中用伪代码解释了 reactive 基于 Proxy 拦截 get/set,get 时 track,set 时 trigger;ref 则是一个带 .value 的响应式容器,访问时追踪依赖,修改时触发更新。(Vue.js)
1. ref:适合基本类型,也常用于对象
import { ref } from 'vue'
const count = ref(0)
count.value++
const user = ref({ name: 'Tom', age: 18 })
user.value.name = 'Jerry'
ref 的优势是统一,基本类型、对象、数组都能用。大型项目里很多团队更偏向 ref,因为重构和解构更安全。
2. reactive:适合对象状态
import { reactive } from 'vue'
const form = reactive({
username: '',
password: ''
})
form.username = 'admin'
reactive 返回的是响应式代理对象。注意不要直接解构:
const { username } = form // 这样容易丢响应式
需要解构时用:
import { toRefs } from 'vue'
const { username, password } = toRefs(form)
3. computed:有缓存的派生状态
import { computed, ref } from 'vue'
const price = ref(100)
const count = ref(2)
const total = computed(() => price.value * count.value)
computed 适合“由已有数据计算出来的新数据”,比如总价、过滤后的列表、按钮是否禁用。它有缓存,依赖不变就不会重复计算。
4. watch:监听数据变化后做副作用
import { watch, ref } from 'vue'
const keyword = ref('')
watch(keyword, async (newVal, oldVal) => {
console.log('搜索关键词变化', newVal)
})
watch 适合:监听搜索条件变化后请求接口、监听路由参数变化、监听弹窗打开后初始化数据。不要滥用 watch,很多场景用 computed 更干净。
六、组件通信:父子、兄弟、跨层级、全局
组件化是 Vue 的核心。大型项目页面一般不是一个大文件,而是拆成“页面组件 + 业务组件 + 通用组件”。
五、Composition API:Vue3 项目主流写法
Vue2 常用 Options API:data、methods、computed、watch 分开写。Vue3 大型项目更常用 Composition API,把一个功能相关的状态、方法、监听逻辑放在一起。Vue 官方说明 Composition API 的 setup 中可以访问响应式 props,现代项目常用 <script setup> 简化写法。(Vue.js)
Options API:
<script>
export default {
data() {
return { count: 0 }
},
methods: {
add() {
this.count++
}
}
}
</script>
Composition API:
<script setup>
import { ref } from 'vue'
const count = ref(0)
const add = () => count.value++
</script>
找工作建议直接掌握 Composition API,因为现在 Vue3 + TypeScript + <script setup> 是主流写法。面试可以这样说:Composition API 不是简单换语法,而是为了把同一业务逻辑聚合到一起,方便复用、维护和类型推导。
1. 父传子:props
父组件:
<UserCard :user="userInfo" />
子组件:
<script setup lang="ts">
defineProps<{
user: {
id: number
name: string
}
}>()
</script>
<template>
<div>{{ user.name }}</div>
</template>
props 是父组件传给子组件的数据,子组件不要直接修改 props。要修改就通知父组件,或者复制成内部状态。
2. 子传父:emit
子组件:
<script setup lang="ts">
const emit = defineEmits<{
save: [id: number]
}>()
function handleSave() {
emit('save', 1)
}
</script>
<template>
<button @click="handleSave">保存</button>
</template>
父组件:
<UserCard @save="handleSaveUser" />
3. 插槽:父组件决定子组件部分内容
<!-- 子组件 BaseCard.vue -->
<template>
<div class="card">
<header><slot name="title" /></header>
<main><slot /></main>
</div>
</template>
<!-- 父组件 -->
<BaseCard>
<template #title>用户信息</template>
<div>具体内容</div>
</BaseCard>
插槽适合封装布局类组件,比如弹窗、卡片、表格操作列。
4. provide / inject:跨层级传递
provide/inject 适合祖先组件给后代组件传数据,避免一层层 props 传下去。Vue 官方说明 provide() 接收 key 和 value,后代组件可以通过对应 key 注入,value 可以是普通值,也可以是响应式状态。(Vue.js)
// 父级或祖先组件
provide('theme', theme)
// 深层子组件
const theme = inject('theme')
大型项目里不要把所有全局状态都塞进 provide/inject;用户信息、权限、token、菜单状态更适合 Pinia。
七、生命周期:页面什么时候请求数据
生命周期就是组件从创建、挂载、更新到销毁的过程。Vue 官方说明 onMounted 通常用于需要访问已渲染 DOM 的副作用,也常用于客户端渲染场景下的初始化逻辑。(Vue.js)
常用:
import { onMounted, onUnmounted } from 'vue'
onMounted(() => {
// 组件挂载后执行:请求数据、初始化图表、绑定事件
})
onUnmounted(() => {
// 组件销毁时执行:清定时器、解绑事件、销毁图表实例
})
后台管理页面常见写法:
onMounted(() => {
getList()
})
注意:请求列表不一定非要写在 onMounted,也可以在路由进入、弹窗打开、搜索条件变化时调用。关键是搞清楚“数据什么时候需要刷新”。
八、路由 Vue Router:页面怎么跳转
Vue Router 是 Vue 官方路由库,核心思想是:把 URL path 映射到组件,访问某个路径时显示对应页面组件。Vue Router 官方文档说明,你通过配置 routes 告诉 Router 每个 URL path 应该展示哪个组件。(router.vuejs.org)
import { createRouter, createWebHistory } from 'vue-router'
const routes = [
{
path: '/login',
component: () => import('@/views/login/index.vue')
},
{
path: '/user',
component: () => import('@/views/user/index.vue')
}
]
export const router = createRouter({
history: createWebHistory(),
routes
})
App.vue 通常这样写:
<template>
<router-view />
</template>
页面跳转:
import { useRouter, useRoute } from 'vue-router'
const router = useRouter()
const route = useRoute()
router.push('/user')
router.push({ path: '/user', query: { id: 1 } })
console.log(route.query.id)
动态路由:
{
path: '/user/:id',
component: () => import('@/views/user/detail.vue')
}
获取参数:
const route = useRoute()
console.log(route.params.id)
路由守卫:权限控制核心
Vue Router 的导航守卫主要用于拦截、重定向或取消导航,可以分为全局守卫、路由独享守卫、组件内守卫。(router.vuejs.org)
典型登录权限逻辑:
router.beforeEach((to) => {
const token = localStorage.getItem('token')
if (to.path !== '/login' && !token) {
return '/login'
}
if (to.path === '/login' && token) {
return '/'
}
})
大型后台项目通常是:用户登录拿 token → 存本地或 Pinia → 路由守卫判断 token → 没 token 去登录 → 有 token 再请求用户信息和权限菜单 → 根据权限生成动态路由。
九、Pinia:全局状态管理
Pinia 是 Vue 官方推荐生态里的状态管理库。它用 store 管理跨组件共享状态,store 通常包含 state、getters、actions;Pinia 官方建议可以定义多个 store,并且通常每个 store 单独放一个文件。actions 可以是异步的,适合写登录、获取用户信息、请求接口等逻辑。(pinia.vuejs.org)
// stores/user.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useUserStore = defineStore('user', () => {
const token = ref('')
const userInfo = ref<any>(null)
const isLogin = computed(() => !!token.value)
function setToken(value: string) {
token.value = value
}
async function getUserInfo() {
// const res = await apiGetUserInfo()
// userInfo.value = res.data
}
return {
token,
userInfo,
isLogin,
setToken,
getUserInfo
}
})
组件中使用:
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
userStore.setToken('xxx')
console.log(userStore.isLogin)
Pinia 适合放:token、用户信息、权限菜单、系统配置、购物车、全局字典、主题状态。不适合把所有页面表单都放进去,页面内部状态直接放组件里更清晰。
十、接口请求:前后端分离项目最重要
大型项目里一般不会在页面里直接写一堆 axios.get,而是做三层封装:
utils/request.ts # axios 实例、请求拦截器、响应拦截器
api/user.ts # 按业务模块封装接口函数
views/user/index.vue # 页面调用接口函数
request.ts:
import axios from 'axios'
const request = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
timeout: 10000
})
request.interceptors.request.use((config) => {
const token = localStorage.getItem('token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
})
request.interceptors.response.use(
(response) => response.data,
(error) => {
if (error.response?.status === 401) {
localStorage.removeItem('token')
location.href = '/login'
}
return Promise.reject(error)
}
)
export default request
api/user.ts:
import request from '@/utils/request'
export function loginApi(data: { username: string; password: string }) {
return request.post('/auth/login', data)
}
export function getUserListApi(params: { page: number; size: number }) {
return request.get('/users', { params })
}
页面:
import { ref, onMounted } from 'vue'
import { getUserListApi } from '@/api/user'
const list = ref([])
const loading = ref(false)
async function getList() {
loading.value = true
try {
const res = await getUserListApi({ page: 1, size: 10 })
list.value = res.data.records
} finally {
loading.value = false
}
}
onMounted(getList)
这里的核心链路是:页面触发事件 → 调用 api 函数 → request 统一加 token → 后端返回 JSON → response 拦截器统一处理错误 → 页面拿数据渲染。你找工作时必须能说清楚这个流程。
十一、Vite 代理和环境变量
前端开发时经常遇到跨域。开发环境一般通过 Vite dev server 代理解决:
// vite.config.ts
export default defineConfig({
server: {
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
rewrite: path => path.replace(/^\/api/, '')
}
}
}
})
.env.development:
VITE_API_BASE_URL=/api
.env.production:
VITE_API_BASE_URL=https://example.com/api
注意:Vite 暴露给前端代码的环境变量通常要以 VITE_ 开头。开发时请求 /api/user,Vite 转发到后端;生产环境打包后,通常由 Nginx 代理 /api 到后端服务。
十二、后台管理系统常见业务套路
你要快速看懂大型前后端分离项目,优先掌握这些固定套路:
1. 登录
输入账号密码 → 调 login 接口 → 后端返回 token → 前端保存 token → 跳转首页 → 后续请求 Header 带 token
2. 权限
token → 获取用户信息 → 获取角色/权限/菜单 → 生成路由 → 控制按钮权限
常见按钮权限写法:
<button v-if="hasPermission('user:add')">新增用户</button>
3. 列表页
搜索表单 → 分页参数 → 调列表接口 → 表格渲染 → 分页切换重新请求
const query = reactive({
keyword: '',
page: 1,
size: 10
})
function search() {
query.page = 1
getList()
}
4. 新增 / 编辑弹窗
点击新增 → 清空表单 → 打开弹窗
点击编辑 → 查询详情 / 回填当前行 → 打开弹窗
点击保存 → 表单校验 → 调新增或更新接口 → 关闭弹窗 → 刷新列表
5. 删除
点击删除 → 二次确认 → 调删除接口 → 成功后刷新列表
6. 文件上传
选择文件 → 上传接口 → 后端返回 URL / 文件 ID → 前端保存到表单 → 提交业务数据
十三、组件封装思想
大型项目最怕页面复制粘贴。Vue 组件封装一般分三类:
通用组件:Button、Dialog、Table、Upload
业务组件:UserSelect、RoleTree、FilePicker
页面组件:UserList、OrderDetail、Dashboard
封装时核心看三件事:输入用 props,输出用 emit,可扩展区域用 slot。
<script setup lang="ts">
defineProps<{
title: string
visible: boolean
}>()
const emit = defineEmits<{
close: []
confirm: []
}>()
</script>
<template>
<div v-if="visible" class="dialog">
<h3>{{ title }}</h3>
<slot />
<button @click="emit('close')">取消</button>
<button @click="emit('confirm')">确定</button>
</div>
</template>
面试可以说:组件封装不是为了“少写代码”,而是为了统一交互、降低重复、隔离变化、提升可维护性。
十四、TypeScript 在 Vue3 里的作用
Vue3 项目经常配 TypeScript。它主要解决:接口返回数据有类型、props 有类型、emit 有类型、store 有类型、表单对象有类型。
interface User {
id: number
username: string
email: string
status: number
}
const userList = ref<User[]>([])
接口类型:
interface PageResult<T> {
records: T[]
total: number
}
async function getList() {
const res = await getUserListApi()
userList.value = res.data.records as User[]
}
不要一开始就追求 TS 写得特别高级,先做到:接口数据有 interface,props/emit 有类型,避免满屏 any。
十五、Vue3 原理面试版
你找工作时,Vue3 原理不用讲源码,但要讲得像懂项目。
1. Vue 为什么数据变了页面会变?
Vue 把状态变成响应式数据。组件渲染时会读取这些数据,Vue 会记录“这个组件依赖了哪些数据”。当数据被修改时,Vue 触发相关依赖更新,重新计算虚拟 DOM,并把变化 patch 到真实 DOM。Vue3 的对象响应式主要依赖 Proxy,ref 是一个带 .value 的响应式容器。(Vue.js)
2. 为什么要组件化?
组件化把页面拆成独立模块,每个组件管理自己的模板、逻辑和样式。这样可以复用、隔离复杂度,也方便多人协作。后台系统里的搜索表单、表格、弹窗、上传、权限按钮都适合组件化。
3. computed 和 watch 区别?
computed 是派生数据,有缓存,适合“根据已有状态算一个值”;watch 是监听变化做副作用,适合“状态变化后请求接口、写缓存、操作 DOM”。能用 computed 就不要硬用 watch。
4. ref 和 reactive 区别?
ref 用 .value 包装值,适合基本类型,也能包对象;reactive 返回对象代理,适合复杂对象。模板里 ref 会自动解包,脚本里要 .value。大型项目里表单常用 reactive,单个状态常用 ref。
5. Vue Router 做什么?
它把 URL 和组件对应起来,让单页应用在不刷新整个页面的情况下切换页面。路由守卫可以做登录校验、权限判断、动态路由加载。
6. Pinia 做什么?
Pinia 管理跨组件共享状态,比如 token、用户信息、权限菜单、系统配置。页面自己的临时状态不要都放 Pinia,否则全局状态会越来越乱。
十六、快速学习路线:按找工作优先级
第一阶段:先会跑项目。你要能看懂 package.json、.env、vite.config.ts、main.ts,知道项目怎么启动、代理怎么配、接口 baseURL 在哪里。
第二阶段:看懂页面。掌握 template、ref/reactive、computed/watch、onMounted、事件绑定、表单绑定、列表渲染。能独立改一个列表页:查、新增、编辑、删除、分页。
第三阶段:看懂工程链路。掌握 router、Pinia、request.ts、权限守卫、token、动态菜单。能说清楚从登录到进入首页发生了什么。
第四阶段:学组件封装。能把重复弹窗、表格、搜索表单抽成组件,知道 props、emit、slot 怎么设计。
第五阶段:准备面试表达。重点背熟:响应式原理、Composition API 优势、组件通信、路由守卫、Pinia、Axios 拦截器、权限控制、Vite 打包部署。
十七、一句话项目阅读法
看到一个 Vue3 大项目,按这个顺序读:
package.json → vite.config.ts → main.ts → router → stores → utils/request.ts → api → views → components
看到一个页面,按这个顺序读:
template 看页面结构 → script 看状态变量 → onMounted 看初始化请求 → methods 看事件 → api 看后端接口 → components 看子组件
看到一个接口功能,按这个链路追:
按钮点击 → 页面方法 → api 函数 → request 拦截器 → 后端 URL → 返回数据 → 修改响应式状态 → 页面更新
你现在最该练的是:找项目里一个“用户管理 / 文件管理 / 角色管理”页面,完整追一遍列表查询、新增、编辑、删除、权限按钮、接口封装、路由配置。这一套通了,Vue3 大型前后端分离项目基本就不会一头雾水了。