Vue 全家桶核心知识点

11 阅读32分钟

Vue2、Vue3、Vite、Vue Router、Vuex、Pinia 前端面试题大全(含详细解析)

一、Vue 2 与 Vue 3 核心概念

1. Vue2 和 Vue3 响应式原理的区别?分别简述 Object.definePropertyProxy

解析:

  • Vue2 响应式原理:使用 Object.definePropertydata 中的属性进行递归劫持,为每个属性添加 getter 和 setter,实现依赖收集和派发更新。但存在以下缺陷:
    • 无法检测对象属性的添加和删除(需要使用 Vue.set / Vue.delete)。
    • 无法直接监听数组索引变化和数组长度的修改(Vue2 重写了数组的 7 个变异方法来实现响应式)。
  • Vue3 响应式原理:使用 ES6 的 Proxy 代理整个对象,通过 reactiveref 创建响应式数据。Proxy 可以拦截对象任意属性的读写、删除、枚举等操作,因此能够动态检测新增/删除属性,并原生支持数组索引和 length 修改。
    • Proxy 的性能优于 Object.defineProperty(后者需要递归遍历所有属性,而 Proxy 只在 get 时惰性收集依赖)。
    • Vue3 中还引入了 Reflect 来保证 this 指向正确。

示例:

// Vue2 模拟
function defineReactive(obj, key) {
  let val = obj[key];
  Object.defineProperty(obj, key, {
    get() { /* 依赖收集 */ return val; },
    set(newVal) { /* 派发更新 */ val = newVal; }
  });
}

// Vue3 模拟
const proxy = new Proxy(obj, {
  get(target, key, receiver) {
    track(target, key); // 依赖收集
    return Reflect.get(target, key, receiver);
  },
  set(target, key, value, receiver) {
    const result = Reflect.set(target, key, value, receiver);
    trigger(target, key); // 派发更新
    return result;
  }
});

2. Vue3 组合式 API(Composition API)和 Vue2 选项式 API(Options API)的区别?何时使用组合式 API?

解析:

  • 选项式 API:通过 datamethodscomputedwatch 等选项组织代码,逻辑分散在不同选项中。对于复杂组件,同一功能的代码可能被拆分到多个选项中,导致代码难以阅读和维护。
  • 组合式 API:允许开发者根据逻辑功能组织代码,将相关代码集中在一起(例如使用 setup 函数,配合 refreactivecomputedwatch 等)。提高了代码的可复用性(通过自定义 Hook)和 TypeScript 支持。
  • 何时使用组合式 API
    • 组件逻辑复杂,需要更好的组织代码(如大型项目)。
    • 需要在多个组件间复用逻辑(替代 mixins,避免命名冲突和数据来源不明确)。
    • 项目使用 TypeScript,组合式 API 类型推导更友好。
    • 简单组件依然可以使用选项式 API,两者可以混用(但不推荐在同一个组件中混用)。

示例:

// 选项式 API
export default {
  data() { return { count: 0 } },
  methods: { increment() { this.count++ } },
  computed: { double() { return this.count * 2 } }
}

// 组合式 API
import { ref, computed } from 'vue'
export default {
  setup() {
    const count = ref(0)
    const increment = () => count.value++
    const double = computed(() => count.value * 2)
    return { count, increment, double }
  }
}

3. Vue3 中的 refreactive 有什么区别?如何使用?

解析:

  • ref
    • 用于定义基本类型(如 String、Number、Boolean)的响应式数据,也可以定义对象(内部会通过 reactive 转换)。
    • 返回一个带有 .value 属性的对象,在模板中自动解包(不需要 .value),但在 JavaScript 中必须通过 .value 访问。
    • 常用于定义单个响应式值。
  • reactive
    • 用于定义对象(或数组)的响应式数据,基于 Proxy 实现。
    • 直接访问属性,无需 .value
    • 如果直接解构 reactive 对象,会失去响应性(需要使用 toRefstoRef 进行转换)。

选择建议

  • 简单场景使用 ref 更灵活。
  • 当需要定义一组相关的数据时,使用 reactive 可以将它们组织成一个对象,代码更简洁。

示例:

import { ref, reactive, toRefs } from 'vue'

// ref
const count = ref(0)
count.value++ // 修改
// 模板中 <div>{{ count }}</div>

// reactive
const state = reactive({ count: 0, name: 'vue' })
state.count++

// 解构后保持响应式
const { count, name } = toRefs(state)
// 现在 count 和 name 都是 ref 对象

4. Vue3 中的 Teleport 组件的作用和使用场景?

解析:

  • 作用<Teleport> 可以将组件的模板内容渲染到 DOM 树中的任意指定位置,而不是局限在当前组件的 DOM 层级中。它接收一个 to 属性(CSS 选择器或 DOM 元素)指定目标容器。
  • 使用场景
    • 全局模态框、通知提示,需要脱离当前组件层级(避免被父组件的 overflow: hidden 或 z-index 影响)。
    • 将某些内容渲染到 body 或其他特定元素下。
    • <Suspense> 配合实现更灵活的加载状态。

示例:

<template>
  <button @click="openModal">打开模态框</button>
  <Teleport to="body">
    <div v-if="modalVisible" class="modal">我是全局模态框</div>
  </Teleport>
</template>

5. Vue3 中的 Fragment 是什么?有什么好处?

解析:

  • Fragment 是 Vue3 中新增的特性,允许组件模板拥有多个根节点(即不再强制要求模板只有一个根元素)。Vue3 内部会将多个根节点自动包装为一个 Fragment 片段。
  • 好处
    • 减少不必要的包装 DOM 元素(例如多余的 <div>),使 HTML 结构更简洁。
    • 改善 CSS 布局(如 Flex 或 Grid 中,多余的父元素可能破坏样式)。
    • 提升渲染性能(减少嵌套层级)。

示例:

<!-- Vue3 允许多根节点 -->
<template>
  <header>...</header>
  <main>...</main>
  <footer>...</footer>
</template>

6. Vue3 中 setup 函数的执行时机?为什么在 setup 中不能使用 this

解析:

  • 执行时机setup 是 Composition API 的入口函数,它在组件实例创建之前(beforeCreate 钩子之前)执行。此时组件实例尚未完全初始化,因此无法访问 datacomputedmethods 等选项式 API 中定义的属性。
  • 不能使用 this 的原因
    • setup 在组件实例创建前调用,this 指向 undefined(或全局对象,但严格模式下是 undefined)。
    • 设计上,setup 中的 this 不应该指向组件实例,以避免与选项式 API 的混淆。Vue3 推荐通过参数(propscontext)来访问组件相关属性和方法。
  • setup 接收两个参数
    • props:响应式的 props 对象。
    • context:包含 attrsslotsemit 等非响应式属性的对象。

示例:

export default {
  props: { msg: String },
  setup(props, context) {
    console.log(props.msg) // 访问 props
    context.emit('update') // 触发事件
    // 不能使用 this
  }
}

7. Vue2 中的 $set$delete 是做什么的?Vue3 中还需要它们吗?

解析:

  • Vue2 中的 $set$delete
    • Vue.setvm.$set:用于向响应式对象添加新属性,并确保新属性也是响应式的,同时触发视图更新。因为 Vue2 无法检测属性添加。
    • Vue.deletevm.$delete:用于删除对象的属性,并触发视图更新。
  • Vue3 中
    • 由于 Proxy 可以拦截属性的添加和删除,因此直接添加/删除属性会自动触发响应式更新,不再需要 $set$delete
    • 但是需要注意:如果使用 reactiveref 定义的数组/对象,直接通过索引修改数组元素或修改 length 也是响应式的(Vue3 中已原生支持)。

示例:

// Vue2
this.$set(this.obj, 'newProp', 'value')

// Vue3
const state = reactive({ obj: {} })
state.obj.newProp = 'value' // 直接添加即可

8. Vue3 中如何定义全局变量/方法?与 Vue2 的 prototype 有什么区别?

解析:

  • Vue2:通过 Vue.prototype.$myGlobal = ... 挂载到原型上,所有组件实例都可以通过 this.$myGlobal 访问。
  • Vue3:移除了 Vue.prototype,推荐使用 app.config.globalProperties 来定义全局变量/方法。
    // main.js
    const app = createApp(App)
    app.config.globalProperties.$myGlobal = 'hello'
    
    然后在组件中可以通过 getCurrentInstance()this(如果使用选项式 API)访问。但在组合式 API 中,建议使用依赖注入(provide / inject)或单独的模块来管理全局变量。
  • 区别
    • Vue3 的 globalProperties 本质也是挂载到组件实例原型上,但作用域限定于该应用实例(createApp 返回的 app),避免污染全局 Vue。
    • 组合式 API 中更推荐使用 provide / inject 或通过 import 引入模块,使依赖关系更清晰。

9. 简述 Vue 的虚拟 DOM 和 diff 算法?Vue2 和 Vue3 的 diff 算法有什么优化?

解析:

  • 虚拟 DOM:用 JavaScript 对象来描述真实 DOM 结构。当数据变化时,Vue 会生成新的虚拟 DOM 树,通过 diff 算法比较新旧两棵树,计算出最小的变更操作并更新到真实 DOM。
  • diff 算法:Vue 的 diff 过程采用双端比较(同层比较,不跨层级)的策略:
    1. 首先比较新旧节点的标签名和 key 是否相同,若不同则直接替换。
    2. 对于相同节点,比较属性并更新子节点。
    3. 子节点的比较使用双端指针:同时从新旧子节点数组的头尾开始比较,尽可能复用节点。
  • Vue2 和 Vue3 diff 优化的主要区别
    • Vue2:使用全量比较,但通过 key 进行优化。对于静态节点,也会进行比较。
    • Vue3
      • 增加了静态标记(PatchFlags):在编译阶段,对动态绑定的节点添加标记,diff 时只比较有标记的节点,大幅提升性能。
      • Fragment 支持:允许多根节点比较。
      • 事件缓存:默认将事件缓存为静态节点(如 @click="handleClick" 视为静态,除非事件函数依赖响应式数据)。
      • Block Tree:将模板基于动态节点拆分为一个个“块”,diff 时以块为单位,减少比较范围。

10. Vue 的模板编译原理大致过程?

解析: Vue 的模板编译分为三个阶段:解析(Parse)优化(Optimize)生成(Generate)

  1. 解析:将模板字符串解析为抽象语法树(AST)。通过正则表达式匹配标签、属性、指令等,构建 AST 节点。
  2. 优化:遍历 AST,标记静态节点(不会变化的节点),便于后续 diff 时跳过这些静态节点(Vue2 的优化)。Vue3 中则生成静态提升和 PatchFlags。
  3. 生成:将优化后的 AST 转换为渲染函数的代码字符串。最终通过 new Function 生成可执行的渲染函数。

在 Vue3 中,编译过程进一步优化,例如:

  • 对动态绑定进行 PatchFlags 标记。
  • 静态节点提升到渲染函数外部,避免重复创建。
  • 事件监听器缓存等。

11. 什么是 Vue 的生命周期?Vue2 和 Vue3 的生命周期有哪些变化?

解析:

  • 生命周期:Vue 实例从创建、挂载、更新到销毁的一系列过程,每个阶段提供了钩子函数供开发者执行自定义逻辑。
  • Vue2 生命周期钩子beforeCreatecreatedbeforeMountmountedbeforeUpdateupdatedbeforeDestroydestroyed(以及 activated/deactivated 用于 keep-alive)。
  • Vue3 生命周期变化
    • beforeDestroydestroyed 重命名为 beforeUnmountunmounted
    • 新增 renderTrackedrenderTriggered 用于调试(仅在开发模式下)。
    • 在组合式 API 中,生命周期钩子通过 onXxx 函数使用(如 onMounted),需要在 setup 中调用。
  • 对应关系(组合式 API):
    • beforeCreate → 使用 setup()
    • created → 使用 setup()
    • beforeMountonBeforeMount
    • mountedonMounted
    • beforeUpdateonBeforeUpdate
    • updatedonUpdated
    • beforeUnmountonBeforeUnmount
    • unmountedonUnmounted
    • errorCapturedonErrorCaptured

示例:

import { onMounted } from 'vue'
export default {
  setup() {
    onMounted(() => {
      console.log('组件已挂载')
    })
  }
}

12. Vue 中组件通信的方式有哪些?列举并简要说明。

解析:

  1. **props / emit:父组件通过props向子组件传递数据,子组件通过emit**:父组件通过 props 向子组件传递数据,子组件通过 `emit` 触发事件向父组件通信。
  2. parent/parent / children:直接访问父/子组件实例(Vue3 中 $children 被移除,可使用 $refsprovide/inject)。
  3. $refs:父组件通过 ref 获取子组件实例,直接调用子组件的方法或访问数据。
  4. provide / inject:祖先组件通过 provide 提供数据,后代组件通过 inject 注入,适用于跨多层组件通信(非响应式默认,但可传递响应式数据)。
  5. 事件总线(Event Bus):Vue2 中通过空的 Vue 实例作为事件中心,Vue3 中推荐使用 mitt 库替代。
  6. Vuex / Pinia:集中式状态管理,适合复杂状态共享。
  7. attrs/attrs / listeners:Vue2 中 $attrs 包含未声明的 props,$listeners 包含父组件传递的事件;Vue3 中 $listeners 被合并到 $attrs 中。
  8. v-model:本质上也是 props + $emit 的语法糖,用于双向绑定。
  9. 插槽(slot):父组件向子组件传递模板内容。
  10. localStorage / sessionStorage:通过浏览器存储实现跨组件通信(非响应式,需手动同步)。

二、Vue Router

13. Vue Router 中路由模式有几种?分别是什么原理?

解析:

