1
0

Vue 3 笔记

2026-04-03
2026-07-23

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.envvite.config.tsmain.ts,知道项目怎么启动、代理怎么配、接口 baseURL 在哪里。

第二阶段:看懂页面。掌握 templateref/reactivecomputed/watchonMounted、事件绑定、表单绑定、列表渲染。能独立改一个列表页:查、新增、编辑、删除、分页。

第三阶段:看懂工程链路。掌握 routerPiniarequest.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 大型前后端分离项目基本就不会一头雾水了。