前端面试&项目实战核心知识点总结(Vue3+Pinia+UniApp+Axios)
前言
本文基于真实项目实战(电商管理系统+UniApp壁纸App)和前端面试高频考点,梳理了Vue3生态、状态管理、路由参数、网络请求、跨端适配等核心知识点,包含具体实现代码、问题解决方案及面试延伸思考,适合前端求职者及开发人员参考。
一、Vue3生态核心应用
1. 自定义指令封装(图片懒加载)
核心实现
基于@vueuse/core的useIntersectionObserver钩子封装全局懒加载指令v-img-lazy,相比传统scroll事件监听性能更优(无需频繁计算元素位置)。
js
// 懒加载插件:src/plugins/lazyPlugin.js
import { useIntersectionObserver } from '@vueuse/core'
export const lazyPlugin = {
install(app) {
app.directive('img-lazy', {
mounted(el, binding) {
// 监听元素是否进入视口
const { stop } = useIntersectionObserver(
el,
([{ isIntersecting }]) => {
if (isIntersecting) {
el.src = binding.value // 进入视口后加载真实图片
stop() // 停止监听,避免重复触发
}
},
{ rootMargin: '100px 0px' } // 提前100px监听,解决滚动过快漏加载问题
)
// 图片加载失败兜底
el.addEventListener('error', () => {
el.src = 'error-placeholder.png'
})
},
// 元素卸载前兜底加载
unbind(el, binding) {
if (!el.src || el.src === el.dataset.src) {
el.src = binding.value
}
}
})
}
}
// 全局注册:main.js
import { createApp } from 'vue'
import App from './App.vue'
import { lazyPlugin } from '@/plugins/lazyPlugin'
createApp(App).use(lazyPlugin).mount('#app')
面试延伸
- 为什么用
IntersectionObserver而非scroll事件?
答:scroll事件触发频率高,需手动计算元素位置,性能开销大;IntersectionObserver是浏览器原生API,异步监听元素可见性变化,性能更优。 - 如何解决滚动过快导致图片加载失败?
答:1. 配置rootMargin提前监听;2. 在unbind钩子中兜底加载;3. 结合占位图避免空白。
2. Pinia状态管理(购物车+持久化)
核心设计
- 状态结构设计:包含商品列表、全选状态、总价等核心字段
- 持久化方案:支持手动实现和
pinia-plugin-persistedstate插件两种方式 - 多标签页冲突解决:监听
storage事件同步状态
js
// 购物车Store:src/stores/cartStore.js
import { defineStore } from 'pinia'
export const useCartStore = defineStore('cart', {
state: () => ({
items: JSON.parse(localStorage.getItem('cartItems')) || [], // 商品列表
selectedAll: JSON.parse(localStorage.getItem('cartSelectedAll')) || false, // 全选
totalPrice: 0 // 选中总价
}),
actions: {
// 计算选中商品总价
calcTotalPrice() {
this.totalPrice = this.items.filter(item => item.selected).reduce((sum, item) => sum + item.price * item.quantity, 0)
},
// 同步其他标签页的状态变化
syncFromStorage(e) {
if (e.key === 'cart') {
const newState = JSON.parse(e.newValue)
this.$patch(newState) // 批量更新状态
this.calcTotalPrice()
}
}
},
// 初始化监听storage事件
$onInit() {
window.addEventListener('storage', this.syncFromStorage)
},
// 卸载时移除监听,避免内存泄漏
$onDispose() {
window.removeEventListener('storage', this.syncFromStorage)
},
// 插件持久化配置(推荐)
persist: {
storage: localStorage,
paths: ['items', 'selectedAll'], // 只持久化指定字段
key: 'cart'
}
})
面试延伸
- Pinia相比Vuex的优势?
答:1. 无需嵌套模块,支持扁平化结构;2. 原生支持TypeScript;3. 简化API(无需mutations,直接在actions中修改状态);4. 体积更小。 - 多标签页状态冲突的原因及解决?
答:原因:localStorage是同源共享的,但单个标签页修改后不会主动通知其他标签页。解决:监听storage事件,当localStorage变化时同步当前标签页的Pinia状态。
二、网络请求封装(Axios拦截器)
核心功能
- 请求拦截:统一添加Token、设置超时时间、请求参数序列化
- 响应拦截:统一错误处理、数据解构、重复请求拦截
- 错误兜底:网络错误、4xx/5xx状态码细分处理
js
// Axios实例:src/utils/http.js
import axios from 'axios'
import { ElMessage, ElLoading } from 'element-plus'
import { useUserStore } from '@/stores/userStore'
import router from '@/router'
import qs from 'qs'
// 创建实例
const httpInstance = axios.create({
baseURL: 'http://pcapi-xiaotuxian-front-devtest.itheima.net',
timeout: 50000,
// GET请求参数序列化
paramsSerializer: {
serialize: (params) => qs.stringify(params, { arrayFormat: 'brackets' })
}
})
// 重复请求拦截:维护请求池
const pendingRequests = new Map()
const getRequestKey = (config) => {
return [config.method, config.url, JSON.stringify(config.params), JSON.stringify(config.data)].join('&')
}
// 请求拦截器
httpInstance.interceptors.request.use(config => {
// 重复请求处理
const requestKey = getRequestKey(config)
if (pendingRequests.has(requestKey)) {
pendingRequests.get(requestKey)('重复请求已取消')
pendingRequests.delete(requestKey)
}
// 生成取消函数
config.cancelToken = new axios.CancelToken(cancel => {
pendingRequests.set(requestKey, cancel)
})
// 全局Loading
config.loadingInstance = ElLoading.service({ lock: true, text: '加载中...' })
// 携带Token
const userStore = useUserStore()
if (userStore.userInfo.token) {
config.headers.Authorization = `Bearer ${userStore.userInfo.token}`
}
return config
}, e => Promise.reject(e))
// 响应拦截器
httpInstance.interceptors.response.use(res => {
// 关闭Loading
res.config.loadingInstance.close()
// 移除请求池记录
const requestKey = getRequestKey(res.config)
pendingRequests.delete(requestKey)
// 直接返回响应数据,简化业务层调用
return res.data
}, e => {
// 关闭Loading
e.config?.loadingInstance?.close()
// 移除请求池记录
const requestKey = getRequestKey(e.config || {})
pendingRequests.delete(requestKey)
// 错误提示兜底
const errMsg = e.response?.data?.message || e.message || '网络异常,请稍后重试'
ElMessage({ type: 'warning', message: errMsg })
// 401 Token过期处理
if (e.response?.status === 401) {
const userStore = useUserStore()
userStore.clearUserInfo()
router.push('/login')
}
return Promise.reject(e)
})
export default httpInstance
面试延伸
- 如何处理重复请求?
答:1. 维护一个请求池(Map),存储请求标识和取消函数;2. 请求拦截时检查是否存在重复请求,若存在则取消旧请求;3. 响应完成后移除请求池记录。 - 401和403状态码的区别及处理?
答:401(未授权):Token过期或未携带,需清除用户信息并跳转到登录页;403(权限不足):用户无该操作权限,提示用户并返回上一页。
三、UniApp跨端开发核心
1. 端差异适配(条件编译)
UniApp通过条件编译实现多端(H5、小程序、App)差异化逻辑,核心场景包括下载功能、客服服务、支付分享等。
js
// 下载功能端差异:src/pages/preview/preview.vue
const clickDownLoad = async () => {
// H5端:无原生保存API,提示长按保存
// #ifdef H5
uni.showModal({ content: "请长按保存壁纸", showCancel: false })
// #endif
// 非H5端(小程序/App):调用原生API保存图片
// #ifndef H5
try {
uni.showLoading({ title: "下载中...", mask: true })
const { picurl } = currentInfo.value
// 获取图片本地临时路径
const imgRes = await uni.getImageInfo({ src: picurl })
// 保存到相册
await uni.saveImageToPhotosAlbum({ filePath: imgRes.path })
uni.showToast({ title: "保存成功", icon: "success" })
} catch (err) {
// 权限不足处理
if (err.errMsg.includes('auth')) {
uni.showModal({
title: "提示",
content: "需要相册权限才能保存图片",
success: (modalRes) => {
if (modalRes.confirm) uni.openSetting()
}
})
} else {
uni.showToast({ title: "保存失败", icon: "none" })
}
} finally {
uni.hideLoading()
}
// #endif
}
常用条件编译标识
| 标识 | 说明 |
|---|---|
#ifdef H5 |
仅H5端生效 |
#ifdef MP-WEIXIN |
仅微信小程序生效 |
#ifdef APP-PLUS |
仅App端生效 |
#ifndef H5 |
除H5端外所有端生效 |
2. 路由参数传递(query vs params)
UniApp路由参数传递有两种方式,核心区别在于存储位置、可见性和路由依赖:
1. Query(查询参数)
- 存储位置:URL查询字符串(
?key=value) - 可见性:URL中可见,刷新不丢失
- 适用场景:非敏感、可选参数(如分享标识、筛选条件)
- 示例:
js
// 跳转传递
uni.navigateTo({
url: `/pages/preview/preview?id=${currentId.value}&type=share`
})
// 接收参数(onLoad生命周期)
onLoad(options) {
const id = options.id // 直接从options获取
const type = options.type
}
2. Params(路径参数)
- 存储位置:URL路径片段(需配置动态路由)
- 可见性:URL中可见,刷新不丢失
- 适用场景:核心、必填参数(如商品ID、用户ID)
- 示例:
js
// 1. pages.json配置动态路由
{
"pages": [
{
"path": "pages/detail/:id", // :id为params参数
"style": {}
}
]
}
// 2. 跳转传递
uni.navigateTo({
url: `/pages/detail/${id}`
})
// 3. 接收参数
onLoad(options) {
const id = options.id // 同样从options获取
}
核心区别对比
| 维度 | Query | Params |
|---|---|---|
| 路由配置 | 无需特殊配置 | 需配置动态路由(:key) |
| 必要性 | 可选(不传不影响路由) | 必要(不传路由匹配失败) |
| 适用场景 | 筛选条件、分享标识 | 商品ID、用户ID等核心参数 |
3. 长列表优化(Swiper预加载)
针对UniApp中Swiper长列表滑动卡顿问题,实现图片预加载策略:
js
// src/pages/preview/preview.vue
const readImg = ref([]) // 已加载图片索引集合
const currentIndex = ref(0) // 当前Swiper索引
const ClassList = ref([]) // 壁纸列表数据
// 预加载当前页、上一页、下一页图片
const readImgfun = () => {
const len = ClassList.value.length
readImg.value.push(
currentIndex.value - 1 < 0 ? len - 1 : currentIndex.value - 1, // 上一页(边界处理)
currentIndex.value, // 当前页
currentIndex.value + 1 >= len ? 0 : currentIndex.value + 1 // 下一页(边界处理)
)
readImg.value = [...new Set(readImg.value)] // 去重,避免重复加载
}
// Swiper切换时触发预加载
const swiperChange = (e) => {
currentIndex.value = e.detail.current
readImgfun()
}
// 模板中控制图片渲染
<swiper-item v-for="(item,index) in ClassList" :key="item._id">
<image v-if="readImg.includes(index)" :src="item.picurl" mode="aspectFill"></image>
</swiper-item>
四、面试高频基础概念
1. 骨架屏
- 定义:页面加载过程中的过渡占位UI,用灰色块模拟页面结构
- 核心作用:降低用户等待焦虑、优化视觉过渡、传递页面结构
- 实现方式:纯CSS绘制、组件库(Element Plus
el-skeleton)、图片/SVG - 适用场景:首屏加载慢、数据请求耗时久的页面(电商首页、列表页)
2. 其他基础概念
- 虚拟列表:只渲染可视区域内的列表项,解决长列表卡顿问题(核心思路:计算可视区域索引→动态截取数据→定位列表项)
- 响应式原理:Vue3用
Proxy代理对象,通过track收集依赖、trigger触发更新(相比Vue2Object.defineProperty,支持监听数组索引、新增属性) - Composition API vs Options API:Composition API按功能组织代码,支持TypeScript,解决Options API代码分散问题
五、面试小贴士
- 项目介绍逻辑:技术栈→核心功能→个人负责模块→难点及解决方案→优化点(STAR法则)
- 技术细节准备:每个实现都要清楚"为什么这么做"(如为什么用Pinia而非Vuex、为什么用IntersectionObserver而非scroll)
- 优化思路延伸:从性能(懒加载、虚拟列表)、用户体验(骨架屏、错误提示)、代码可维护性(封装指令、工具函数)三个维度思考
- 跨端开发重点:端差异处理、性能优化、原生API调用限制
总结
本文覆盖了Vue3生态、状态管理、网络请求、跨端开发等前端核心知识点,结合项目实战代码和面试延伸思考,适合作为前端面试复习资料。实际开发中,需注重"原理+实战"结合,不仅要会用,还要理解底层逻辑和优化方案,才能在面试中脱颖而出。
如果本文对你有帮助,欢迎点赞、收藏、转发~ 如有疑问,可在评论区留言交流!