# Vue 3 + Vite 学习笔记
常用 npm 命令
npm init vue@latest # 使用 create-vue 脚手架交互式创建项目
npm install # 根据 package.json 安装所有依赖
npm run dev # 启动 Vite 开发服务器(默认 http://localhost:5173)
npm run build # 生产打包,产物输出到 dist/
npm run preview # 本地预览 dist/ 打包结果
npm i vue-router #下载路由
npm install pinia #下载pinia 共享数据管理
npm i mitt #邮局任意组件间通信
npm install axios #安装axios
npm create vue@latest #使用vite启动vue
Vue 3 应用启动流程
main.js — 应用入口
// 04—vue/src/main.js
import './assets/main.css' // 导入全局样式
import { createApp } from 'vue' // Vue 3 的应用创建函数
import { createPinia } from 'pinia' // 状态管理
import App from './App.vue' // 根组件
import router from './router' // 路由配置
const app = createApp(App) // 创建 Vue 应用实例
app.use(createPinia()) // 安装 Pinia 插件
app.use(router) // 安装路由插件
app.mount('#app') // 挂载到 DOM 的 #app 元素
启动流程
index.html 加载
└→ main.js 执行
├→ createApp(App) — 创建应用,绑定根组件
├→ app.use(...) — 注册插件(Pinia、Router)
└→ app.mount('#app') — 挂载到页面
关键认知:Vue 3 不再像 Vue 2 那样
new Vue(),而是createApp()创建实例 →use()装插件 →mount()挂载。use()必须在mount()之前调用。
基本结构
Vue 单文件组件以 .vue 为后缀,由三部分组成(对应 04—vue/src/components/HelloVue3.vue):
<template>
<!-- html 的代码 -->
<div class="c">hello Vue</div>
</template>
<script setup>
// vue 的 js 代码(组合式 API)
</script>
<style scoped>
/* css 的代码 */
.c {
color: blue;
border: 1px solid black;
text-align: center;
}
</style>
| 部分 | 作用 | 说明 |
|---|---|---|
<template> |
HTML 模板 | Vue 3 支持多根节点(不再强制单个根) |
<script setup> |
JS 逻辑 | setup 语法糖,自动暴露顶层变量给模板 |
<style scoped> |
CSS 样式 | scoped 表示样式仅作用于当前组件 |
<script setup> 语法糖
<script setup> 是 Vue 3.2+ 引入的组合式 API 语法糖,相比传统写法更简洁:
- 顶层声明的变量、函数自动暴露给模板,无需
return import进来的组件自动注册,无需在components中声明- 每次组件实例创建时执行
对比传统写法(仅作了解,本项目统一用 setup 语法糖):
<!-- 传统 Options API / 显式 setup() 写法(本项目未使用) -->
<script>
export default {
data() { return { count: 0 } },
methods: { incr() { this.count++ } }
}
</script>
##
Props — 父传子通信
defineProps 父向子传参(子组件接收父组件的数据)父传子
儿子
<template>
<div class="child">
<h3>子组件</h3>
<h4>玩具:{{ toy }}</h4>
<h4>父给的车:{{ car }}</h4>
<button @click="sendToy(toy)">把玩具给父亲</button>
</div>
</template>
<script setup name="Child">
import {ref} from 'vue'
// 数据
let toy = ref('奥特曼')
// 声明接收props
defineProps(['car', 'sendToy'])
</script>
父亲
<template>
<div class="father">
<h3>父组件</h3>
<h4>汽车:{{ car }}</h4>
<h4 v-show="toy">子给的玩具:{{ toy }}</h4>
<Child :car="car" :sendToy="getToy"/>
</div>
</template>
<script setup name="Father">
import Child from './Child.vue'
import {ref} from 'vue'
// 数据
let car = ref('奔驰')
let toy = ref('')
// 方法
function getToy(value) {
toy.value = value
}
</script>
defineEmits 子向父通信(子触发父组件函数)子传父
<template>
<div class="father">
<h3>父组件</h3>
<h4 v-show="toy">子给的玩具:{{ toy }}</h4>
<!-- 给子组件Child绑定事件 -->
<Child @send-toy="saveToy"/>
</div>
</template>
<script setup name="Father">
import Child from './Child.vue'
import {ref} from "vue";
// 数据
let toy = ref('')
// 用于保存传递过来的玩具
function saveToy(value) {
console.log('saveToy', value)
toy.value = value
}
</script>
<template>
<div class="child">
<h3>子组件</h3>
<h4>玩具:{{ toy }}</h4>
<button @click = toy></button>
<button @click="emit('send-toy',toy)">测试</button>
</div>
</template>
<script setup name="Child">
import {ref} from "vue";
// 数据
let toy = ref('奥特曼')
// 声明事件
const emit = defineEmits(['send-toy'])
</script>
| 宏函数 | 作用 | 核心要点 & 面试考点 | 简单示例 |
|---|---|---|---|
defineProps |
父 → 子,子组件接收父组件传递的属性 | 1. props 只读,子组件不能直接修改2. 支持类型、required、default 校验 | const props = defineProps({name:String}) |
defineEmits |
子 → 父,子组件触发自定义事件向父传参 | 调用emit(事件名,参数),父通过@xxx监听事件 |
const emit = defineEmits(['change'])emit('change', 10) |
defineExpose |
把组件内部变量 / 方法暴露出去,供父组件ref调用 |
<script setup>内部默认全部私有,父 ref 默认拿不到,必须显式暴露 |
defineExpose({ count, fn }) |
defineModel |
Vue3.4+,简化组件v‑model双向绑定 |
替代老式modelValue + update:modelValue,支持校验、默认值 |
const msg = defineModel({default:"hi"}) |
defineOptions |
设置组件配置项 | 可设置组件name、inheritAttrs,不用写双 script 标签 |
defineOptions({name:'MyComp',inheritAttrs:false}) |
defineSlots |
对插槽做 TS 类型约束 | 普通业务几乎不用;仅 TypeScript 项目限定插槽入参类型 | defineSlots<{default:(p:{})=>any}>() |
要点:
defineProps()是<script setup>中专用的编译器宏,无需 import- 支持类型校验、默认值、必填校验等
- 父组件通过「属性」把数据传给子组件(单向数据流:父 → 子)
总结
defineProps:父 → 子,接收父组件传入属性;props只读不可改。defineEmits:子 → 父,子组件触发自定义事件,通过emit()传递参数。- 两者都是
<script setup>的编译宏,不需要导入,只能在setup中使用。 - defineProps支持:
type类型、required是否必填、default默认值。 - 子组件不能直接修改props;如果要修改,复制到本地变量,或者用
v-model。
4. 完整最小示例
子组件 Child.vue
<template>
<div>{{ props.name }} -- {{ props.age }}</div>
<button @click="add">子组件按钮</button>
</template>
<script setup>
const props = defineProps({
name: { type: String, required: true },
age: { type: Number, default: 0 }
})
const emit = defineEmits(['change'])
const add = () => {
emit('change', props.age + 1)
}
</script>
父组件 Parent.vue
<template>
<Child :name="'李四'" :age="count" @change="onChange"/>
</template>
<script setup>
import Child from './Child.vue'
import { ref } from 'vue'
const count = ref(10)
const onChange = (newVal) => {
count.value = newVal
}
</script>
Emits通信
// 引入mitt
import mitt from 'mitt'
// 调用mitt得到emitter,emitter能:绑定事件、触发事件
const emitter = mitt()
// 绑定事件 emitter.on
emitter.on('test1', () => {
console.log('test1被调用了')
})
emitter.on('test2', () => {
console.log('test2被调用了')
})
// 触发事件 emitter.emit
setInterval(() => {
emitter.emit('test1')
emitter.emit('test2')
}, 1000);
// 解绑 emitter.off
setTimeout(() => {
// emitter.off('test1')
// emitter.off('test2')
emitter.all.clear()
}, 3000);
// 暴露emitter
export default emitter
child1
<template>
<div class="child1">
<h3>子组件1</h3>
<h4>玩具:{{ toy }}</h4>
<button @click="emitter.emit('send-toy',toy)">玩具给弟弟</button>
</div>
</template>
<script setup name="Child1">
import {ref} from 'vue'
import emitter from '@/utils/emitter';
// 数据
let toy = ref('奥特曼')
</script>
child2
<template>
<div class="child2">
<h3>子组件2</h3>
<h4>电脑:{{ computer }}</h4>
<h4>哥哥给的玩具:{{ toy }}</h4>
</div>
</template>
<script setup name="Child2">
import {ref, onUnmounted} from 'vue'
import emitter from '@/utils/emitter';
// 数据
let computer = ref('联想')
let toy = ref('')
// 给emitter绑定send-toy事件
emitter.on('send-toy', (value) => {
toy.value = value
})
// 在组件卸载时解绑send-toy事件
onUnmounted(() => {
emitter.off('send-toy')
})
</script>
父页面 显示两个孩子
<template>
<div class="father">
<h3>父组件</h3>
<Child1/>
<Child2/>
</div>
</template>
<script setup name="Father">
import Child1 from './Child1.vue'
import Child2 from './Child2.vue'
</script>

插槽 — Slots
<!-- WelcomeItem.vue — 定义插槽 -->
<template>
<div class="item">
<i>
<slot name="icon"></slot> <!-- 具名插槽:icon -->
</i>
<div class="details">
<h3>
<slot name="heading"></slot> <!-- 具名插槽:heading -->
</h3>
<slot></slot> <!-- 默认插槽 -->
</div>
</div>
</template>
<!-- TheWelcome.vue — 使用插槽 -->
<WelcomeItem>
<template #icon> <!-- 插入到 icon 插槽 -->
<DocumentationIcon />
</template>
<template #heading>Documentation</template>
<p>浏览官方文档...</p> <!-- 插入到默认插槽 -->
</WelcomeItem>
| 插槽类型 | 定义语法 | 使用语法 |
|---|---|---|
| 默认插槽 | <slot></slot> |
直接写内容 |
| 具名插槽 | <slot name="xxx"> |
<template #xxx> 或 <template v-slot:xxx> |
提示:
#是v-slot:的简写。
路由 — vue-router
路由配置
// 04—vue/src/router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'
import HelloView from '@/views/HelloView.vue'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'home',
component: HomeView, // 立即加载(打包进主包)
},
{
path: '/about',
name: 'about',
// route level code-splitting:访问时才加载(懒加载)
component: () => import('../views/AboutView.vue'),
},
{
path: '/hello',
name: 'hello',
component: HelloView, // 立即加载
},
],
})
export default router
要点:
createWebHistory:使用 HTML5 History 模式(URL 无#,需要服务器配合重定向)- 直接
import:组件打包进主 bundle,首屏加载 () => import(...):懒加载,访问该路由时才加载,实现代码分割(生成独立的About.[hash].jschunk)
路由懒加载 vs 立即加载:
首屏加载
├── HomeView (立即加载,打包进主包)
├── HelloView (立即加载,打包进主包)
└── AboutView (懒加载,访问 /about 时按需加载)
路由使用
<!-- App.vue -->
<script setup>
import { RouterLink, RouterView } from 'vue-router'
</script>
<template>
<nav>
<RouterLink to="/">Home</RouterLink>
<RouterLink to="/about">About</RouterLink>
<RouterLink to="/hello">About</RouterLink> <!-- ⚠️ 见下方说明 -->
</nav>
<!-- RouterView:路由出口,匹配到的组件渲染在这里 -->
<RouterView />
</template>
要点:
RouterLink:声明式导航(类似<a>,但不刷新页面),默认渲染为<a>标签RouterView:路由匹配组件的渲染位置- 激活的链接会自动添加
router-link-exact-active/router-link-activeclass(可用于高亮当前导航)
⚠️ 易错点: 项目里第三个链接文本写的是
About,但它实际指向/hello,属于复制粘贴遗留的小笔误,正确文本应为Hello。
状态管理 — Pinia
Store 定义(Setup Store 语法)
// 04—vue/src/stores/counter.js
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', () => {
const count = ref(0) // State:响应式状态
const doubleCount = computed(() => count.value * 2) // Getters:计算属性
function increment() { // Actions:方法
count.value++
}
return { count, doubleCount, increment } // 返回的就是 store 的公共 API
})
Store 使用
<script setup>
import { useCounterStore } from '@/stores/counter'
const store = useCounterStore()
</script>
<template>
<div>{{ store.count }}</div>
<div>{{ store.doubleCount }}</div>
<button @click="store.increment()">+1</button>
</template>
Pinia 两种写法对比:
| 概念 | Setup Store(本项目使用) | Options Store |
|---|---|---|
| State | ref() |
state: () => ({...}) |
| Getters | computed() |
getters: {...} |
| Actions | 普通函数 | actions: {...} |
Setup Store 更贴近组合式 API,与
<script setup>风格统一,是官方推荐写法。
结构总结
| 文件夹 | 作用 |
|---|---|
| api | 接口请求层。存放 axios 请求封装,所有后端接口函数,把接口统一抽离,页面只调用函数,不写axios。 |
| assets | 静态资源。放图片、字体、全局 css、图标。⚠️这里资源会被 vite 打包处理;大文件放public文件夹。 |
| components | 公共组件。可复用小组件,比如按钮、弹窗、卡片,不会直接作为路由页面。 |
| router | 路由配置。index.js写路由表,配置路径对应哪个页面、路由守卫、权限拦截。 |
| stores | Pinia 状态管理(替代 Vuex)。全局共享状态,用户登录信息、全局配置、跨组件共享数据放这里。 |
| views | 页面组件。对应路由的页面,一个路由对应 views 下一个 vue 文件。和 components 区别:views 是完整页面,components 是碎片小组件。 |
App.vue |
根组件,所有页面挂载在这里。一般写全局布局。 |
main.js |
项目入口文件。创建 vue 实例、注册插件(router/pinia)、挂载到 DOM 节点。 |
跨域问题
什么是跨域
跨域就是违背了浏览器同源策略。
同源:两个页面具有相同的协议(protocol)、主机(host)、端口号(port)。
当请求url的协议、域名、端口三者任意一项和当前页面不同,即为跨域。