Vue Router 支持三种模式:

  • Hash 模式

    • 使用 URL 的 hash(#)部分来模拟一个完整的 URL。
    • 原理:监听 windowhashchange 事件,当 hash 变化时,根据当前 hash 加载对应组件。
    • 优点:兼容性好,无需服务器配置。
    • 缺点:URL 中带有 #,不美观;且 hash 部分不会被发送到服务器。
  • History 模式

    • 利用 HTML5 History API(pushStatereplaceStatepopstate 事件)实现 URL 变化而不刷新页面。
    • 原理:通过 history.pushState 修改浏览器历史记录栈,并监听 popstate 事件响应浏览器前进/后退。
    • 优点:URL 干净美观,像正常网站路径。
    • 缺点:需要服务器配置支持(所有路由都指向同一个入口文件,否则刷新页面会 404)。
  • Memory 模式(Vue3 新增,Vue2 中为 Abstract 模式):

    • 不依赖浏览器历史,使用内存中的栈来管理路由状态。
    • 通常用于非浏览器环境(如 Node.js 服务端渲染)或测试环境。

14. Vue Router 如何实现路由守卫?有哪些类型的守卫?

解析:

路由守卫用于控制路由的访问权限或执行一些逻辑(如登录验证)。Vue Router 提供了丰富的守卫:

  • 全局守卫
    • router.beforeEach(to, from, next):全局前置守卫,路由跳转前触发。
    • router.beforeResolve(to, from, next):全局解析守卫,在导航被确认之前,同时在所有组件内守卫和异步路由组件被解析之后触发。
    • router.afterEach(to, from, failure):全局后置钩子,不会改变导航本身。
  • 路由独享守卫
    • beforeEnter(to, from, next):定义在路由配置中,只针对该路由。
  • 组件内守卫
    • beforeRouteEnter(to, from, next):在渲染该组件的对应路由被 confirm 前调用,此时组件实例尚未创建,无法访问 this
    • beforeRouteUpdate(to, from, next):在当前路由改变,但该组件被复用时调用(例如 /user/1 跳转到 /user/2)。
    • beforeRouteLeave(to, from, next):导航离开该组件的对应路由时调用。

参数说明

  • to:即将要进入的目标路由对象。
  • from:当前导航正要离开的路由。
  • next:必须调用一次来 resolve 这个钩子。可以不带参数(默认放行),或传入一个路径(重定向),或传入 false(取消导航)。

15. 如何动态添加路由?应用场景?

解析:

  • 动态添加路由:使用 router.addRoute() 方法可以在运行时添加新的路由规则。
    • Vue Router 4(Vue3)中:router.addRoute(parentName, route)router.addRoute(route)
    • 也可以使用 router.removeRoute(name) 移除路由。
  • 应用场景
    • 权限控制:根据用户角色动态添加可访问的路由,例如管理员才有某个管理页面。
    • 懒加载模块:根据用户操作动态加载某些模块的路由。
    • 插件系统:允许第三方插件注册自己的路由。

示例:

// 添加一级路由
router.addRoute({ path: '/about', component: About })

// 添加嵌套路由
router.addRoute('parentName', { path: 'child', component: Child })

16. 路由传参的方式及区别?params 和 query。

解析:

  • query 传参
    • 通过 URL 的查询字符串传递参数,例如 /user?id=1
    • 使用 $route.query 接收。
    • 参数会显示在 URL 中,刷新页面不会丢失。
    • 适合传递非敏感数据,或者需要分享链接的场景。
  • params 传参
    • 通过路由配置中的动态路径参数传递,例如 /user/:id
    • 使用 $route.params 接收。
    • 参数值会显示在 URL 路径中(除非使用了动态路径),刷新页面不会丢失。
    • 如果使用 name 进行路由跳转,params 传参不会出现在 URL 路径中,但刷新页面参数会丢失(因为 URL 中没有记录)。所以通常 params 与动态路径搭配使用。
  • 区别总结
    • query 类似于 GET 请求的查询字符串,参数在 URL 中可见;params 一般用于路径参数。
    • 当使用 router.push({ name: 'user', params: { id: 1 } }) 时,如果路由配置中定义了路径参数 :id,则 URL 会变为 /user/1;如果没有定义路径参数,则 params 会被忽略,刷新丢失。
    • 推荐用法:需要路径参数时用 params(配置动态路径),需要可选的额外参数时用 query。

三、Vuex

17. Vuex 的核心概念有哪些?简述工作流程。

解析:

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它包含以下核心概念:

  1. State:单一状态树,存储应用状态数据。
  2. Getters:类似于计算属性,从 state 派生出新状态(可缓存)。
  3. Mutations:同步修改 state 的唯一途径,每个 mutation 都有一个字符串事件类型和一个回调函数。
  4. Actions:可以包含异步操作,提交 mutation 来修改 state,而不是直接变更。
  5. Modules:允许将 store 分割成模块,每个模块拥有自己的 state、mutations、actions、getters。

工作流程

  • 组件通过 dispatch 触发 Action(可异步)。
  • Action 通过 commit 提交 Mutation。
  • Mutation 修改 State。
  • State 变化后,所有依赖的组件自动更新。

Vuex 工作流程图

示例:

// store.js
const store = new Vuex.Store({
  state: { count: 0 },
  mutations: { increment(state) { state.count++ } },
  actions: { incrementAsync({ commit }) { setTimeout(() => commit('increment'), 1000) } }
})

// 组件中
this.$store.dispatch('incrementAsync')

18. Vuex 的辅助函数有哪些?如何使用?

解析:

辅助函数用于简化在组件中访问 store 的写法,将 state、getters、mutations、actions 映射到组件的计算属性或方法上。

  • mapState:将 state 映射为计算属性。
    import { mapState } from 'vuex'
    export default {
      computed: {
        ...mapState(['count', 'user']) // 映射 this.count 为 this.$store.state.count
      }
    }
    
  • mapGetters:将 getters 映射为计算属性。
    ...mapGetters(['doneTodos'])
    
  • mapMutations:将 mutations 映射为组件方法。
    methods: {
      ...mapMutations(['increment']),
      handleClick() {
        this.increment() // 调用 this.$store.commit('increment')
      }
    }
    
  • mapActions:将 actions 映射为组件方法。
    methods: {
      ...mapActions(['incrementAsync'])
    }
    
  • createNamespacedHelpers:用于模块化命名空间,创建基于某个模块的辅助函数。

19. Vuex 的模块化如何使用?命名空间的作用?

解析:

  • 模块化:当应用复杂时,可以将 store 分割成模块(module),每个模块拥有自己的 state、mutations、actions、getters,甚至可以嵌套子模块。
    const moduleA = {
      state: () => ({ count: 0 }),
      mutations: { increment(state) { state.count++ } },
      actions: { ... },
      getters: { ... }
    }
    const store = new Vuex.Store({
      modules: { a: moduleA, b: moduleB }
    })
    // 访问 state: store.state.a.count
    
  • 命名空间(namespaced)
    • 默认情况下,模块内部的 actions、mutations、getters 是注册在全局命名空间下的,这样多个模块可以响应同一个 mutation/action 类型。
    • 如果希望模块具有更高的封装性和复用性,可以设置 namespaced: true,使其成为带命名空间的模块。启用后,模块内的 getters、actions、mutations 都会自动根据模块路径调整名称。例如 dispatch('a/increment')
    • 好处:避免命名冲突,模块间解耦。

示例:

const moduleA = {
  namespaced: true,
  state: { count: 0 },
  mutations: { increment(state) { state.count++ } },
  actions: { increment({ commit }) { commit('increment') } }
}
// 组件中调用
this.$store.dispatch('a/increment')

四、Pinia

20. Pinia 和 Vuex 的区别?Pinia 有什么优势?

解析:

Pinia 是 Vue 官方推荐的新一代状态管理库,可以看作是 Vuex 5 的原型。主要区别和优势:

  • API 设计
    • Pinia 完全拥抱 Composition API,使用 defineStore 定义 store,内部使用 refcomputed 等创建 state 和 getters,更直观。
    • Vuex 4 之前使用 Options API,Vuex 4 虽然支持 Vue3,但 API 风格仍偏向选项式。
  • TypeScript 支持:Pinia 天然支持 TypeScript,类型推导非常友好,无需额外配置。
  • 模块化:Pinia 没有 modules 概念,每个 store 独立定义,自动实现代码分割。Vuex 需要手动注册模块。
  • Devtools 支持:Pinia 提供更好的开发工具支持,包括时间线追踪、store 状态编辑等。
  • Mutation 的弃用:Pinia 移除了 mutations,只有 state、getters、actions。Actions 可以同步或异步直接修改 state(通过 this 访问)。
  • 服务端渲染支持:Pinia 对 SSR 支持更友好。
  • 体积小:Pinia 核心代码体积更小。

优势总结:更简洁、更符合 Vue3 组合式 API 风格、类型安全、更好的开发体验。

21. Pinia 中如何定义 store?如何使用 state、getters、actions?

解析:

  • 定义 store:使用 defineStore 函数,第一个参数是 store 的唯一 id,第二个参数是配置对象(Options Store)或一个 Setup 函数(Setup Store)。
    • Options Store:类似于 Vuex,包含 stategettersactions
    • Setup Store:接收一个函数,返回一个对象,内部使用 refcomputed、函数等。
  • 使用 store:在组件中通过 useStore 函数(即定义的 store)获取 store 实例。

示例(Options Store):

// stores/counter.js
import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', {
  state: () => ({ count: 0, name: 'counter' }),
  getters: {
    doubleCount: (state) => state.count * 2
  },
  actions: {
    increment() {
      this.count++ // 直接修改 state
    },
    async fetchData() {
      // 异步操作
    }
  }
})

// 组件中
import { useCounterStore } from '@/stores/counter'
const counter = useCounterStore()
console.log(counter.count) // 无需 .value
counter.increment()

示例(Setup Store):

export const useCounterStore = defineStore('counter', () => {
  const count = ref(0)
  const doubleCount = computed(() => count.value * 2)
  function increment() {
    count.value++
  }
  return { count, doubleCount, increment }
})

22. Pinia 支持组合式 API 吗?如何配合 Vue3 使用?

解析:

Pinia 本身就是为组合式 API 设计的,完美支持在 Vue3 的 <script setup>setup() 函数中使用。

  • <script setup> 中直接调用 useStore 即可,返回的 store 对象是响应式的,可以直接在模板中使用,无需 .value
  • 可以在组合式函数(Composables)中使用 Pinia stores。
  • 支持 store 之间相互调用,只需在 action 中引入其他 store 并使用即可。

示例:

<template>
  <div>{{ counter.count }}</div>
</template>

<script setup>
import { useCounterStore } from '@/stores/counter'
const counter = useCounterStore()
</script>
  • 如果要在组件外使用(如路由守卫),需要确保 pinia 实例已激活,通常通过 useStore(pinia) 传入 pinia 实例。

五、Vite

23. Vite 相比 Webpack 有哪些优势?它是如何实现快速冷启动的?

解析:

Vite 是一个新型前端构建工具,相比 Webpack 主要有以下优势:

  • 极速冷启动:Vite 利用浏览器原生 ES Module 支持,在开发环境下无需打包,直接按需加载模块,启动速度极快。
  • 热更新(HMR)速度快:Vite 在 HMR 时只更新修改的模块,利用浏览器 ESM 的能力,速度不受项目规模影响。
  • 开箱即用:内置了对 TypeScript、JSX、CSS 预处理器等的支持,配置简单。
  • 构建使用 Rollup:生产环境使用 Rollup 打包,充分利用其 tree-shaking 和插件生态。

实现快速冷启动的原理

  • 开发环境下,Vite 将应用模块分为依赖源码两类:
    • 依赖(如 lodashvue):使用 esbuild 预构建,转换为 ESM 格式并缓存。esbuild 使用 Go 编写,比 JavaScript 打包器快 10-100 倍。
    • 源码:浏览器通过 <script type="module"> 直接请求源码文件,Vite 服务器按需编译并返回(如转换 TypeScript、编译 Vue SFC 等),充分利用浏览器的原生 ESM 加载能力,避免打包整个应用。

24. Vite 的依赖预构建是做什么的?为什么需要?

解析:

  • 依赖预构建:Vite 在开发服务器启动前,使用 esbuild 对项目的依赖(node_modules 中的模块)进行预打包,将它们转换为浏览器友好的 ESM 格式,并合并成少量文件。
  • 为什么需要
    1. 兼容性:许多 npm 包以 CommonJS 或 UMD 格式发布,浏览器无法直接识别。预构建将其转换为 ESM。
    2. 减少请求数:如果不预构建,每个依赖包可能拆分成大量内部模块,浏览器会发起数百个请求,严重影响性能。预构建将多个内部模块合并为一个文件,减少请求。
    3. 缓存:预构建的结果会被缓存(node_modules/.vite),只要依赖没有变化,下次启动直接复用。

25. Vite 中如何处理静态资源?环境变量如何配置?

解析:

  • 静态资源
    • 直接导入:在 JavaScript 中导入静态资源(如图片、JSON)会返回资源的 URL。
      import imgUrl from './img.png' // 得到 /img.png 或 base64 字符串
      
    • 放在 public 目录下的资源会被原样复制到构建输出目录,可以通过根路径引用(如 /public/logo.png 对应 /logo.png)。
    • 支持资源内联:通过 ?inline 后缀或配置 assetsInlineLimit 控制。
  • 环境变量
    • Vite 使用 import.meta.env 暴露环境变量。默认有 MODEBASE_URLPRODDEV
    • 自定义环境变量需以 VITE_ 为前缀(防止意外暴露敏感信息),并放在 .env 文件中(如 .env.development.env.production)。
    • 在代码中通过 import.meta.env.VITE_API_URL 访问。
    • TypeScript 中可以通过在 env.d.ts 中添加类型声明来获得提示。

26. 如何在 Vite 项目中配置 proxy 代理解决跨域?

解析:

在开发环境下,Vite 通过配置 server.proxy 来解决跨域问题。在 vite.config.js 中:

export default {
  server: {
    proxy: {
      '/api': {
        target: 'http://localhost:3000',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, '')
      }
    }
  }
}
  • /api:需要代理的请求路径前缀。
  • target:目标服务器地址。
  • changeOrigin:改变请求头中的 Origin 为目标地址,避免被服务器拒绝。
  • rewrite:可选,重写路径,去掉前缀。

这样,前端请求 /api/users 会被代理到 http://localhost:3000/users,绕过浏览器的同源策略。

27. 说说你对 Vite 插件的理解?如何编写一个简单的 Vite 插件?

解析:

  • Vite 插件:Vite 在开发环境和构建时都支持插件扩展,插件可以修改模块解析、转换、注入代码等。Vite 插件基于 Rollup 插件接口,并额外提供了一些 Vite 特有的钩子(如 configureServer)。
  • 插件结构:一个插件通常是一个对象,包含 name 属性和一些钩子函数(如 transformresolveIdload 等)。
  • 简单示例:创建一个插件,在代码中注入全局变量。
    // my-plugin.js
    export default function myPlugin() {
      return {
        name: 'my-plugin',
        transform(code, id) {
          if (id.endsWith('.vue')) {
            // 修改 Vue 单文件组件内容
            return code.replace(/__VERSION__/g, '1.0.0')
          }
        },
        configureServer(server) {
          // 添加中间件,处理自定义请求
          server.middlewares.use((req, res, next) => {
            if (req.url === '/custom') {
              res.end('hello from plugin')
            } else {
              next()
            }
          })
        }
      }
    }
    
    vite.config.js 中使用:
    import myPlugin from './my-plugin'
    export default {
      plugins: [myPlugin()]
    }
    

六、Vue3 高级特性

28. Vue3 中的 Suspense 组件有什么用?

解析:

<Suspense> 是一个实验性(现已稳定)的组件,用于协调对异步依赖的处理。它允许在等待异步组件或异步 setup 函数解析时,显示 fallback 内容(如 loading 状态)。

  • 使用场景
    • 包裹一个或多个异步组件(使用 defineAsyncComponent 加载的组件)。
    • 组件在 setup 中返回一个 Promise(如异步请求数据)。
  • 插槽
    • #default:异步组件加载完成后显示的内容。
    • #fallback:加载过程中显示的占位内容。
  • 事件<Suspense> 会触发 pendingresolvefallback 事件。

示例:

<template>
  <Suspense>
    <template #default>
      <AsyncComponent />
    </template>
    <template #fallback>
      <div>Loading...</div>
    </template>
  </Suspense>
</template>

