前端面试&项目实战核心知识点总结(Vue3+Pinia+UniApp+Axios)

前端面试&项目实战核心知识点总结(Vue3+Pinia+UniApp+Axios)

前言

本文基于真实项目实战(电商管理系统+UniApp壁纸App)和前端面试高频考点,梳理了Vue3生态、状态管理、路由参数、网络请求、跨端适配等核心知识点,包含具体实现代码、问题解决方案及面试延伸思考,适合前端求职者及开发人员参考。

一、Vue3生态核心应用

1. 自定义指令封装(图片懒加载)

核心实现

基于@vueuse/coreuseIntersectionObserver钩子封装全局懒加载指令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触发更新(相比Vue2 Object.defineProperty,支持监听数组索引、新增属性)
  • Composition API vs Options API:Composition API按功能组织代码,支持TypeScript,解决Options API代码分散问题

五、面试小贴士

  1. 项目介绍逻辑:技术栈→核心功能→个人负责模块→难点及解决方案→优化点(STAR法则)
  2. 技术细节准备:每个实现都要清楚"为什么这么做"(如为什么用Pinia而非Vuex、为什么用IntersectionObserver而非scroll)
  3. 优化思路延伸:从性能(懒加载、虚拟列表)、用户体验(骨架屏、错误提示)、代码可维护性(封装指令、工具函数)三个维度思考
  4. 跨端开发重点:端差异处理、性能优化、原生API调用限制

总结

本文覆盖了Vue3生态、状态管理、网络请求、跨端开发等前端核心知识点,结合项目实战代码和面试延伸思考,适合作为前端面试复习资料。实际开发中,需注重"原理+实战"结合,不仅要会用,还要理解底层逻辑和优化方案,才能在面试中脱颖而出。

如果本文对你有帮助,欢迎点赞、收藏、转发~ 如有疑问,可在评论区留言交流!

相关推荐
崔庆才丨静觅13 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby606114 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了14 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅14 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅14 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅15 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment15 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅15 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊15 小时前
jwt介绍
前端
爱敲代码的小鱼15 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax