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

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

相关推荐
q_191328469537 分钟前
基于Springboot+uniapp的智慧停车场收费小程序
java·vue.js·spring boot·小程序·uni-app·毕业设计·计算机毕业设计
GISer_Jing41 分钟前
SSE Conf大会分享——大模型驱动的智能 可视分析与故事叙述
前端·人工智能·信息可视化
Lovely Ruby42 分钟前
前端er Go-Frame 的学习笔记:实现 to-do 功能(一)
前端·学习·golang
fruge43 分钟前
Vue3 与 Vue2 核心差异:响应式原理、生命周期及迁移方案
前端·javascript·vue.js
zlpzlpzyd1 小时前
vue.js 3项目整合vue-router 4的问题
前端·javascript·vue.js
crary,记忆1 小时前
Angular.json中的commonChunk 的作用
前端·javascript·学习·angular.js
Highcharts.js1 小时前
Highcharts 仪表板 CSS 样式定制使用说明
前端·css·仪表板·highcharts·css 样式
2501_916007471 小时前
深入理解 iOS 文件管理体系,从沙盒结构到多工具协同的工程化文件管理实践
android·ios·小程序·https·uni-app·iphone·webview