vue3-2

# 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
  • 支持类型校验、默认值、必填校验等
  • 父组件通过「属性」把数据传给子组件(单向数据流:父 → 子)

总结

  1. defineProps:父 → 子,接收父组件传入属性;props只读不可改。
  2. defineEmits:子 → 父,子组件触发自定义事件,通过emit()传递参数。
  3. 两者都是 <script setup> 的编译宏,不需要导入,只能在setup中使用。
  4. defineProps支持:type类型、required是否必填、default默认值。
  5. 子组件不能直接修改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>

image-20260813201318289

插槽 — 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].js chunk)

路由懒加载 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-active class(可用于高亮当前导航)

⚠️ 易错点: 项目里第三个链接文本写的是 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的协议、域名、端口三者任意一项和当前页面不同,即为跨域。

image-20260813200519207

当前页面 请求地址 是否跨域 原因
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响应头,放行跨域请求。

image-20260813200542342

3、解决方案二:代理转发

代理原理

在前端服务和后端接口之间架设中间代理服务:

  1. 代理服务与前端地址保持同源,浏览器访问代理无跨域问题;
  2. 代理服务器向后端真实接口转发请求;服务端之间通信不受浏览器同源策略约束。

代理的两种实现方式

  1. Nginx反向代理

反向代理架设在客户端与后端服务中间;代理代表客户端发请求,反向代理代表服务端接收客户端请求,做转发。

  1. 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

image-20260813200620894

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。


补充面试记忆点

  1. 同源策略是浏览器的安全机制,不是HTTP协议限制;
  2. CORS:改后端响应头,生产环境常用;
  3. Vite代理/Nginx代理:中间层转发请求,规避浏览器跨域限制;
  4. Vite的proxy只在npm run dev开发模式生效,build打包之后失效。

axios的回调函数会套一层对象,ref的响应函数也会套一层对象,data在result中也一层对象

上一篇
下一篇