<script setup>
import { defineAsyncComponent } from 'vue'
const AsyncComponent = defineAsyncComponent(() => import('./AsyncComponent.vue'))
</script>

29. 什么是 Vue3 中的 Tree-shaking?它是如何实现的?

解析:

  • Tree-shaking:即“摇树优化”,指移除 JavaScript 上下文中未引用的代码(dead code),以减小打包体积。Vue3 在设计时就考虑了 Tree-shaking 支持。
  • Vue3 中实现 Tree-shaking 的方式
    • 模块化导出:Vue3 的 API 采用命名导出方式(如 refreactivecomputed 等),而不是将所有 API 挂载在单个 Vue 对象上。这样构建工具(如 Rollup、Webpack)可以识别哪些 API 被实际使用,只打包这些模块。
    • 内置组件按需引入:例如 <Transition><KeepAlive> 等内置组件也需要显式导入才能使用。
    • 编译时标记:模板编译时,会根据使用的指令和特性生成对应的导入语句,确保只引入必要的运行时辅助函数。
  • 效果:未使用的 API 不会出现在最终的 bundle 中,大幅减小体积。例如只使用 ref 的应用,不会包含 reactive 的相关代码。

30. 在 Vue3 中如何使用 JSX?与模板语法相比有什么优缺点?

解析:

  • 使用 JSX:Vue3 支持 JSX,但需要安装 @vitejs/plugin-vue-jsx 插件(Vite 项目)或配置 Babel 转换。JSX 写法类似于 React,但可以使用 Vue 的指令(如 v-model 需要用 v-model={val})和插槽。
    export default {
      setup() {
        const count = ref(0)
        return () => (
          <div>
            <button onClick={() => count.value++}>+</button>
            <span>{count.value}</span>
          </div>
        )
      }
    }
    
  • 与模板语法比较
    • 模板语法
      • 优点:更接近 HTML,易于理解和设计;内置指令(v-ifv-for)简化常见操作;编译时优化(静态提升、补丁标记)性能更好。
      • 缺点:灵活性受限,复杂的逻辑可能需要使用计算属性或方法。
    • JSX
      • 优点:完全编程能力,可以使用完整的 JavaScript 表达式;适合逻辑复杂的渲染;与 TypeScript 结合更自然。
      • 缺点:缺乏编译时优化(但 Vue 的 JSX 插件也会进行一定优化);可读性可能不如模板;需要额外配置。
    • 选择:一般推荐使用模板语法,除非遇到模板无法满足的复杂动态渲染需求(如高阶组件、渲染函数),或者项目团队习惯 JSX。

七、Vite 核心原理与进阶

31. Vite 的依赖预构建具体过程是怎样的?esbuild 在其中扮演什么角色?

解析:

Vite 在开发服务器启动前,会执行依赖预构建,主要步骤如下:

  1. 扫描依赖:Vite 从入口文件(如 index.html)开始,通过静态分析(或插件提供的 resolveId/load 钩子)找到项目中使用到的裸模块导入(如 import vue from 'vue'),生成依赖列表。
  2. 预构建:使用 esbuild 对这些依赖进行打包,将它们转换为标准的 ESM 格式,并合并成少量文件。esbuild 是用 Go 编写的,速度极快,比传统的 JavaScript 打包器快 10-100 倍。
  3. 输出:预构建结果存放在 node_modules/.vite/deps 目录下,同时生成一个 _metadata.json 文件记录依赖的哈希值等信息。
  4. 缓存:Vite 会根据依赖的锁定文件(如 package-lock.json)和 _metadata.json 判断是否命中缓存。如果依赖未变,下次启动直接复用,几乎瞬间完成。

esbuild 的角色

  • 作为预构建的核心工具,负责将 CommonJS、UMD 等格式的依赖转换为 ESM,并进行打包合并,减少请求数。
  • 因为 esbuild 的高性能,预构建几乎不影响启动时间。
  • 注意:生产环境打包时,Vite 默认使用 Rollup(因为 esbuild 的打包功能和代码分割灵活性相对较弱),但未来可能逐步支持 esbuild 生产构建。

32. Vite 开发服务器是如何工作的?中间件机制是怎样的?

解析:

Vite 开发服务器是一个基于 Connect(Node.js 中间件框架)的服务器,它利用浏览器原生 ESM 的能力,按需编译和提供源码。工作流程如下:

  1. 启动服务器:Vite 启动一个开发服务器,监听指定端口。
  2. 请求拦截:浏览器请求 index.html,Vite 返回原始 HTML,但会对内容进行转换(如注入环境变量、客户端代码等)。
  3. 模块解析:当浏览器通过 <script type="module"> 请求 JavaScript 模块时,Vite 服务器根据请求路径,使用中间件处理:
    • 路径重写:将裸模块(如 import vue from 'vue')转换为预构建后的路径(如 /@modules/vue.js)。
    • 编译转换:如果是 Vue 单文件组件(SFC),使用 @vitejs/plugin-vue 将其编译为 JavaScript 模块;如果是 TypeScript,则使用 esbuild 快速转译。
    • 返回内容:将转换后的代码以 ESM 格式返回给浏览器。
  4. 热更新(HMR):Vite 通过 WebSocket 与客户端建立连接,当模块变化时,服务器通知客户端重新请求该模块,仅更新变化部分,实现极速 HMR。

中间件机制: Vite 内部使用了许多中间件处理不同请求:

  • viteTransformMiddleware:负责转换模块(如 SFC、TS、CSS 等)。
  • viteServeStaticMiddleware:提供静态文件服务(public 目录)。
  • viteProxyMiddleware:处理代理配置。
  • viteHMRMiddleware:处理 HMR 相关请求。 开发者也可以编写自定义中间件,通过 configureServer 钩子注入。

33. Vite 的生产环境打包为什么选择 Rollup 而不是 esbuild?有哪些优化配置?

解析:

虽然 esbuild 在开发环境预构建中表现出色,但 Vite 的生产打包仍默认使用 Rollup,原因如下:

  • 打包灵活性:Rollup 提供了丰富的插件接口和精细的打包控制,尤其对于代码分割(code splitting)、动态导入、自定义 chunk 等复杂场景,Rollup 更加成熟和可配置。
  • Tree-shaking 能力:Rollup 的 tree-shaking 基于静态分析,可以高效地去除未使用代码,且支持深层优化(如 /*#__PURE__*/ 注释)。
  • 社区生态:Rollup 拥有大量高质量的插件,可以处理各种资源(如图片、CSS、JSON 等),与 Vite 插件体系无缝兼容。
  • 输出质量:Rollup 生成的代码更干净、更优化,适合生产环境。

Vite 生产打包的优化配置: Vite 通过 build.rollupOptions 暴露 Rollup 配置,常见优化包括:

  • 代码分割:使用 output.manualChunks 将第三方库(如 vuelodash)拆分为独立 chunk,利用浏览器并行加载。
    // vite.config.js
    export default {
      build: {
        rollupOptions: {
          output: {
            manualChunks: {
              'vue-vendor': ['vue', 'vue-router', 'pinia'],
              'ui-lib': ['element-plus']
            }
          }
        }
      }
    }
    
  • 动态导入:通过 import() 实现路由懒加载,Vite 自动提取为单独 chunk。
  • 资源内联:配置 assetsInlineLimit,小于该值的图片或字体会被转为 base64 内联,减少请求。
  • CSS 代码分割:Vite 默认将异步 chunk 中的 CSS 提取为单独文件,避免重复。
  • 预加载指令生成:Vite 会自动为入口 chunk 生成 <link rel="modulepreload">,提升加载性能。
  • 压缩:默认使用 esbuild 进行代码压缩(build.minify = 'esbuild'),可切换为 terser
  • 移除调试语句:通过 build.rollupOptions.plugins 加入 terserdrop_console 等。

34. Vite 中如何处理 CSS?支持哪些预处理器?CSS Modules 如何配置?

解析:

Vite 对 CSS 的处理开箱即用,支持:

  • 普通 CSS:直接导入 .css 文件,Vite 会将其注入到 <style> 标签或提取为单独文件(生产环境)。
  • CSS 预处理器:内置支持 .scss.sass.less.styl 等文件,只需安装对应的预处理器(如 sassless),无需额外配置。
    npm install -D sass
    # 然后直接导入 .scss 文件即可
    
  • CSS Modules:任何以 .module.css 结尾的文件都被视为 CSS Modules,导入后会返回一个类名映射对象。
    import styles from './style.module.css'
    // styles 为 { container: '_container_1abc2' }
    
    也可以配置自定义的 CSS Modules 选项:
    // vite.config.js
    export default {
      css: {
        modules: {
          scopeBehaviour: 'local', // 或 'global'
          generateScopedName: '[name]__[local]___[hash:base64:5]'
        }
      }
    }
    
  • PostCSS:如果项目根目录包含 postcss.config.js,Vite 会自动应用 PostCSS 配置,支持 autoprefixer、tailwindcss 等。
  • CSS 代码分割:生产构建时,Vite 会自动将异步 chunk 中的 CSS 提取为单独文件,避免 FOUC。

35. Vite 的 HMR 原理是什么?与 Webpack 的 HMR 有何异同?

解析:

Vite HMR 原理

  • Vite 在开发服务器和浏览器客户端之间建立 WebSocket 连接
  • 当文件变化时,Vite 服务器通过文件监听(如 chokidar)捕获变更,确定受影响的模块。
  • 服务器向客户端发送 HMR 更新消息,包含模块路径和更新类型(如 updatefull-reload)。
  • 客户端接收到消息后,通过 import() 动态重新请求变更模块,并执行 HMR 运行时(由 Vite 注入)的回调函数(如 import.meta.hot.accept),替换模块并触发组件重新渲染。
  • 由于只重新请求变更模块,且无需打包,HMR 速度极快,与项目规模无关。

与 Webpack HMR 的异同

  • 相同点
    • 都通过 WebSocket 实现实时通信。
    • 都支持模块热替换,保留应用状态。
  • 主要区别
    • 实现方式:Vite 基于原生 ESM,HMR 时直接请求变更模块;Webpack 基于打包后的模块系统,需要重新编译打包受影响的模块。
    • 速度:Vite 的 HMR 通常比 Webpack 快,尤其是大型项目,因为 Vite 无需重新构建依赖图。
    • 复杂度:Vite 的 HMR 实现更轻量,而 Webpack 需要复杂的模块热替换运行时和 module.hot API。
    • 插件生态:两者都提供 HMR API,但 Vite 的 import.meta.hot 更简洁,与原生 ESM 集成良好。

36. Vite 的构建优化:如何配置代码分割(chunking)?动态导入如何处理?

解析:

Vite 默认基于 Rollup 进行代码分割,开发者可以通过 build.rollupOptions.output.manualChunks 自定义 chunk 拆分策略。

1. 动态导入自动分割: 在代码中使用 import() 语法,Vite 会自动将动态导入的模块及其依赖打包为单独的 chunk,并在运行时通过 JSONP 加载。例如:

// 路由懒加载
const UserList = () => import('./views/UserList.vue')

Vite 会生成类似 UserList.[hash].js 的文件。

2. 手动配置 manualChunks

// vite.config.js
export default {
  build: {
    rollupOptions: {
      output: {
        manualChunks(id) {
          // 将 node_modules 中的依赖拆分为单独的 vendor chunk
          if (id.includes('node_modules')) {
            // 进一步细分:如将 vue 相关打包在一起
            if (id.includes('vue')) {
              return 'vue-vendor';
            }
            // 其他依赖打包为 vendor
            return 'vendor';
          }
        }
      }
    }
  }
}

也可以使用对象形式:

manualChunks: {
  'vue-vendor': ['vue', 'vue-router', 'pinia'],
  'ui-lib': ['element-plus']
}

3. 其他优化配置

  • build.chunkSizeWarningLimit:调整 chunk 大小警告阈值(默认 500KB)。
  • build.rollupOptions.output.entryFileNameschunkFileNamesassetFileNames:自定义输出文件名格式,可利用 hash 实现长效缓存。
  • build.rollupOptions.external:将某些依赖标记为外部,避免打包,适合库开发。

37. Vite 中如何配置多页面应用模式?

解析:

Vite 原生支持多页面应用,只需在项目根目录创建对应的 HTML 文件,并在配置中指定 build.rollupOptions.input 即可。

步骤

  1. 创建多个 HTML 文件,例如 index.htmlabout.htmlcontact.html
  2. vite.config.js 中配置输入入口:
    // vite.config.js
    import { resolve } from 'path'
    export default {
      build: {
        rollupOptions: {
          input: {
            main: resolve(__dirname, 'index.html'),
            about: resolve(__dirname, 'about.html'),
            contact: resolve(__dirname, 'contact.html')
          }
        }
      }
    }
    
  3. 每个 HTML 文件可以通过 <script type="module" src="/src/main.js"> 引入各自的入口脚本。

开发环境下,访问 http://localhost:5173/about.html 即可看到 about 页面。

注意:如果使用历史模式(History API)的路由,需要配置服务器回退到对应的 HTML 文件,Vite 开发服务器默认支持。

38. Vite 如何支持服务端渲染(SSR)?核心思路是什么?

解析:

Vite 提供了对 SSR 的一流支持,核心思路是将 Vite 作为开发服务器同时处理客户端和服务器端代码,生产环境则通过打包生成 SSR 所需文件。

SSR 开发流程

  1. 服务端入口:编写一个服务器入口文件(如 entry-server.js),导出创建应用实例的函数。
  2. 客户端入口:编写客户端入口(如 entry-client.js),用于挂载应用。
  3. 开发环境
    • 使用 Vite 中间件在 Node.js 服务器中运行,拦截请求,动态编译 Vue 组件。
    • 通过 @vitejs/plugin-vue 处理 SFC,并通过 vite.ssrLoadModule 加载服务器端模块。
  4. 生产环境
    • 使用 Vite 构建客户端 bundle 和服务器端 bundle(通过 build.ssr 选项)。
    • 服务器引入服务器端 bundle,调用其渲染函数生成 HTML,同时注入客户端资源链接。

关键点

  • 条件导入:在服务器端需要避免使用浏览器特有 API,可通过 import.meta.env.SSR 进行判断。
  • 数据预取:在服务器端渲染前,预先获取数据并注入到 store 中,然后序列化传递给客户端。
  • 客户端激活:客户端渲染时,需要复用服务器生成的 DOM 并绑定事件(hydration)。

官方示例:Vite 提供了 create-vitevue-ssr 模板,可以快速上手。

39. Vite 的环境变量和模式管理:如何根据模式加载不同 .env 文件?

解析:

Vite 使用 dotenv 加载环境变量,并支持模式(mode)概念,根据模式加载对应的 .env 文件。

  • 文件命名规则
    • .env:所有情况下加载。
    • .env.local:所有情况下加载,但应被 git 忽略。
    • .env.[mode]:指定模式下加载(如 .env.development)。
    • .env.[mode].local:指定模式且本地覆盖,通常被忽略。
  • 优先级:特定模式的文件优先级更高,后面的变量会覆盖前面的。

使用方式

  1. 在项目根目录创建 .env.development: VITE_API_URL=http://localhost:3000/api
  2. 在代码中通过 import.meta.env.VITE_API_URL 访问。
  3. 启动时指定模式:
    • 开发:vite --mode development(默认模式为 development)。
    • 构建:vite build --mode production(默认模式为 production)。
  4. TypeScript 支持:在 env.d.ts 中添加类型声明:
    /// <reference types="vite/client" />
    interface ImportMetaEnv {
      readonly VITE_API_URL: string
    }
    

注意:只有以 VITE_ 开头的变量才会暴露给客户端,避免敏感信息泄露。

40. Vite 插件开发:常用钩子及一个简单示例。

解析:

Vite 插件基于 Rollup 插件接口,并添加了一些 Vite 特有钩子。一个插件是一个对象,包含 name 属性和各种钩子函数。

常用钩子

  • 通用钩子(Rollup 钩子):
    • resolveId:解析模块 ID。
    • load:加载模块内容。
    • transform:转换模块代码。
    • buildEnd:构建完成时调用。
  • Vite 特有钩子
    • configureServer:配置开发服务器,可添加中间件。
    • handleHotUpdate:自定义 HMR 更新行为。
    • config:在解析 Vite 配置前调用,可修改配置。
    • configResolved:在解析完配置后调用。
    • transformIndexHtml:转换 index.html 内容。

简单示例:创建一个插件,在构建时给所有 Vue 组件的模板添加一个自定义属性。

// add-attribute-plugin.js
export default function addAttributePlugin(attributeName, value) {
  return {
    name: 'add-attribute',
    transform(code, id) {
      if (id.endsWith('.vue')) {
        // 简单的字符串替换,实际可能需要解析 AST
        const newCode = code.replace(
          /<template>/,
          `<template ${attributeName}="${value}">`
        );
        return newCode;
      }
    }
  }
}

vite.config.js 中使用:

import addAttribute from './add-attribute-plugin'
export default {
  plugins: [addAttribute('data-version', '1.0')]
}

更高级的用法:通过 configureServer 添加中间件,拦截请求:

configureServer(server) {
  server.middlewares.use((req, res, next) => {
    if (req.url === '/custom') {
      res.end('hello from plugin')
    } else {
      next()
    }
  })
}

41. Vite 的未来趋势:Rust 版 Vite(Rolldown)是什么?

解析:

Vite 团队正在开发一个基于 Rust 的新打包器 Rolldown,旨在统一开发环境和生产环境的工具链。目前 Vite 开发用 esbuild,生产用 Rollup,两者存在差异。Rolldown 的目标是:

  • 提供与 Rollup 兼容的 API 和插件接口,但底层用 Rust 实现,性能媲美 esbuild。
  • 同时支持开发和生产环境,消除两套工具的差异。
  • 进一步提升构建速度,尤其在大型项目中。

未来 Vite 可能会将核心打包部分迁移到 Rolldown,保持生态兼容的同时获得极致性能。这一变化对于普通开发者将是透明的,但底层将更加强大。


以上是 Vue 全家桶及相关工具的面试题大全。希望这些题目和详细解析能够帮助你全面掌握 Vue 技术栈,顺利应对前端面试!