基础与项目搭建
本页关键词:Composition API、ref/reactive、computed、watch、生命周期、父子通信、provide/inject、Pinia、defineStore、storeToRefs、ElementPlus 按需导入、Axios 三层封装、Vue Router 嵌套路由、SCSS 变量
一、Vue3 Composition API 速览
setup 与 <script setup>
Vue3 提供了两种编写组件逻辑的方式。<script setup> 是 setup 函数的语法糖 — 在这个标签内声明的所有顶层变量和函数会自动暴露给 template,无需像 Options API 那样在 data、methods、computed 中分散定义。
核心思路:Composition API 让我们按「功能」而非「选项类型」组织代码。比如一个搜索功能的 state、computed、watch、方法可以写在一起,而不是分散在 data/computed/methods/watch 四个选项中。
<script setup>
import { ref } from 'vue'
const count = ref(0)
const addCount = () => count.value++
</script>reactive vs ref
import { reactive, ref } from 'vue'
const state = reactive({ msg: 'hello' }) // 仅接受对象类型,直接解构会丢失响应式
const count = ref(0) // 接受任意类型,访问/修改需 .value两者的关系:ref 内部对对象类型也是调用 reactive 实现的。实际开发中统一使用 ref 可以减少心智负担 — 不用区分"这是对象用 reactive 还是基本类型用 ref"。
面试要点:
ref包装基本类型时通过 getter/setter 拦截实现响应式;包装对象类型时底层走的是reactive(Proxy)。reactive返回的是 Proxy 对象,直接解构会丢失响应性,需要用toRefs或storeToRefs。
computed
computed 接收一个 getter 函数,返回一个只读的响应式 ref。它的值会基于依赖自动缓存 — 只有依赖变化时才重新计算:
import { ref, computed } from 'vue'
const list = ref([1, 2, 3, 4, 5])
const filterList = computed(() => list.value.filter(item => item > 2))
// filterList 会随 list 变化自动更新,但在 list 不变时直接返回缓存值watch
watch 用于在响应式数据变化时执行副作用(如发请求、操作 DOM)。与 computed 的区别在于:computed 关注「根据已有数据派生新值」,watch 关注「数据变了之后要做什么」。
import { ref, watch } from 'vue'
// 侦听单个 ref
watch(count, (newVal, oldVal) => { /* 执行副作用 */ })
// 侦听多个源(数组形式)
watch([count, name], ([newCount, newName], [oldCount, oldName]) => { /* ... */ })
// immediate:创建时立即执行一次回调(常用于初始化需要用到该数据的场景)
watch(source, callback, { immediate: true })
// deep:深层侦听对象内部属性变化(对 reactive 对象默认就是 deep)
watch(state, callback, { deep: true })生命周期
Composition API 将生命周期钩子改为函数调用形式,可以在 setup 中多次调用同一个钩子(会按顺序执行):
| Options API | Composition API | 说明 |
|---|---|---|
| beforeCreate | setup() 自身 | 组件实例创建前,此时 data/methods 尚未初始化 |
| created | setup() 自身 | 组件实例已创建,但 DOM 尚未挂载 |
| mounted | onMounted | DOM 已渲染,适合发起请求、操作 DOM |
| unmounted | onUnmounted | 组件销毁,适合清理定时器、事件监听 |
import { onMounted } from 'vue'
onMounted(() => { /* DOM 已就绪,可以获取 ref 引用的 DOM 元素 */ })父子通信
Vue3 的父子通信通过 defineProps 和 defineEmits 两个编译器宏实现(不需要 import):
// 父传子 — defineProps 声明接收的属性
const props = defineProps({
title: { type: String, default: '' }
})
// 子传父 — defineEmits 声明要触发的事件
const emit = defineEmits(['change'])
emit('change', payload)设计思路:单向数据流 — 父组件通过 props 向下传数据,子组件通过 emit 向上通知事件。子组件不应直接修改 props(会破坏数据溯源)。
provide / inject(跨层通信)
当组件层级很深时(比如 Layout > Header > Nav > NavItem),逐层传 props 很繁琐。provide/inject 允许顶层组件「提供」数据,任意后代组件直接「注入」使用:
// 顶层组件 — 提供数据(ref 保持响应式)
import { provide, ref } from 'vue'
const data = ref('hello')
provide('key', data)
// 任意后代组件 — 注入使用
import { inject } from 'vue'
const data = inject('key')模版引用 & defineExpose
通过 ref 可以获取 DOM 元素或子组件实例。在 <script setup> 中,组件默认不暴露任何属性给父组件,需要通过 defineExpose 显式声明:
// 获取 DOM 元素
const inputRef = ref(null)
onMounted(() => inputRef.value.focus())
// 子组件暴露方法给父组件
defineExpose({ publicMethod })面试要点:Composition API 的核心优势 — 按功能组织代码(而非选项分散),composable 函数可复用逻辑,TypeScript 类型推导更好,不依赖
this。
二、Pinia 核心用法
为什么用 Pinia
多个组件需要共享同一份数据时(如用户登录信息、购物车、分类列表),如果用 props 层层传递会非常麻烦。Pinia 提供了一个「全局单例仓库」,任何组件都可以直接读写。
定义 Store(组合式写法)
Pinia 支持 Options API 和 Composition API 两种风格定义 Store。项目中统一使用组合式写法,和组件内的写法完全一致 — ref 当 state,computed 当 getter,普通函数当 action:
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useCounterStore = defineStore('counter', () => {
// state — ref()
const count = ref(0)
// getter — computed()
const doubleCount = computed(() => count.value * 2)
// action — 普通函数(可以是同步/异步)
const increment = () => count.value++
const loadData = async () => {
const res = await axios.get(API_URL)
list.value = res.data.data.channels
}
return { count, doubleCount, increment, loadData }
})组件中使用
<script setup>
import { storeToRefs } from 'pinia'
import { useCounterStore } from '@/stores/counter'
const counterStore = useCounterStore()
// storeToRefs 解构 state/getter 时保持响应式
const { count, doubleCount } = storeToRefs(counterStore)
// action 方法直接解构(不需要 storeToRefs,函数不涉及响应式)
const { increment } = counterStore
</script>为什么需要 storeToRefs:直接 const { count } = counterStore 会丢失响应式(等于取出了一个普通值)。storeToRefs 把 store 内的 state/getter 转为独立的 ref,解构后仍能响应式更新。但 action 是普通函数,不需要响应式转换。
面试要点:Pinia vs Vuex — 去掉了 mutation(action 直接修改 state),天然支持 Composition API,每个 store 独立模块无需 modules 配置;Store 是单例模式,多个组件
useXxxStore()拿到的是同一个实例。
三、项目初始化与工程配置
1. 创建项目
npm init vue@latest脚手架选择 TypeScript + Vue Router + Pinia + ESLint,自动生成标准项目结构。
2. ElementPlus 按需导入 + 主题定制
思路:ElementPlus 组件库很大,全量引入会严重影响打包体积。通过 unplugin-auto-import + unplugin-vue-components 实现按需导入 — 在 template 中直接使用 <el-button> 即可,插件在编译时自动添加 import 语句。
主题定制的原理是通过 SCSS 的 @forward ... with (...) 覆盖 ElementPlus 内部的 SCSS 变量,而非在运行时覆盖 CSS(更高效,包体积更小)。
// vite.config.ts
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
import ElementPlus from 'unplugin-element-plus/vite'
export default defineConfig({
plugins: [
AutoImport({ resolvers: [ElementPlusResolver()] }),
Components({
resolvers: [ElementPlusResolver({ importStyle: 'sass' })] // sass 才能使用主题定制
}),
ElementPlus({ useSource: true }), // 使用源码级 SCSS 而非编译后的 CSS
],
css: {
preprocessorOptions: {
scss: {
additionalData: `
@use "@/styles/element/index.scss" as *;
@use "@/styles/var.scss" as *;
`, // 每个 SCSS 文件编译时自动注入这两行
}
}
}
})// styles/element/index.scss — 覆盖 ElementPlus 主题色
@forward 'element-plus/theme-chalk/src/common/var.scss' with (
$colors: (
'primary': ('base': #27ba9b),
'success': ('base': #1dc779),
'warning': ('base': #ffb302),
'danger': ('base': #e26237),
'error': ('base': #cf4444),
)
)3. Axios 三层封装
设计思路:将 HTTP 请求分为三层,实现关注点分离:
| 层 | 文件 | 职责 |
|---|---|---|
| HTTP 层 | utils/http.js | 创建 Axios 实例,配置 baseURL / timeout / 拦截器 |
| API 层 | apis/xxx.js | 定义具体接口函数,封装 URL 和参数 |
| 组件层 | views/xxx.vue | 调用 API 函数,处理业务逻辑和 UI |
// utils/http.js — 基础版本(后续会增加 token 和错误处理)
import axios from 'axios'
const httpInstance = axios.create({
baseURL: 'http://pcapi-xiaotuxian-front-devtest.itheima.net',
timeout: 5000
})
// 请求拦截器 — 后续在此注入 token
httpInstance.interceptors.request.use(config => {
return config
}, e => Promise.reject(e))
// 响应拦截器 — 直接返回 response.data,免去组件层每次 .data 取值
httpInstance.interceptors.response.use(
res => res.data,
e => Promise.reject(e)
)
export default httpInstance// apis/layout.js — API 层封装接口函数
import httpInstance from '@/utils/http'
export const getCategoryAPI = () => httpInstance({ url: '/home/category/head' })为什么响应拦截器做 res.data 解包:后端返回的完整响应体是 { headers, data: { code, result, message } },实际业务只关心 data 部分。在拦截器统一处理后,组件层调用时直接拿到 { code, result, message },减少重复代码。
面试要点:Axios 拦截器是 AOP(面向切面编程)思想的体现 — 请求拦截统一携带 token,响应拦截统一解包数据和处理错误。这种设计使得业务组件完全不需要关心认证和错误处理的通用逻辑。
4. 路由设计
核心原则:整体页面切换(如首页↔登录页)用一级路由,局部区域内容切换(如首页内的 Home/Category)用嵌套子路由。
观察页面结构可以发现:首页、分类页、详情页都共享顶部导航 + 底部 Footer 的外壳(Layout),只有中间内容区不同。因此 Layout 作为一级路由组件,内部通过 <RouterView /> 渲染子路由:
// router/index.ts(初始版本,后续会添加更多路由)
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
component: Layout,
children: [
{ path: '', component: Home }, // 默认子路由 — 首页
{ path: 'category', component: Category }
]
},
{ path: '/login', component: Login } // 登录页不需要 Layout 外壳
]
})5. SCSS 变量自动导入
项目中定义了一组全局 SCSS 变量(品牌色、辅助色、价格色等),通过 vite.config.ts 的 additionalData 配置自动注入到每个 SCSS 文件中,组件内可以直接使用 $xtxColor 等变量,无需手动 @use:
// styles/var.scss — 全局色值变量
$xtxColor: #27ba9b; // 品牌主色
$helpColor: #e26237; // 辅助色
$sucColor: #1dc779; // 成功色
$warnColor: #ffb302; // 警告色
$priceColor: #cf4444; // 价格色