Vue CLI笔记

一、Vue 脚手架(Vue CLI / Vite)

1. 什么是脚手架?

脚手架 就是帮你自动生成一个 "已经配好工程化环境"的 Vue 项目文件夹

打个比方:你要盖房子,以前你得自己打地基、拉水电、砌墙;现在你直接买一套"精装样板间"的图纸和材料包,拿到手就能开始装修(写业务代码)了。

2. 为什么需要脚手架?

原生方式(CDN 引入) 脚手架方式
所有代码写在一个 .html 组件拆分成 .vue 文件,按模块组织
只能写简单 demo 支持大型项目、多人协作
没有打包优化 自动压缩、代码分割、热更新
手动引入库 通过 npm 管理依赖

3. 创建项目的两种方式

方式一:Vue CLI(老牌,稳定)

bash 复制代码
# 全局安装
npm install -g @vue/cli

# 创建项目
vue create my-project

方式二:Vite(新一代,速度更快)

bash 复制代码
# 创建项目
npm create vue@latest

官方现在推荐 Vite,启动速度飞快。

4. 项目结构(看一眼就懂)

复制代码
my-project/
├── public/              # 静态资源(不经过打包)
│   └── index.html       # 入口 HTML
├── src/                 # 核心代码(你主要在这里工作)
│   ├── assets/          # 图片、样式等资源
│   ├── components/      # 组件目录(.vue 文件)
│   ├── views/           # 页面级组件(路由用)
│   ├── router/          # 路由配置
│   ├── store/           # Vuex 状态管理
│   ├── App.vue          # 根组件(整个应用的入口)
│   └── main.js          # 入口 JS(创建 Vue 实例)
├── package.json         # 依赖管理
└── vite.config.js       # 构建配置

5. main.js 里发生了什么?

javascript 复制代码
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'

// 创建 Vue 应用,挂载到 index.html 里的 #app 上
createApp(App)
    .use(router)   // 装上路由
    .use(store)    // 装上状态管理
    .mount('#app')

总结 :脚手架就是给你一个"开箱即用"的工程目录,你只需要往 src/ 里填业务代码就行。


二、组件(Component)

1. 什么是组件?

组件就是一块独立的、可复用的 UI 片段 ,它把 HTML + CSS + JS 打包在一个 .vue 文件里。

你可以把它理解成乐高积木------一个小按钮是一个组件,一个导航栏是一个组件,一个页面是由多个组件拼起来的。

2. .vue 文件的构成(Vue 2 / Vue 3 / Vue 3 + TS)

每个 .vue 文件本质上是一个单文件组件(SFC),由三部分组成:

vue 复制代码
<template>
  <!-- 视图层:HTML 结构 -->
</template>

<script>
  // 逻辑层:JS / TS 代码
</script>

<style>
  /* 样式层:CSS(scoped 可选) */
</style>

不同版本的写法对比:

版本 <script> 写法 特点
Vue 2 export default { data() {...}, methods: {...} } 选项式 API,this 指向实例
Vue 3(选项式) 同上,但底层已升级为 Vue 3 响应式 兼容 Vue 2 写法,适合过渡
Vue 3(组合式) export default { setup() {...} } ref/reactive,逻辑更聚合
Vue 3 + TS(推荐) <script setup lang="ts"> 语法糖 + 类型推断,最简洁

本文统一采用 Vue 3 + 组合式 API(<script setup>)写法,这是目前最主流、最推荐的方式。

3. 一个组件的完整写法(.vue 文件)

vue 复制代码
<template>
    <!-- 模板:写 HTML 结构 -->
    <div class="card">
        <h2>{{ title }}</h2>
        <p>{{ content }}</p>
        <button @click="handleClick">点我</button>
    </div>
</template>

<script setup>
// 组合式 API 写法(Vue 3 推荐)
import { ref } from 'vue'

// 接收父组件传进来的数据
const props = defineProps({
    title: String,
    content: String
})

// 组件自己的数据
const isLiked = ref(false)

// 方法
const handleClick = () => {
    isLiked.value = !isLiked.value
    emit('like', isLiked.value)   // 向父组件发射事件
}

// 声明可触发的事件
const emit = defineEmits(['like'])
</script>

<style scoped>
/* scoped 表示样式只对当前组件生效,不影响外面 */
.card {
    border: 1px solid #ddd;
    padding: 16px;
    border-radius: 8px;
}
</style>

4. 组件三大核心

概念 说明 示例
props 父组件传给子组件的数据(单向流,只能父→子) <Card title="标题" />
events 子组件向父组件发消息(用 defineEmits emit('like', true)
slots 插槽,让父组件往子组件里"塞"内容 <Card><p>自定义内容</p></Card>

5. 组件间值传递的四种场景

场景一:父传子 → props
vue 复制代码
<!-- 父组件 Parent.vue -->
<template>
  <Child :userName="name" :age="18" />
</template>

<script setup>
import Child from './Child.vue'
const name = '张三'
</script>
vue 复制代码
<!-- 子组件 Child.vue -->
<script setup>
// 方式一:声明接收(无类型)
const props = defineProps(['userName', 'age'])

// 方式二:带类型(TS 写法)
// const props = defineProps<{ userName: string; age: number }>()
</script>

<template>
  <p>{{ userName }} 今年 {{ age }} 岁</p>
</template>
场景二:子传父 → defineEmits
vue 复制代码
<!-- 子组件 Child.vue -->
<script setup>
const emit = defineEmits(['update', 'delete'])

const handleUpdate = () => {
  emit('update', { id: 1, name: '新名字' })
}
</script>
vue 复制代码
<!-- 父组件 Parent.vue -->
<template>
  <Child @update="onUpdate" @delete="onDelete" />
</template>

<script setup>
import Child from './Child.vue'

const onUpdate = (data) => console.log('收到子组件数据:', data)
const onDelete = () => console.log('删除了')
</script>
场景三:同级传值 → 通过父组件中转 或 事件总线

方式一:父组件中转(推荐)

复制代码
组件A → 触发事件 → 父组件接收 → 通过 props 传给 组件B

方式二:事件总线(Vue 3 用 mitt 库)

javascript 复制代码
// bus.js
import mitt from 'mitt'
export const bus = mitt()

// 组件A 发送
import { bus } from './bus'
bus.emit('message', 'hello')

// 组件B 接收
bus.on('message', (data) => console.log(data))
场景四:爷孙传值(跨层级) → provide / inject
vue 复制代码
<!-- 爷爷组件 Grandparent.vue -->
<script setup>
import { provide, ref } from 'vue'

const theme = ref('dark')
provide('theme', theme)        // 提供数据
provide('updateTheme', (val) => theme.value = val)  // 提供方法
</script>
vue 复制代码
<!-- 孙组件 Grandchild.vue(跳过父组件,直接拿到) -->
<script setup>
import { inject } from 'vue'

const theme = inject('theme')
const updateTheme = inject('updateTheme')
</script>

<template>
  <p>当前主题:{{ theme }}</p>
  <button @click="updateTheme('light')">切换亮色</button>
</template>

6. 组件的使用流程

vue 复制代码
<!-- 父组件 Parent.vue -->
<template>
    <div>
        <!-- 1. 使用子组件,并传 props,监听事件 -->
        <Card title="新闻标题" content="新闻内容" @like="onLike" />
    </div>
</template>

<script setup>
// 2. 引入子组件
import Card from './Card.vue'

// 3. 定义事件处理函数
const onLike = (val) => {
    console.log('点赞状态:', val)
}
</script>

总结 :组件就是 Vue 里的"最小工作单元",一个页面就是一个组件树,从根组件 App.vue 一层层往下嵌套。

7 插槽

插槽是 Vue 的内容分发机制 ,允许父组件向子组件传递 HTML 内容,实现组件的高度可定制化

1. 基础插槽(默认插槽)

子组件(Child.vue)

vue 复制代码
<template>
  <div class="card">
    <div class="card-header">
      <h3>卡片标题</h3>
    </div>
    <div class="card-body">
      <!-- 默认插槽:父组件传入的内容将显示在这里 -->
      <slot></slot>
    </div>
  </div>
</template>

父组件使用

vue 复制代码
<template>
  <Card>
    <!-- 这里的内容会替换 <slot></slot> -->
    <p>这是卡片的内容</p>
    <button>点击我</button>
  </Card>
</template>

渲染结果

html 复制代码
<div class="card">
  <div class="card-header">
    <h3>卡片标题</h3>
  </div>
  <div class="card-body">
    <p>这是卡片的内容</p>
    <button>点击我</button>
  </div>
</div>

2. 具名插槽(Named Slots)

可以有多个插槽 ,通过 name 属性区分。

子组件(Layout.vue)

vue 复制代码
<template>
  <div class="layout">
    <header class="header">
      <!-- 具名插槽:header -->
      <slot name="header"></slot>
    </header>
    
    <main class="main">
      <!-- 默认插槽 -->
      <slot></slot>
    </main>
    
    <footer class="footer">
      <!-- 具名插槽:footer -->
      <slot name="footer"></slot>
    </footer>
  </div>
</template>

父组件使用(Vue 3 语法)

vue 复制代码
<template>
  <Layout>
    <!-- 使用 v-slot:name 或 #name 指定插槽 -->
    <template #header>
      <h1>页面标题</h1>
      <nav>导航栏</nav>
    </template>
    
    <!-- 默认插槽(没有 # 的就是默认) -->
    <article>
      <p>主要内容区域</p>
    </article>
    
    <template #footer>
      <p>版权信息 ©2026</p>
    </template>
  </Layout>
</template>

渲染结果

html 复制代码
<div class="layout">
  <header class="header">
    <h1>页面标题</h1>
    <nav>导航栏</nav>
  </header>
  
  <main class="main">
    <article>
      <p>主要内容区域</p>
    </article>
  </main>
  
  <footer class="footer">
    <p>版权信息 ©2026</p>
  </footer>
</div>

3. 作用域插槽(Scoped Slots)

核心概念 :子组件可以把数据传递给父组件,父组件决定如何渲染。

子组件(TodoList.vue)

vue 复制代码
<template>
  <ul>
    <li v-for="item in list" :key="item.id">
      <!-- 
        通过 v-bind 把数据暴露给父组件 
        父组件可以用 "slotProps" 接收
      -->
      <slot :item="item" :index="index">
        <!-- 默认内容(当父组件没传内容时显示) -->
        {{ item.text }}
      </slot>
    </li>
  </ul>
</template>

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

const list = ref([
  { id: 1, text: '学习 Vue', completed: false },
  { id: 2, text: '练习插槽', completed: true },
  { id: 3, text: '写项目', completed: false }
])
</script>

父组件使用

vue 复制代码
<template>
  <TodoList>
    <!-- 
      v-slot:default="slotProps" 
      可以简写为 #default="slotProps"
      也可以直接解构 { item, index }
    -->
    <template #default="{ item, index }">
      <span :style="{ textDecoration: item.completed ? 'line-through' : 'none' }">
        {{ index + 1 }}. {{ item.text }}
      </span>
      <span v-if="item.completed">✅</span>
      <span v-else>⏳</span>
    </template>
  </TodoList>
</template>

渲染结果

html 复制代码
<ul>
  <li>
    <span>1. 学习 Vue ⏳</span>
  </li>
  <li>
    <span style="text-decoration: line-through">2. 练习插槽 ✅</span>
  </li>
  <li>
    <span>3. 写项目 ⏳</span>
  </li>
</ul>

4. 具名作用域插槽

结合具名插槽和作用域插槽。

子组件(Table.vue)

vue 复制代码
<template>
  <table>
    <thead>
      <tr>
        <slot name="header" :columns="columns"></slot>
      </tr>
    </thead>
    <tbody>
      <tr v-for="(item, index) in data" :key="index">
        <slot name="row" :item="item" :index="index"></slot>
      </tr>
    </tbody>
  </table>
</template>

<script setup>
const props = defineProps({
  columns: Array,
  data: Array
})
</script>

父组件使用

vue 复制代码
<template>
  <Table :columns="columns" :data="users">
    <!-- 表头插槽 -->
    <template #header="{ columns }">
      <th v-for="col in columns" :key="col.key">
        {{ col.label }}
      </th>
      <th>操作</th>
    </template>
    
    <!-- 行插槽 -->
    <template #row="{ item, index }">
      <td>{{ item.name }}</td>
      <td>{{ item.age }}</td>
      <td>{{ item.email }}</td>
      <td>
        <button @click="editUser(index)">编辑</button>
        <button @click="deleteUser(index)">删除</button>
      </td>
    </template>
  </Table>
</template>

<script setup>
const columns = [
  { key: 'name', label: '姓名' },
  { key: 'age', label: '年龄' },
  { key: 'email', label: '邮箱' }
]

const users = ref([
  { name: '张三', age: 25, email: 'zhangsan@example.com' },
  { name: '李四', age: 30, email: 'lisi@example.com' }
])
</script>

5. 插槽简写语法
完整写法 简写 说明
v-slot:header #header 具名插槽
v-slot:default #default 默认插槽
v-slot:default="props" #default="props" 带数据
v-slot="{ item }" #default="{ item }" 解构数据
vue 复制代码
<!-- 完整写法 -->
<template v-slot:header="slotProps">
  {{ slotProps.title }}
</template>

<!-- 简写 -->
<template #header="{ title }">
  {{ title }}
</template>

6. 动态插槽名

根据变量动态决定使用哪个插槽。

vue 复制代码
<template>
  <Layout>
    <template #[dynamicSlotName]>
      <p>动态插槽内容</p>
    </template>
  </Layout>
</template>

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

const dynamicSlotName = ref('header')  // 可以是 'header', 'footer', 'sidebar'
</script>

7. 插槽默认内容

当父组件没有传递内容时,显示默认内容。

vue 复制代码
<!-- 子组件 -->
<template>
  <div class="alert">
    <slot>
      <!-- 默认内容 -->
      <span>默认提示信息</span>
    </slot>
  </div>
</template>
vue 复制代码
<!-- 父组件 -->
<template>
  <!-- 不传内容 → 显示 "默认提示信息" -->
  <Alert />
  
  <!-- 传内容 → 覆盖默认内容 -->
  <Alert>
    <strong>自定义错误信息</strong>
  </Alert>
</template>

三、指令

指令是 Vue 模板中v- 前缀的特殊属性,用于在 DOM 上应用响应式行为。

1. v-if --- 条件渲染

作用 :根据条件决定是否渲染元素(真正的销毁/重建)。

vue 复制代码
<template>
  <!-- 为 true 时渲染,为 false 时从 DOM 移除 -->
  <div v-if="isShow">我是显示的</div>
  
  <!-- v-else-if 必须跟在 v-if 后面 -->
  <div v-else-if="status === 'loading'">加载中...</div>
  
  <!-- v-else 必须跟在 v-if 或 v-else-if 后面 -->
  <div v-else>加载失败</div>
</template>

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

const isShow = ref(true)
const status = ref('success')
</script>

v-if vs v-show

vue 复制代码
<template>
  <!-- v-if:真正的条件渲染,切换开销大,适合不频繁切换 -->
  <div v-if="isShow">我是 v-if</div>
  
  <!-- v-show:始终渲染,用 display:none 控制,适合频繁切换 -->
  <div v-show="isShow">我是 v-show</div>
</template>
特性 v-if v-show
渲染方式 真实销毁/重建 CSS display 控制
初始渲染 条件为 false 时不渲染 始终渲染
切换开销 大(销毁重建) 小(切换 CSS)
适用场景 不频繁切换 频繁切换

2. v-bind --- 属性绑定(简写 :

作用:动态绑定 HTML 属性、CSS 类、样式等。

vue 复制代码
<template>
  <!-- 完整写法 -->
  <img v-bind:src="imageUrl" v-bind:alt="imageAlt">
  
  <!-- 简写 : -->
  <img :src="imageUrl" :alt="imageAlt">
  
  <!-- 绑定布尔属性 -->
  <button :disabled="isDisabled">提交</button>
  
  <!-- 绑定多个属性(对象) -->
  <div v-bind="attrsObj">内容</div>
</template>

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

const imageUrl = ref('https://example.com/photo.jpg')
const imageAlt = ref('示例图片')
const isDisabled = ref(true)

const attrsObj = {
  id: 'main',
  class: 'container',
  'data-role': 'admin'
}
</script>

绑定 CSS 类

vue 复制代码
<template>
  <!-- 对象语法 -->
  <div :class="{ active: isActive, 'text-danger': hasError }">
    类名动态切换
  </div>
  
  <!-- 数组语法 -->
  <div :class="[activeClass, errorClass]">多个类</div>
  
  <!-- 混合使用 -->
  <div :class="['base', { active: isActive }]">混合</div>
</template>

<script setup>
const isActive = ref(true)
const hasError = ref(false)
const activeClass = ref('active')
const errorClass = ref('error')
</script>

绑定内联样式

vue 复制代码
<template>
  <!-- 对象语法 -->
  <div :style="{ color: textColor, fontSize: fontSize + 'px' }">
    动态样式
  </div>
  
  <!-- 数组语法 -->
  <div :style="[baseStyles, overrideStyles]">合并样式</div>
</template>

<script setup>
const textColor = ref('red')
const fontSize = ref(20)
const baseStyles = { color: 'blue', fontSize: '14px' }
const overrideStyles = { fontSize: '18px' }
</script>

3. v-on --- 事件监听(简写 @

作用:监听 DOM 事件并执行 JavaScript 代码。

vue 复制代码
<template>
  <!-- 完整写法 -->
  <button v-on:click="handleClick">点击我</button>
  
  <!-- 简写 @ -->
  <button @click="handleClick">点击我</button>
  
  <!-- 直接写表达式 -->
  <button @click="count++">计数器: {{ count }}</button>
  
  <!-- 传递参数 -->
  <button @click="sayHello('张三')">打招呼</button>
  
  <!-- 传递事件对象 -->
  <button @click="handleEvent($event)">事件对象</button>
  
  <!-- 同时传递参数和事件对象 -->
  <button @click="handleData('参数', $event)">带事件</button>
</template>

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

const count = ref(0)

const handleClick = () => {
  console.log('按钮被点击了')
}

const sayHello = (name) => {
  console.log(`你好,${name}`)
}

const handleEvent = (event) => {
  console.log('事件对象:', event)
  console.log('目标元素:', event.target)
}

const handleData = (data, event) => {
  console.log('数据:', data)
  console.log('事件:', event)
}
</script>

事件修饰符

vue 复制代码
<template>
  <!-- .stop:阻止事件冒泡 -->
  <div @click.stop="handleClick">阻止冒泡</div>
  
  <!-- .prevent:阻止默认行为 -->
  <form @submit.prevent="handleSubmit">阻止提交</form>
  
  <!-- .once:只触发一次 -->
  <button @click.once="handleOnce">只触发一次</button>
  
  <!-- .self:只有元素自身触发时才执行 -->
  <div @click.self="handleSelf">只有点击自己才触发</div>
  
  <!-- 链式使用 -->
  <a @click.stop.prevent="handleLink">阻止冒泡+默认行为</a>
  
  <!-- 按键修饰符 -->
  <input @keyup.enter="handleEnter" placeholder="按回车触发">
  <input @keyup.esc="handleEsc" placeholder="按 ESC 触发">
  <input @keyup.ctrl.s="handleSave" placeholder="Ctrl+S 触发">
</template>

4. v-for --- 列表渲染

作用:基于数据多次渲染元素或模板。

vue 复制代码
<template>
  <!-- 遍历数组 -->
  <ul>
    <li v-for="item in items" :key="item.id">
      {{ item.name }}
    </li>
  </ul>
  
  <!-- 带索引 -->
  <ul>
    <li v-for="(item, index) in items" :key="item.id">
      {{ index + 1 }}. {{ item.name }}
    </li>
  </ul>
  
  <!-- 遍历对象 -->
  <ul>
    <li v-for="(value, key, index) in user" :key="key">
      {{ index }}. {{ key }}: {{ value }}
    </li>
  </ul>
  
  <!-- 遍历数字 -->
  <span v-for="n in 10" :key="n">{{ n }}</span>
  
  <!-- 遍历字符串 -->
  <span v-for="char in 'Hello'" :key="char">{{ char }}</span>
</template>

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

const items = ref([
  { id: 1, name: '张三' },
  { id: 2, name: '李四' },
  { id: 3, name: '王五' }
])

const user = ref({
  name: '张三',
  age: 25,
  email: 'zhangsan@example.com'
})
</script>

⚠️ 重要:必须使用 :key

vue 复制代码
<template>
  <!-- ✅ 正确:使用唯一 ID -->
  <div v-for="item in items" :key="item.id">
    {{ item.name }}
  </div>
  
  <!-- ⚠️ 也可以使用 index(不推荐,列表会变化时) -->
  <div v-for="(item, index) in items" :key="index">
    {{ item.name }}
  </div>
  
  <!-- ❌ 错误:没有 key(会有性能问题和状态混乱) -->
  <div v-for="item in items">
    {{ item.name }}
  </div>
</template>

v-for 与 v-if 同时使用

vue 复制代码
<template>
  <!-- ❌ 不推荐:v-for 优先级更高,会先渲染所有再过滤 -->
  <div v-for="item in items" v-if="item.active" :key="item.id">
    {{ item.name }}
  </div>
  
  <!-- ✅ 推荐:先用计算属性过滤 -->
  <div v-for="item in activeItems" :key="item.id">
    {{ item.name }}
  </div>
</template>

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

const items = ref([
  { id: 1, name: '张三', active: true },
  { id: 2, name: '李四', active: false },
  { id: 3, name: '王五', active: true }
])

const activeItems = computed(() => {
  return items.value.filter(item => item.active)
})
</script>

5. v-model --- 双向数据绑定

作用:在表单元素上创建双向数据绑定(数据变化 → 视图更新,视图变化 → 数据更新)。

vue 复制代码
<template>
  <!-- 文本输入 -->
  <input v-model="username" placeholder="输入用户名">
  <p>用户名: {{ username }}</p>
  
  <!-- 多行文本 -->
  <textarea v-model="description" placeholder="描述"></textarea>
  
  <!-- 复选框(单个) -->
  <input type="checkbox" v-model="isChecked">
  <p>已选择: {{ isChecked }}</p>
  
  <!-- 复选框(多个) -->
  <input type="checkbox" value="篮球" v-model="hobbies"> 篮球
  <input type="checkbox" value="足球" v-model="hobbies"> 足球
  <input type="checkbox" value="羽毛球" v-model="hobbies"> 羽毛球
  <p>爱好: {{ hobbies }}</p>
  
  <!-- 单选框 -->
  <input type="radio" value="男" v-model="gender"> 男
  <input type="radio" value="女" v-model="gender"> 女
  <p>性别: {{ gender }}</p>
  
  <!-- 下拉选择框 -->
  <select v-model="selectedCity">
    <option value="">请选择城市</option>
    <option value="北京">北京</option>
    <option value="上海">上海</option>
    <option value="广州">广州</option>
  </select>
  <p>城市: {{ selectedCity }}</p>
</template>

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

const username = ref('')
const description = ref('')
const isChecked = ref(false)
const hobbies = ref([])
const gender = ref('')
const selectedCity = ref('')
</script>

v-model 修饰符

vue 复制代码
<template>
  <!-- .trim:去除首尾空格 -->
  <input v-model.trim="username" placeholder="自动去除空格">
  
  <!-- .number:自动转为数字 -->
  <input v-model.number="age" type="number" placeholder="年龄">
  
  <!-- .lazy:change 事件触发(而非 input) -->
  <input v-model.lazy="content" placeholder="失去焦点时更新">
</template>

自定义 v-model

vue 复制代码
<!-- 子组件 Child.vue -->
<template>
  <input 
    :value="modelValue" 
    @input="$emit('update:modelValue', $event.target.value)"
  />
</template>

<script setup>
defineProps(['modelValue'])
defineEmits(['update:modelValue'])
</script>

<!-- 父组件 -->
<template>
  <Child v-model="parentData" />
  <p>父组件数据: {{ parentData }}</p>
</template>

<script setup>
import { ref } from 'vue'
const parentData = ref('')
</script>

多个 v-model 绑定

vue 复制代码
<!-- 子组件 -->
<template>
  <div>
    <input 
      :value="firstName" 
      @input="$emit('update:firstName', $event.target.value)"
      placeholder="姓"
    />
    <input 
      :value="lastName" 
      @input="$emit('update:lastName', $event.target.value)"
      placeholder="名"
    />
  </div>
</template>

<script setup>
defineProps(['firstName', 'lastName'])
defineEmits(['update:firstName', 'update:lastName'])
</script>

<!-- 父组件 -->
<template>
  <Child 
    v-model:first-name="user.firstName"
    v-model:last-name="user.lastName"
  />
</template>

6. 其他常用指令

vue 复制代码
<template>
  <!-- v-text:更新文本内容 -->
  <span v-text="message"></span>
  <!-- 等价于 -->
  <span>{{ message }}</span>
  
  <!-- v-html:渲染 HTML(⚠️ 有 XSS 风险) -->
  <div v-html="htmlContent"></div>
  
  <!-- v-once:只渲染一次,不更新 -->
  <div v-once>{{ staticData }}</div>
  
  <!-- v-pre:跳过编译,显示原始内容 -->
  <div v-pre>{{ 这会原样显示,不会编译 }}</div>
  
  <!-- v-cloak:未编译时隐藏,防止闪烁 -->
  <div v-cloak>{{ 编译后才显示 }}</div>
</template>

四、路由(Vue Router)

1. 什么是路由?

路由就是控制"页面上显示哪个组件"的开关

比如:你点"首页" → 显示 Home.vue;点"关于" → 显示 About.vue。这个"URL 变化 → 组件切换"的机制就叫路由。

记住:Vue 是 SPA(单页应用) ,整个应用只有一个 .html 文件,路由切换其实是在替换页面中间的组件,不会真正刷新浏览器。

2. 路由的核心配置

javascript 复制代码
// router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import Home from '../views/Home.vue'
import About from '../views/About.vue'

const routes = [
    { path: '/', component: Home },
    { path: '/about', component: About },
    { 
        path: '/user/:id',     // 动态路径参数
        component: () => import('../views/User.vue')  // 懒加载
    }
]

const router = createRouter({
    history: createWebHistory(),
    routes
})

export default router

3. 页面里怎么用路由

vue 复制代码
<template>
    <div>
        <!-- 导航链接 -->
        <router-link to="/">首页</router-link>
        <router-link to="/about">关于</router-link>
        
        <!-- 路由出口:匹配到的组件会显示在这里 -->
        <router-view />
    </div>
</template>

4. 路由传参

方式一:路径参数(URL 里带 id)

javascript 复制代码
// 配置
{ path: '/user/:id', component: User }

// 跳转
<router-link :to="'/user/' + userId">用户</router-link>

// 获取(组合式 API)
import { useRoute } from 'vue-router'
const route = useRoute()
const id = route.params.id

方式二:查询参数(?key=value)

javascript 复制代码
<router-link :to="{ path: '/user', query: { id: 1 } }">用户</router-link>

// 获取
import { useRoute } from 'vue-router'
const route = useRoute()
const id = route.query.id

方式三:编程式跳转(JS 里跳)

javascript 复制代码
import { useRouter } from 'vue-router'
const router = useRouter()

router.push('/about')
router.push({ name: 'User', params: { id: 1 } })
router.go(-1)  // 后退

5. 动态路由

场景:菜单、路由路径、权限都由后端控制,前端只负责渲染。用户登录后(或刷新页面时 token 仍有效),前端请求后端获取该用户的路由/菜单数据,动态生成路由。

后端返回的菜单数据示例:

json 复制代码
[
  { 
    path: '/dashboard', 
    name: 'Dashboard', 
    component: 'Dashboard',  // 对应前端组件名
    meta: { title: '仪表盘', icon: 'home' } 
  },
  { 
    path: '/user', 
    name: 'User', 
    component: 'Layout',
    meta: { title: '用户管理', icon: 'user' },
    children: [
      { path: 'list', name: 'UserList', component: 'UserList', meta: { title: '用户列表' } },
      { path: 'add', name: 'UserAdd', component: 'UserAdd', meta: { title: '添加用户' } }
    ]
  }
]
步骤一:定义基础路由(不需要权限的页面)
javascript 复制代码
// router/index.js
import { createRouter, createWebHistory } from 'vue-router'

// 常量路由:所有人可访问(404、登录页等)
const constantRoutes = [
  {
    path: '/login',
    name: 'Login',
    component: () => import('@/views/Login.vue')
  },
  {
    path: '/404',
    name: 'NotFound',
    component: () => import('@/views/404.vue')
  }
]

const router = createRouter({
  history: createWebHistory(),
  routes: constantRoutes
})

export default router
步骤二:维护前端组件映射表
javascript 复制代码
// router/componentMap.js
// 后端返回的是组件名字符串,前端需要根据名字找到对应的组件
export const componentMap = {
  Dashboard: () => import('@/views/Dashboard.vue'),
  UserList: () => import('@/views/user/List.vue'),
  UserAdd: () => import('@/views/user/Add.vue'),
  Layout: () => import('@/layouts/Layout.vue'),  // 布局组件
  // ... 所有需要动态加载的页面组件
}
步骤三:将后端菜单数据转为路由配置
javascript 复制代码
// router/dynamic.js
import { componentMap } from './componentMap'

/**
 * 递归生成路由配置
 */
export function generateRoutes(menuData) {
  const routes = []

  menuData.forEach(menu => {
    const route = {
      path: menu.path,
      name: menu.name,
      meta: menu.meta || {},
      // 根据后端返回的组件名,从映射表里取组件
      component: componentMap[menu.component] || null
    }

    // 如果有子菜单,递归处理
    if (menu.children && menu.children.length > 0) {
      route.children = generateRoutes(menu.children)
    }

    routes.push(route)
  })

  return routes
}

/**
 * 动态添加路由到 router
 */
export function addDynamicRoutes(router, menuData) {
  const routes = generateRoutes(menuData)
  
  routes.forEach(route => {
    router.addRoute(route)
  })

  // 最后添加 404 兜底路由(必须放在最后,否则会拦截所有未匹配路径)
  router.addRoute({
    path: '/:pathMatch(.*)*',
    redirect: '/404'
  })

  return routes
}
步骤四:在登录后或页面刷新时获取路由
javascript 复制代码
// utils/initRouter.js
import { getMenus } from '@/api/menu'
import { addDynamicRoutes } from '@/router/dynamic'

// 标记是否已添加动态路由(避免重复添加)
let isDynamicRoutesAdded = false

/**
 * 初始化动态路由
 * 在登录成功后 或 页面刷新时调用
 */
export async function initDynamicRoutes(router) {
  // 如果已经添加过,直接返回
  if (isDynamicRoutesAdded) {
    return
  }

  try {
    // 从后端获取当前用户的路由/菜单数据
    const res = await getMenus()
    const menuData = res.data

    // 动态添加路由
    addDynamicRoutes(router, menuData)
    isDynamicRoutesAdded = true

    console.log('✅ 动态路由加载完成')
  } catch (error) {
    console.error('❌ 获取动态路由失败:', error)
    // 可以跳转到 404 或登录页
  }
}
步骤五:在路由守卫中自动初始化
javascript 复制代码
// router/index.js(接步骤一,在 router 实例后面添加)
import { initDynamicRoutes } from '@/utils/initRouter'

let hasInit = false  // 防止重复初始化

router.beforeEach(async (to, from, next) => {
  const token = localStorage.getItem('token')

  // 如果有 token,说明用户已登录
  if (token) {
    // 如果是登录页,直接去首页
    if (to.path === '/login') {
      return next('/dashboard')
    }

    // 如果动态路由还没初始化,就初始化
    if (!hasInit) {
      await initDynamicRoutes(router)
      hasInit = true
      // 刷新页面后重新进入,需要重定向到目标路径让路由重新匹配
      return next({ ...to, replace: true })
    }

    // 已初始化,正常放行
    next()
  } else {
    // 没有 token,只能访问白名单页面
    if (['/login', '/404'].includes(to.path)) {
      next()
    } else {
      next('/login')
    }
  }
})
步骤六:登录成功后调用(可选)
javascript 复制代码
// 在登录接口成功后的回调中
import { initDynamicRoutes } from '@/utils/initRouter'

const handleLoginSuccess = async () => {
  // 保存 token
  localStorage.setItem('token', res.data.token)
  
  // 初始化动态路由
  await initDynamicRoutes(router)
  
  // 跳转到首页
  router.push('/dashboard')
}

总结

  • 核心逻辑就三步:请求后端菜单 → 转成路由配置 → router.addRoute() 添加
  • 登录后调用一次,刷新页面时路由守卫里再调用一次(但不会重复请求)
  • 前端只需要维护一个"组件名字符串 → 实际组件"的映射表

后端返回数据示例:

json 复制代码
[
  { path: '/dashboard', name: 'Dashboard', meta: { title: '仪表盘', icon: 'home' } },
  { path: '/user', name: 'User', meta: { title: '用户管理', icon: 'user' }, 
    children: [
      { path: 'list', name: 'UserList', meta: { title: '用户列表' } },
      { path: 'add', name: 'UserAdd', meta: { title: '添加用户' } }
    ]
  },
  { path: '/404', name: 'NotFound', meta: { title: '404' } }
]
步骤六:404 页面特殊处理
vue 复制代码
<!-- views/404.vue -->
<template>
  <div class="not-found">
    <h1>404</h1>
    <p>页面不存在</p>
    <router-link to="/">回到首页</router-link>
  </div>
</template>

⚠️ 注意:404 路由必须在动态路由的最后添加,否则会拦截所有未匹配路径。

javascript 复制代码
router.addRoute(route)  // 添加动态路由
router.addRoute({       // 最后添加 404 兜底
  path: '/:pathMatch(.*)*',
  redirect: '/404'
})
完整流程图
复制代码
用户登录
    ↓
请求后端获取菜单数据
    ↓
前端通过 router.addRoute() 动态添加路由
    ↓
用户访问页面 → 路由守卫拦截 → 校验权限
    ↓
有权限 → 正常显示;无权限 → 跳转 404

总结 :动态路由的核心思想是 "前端只定义基础路由,业务路由由后端返回,前端动态添加"。这样权限控制完全在后端,前端只需维护一个"组件名 ↔ 组件"的映射表即可。


五、Vuex(状态管理)

1. 什么是 Vuex?

Vuex 是 Vue 的全局数据仓库 ,专门用来管理多个组件共享的数据

什么时候用?当你的数据被两个以上不相干的组件需要的时候(比如用户登录信息、购物车数据、主题颜色等)。

2. 为什么要用 Vuex?

问题场景 没有 Vuex 有 Vuex
A 组件改了数据,B 组件要知道 需要一层层传,复杂 直接修改仓库,所有用到的组件自动更新
多个页面共享登录状态 每次刷新都要重新请求 存到 Vuex 里,全局可用
调试困难 不知道数据是谁改的 Vuex 提供时间旅行调试,每一步都能追踪

3. Vuex 的核心概念(五行口诀)

javascript 复制代码
// store/index.js
import { createStore } from 'vuex'

const store = createStore({
    // 1. state:存数据
    state() {
        return {
            count: 0,
            user: null
        }
    },
    // 2. mutations:改数据(必须同步)
    mutations: {
        increment(state) {
            state.count++
        },
        setUser(state, user) {
            state.user = user
        }
    },
    // 3. actions:处理异步 + 提交 mutations
    actions: {
        async login({ commit }, userInfo) {
            const res = await api.login(userInfo)
            commit('setUser', res.data)
        }
    },
    // 4. getters:计算派生数据(类似计算属性)
    getters: {
        doubleCount(state) {
            return state.count * 2
        }
    }
})

4. 在组件里怎么用(组合式 API)

vue 复制代码
<template>
    <div>
        <!-- 直接显示 state -->
        <p>{{ store.state.count }}</p>
        
        <!-- 用 getters -->
        <p>{{ store.getters.doubleCount }}</p>
        
        <!-- 提交 mutation(同步) -->
        <button @click="store.commit('increment')">+1</button>
        
        <!-- 派发 action(异步) -->
        <button @click="store.dispatch('login', { name: '张三' })">登录</button>
    </div>
</template>

<script setup>
import { useStore } from 'vuex'

const store = useStore()
</script>

5. 辅助函数(在组合式 API 中结合 computed 使用)

vue 复制代码
<script setup>
import { computed } from 'vue'
import { useStore } from 'vuex'
import { mapState, mapGetters, mapMutations, mapActions } from 'vuex'

const store = useStore()

// 把 state 映射成计算属性
const count = computed(() => store.state.count)
const user = computed(() => store.state.user)

// 把 getters 映射成计算属性
const doubleCount = computed(() => store.getters.doubleCount)

// 把 mutations 映射成方法(直接解构)
const { increment } = mapMutations(['increment'])
// 使用时:increment()

// 把 actions 映射成方法
const { login } = mapActions(['login'])
// 使用时:login({ name: '张三' })
</script>

6. 模块化(项目大了拆分)

javascript 复制代码
const userModule = {
    namespaced: true,
    state: { name: '张三' },
    mutations: { setName(state, name) { state.name = name } }
}

const store = createStore({
    modules: {
        user: userModule
    }
})

// 使用时带上模块名
// 在组件中:
store.state.user.name
store.commit('user/setName', '李四')

总结:Vuex 就是 Vue 全家桶里的"全局数据中心",适合管理多组件共享的数据,解决"数据传着传着就乱了"的问题。

六、Fetch API

Fetch API 是浏览器提供的原生 JavaScript 接口 ,用于发送 HTTP 请求(获取资源、提交数据等)。它是 Ajax 的现代替代方案 ,比传统的 XMLHttpRequest(XHR)更强大、更简洁。

对比项 XMLHttpRequest (XHR) Fetch API
年代 1999 年推出(老古董) 2015 年推出(现代标准)
语法 回调地狱,代码冗长 Promise 风格,简洁优雅
流式处理 不支持 支持(response.body 可流式读取)
默认行为 默认携带 Cookie 默认携带 Cookie(需手动配置)
错误处理 网络错误不抛异常 网络错误会 reject,但 HTTP 状态码(404/500)不抛异常

Fetch 的优缺点

✅ 优点

优点 说明
语法简洁 Promise + async/await,告别回调地狱
原生支持 浏览器内置,无需安装第三方库
流式处理 支持流(Stream),可边下载边处理大文件
更灵活 可配置 cache、credentials、redirect 等
Service Worker 支持 是 PWA 离线缓存的基石

❌ 缺点

缺点 说明
不自动携带 Cookie 需要手动配置 credentials: 'include'
不自动抛异常 404/500 不会进入 catch,需手动检查 response.ok
不支持超时控制 需借助 AbortController 实现
不支持上传进度 无法像 XHR 那样监听 onprogress
无请求取消(旧浏览器) 新浏览器支持 AbortController

Fetch Demo

1. 注册拦截器(main.js 或单独文件)

javascript 复制代码
// utils/interceptors.js
import { addRequestInterceptor, addResponseInterceptor } from './request'

let routerInstance = null

// 设置路由实例(在 main.js 中调用)
export const setRouter = (router) => {
  routerInstance = router
}

// ---------- 请求拦截器 ----------
// 自动添加 Token
addRequestInterceptor(
  (config) => {
    const token = localStorage.getItem('token')
    if (token) {
      config.headers = {
        ...config.headers,
        'Authorization': `Bearer ${token}`
      }
    }
    return config
  },
  (error) => {
    console.error('请求拦截器错误:', error)
    return Promise.reject(error)
  }
)

// ---------- 响应拦截器 ----------
// 统一处理响应和错误
addResponseInterceptor(
  (response) => {
    // 如果后端返回了新的 token,自动更新
    if (response.headers && response.headers.get('x-new-token')) {
      const newToken = response.headers.get('x-new-token')
      localStorage.setItem('token', newToken)
    }

    // 业务状态码处理
    if (response.data && response.data.code !== undefined && response.data.code !== 0) {
      // 如果是登录过期(假设 code 为 401 或 403 表示未登录)
      if (response.data.code === 401 || response.data.code === 403) {
        localStorage.removeItem('token')
        localStorage.removeItem('userInfo')
        if (routerInstance) {
          routerInstance.push('/login')
        } else {
          window.location.href = '/login'
        }
      }
      const error = new Error(response.data.message || '业务处理失败')
      error.code = response.data.code
      error.response = response
      return Promise.reject(error)
    }

    return response
  },
  (error) => {
    // HTTP 401 未授权处理
    if (error.response && error.response.status === 401) {
      localStorage.removeItem('token')
      localStorage.removeItem('userInfo')
      if (routerInstance) {
        routerInstance.push('/login')
      } else {
        window.location.href = '/login'
      }
    }

    // 网络错误统一提示
    if (error.code === 'ECONNABORTED' || error.message.includes('timeout')) {
      console.error('请求超时,请稍后重试')
    } else if (error.message.includes('NetworkError') || !error.response) {
      console.error('网络异常,请检查网络连接')
    } else {
      console.error(error.message || '请求失败')
    }

    return Promise.reject(error)
  }
)

2. 在 main.js 中挂载(便于拦截器使用)

javascript 复制代码
// main.js
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'

// 导入拦截器(注册)
import './utils/interceptors'
import { setRouter } from './utils/interceptors'

const app = createApp(App)

// 将 router 注入到拦截器中
setRouter(router)

app.use(router)
app.use(store)
app.mount('#app')

3. 请求封装(utils/request.js)

javascript 复制代码
// utils/request.js
import axios from 'axios'

// 创建 axios 实例
const service = axios.create({
  baseURL: import.meta.env.VITE_API_BASE_URL || '/api',
  timeout: 10000,
  headers: {
    'Content-Type': 'application/json'
  }
})

// 存储拦截器
const requestInterceptors = []
const responseInterceptors = []

// 添加请求拦截器
export const addRequestInterceptor = (success, error) => {
  const interceptor = service.interceptors.request.use(success, error)
  requestInterceptors.push(interceptor)
  return interceptor
}

// 添加响应拦截器
export const addResponseInterceptor = (success, error) => {
  const interceptor = service.interceptors.response.use(success, error)
  responseInterceptors.push(interceptor)
  return interceptor
}

// 基础请求方法
export const request = (config) => {
  return service(config)
}

// GET 请求
export const get = (url, params = {}, config = {}) => {
  return service.get(url, { params, ...config })
}

// POST 请求
export const post = (url, data = {}, config = {}) => {
  return service.post(url, data, config)
}

// PUT 请求
export const put = (url, data = {}, config = {}) => {
  return service.put(url, data, config)
}

// DELETE 请求
export const del = (url, params = {}, config = {}) => {
  return service.delete(url, { params, ...config })
}

// 文件上传
export const upload = (url, formData, config = {}) => {
  return service.post(url, formData, {
    headers: {
      'Content-Type': 'multipart/form-data'
    },
    ...config
  })
}

// 文件下载(获取 blob)
export const download = (url, params = {}, config = {}) => {
  return service.get(url, {
    params,
    responseType: 'blob',
    ...config
  })
}

export default service

4. API 接口封装(api/user.js)

javascript 复制代码
// api/user.js
import { get, post, put, del, upload } from '@/utils/request'

// 用户登录
export const login = (username, password) => {
  return post('/user/login', { username, password })
}

// 获取用户信息
export const getUserInfo = () => {
  return get('/user/info')
}

// 获取菜单路由
export const getMenus = () => {
  return get('/user/menus')
}

// 更新用户信息
export const updateUser = (data) => {
  return put('/user/update', data)
}

// 修改密码
export const changePassword = (oldPassword, newPassword) => {
  return post('/user/change-password', { oldPassword, newPassword })
}

// 上传头像
export const uploadAvatar = (file) => {
  const formData = new FormData()
  formData.append('avatar', file)
  return upload('/user/avatar', formData)
}

5. 在组件中使用

vue 复制代码
<template>
  <div>
    <button @click="fetchUserInfo">获取用户信息</button>
    <button @click="updateUser">更新用户</button>
    <button @click="uploadAvatar">上传头像</button>
  </div>
</template>

<script setup>
import { ref } from 'vue'
import { getUserInfo, updateUser, uploadAvatar } from '@/api/user'

const userInfo = ref(null)

// 获取用户信息
const fetchUserInfo = async () => {
  try {
    const res = await getUserInfo()
    userInfo.value = res.data
    console.log('用户信息:', res)
  } catch (error) {
    console.error('获取用户信息失败:', error)
  }
}

// 更新用户
const updateUser = async () => {
  try {
    const res = await updateUser({
      name: '张三',
      age: 25
    })
    console.log('更新成功:', res)
  } catch (error) {
    console.error('更新失败:', error)
  }
}

// 上传头像
const uploadAvatar = async () => {
  const fileInput = document.createElement('input')
  fileInput.type = 'file'
  fileInput.accept = 'image/*'
  fileInput.onchange = async (e) => {
    const file = e.target.files[0]
    if (file) {
      try {
        const res = await uploadAvatar(file)
        console.log('上传成功:', res)
      } catch (error) {
        console.error('上传失败:', error)
      }
    }
  }
  fileInput.click()
}
</script>

6. 环境变量配置(.env)

bash 复制代码
# .env.development
VITE_API_BASE_URL=/api

# .env.production
VITE_API_BASE_URL=https://api.example.com
相关推荐
Csvn14 分钟前
Proxy / Reflect 与响应式原理:完整手写一个 mini Vue3 响应式系统
前端
Easy_API16 分钟前
OpenAI三周内第二次降价,GPT-5.6 Sol砍了20%到33
大数据·前端·人工智能·gpt·深度学习
深念Y17 分钟前
07-SSR水合问题实战排查与修复记录
前端·vue·vite·nuxt·ssr·csr·水合
前端炒粉19 分钟前
容器预热预跳转方案
前端·vue.js·性能优化
摇滚侠22 分钟前
《SpringBoot 3:入门与应用实战》第 9 章 使用 WebMvc 开发应用 阅读笔记 20
spring boot·笔记·后端
小二李22 分钟前
第12章 nestjs服务端开发:RBAC权限系统设计
java·linux·前端
蒸蒸yyyyzwd23 分钟前
cpp web server 面试可能问题总结
c++·笔记·八股
tt一点通29 分钟前
前端常用设计模式大全
前端·vue.js
阳光宅男@李光熠31 分钟前
【电子通识】排阻和普通电阻有什么区别?
笔记·学习