| 当前页面 | 请求地址 | 是否跨域 | 原因 |
|---|---|---|---|
| http://www.test.com/ | http://www.test.com/index.html | 否 | 同源(协议、域名、端口号全部相同) |
| http://www.test.com/ | https://www.test.com/index.html | 跨域 | 协议不同(http / https) |
| http://www.test.com/ | http://www.baidu.com/ | 跨域 | 主域名不同(test / baidu) |
| http://www.test.com/ | http://blog.test.com/ | 跨域 | 子域名不同(www / blog) |
| http://www.test.com:8080/ | http://www.test.com:7001/ | 跨域 | 端口号不同(8080 / 7001) |
💡关键结论:跨域只发生在浏览器端。服务端之间发起HTTP请求,不受同源策略限制,不存在跨域问题。
2、解决方案一:CORS 跨域资源共享
CORS(Cross‑Origin Resource Sharing,跨域资源共享)
原理:后端服务器设置响应头实现跨域,前端客户端不需要做额外处理。 浏览器识别后端返回的CORS响应头,放行跨域请求。

3、解决方案二:代理转发
代理原理
在前端服务和后端接口之间架设中间代理服务:
- 代理服务与前端地址保持同源,浏览器访问代理无跨域问题;
- 代理服务器向后端真实接口转发请求;服务端之间通信不受浏览器同源策略约束。
代理的两种实现方式
- Nginx反向代理
反向代理架设在客户端与后端服务中间;代理代表客户端发请求,反向代理代表服务端接收客户端请求,做转发。
- Vite + Vue 配置本地代理(开发环境)
❌错误写法:直接写完整后端地址,触发浏览器跨域
const baseURL = 'http://localhost:8080'
✅正确写法:使用
/api路径前缀,交给vite代理拦截
utils/request.js axios封装
import axios from 'axios'
// 使用/api前缀,不写完整域名
const baseURL = 'api'
const service = axios.create({
baseURL
})
export default service

vite.config.js 代理配置
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { fileURLToPath, URL } from 'node:url'
export default defineConfig({
plugins: [
vue(),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
},
server: {
proxy: {
'/api': { // 匹配请求路径以/api开头 作为标识防止router将其识别为前端路由
target: 'http://localhost:8080', // 后端真实接口地址
changeOrigin: true, // 修改请求源,开启跨域模拟
rewrite: (path) => path.replace(/^\/api/, '') // 重写路径,去掉路径中的/api前缀
}
}
}
})
⚠️注意:Vite代理仅作用于本地开发环境,打包上线之后不生效,上线环境需要Nginx反向代理或者后端CORS。
补充面试记忆点
- 同源策略是浏览器的安全机制,不是HTTP协议限制;
- CORS:改后端响应头,生产环境常用;
- Vite代理/Nginx代理:中间层转发请求,规避浏览器跨域限制;
- Vite的proxy只在
npm run dev开发模式生效,build打包之后失效。
axios的回调函数会套一层对象,ref的响应函数也会套一层对象,data在result中也一层对象