vue前端面试题——记录一次面试当中遇到的题(3)

目录

1.vue3主要是解决vue2的什么问题?

一、解决可维护性和逻辑复用问题(架构问题)

二、解决性能和体积问题(工程问题)

[三、解决 TypeScript 支持问题(开发体验问题)](#三、解决 TypeScript 支持问题(开发体验问题))

四、解决架构灵活性问题(生态问题)

面试回答总结

2.封装一个组件会从那些角度去考虑封装这个组件?举例说明

一、明确组件的职责与边界(核心定位)

[二、设计清晰的输入输出接口(Props & Events)](#二、设计清晰的输入输出接口(Props & Events))

三、提供灵活的插槽机制(Slots)

四、考虑数据流与状态管理

五、注重可访问性与用户体验

六、文档与类型定义

面试回答总结

3.页面针对不同电脑大小的适配

一、核心布局方案

二、相对单位与尺寸策略

三、组件级适配策略

四、工程化与高级方案

五、用户体验优化细节

面试回答要点总结

4.用户进入一个页面,页面加载缓慢,怎么优化?

5.一个页面可能会接收到一个大量的数据,在列表当中显示,怎么优化?

一、基础优化方案

二、高级优化方案

[三、Vue 特定优化技巧](#三、Vue 特定优化技巧)

四、性能监控与调试

五、实际项目中的综合方案

面试回答要点总结

6.现有组件A,组件A当中有组件B和组件C,组件B里面有一个按钮,组件C里面有一个事件,如果想要点击组件B当中按钮,来触发C的事件,有什么方法?(组件通讯)

7.一个页面当中有四个Ecaht图表,可根据自己的喜好,对图表的位置进行拖拽并保存,在下一次进到这个页面时根据自己定义的方案进行加载数据,怎么实现?


1.vue3主要是解决vue2的什么问题?
一、解决可维护性和逻辑复用问题(架构问题)
  • Vue 2 的痛点:选项式 API 与 Mixins

    • 代码组织混乱: 在复杂的组件中,同一个逻辑关注点(例如"用户认证")的代码会被拆分到 data, methods, computed, mounted 等不同的选项中。当组件变得庞大时,理解和维护这些分散的代码非常困难,需要不断上下滚动查看,这被称为"碎片化"问题。

    • 逻辑复用困难: Vue 2 主要的逻辑复用方式是 Mixins。但 Mixins 有致命缺点:

      1. 命名冲突: 多个 Mixin 可能定义了相同的属性或方法,导致冲突。

      2. 数据来源不清晰: 一个组件中使用的属性或方法,很难快速定位是来自哪个 Mixin,可读性差。

      3. 可重用性有限: Mixin 无法接受参数来定制逻辑,缺乏灵活性。

  • Vue 3 的解决方案:组合式 API

    • 更好的代码组织: 允许将与同一逻辑功能相关 的代码(响应式数据、方法、生命周期等)组织在同一个地方。这使得组件可以按功能 而非选项类型来划分,大大提升了复杂组件的可读性和可维护性。

    • 卓越的逻辑复用: 通过 自定义组合函数,可以创建无副作用的、可入参的、类型安全的逻辑函数。这解决了 Mixin 的所有痛点,是实现"高内聚、低耦合"的终极方案。

二、解决性能和体积问题(工程问题)
  • Vue 2 的痛点:

    1. 全量引入: 无论项目用到哪些功能(如 v-model, transition),整个 Vue 核心库都会被打包进去。

    2. 响应式初始化性能: 对于大型嵌套对象,Object.defineProperty 的递归遍历转换会带来不小的初始化性能开销。

    3. 虚拟 DOM 效率: 在组件更新时,需要对比新旧 VNode 树的每个节点,即使其中大部分是永远不会变化的静态内容。

  • Vue 3 的解决方案:

    1. Tree-shaking: 模块被设计为 ES 模块,打包工具可以消除未使用的代码(如没用 v-model,相关代码就不会打包)。这使得 Vue 3 的基础体积比 Vue 2 小 ~40%

    2. 基于 Proxy 的响应式: 初始化时无需递归遍历,只在属性被访问时才惰性转换,性能更好,同时原生支持 Map、Set 等集合类型。

    3. 编译时优化:

      • 静态提升: 将纯静态的节点提升到渲染函数之外,每次渲染时复用,避免重复创建 VNode。

      • Patch Flag: 在编译时标记动态节点及其类型(如只有 class 会变),运行时直接定向对比,跳过大量不必要的递归 Diff。

三、解决 TypeScript 支持问题(开发体验问题)
  • Vue 2 的痛点:

    • Vue 2 的代码是用 ES5 风格的 Flow 编写的,对 TypeScript 的支持是"事后添加"的。这导致 this 的类型推断非常棘手,需要依赖特定的装饰器(如 vue-class-component),并且类型推导常常不完美。
  • Vue 3 的解决方案:

    • 用 TypeScript 重写: Vue 3 的代码库本身就是用 TypeScript 编写的,提供了天生的、完美的类型支持。

    • 组合式 API 的优势: 组合式 API 主要使用普通的变量和函数,几乎完全避免了 this 的上下文问题,使得类型推断变得简单而自然。

四、解决架构灵活性问题(生态问题)
  • Vue 2 的痛点:

    • 内部模块高度耦合,很难将其渲染机制与核心响应式系统分离。这使得创建自定义渲染器(如渲染到 Canvas、小程序)非常困难。
  • Vue 3 的解决方案:

    • 模块化架构: 采用 monorepo 结构,将响应式、运行时、编译器等功能解耦为独立的模块。

    • 自定义渲染器 API: 提供了第一方的 API,允许开发者基于 Vue 的运行时创建针对非 DOM 环境(如 iOS/Android Native、Canvas、WebGL)的渲染器,极大地扩展了 Vue 的应用边界。

面试回答总结
  1. 组合式 API 解决了复杂组件的代码组织和逻辑复用难题。

  2. 基于 Proxy 的响应式系统和编译时优化 解决了大型应用的性能和体积瓶颈。

  3. 用 TypeScript 重写 解决了大规模团队对类型安全和开发体验的迫切需求。

  4. 模块化与自定义渲染器 解决了框架本身在跨端等领域的架构灵活性问题。

2.封装一个组件会从那些角度去考虑封装这个组件?举例说明
一、明确组件的职责与边界(核心定位)

首先问自己:这个组件为什么要存在?它的单一职责是什么?

  • 单一职责原则: 一个组件应该只做好一件事。比如:

    • Button 组件负责触发操作

    • Modal 组件负责展示浮层内容

    • SearchInput 组件负责处理搜索输入和提示

  • 通用性 vs 业务性:

    • 通用组件(UI组件): 与业务无关,可在不同项目中复用,如 Input, Select, Table

    • 业务组件: 包含特定业务逻辑,如 UserProfileCard, OrderList

举例: 封装一个 ImageUploader 组件。它的核心职责应该是"处理图片上传和预览",而不是包含"选择商品分类"这样的额外业务逻辑。

二、设计清晰的输入输出接口(Props & Events)

这是组件与外部世界通信的契约,必须设计得直观且健壮。

  1. Props(输入):

    • 必要的 vs 可选的: 使用 requireddefault 来区分。

    • 类型验证: 使用 TypeScript 或 Vue 的 prop 验证,确保数据类型正确。

    • 合理的默认值: 为可选属性提供符合大多数场景的默认值。

    • 命名规范: 使用小驼峰命名,语义化清晰。

  2. Events(输出):

    • 命名: 使用 kebab-case(如 @update:modelValue),遵循 Vue 约定。

    • 数据: 通过事件参数传递必要的数据,让父组件能做出响应。

三、提供灵活的插槽机制(Slots)

当组件内部结构需要高度定制时,插槽是必不可少的。

  • 默认插槽: 用于主要内容区域。

  • 具名插槽: 用于组件的特定部位。

  • 作用域插槽: 当插槽内容需要访问子组件内部数据时使用。

四、考虑数据流与状态管理
  • 单向数据流: 遵循 Vue 的单向数据流原则,子组件不应该直接修改 props。

  • v-model 支持: 对于需要"双向绑定"的场景,实现 v-model

  • 状态提升: 如果多个组件需要共享状态,考虑将状态提升到共同的父组件。

五、注重可访问性与用户体验
  • 键盘导航: 支持 Tab 键导航和 Enter 键操作。

  • ARIA 属性: 为屏幕阅读器提供必要的语义信息。

  • 加载状态: 提供 loading 状态反馈。

  • 错误处理: 友好的错误提示和恢复机制。

  • 边界情况: 考虑空状态、禁用状态等。

六、文档与类型定义
  • 清晰的注释: 为 props、events 等添加注释。

  • 使用示例: 提供多种使用场景的代码示例。

  • TypeScript 支持: 提供完整的类型定义。

面试回答总结

"在封装一个组件时,我会系统性地从以下几个角度考虑:

  1. 职责明确: 首先确定组件的单一职责和边界,区分是通用组件还是业务组件。

  2. 接口设计: 设计清晰的 Props 和 Events 作为与外部通信的契约,充分考虑类型验证、默认值和语义化命名。

  3. 扩展性: 通过合理的插槽设计(默认插槽、具名插槽、作用域插槽)让组件结构更加灵活可定制。

  4. 数据流: 遵循单向数据流,对需要双向绑定的场景实现 v-model,合理管理组件内部状态。

  5. 用户体验: 注重可访问性、加载状态、错误处理等细节,确保组件友好易用。

  6. 可维护性: 提供完整的类型定义和文档,确保组件易于理解和使用。

以我封装过的 ImageUploader 组件为例 ,我通过 Props 控制文件类型和大小限制,通过 Events 通知上传状态,通过插槽允许自定义触发器和预览样式,并实现了 v-model 来简化图片 URL 的双向绑定。这样的设计让组件既开箱即用,又具备足够的灵活性来适应不同的业务场景。"

3.页面针对不同电脑大小的适配
一、核心布局方案

1. 视口配置(基础前提)

html 复制代码
<!-- 确保视口正确设置,这是所有适配的基础 -->
<meta name="viewport" content="width=device-width, initial-scale=1.0">

2. 响应式布局技术

CSS 媒体查询 - 核心手段

css 复制代码
/* 移动端优先的断点策略 */
/* 默认样式 - 针对小屏幕 */
.container {
  padding: 10px;
  font-size: 14px;
}

/* 平板 */
@media (min-width: 768px) {
  .container {
    padding: 20px;
    font-size: 16px;
  }
}

/* 小桌面 */
@media (min-width: 1024px) {
  .container {
    max-width: 980px;
    margin: 0 auto;
  }
}

/* 大桌面 */
@media (min-width: 1280px) {
  .container {
    max-width: 1200px;
  }
}

/* 超大屏幕 */
@media (min-width: 1920px) {
  .container {
    max-width: 1400px;
  }
}

弹性布局方案:

  • Flexbox - 一维布局

css 复制代码
.nav-menu {
  display: flex;
  flex-wrap: wrap; /* 允许换行 */
  gap: 10px;
}

.nav-item {
  flex: 1; /* 平均分配空间 */
  min-width: 120px; /* 防止过小 */
}

@media (max-width: 768px) {
  .nav-item {
    flex: 0 0 50%; /* 小屏幕每行显示2个 */
  }
}
  • CSS Grid - 二维布局

css 复制代码
.product-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
  gap: 20px;
}

/* 根据屏幕调整列数 */
@media (max-width: 768px) {
  .product-grid {
    grid-template-columns: 1fr;
    gap: 10px;
  }
}
二、相对单位与尺寸策略

1. 相对单位的使用场景

css 复制代码
.container {
  /* 相对于视口宽度 - 适合大布局 */
  width: 90vw;
  max-width: 1200px;
  
  /* 相对于根字体大小 - 适合间距、内边距 */
  padding: 2rem;
  
  /* 相对于父元素 - 适合内部组件 */
  width: 80%;
  
  /* 最小/最大限制 - 防止过度伸缩 */
  min-height: 400px;
  max-width: 100%;
}

/* 响应式字体大小 */
html {
  font-size: 14px;
}

@media (min-width: 768px) {
  html {
    font-size: 16px;
  }
}

.text-content {
  /* 使用rem,会随根字体大小变化 */
  font-size: 1rem;
  line-height: 1.6;
  
  /* 标题使用相对单位 */
  h1 {
    font-size: 2rem; /* 32px在16px根字体下 */
  }
}

2. 图片与媒体的自适应

css 复制代码
.responsive-image {
  max-width: 100%;
  height: auto; /* 保持比例 */
}

.background-section {
  background-image: url('large-bg.jpg');
  background-size: cover;
  background-position: center;
  
  /* 小屏幕使用更小的图片 */
  @media (max-width: 768px) {
    background-image: url('small-bg.jpg');
  }
}

/* 视频容器 */
.video-container {
  position: relative;
  width: 100%;
  height: 0;
  padding-bottom: 56.25%; /* 16:9 比例 */
}

.video-container iframe {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
}
三、组件级适配策略

1. 显示/隐藏特定内容

css 复制代码
/* 移动端隐藏的元素 */
.mobile-hidden {
  display: block;
}

.mobile-only {
  display: none;
}

@media (max-width: 768px) {
  .mobile-hidden {
    display: none;
  }
  
  .mobile-only {
    display: block;
  }
}

2. 导航菜单的适配

javascript 复制代码
<template>
  <!-- 桌面端水平导航 -->
  <nav class="desktop-nav" v-if="!isMobile">
    <a href="/">首页</a>
    <a href="/about">关于</a>
    <a href="/contact">联系</a>
  </nav>
  
  <!-- 移动端汉堡菜单 -->
  <div class="mobile-nav" v-else>
    <button @click="showMenu = !showMenu">☰</button>
    <div class="mobile-menu" v-show="showMenu">
      <a href="/">首页</a>
      <a href="/about">关于</a>
      <a href="/contact">联系</a>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      isMobile: false,
      showMenu: false
    }
  },
  mounted() {
    this.checkScreenSize()
    window.addEventListener('resize', this.checkScreenSize)
  },
  methods: {
    checkScreenSize() {
      this.isMobile = window.innerWidth < 768
    }
  }
}
</script>
四、工程化与高级方案

1. CSS-in-JS 的动态响应式

javascript 复制代码
// 使用 styled-components 或 Emotion
import styled from 'styled-components'

const Container = styled.div`
  padding: 1rem;
  
  ${props => props.theme.breakpoints.up('md')} {
    padding: 2rem;
  }
  
  ${props => props.theme.breakpoints.up('lg')} {
    max-width: 1200px;
    margin: 0 auto;
  }
`

2. 组合式函数检测屏幕变化

javascript 复制代码
// composables/useBreakpoints.js
import { ref, onMounted, onUnmounted } from 'vue'

export function useBreakpoints() {
  const width = ref(window.innerWidth)
  
  const updateWidth = () => {
    width.value = window.innerWidth
  }
  
  onMounted(() => {
    window.addEventListener('resize', updateWidth)
  })
  
  onUnmounted(() => {
    window.removeEventListener('resize', updateWidth)
  })
  
  const breakpoints = {
    isMobile: computed(() => width.value < 768),
    isTablet: computed(() => width.value >= 768 && width.value < 1024),
    isDesktop: computed(() => width.value >= 1024),
    isWidescreen: computed(() => width.value >= 1920)
  }
  
  return { width, ...breakpoints }
}
javascript 复制代码
<script setup>
import { useBreakpoints } from './composables/useBreakpoints'

const { isMobile, isTablet, isDesktop } = useBreakpoints()

// 根据屏幕尺寸返回不同的组件配置
const componentConfig = computed(() => {
  if (isMobile.value) {
    return { columns: 1, showAvatar: false }
  } else if (isTablet.value) {
    return { columns: 2, showAvatar: true }
  } else {
    return { columns: 3, showAvatar: true }
  }
})
</script>
五、用户体验优化细节

1. 触摸友好的交互

javascript 复制代码
/* 移动端增大点击区域 */
.mobile-button {
  min-height: 44px; /* 苹果推荐的最小触摸尺寸 */
  min-width: 44px;
  padding: 12px 16px;
}

/* 防止300ms延迟 */
* {
  touch-action: manipulation;
}

2. 性能考虑

javascript 复制代码
// 防抖的resize监听
function debounce(fn, delay) {
  let timeoutId
  return function(...args) {
    clearTimeout(timeoutId)
    timeoutId = setTimeout(() => fn.apply(this, args), delay)
  }
}

window.addEventListener('resize', debounce(checkScreenSize, 250))
面试回答要点总结

"总结我的适配策略:

  1. 基础配置: 正确设置视口,采用移动端优先的媒体查询

  2. 布局技术: 结合 Flexbox 和 Grid 实现弹性布局

  3. 相对单位: 合理运用 vw、rem、% 等单位,配合 min/max 限制

  4. 组件思维: 组件内部根据屏幕尺寸调整行为和样式

  5. 工程化: 通过组合式函数和设计系统统一管理断点

  6. 用户体验: 考虑触摸交互、加载性能等细节问题

核心原则是: 不是让所有屏幕看起来一样,而是让每个屏幕尺寸都有最佳的阅读和交互体验。"

4.用户进入一个页面,页面加载缓慢,怎么优化?
  1. 诊断先行: 使用专业工具准确找到性能瓶颈

  2. 网络优化: 代码分割、懒加载、资源压缩、CDN

  3. Vue应用优化: 组件懒加载、计算属性、合理使用v-if/v-show、避免不必要的响应式

  4. 构建优化: Tree shaking、分块打包、按需引入

  5. 用户体验: 骨架屏、渐进加载、图片优化

  6. 服务端考虑: SSR、缓存策略、API优化

5.一个页面可能会接收到一个大量的数据,在列表当中显示,怎么优化?
一、基础优化方案

1. 分页加载 - 最常用方案

2.无限滚动 - 用户体验更好

二、高级优化方案

3. 虚拟滚动 - 处理超大数据集的核心方案

javascript 复制代码
<template>
  <div class="virtual-scroll-container" @scroll="handleScroll" ref="container">
    <!-- 滚动占位容器,撑开滚动条 -->
    <div class="scroll-phantom" :style="{ height: totalHeight + 'px' }"></div>
    
    <!-- 可视区域的内容 -->
    <div class="visible-content" :style="{ transform: `translateY(${offsetY}px)` }">
      <div 
        v-for="item in visibleData" 
        :key="item.id" 
        class="item"
        :style="{ height: itemHeight + 'px', lineHeight: itemHeight + 'px' }"
      >
        {{ item.name }} - {{ item.id }}
      </div>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      allData: [],
      itemHeight: 50, // 每个列表项的高度
      visibleCount: 0, // 可视区域能显示的数量
      startIndex: 0, // 起始索引
      endIndex: 0, // 结束索引
      offsetY: 0 // Y轴偏移量
    }
  },
  
  computed: {
    // 列表总高度
    totalHeight() {
      return this.allData.length * this.itemHeight
    },
    
    // 可视区域的数据
    visibleData() {
      return this.allData.slice(this.startIndex, this.endIndex)
    }
  },
  
  methods: {
    handleScroll() {
      const scrollTop = this.$refs.container.scrollTop
      
      // 计算起始索引
      this.startIndex = Math.floor(scrollTop / this.itemHeight)
      
      // 计算结束索引(多渲染一些项,避免滚动时空白)
      this.endIndex = this.startIndex + this.visibleCount + 5
      
      // 计算偏移量
      this.offsetY = this.startIndex * this.itemHeight
    },
    
    calculateVisibleCount() {
      const containerHeight = this.$refs.container.clientHeight
      this.visibleCount = Math.ceil(containerHeight / this.itemHeight)
      this.endIndex = this.startIndex + this.visibleCount + 5 // 预渲染5个
    },
    
    // 使用第三方虚拟滚动库(如 vue-virtual-scroller)
    useVirtualScrollerLibrary() {
      // 安装: npm install vue-virtual-scroller
      // 使用 RecycleScroller 或 DynamicScroller
    }
  },
  
  async mounted() {
    this.allData = await this.fetchAllData()
    
    // 等待DOM更新后计算可视数量
    this.$nextTick(() => {
      this.calculateVisibleCount()
      // 监听窗口大小变化
      window.addEventListener('resize', this.calculateVisibleCount)
    })
  },
  
  beforeUnmount() {
    window.removeEventListener('resize', this.calculateVisibleCount)
  }
}
</script>

<style>
.virtual-scroll-container {
  height: 500px;
  overflow-y: auto;
  position: relative;
}

.scroll-phantom {
  position: absolute;
  left: 0;
  top: 0;
  right: 0;
  z-index: -1;
}

.visible-content {
  position: absolute;
  left: 0;
  top: 0;
  right: 0;
}

.item {
  border-bottom: 1px solid #eee;
  padding: 0 10px;
  box-sizing: border-box;
}
</style>
三、Vue 特定优化技巧

4. 响应式数据优化

javascript 复制代码
export default {
  data() {
    return {
      // 对于纯展示的大数据,使用 Object.freeze 避免响应式开销
      largeData: Object.freeze(largeDataSet),
      
      // 或者使用 shallowRef 只对第一层做响应式
      largeData: shallowRef(largeDataSet)
    }
  },
  
  methods: {
    // 使用函数式组件避免不必要的响应式
    functionalItemRenderer(h, { item }) {
      return h('div', { class: 'item' }, item.name)
    }
  }
}

5. 计算属性和方法优化

javascript 复制代码
export default {
  computed: {
    // 使用计算属性缓存过滤/排序结果
    filteredData() {
      // 复杂的过滤逻辑在这里执行,结果会被缓存
      return this.allData.filter(item => 
        item.name.includes(this.searchKeyword) &&
        item.status === this.selectedStatus
      )
    }
  },
  
  methods: {
    // 防抖搜索
    handleSearch: _.debounce(function(keyword) {
      this.searchKeyword = keyword
    }, 300),
    
    // 使用 Web Worker 处理复杂计算
    processDataWithWorker() {
      const worker = new Worker('./data-processor.js')
      worker.postMessage(this.rawData)
      worker.onmessage = (event) => {
        this.processedData = event.data
      }
    }
  }
}
四、性能监控与调试

6. 性能检测工具

javascript 复制代码
// 在组件中监控渲染性能
export default {
  mounted() {
    // 使用 Performance Observer 监控长任务
    const observer = new PerformanceObserver((list) => {
      for (const entry of list.getEntries()) {
        if (entry.duration > 50) { // 超过50ms的任务
          console.warn('长任务 detected:', entry)
        }
      }
    })
    observer.observe({ entryTypes: ['longtask'] })
  },
  
  // 自定义渲染时间监控
  beforeUpdate() {
    this._startTime = performance.now()
  },
  
  updated() {
    const renderTime = performance.now() - this._startTime
    if (renderTime > 16) { // 超过一帧的时间
      console.warn(`组件渲染耗时: ${renderTime.toFixed(2)}ms`)
    }
  }
}
五、实际项目中的综合方案

7. 根据数据量选择策略

javascript 复制代码
// 数据量分级处理策略
const optimizationStrategy = {
  // 小数据量 (< 1000条): 直接渲染 + 基础优化
  small: (data) => ({
    strategy: 'direct-render',
    optimizations: ['object-freeze', 'keyed-v-for']
  }),
  
  // 中等数据量 (1000-10000条): 分页/无限滚动
  medium: (data) => ({
    strategy: 'pagination',
    pageSize: 100,
    optimizations: ['lazy-loading', 'debounced-search']
  }),
  
  // 大数据量 (> 10000条): 虚拟滚动
  large: (data) => ({
    strategy: 'virtual-scroll',
    itemHeight: 50,
    bufferSize: 10,
    optimizations: ['web-worker-processing', 'chunked-rendering']
  })
}

function getOptimizationStrategy(data) {
  if (data.length < 1000) return optimizationStrategy.small(data)
  if (data.length < 10000) return optimizationStrategy.medium(data)
  return optimizationStrategy.large(data)
}
面试回答要点总结

"总结我的大数据列表优化策略:

  1. 分级处理: 根据数据量选择合适方案

    • < 1000条: 直接渲染 + Object.freeze + 合适的 key

    • 1000-10000条: 分页加载或无限滚动

    • > 10000条: 虚拟滚动是必须的

  2. 核心技术:

    • 虚拟滚动: 只渲染可视区域,处理海量数据

    • 分页/无限加载: 控制单次渲染的数据量

    • 响应式优化: 使用 Object.freezeshallowRef 减少开销

  3. 辅助优化:

    • 防抖搜索: 避免频繁触发重渲染

    • Web Worker: 复杂计算不阻塞主线程

    • 性能监控: 及时发现性能瓶颈

  4. 用户体验:

    • 骨架屏: 加载状态反馈

    • 平滑滚动: 避免跳动和卡顿

    • 错误边界: 优雅降级

选择建议:

  • 需要精确跳转 → 分页

  • 需要流畅浏览 → 无限滚动

  • 数据量极大 → 虚拟滚动

  • 不确定数据量 → 自适应策略

6.现有组件A,组件A当中有组件B和组件C,组件B里面有一个按钮,组件C里面有一个事件,如果想要点击组件B当中按钮,来触发C的事件,有什么方法?(组件通讯)
  1. 简单父子关系 :使用 Props + EventsRefs

  2. 复杂组件树 :使用 provide/inject事件总线

  3. 大型应用,需要状态共享 :使用 Vuex/Pinia

  4. 需要高度解耦 :使用 事件总线Composables

  5. Vue3 项目 :优先考虑 provide/inject + 响应式Pinia

7.一个页面当中有多个Ecaht图表,可根据自己的喜好,对图表的位置进行拖拽并保存,在下一次进到这个页面时根据自己定义的方案进行加载数据,怎么实现?

这是一个典型的可定制化仪表盘需求,我会从前端架构、拖拽实现、状态管理和数据加载四个核心层面来设计解决方案

一、技术选型与架构设计

1. 技术栈选择

拖拽库:

react-grid-layout

该库支持二维可调整尺寸的列表,适用于需要动态调整元素位置和尺寸的场景。其API设计简洁,兼容性好,适合快速构建响应式界面。 ‌

draggable

提供基础拖拽功能,支持与React、Vue等主流框架无缝集成。最新版本(2025年6月更新)优化了类型系统支持,并修复了多个兼容性问题。 ‌

vue-resize-handle

专为Vue.js设计,支持通过拖拽手柄调整元素尺寸。适用于需要精细控制元素尺寸的场景,例如界面布局设计工具。 ‌

javascript 复制代码
// 推荐技术栈
{
  framework: 'Vue 3',          // 响应式能力优秀
  dragLibrary: 'VueDraggable',  // 或 SortableJS
  charts: 'ECharts 5+',        // 功能丰富,API稳定
  storage: 'localStorage',      // 或 IndexedDB / 后端存储
  stateManagement: 'Pinia',     // 状态管理
  layout: 'CSS Grid',           // 灵活的布局系统
}

2. 组件架构设计

javascript 复制代码
// 组件结构
DashboardPage/
├── DashboardLayout.vue     // 布局容器
├── ChartContainer.vue      // 图表容器组件
├── ChartSettings.vue       // 图表配置面板
└── hooks/
    ├── useDragAndDrop.js   // 拖拽逻辑
    ├── useChartLayout.js   // 布局管理
    └── useChartData.js     // 数据加载
二、核心实现方案

1. 拖拽布局系统实现

javascript 复制代码
<!-- DashboardLayout.vue -->
<template>
  <div class="dashboard-layout" :style="gridStyle">
    <ChartContainer
      v-for="chart in charts"
      :key="chart.id"
      :chart-config="chart"
      :style="getChartStyle(chart)"
      @drag-start="handleDragStart"
      @drag-end="handleDragEnd"
      @position-change="handlePositionChange"
    />
  </div>
</template>

<script setup>
import { ref, computed, onMounted } from 'vue'
import { useChartLayout } from '../hooks/useChartLayout'

const { 
  charts, 
  gridConfig, 
  updateChartPosition,
  saveLayout 
} = useChartLayout()

// CSS Grid 布局样式
const gridStyle = computed(() => ({
  display: 'grid',
  gridTemplateColumns: `repeat(${gridConfig.value.columns}, 1fr)`,
  gridTemplateRows: `repeat(${gridConfig.value.rows}, 200px)`,
  gap: `${gridConfig.value.gap}px`
}))

// 获取图表位置样式
const getChartStyle = (chart) => ({
  gridColumn: `${chart.position.colStart} / ${chart.position.colEnd}`,
  gridRow: `${chart.position.rowStart} / ${chart.position.rowEnd}`,
  cursor: 'move'
})

// 拖拽事件处理
const handlePositionChange = (chartId, newPosition) => {
  updateChartPosition(chartId, newPosition)
}

// 防抖保存布局
const saveLayoutDebounced = useDebounceFn(() => {
  saveLayout()
}, 500)
</script>

<style scoped>
.dashboard-layout {
  min-height: 600px;
  position: relative;
}

/* 拖拽时的视觉反馈 */
.dashboard-layout.dragging {
  cursor: grabbing;
}
</style>

2. 可拖拽图表容器组件

javascript 复制代码
<!-- ChartContainer.vue -->
<template>
  <div
    class="chart-container"
    :class="{ 'is-dragging': isDragging }"
    draggable="true"
    @dragstart="handleDragStart"
    @dragend="handleDragEnd"
    @dragover="handleDragOver"
    @drop="handleDrop"
  >
    <!-- 图表标题栏 -->
    <div class="chart-header" @mousedown="startDrag">
      <h3>{{ chartConfig.title }}</h3>
      <div class="chart-actions">
        <button @click="refreshChart">↻</button>
        <button @click="showSettings">⚙</button>
      </div>
    </div>
    
    <!-- ECharts 容器 -->
    <div ref="chartEl" class="chart-content"></div>
    
    <!-- 拖拽手柄 -->
    <div class="drag-handle" @mousedown="startDrag">⋮⋮</div>
    
    <!-- 尺寸调整手柄 -->
    <div
      v-for="handle in resizeHandles"
      :key="handle"
      class="resize-handle"
      :class="handle"
      @mousedown="startResize($event, handle)"
    ></div>
  </div>
</template>

<script setup>
import { ref, onMounted, onUnmounted, nextTick } from 'vue'
import * as echarts from 'echarts'

const props = defineProps({
  chartConfig: {
    type: Object,
    required: true
  }
})

const emit = defineEmits(['position-change', 'drag-start', 'drag-end'])

const chartEl = ref(null)
const chartInstance = ref(null)
const isDragging = ref(false)

// 初始化图表
const initChart = async () => {
  await nextTick()
  
  if (!chartEl.value) return
  
  chartInstance.value = echarts.init(chartEl.value)
  
  // 加载图表数据
  const option = await loadChartData(props.chartConfig)
  chartInstance.value.setOption(option)
  
  // 响应式调整
  window.addEventListener('resize', handleResize)
}

// 拖拽逻辑
const startDrag = (e) => {
  isDragging.value = true
  emit('drag-start', props.chartConfig.id)
  
  // 设置拖拽数据
  e.dataTransfer.setData('text/plain', props.chartConfig.id)
  e.dataTransfer.effectAllowed = 'move'
}

const handleDragOver = (e) => {
  e.preventDefault()
  e.dataTransfer.dropEffect = 'move'
}

const handleDrop = (e) => {
  e.preventDefault()
  const sourceChartId = e.dataTransfer.getData('text/plain')
  const targetChartId = props.chartConfig.id
  
  if (sourceChartId !== targetChartId) {
    // 交换位置
    emit('position-change', sourceChartId, props.chartConfig.position)
  }
  
  isDragging.value = false
  emit('drag-end')
}

const handleDragEnd = () => {
  isDragging.value = false
  emit('drag-end')
}

// 尺寸调整手柄
const resizeHandles = ['n', 's', 'e', 'w', 'ne', 'nw', 'se', 'sw']

const startResize = (e, direction) => {
  e.stopPropagation()
  // 实现尺寸调整逻辑
  console.log('开始调整尺寸:', direction)
}

onMounted(() => {
  initChart()
})

onUnmounted(() => {
  if (chartInstance.value) {
    chartInstance.value.dispose()
  }
  window.removeEventListener('resize', handleResize)
})
</script>

<style scoped>
.chart-container {
  border: 1px solid #e1e4e8;
  border-radius: 8px;
  background: white;
  position: relative;
  transition: all 0.2s ease;
}

.chart-container.is-dragging {
  opacity: 0.7;
  transform: rotate(3deg);
  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
}

.chart-header {
  padding: 12px;
  border-bottom: 1px solid #e1e4e8;
  display: flex;
  justify-content: space-between;
  align-items: center;
  cursor: move;
  background: #fafbfc;
}

.chart-content {
  height: calc(100% - 50px);
  width: 100%;
}

.drag-handle {
  position: absolute;
  top: 12px;
  right: 12px;
  cursor: move;
  opacity: 0.5;
  font-size: 16px;
}

.resize-handle {
  position: absolute;
  background: #007acc;
  opacity: 0;
  transition: opacity 0.2s;
}

.chart-container:hover .resize-handle {
  opacity: 1;
}

.resize-handle.n { top: -2px; left: 0; right: 0; height: 4px; cursor: n-resize; }
.resize-handle.s { bottom: -2px; left: 0; right: 0; height: 4px; cursor: s-resize; }
.resize-handle.e { top: 0; bottom: 0; right: -2px; width: 4px; cursor: e-resize; }
.resize-handle.w { top: 0; bottom: 0; left: -2px; width: 4px; cursor: w-resize; }
.resize-handle.ne { top: -2px; right: -2px; width: 8px; height: 8px; cursor: ne-resize; }
.resize-handle.nw { top: -2px; left: -2px; width: 8px; height: 8px; cursor: nw-resize; }
.resize-handle.se { bottom: -2px; right: -2px; width: 8px; height: 8px; cursor: se-resize; }
.resize-handle.sw { bottom: -2px; left: -2px; width: 8px; height: 8px; cursor: sw-resize; }
</style>

3. 布局状态管理(Composition API)

javascript 复制代码
// hooks/useChartLayout.js
import { ref, computed, onMounted } from 'vue'
import { useStorage } from '@vueuse/core'

export function useChartLayout() {
  // 从本地存储加载布局,没有则使用默认布局
  const layoutStorage = useStorage('dashboard-layout', getDefaultLayout())
  
  const charts = ref(layoutStorage.value)
  const gridConfig = ref({
    columns: 4,
    rows: 3,
    gap: 16
  })

  // 默认布局配置
  function getDefaultLayout() {
    return [
      {
        id: 'chart-1',
        type: 'line',
        title: '销售趋势',
        position: { colStart: 1, colEnd: 3, rowStart: 1, rowEnd: 2 },
        dataSource: 'sales-trend'
      },
      {
        id: 'chart-2',
        type: 'bar',
        title: '用户分布',
        position: { colStart: 3, colEnd: 5, rowStart: 1, rowEnd: 2 },
        dataSource: 'user-distribution'
      },
      {
        id: 'chart-3',
        type: 'pie',
        title: '产品占比',
        position: { colStart: 1, colEnd: 3, rowStart: 2, rowEnd: 3 },
        dataSource: 'product-ratio'
      },
      {
        id: 'chart-4',
        type: 'scatter',
        title: '性能指标',
        position: { colStart: 3, colEnd: 5, rowStart: 2, rowEnd: 3 },
        dataSource: 'performance-metrics'
      }
    ]
  }

  // 更新图表位置
  const updateChartPosition = (chartId, newPosition) => {
    const chartIndex = charts.value.findIndex(chart => chart.id === chartId)
    if (chartIndex !== -1) {
      charts.value[chartIndex].position = newPosition
      saveLayout()
    }
  }

  // 交换两个图表的位置
  const swapChartPositions = (sourceId, targetId) => {
    const sourceIndex = charts.value.findIndex(chart => chart.id === sourceId)
    const targetIndex = charts.value.findIndex(chart => chart.id === targetId)
    
    if (sourceIndex !== -1 && targetIndex !== -1) {
      const tempPosition = { ...charts.value[sourceIndex].position }
      charts.value[sourceIndex].position = { ...charts.value[targetIndex].position }
      charts.value[targetIndex].position = tempPosition
      saveLayout()
    }
  }

  // 保存布局到本地存储
  const saveLayout = () => {
    layoutStorage.value = charts.value
  }

  // 重置为默认布局
  const resetLayout = () => {
    charts.value = getDefaultLayout()
    saveLayout()
  }

  return {
    charts,
    gridConfig,
    updateChartPosition,
    swapChartPositions,
    saveLayout,
    resetLayout
  }
}

4. 数据加载策略

javascript 复制代码
// hooks/useChartData.js
import { ref } from 'vue'

// 图表数据加载器
const chartDataLoaders = {
  'sales-trend': async () => {
    const response = await fetch('/api/charts/sales-trend')
    return await response.json()
  },
  'user-distribution': async () => {
    const response = await fetch('/api/charts/user-distribution')
    return await response.json()
  },
  'product-ratio': async () => {
    const response = await fetch('/api/charts/product-ratio')
    return await response.json()
  },
  'performance-metrics': async () => {
    const response = await fetch('/api/charts/performance-metrics')
    return await response.json()
  }
}

// ECharts 配置生成器
const chartOptionGenerators = {
  line: (data) => ({
    title: { text: data.title },
    tooltip: { trigger: 'axis' },
    xAxis: { type: 'category', data: data.categories },
    yAxis: { type: 'value' },
    series: [{ data: data.values, type: 'line' }]
  }),
  
  bar: (data) => ({
    title: { text: data.title },
    tooltip: { trigger: 'axis' },
    xAxis: { type: 'category', data: data.categories },
    yAxis: { type: 'value' },
    series: [{ data: data.values, type: 'bar' }]
  }),
  
  pie: (data) => ({
    title: { text: data.title },
    tooltip: { trigger: 'item' },
    series: [{
      type: 'pie',
      data: data.items,
      emphasis: { itemStyle: { shadowBlur: 10, shadowOffsetX: 0, shadowColor: 'rgba(0, 0, 0, 0.5)' } }
    }]
  }),
  
  scatter: (data) => ({
    title: { text: data.title },
    tooltip: { trigger: 'item' },
    xAxis: { type: 'value' },
    yAxis: { type: 'value' },
    series: [{
      data: data.points,
      type: 'scatter',
      symbolSize: (data) => Math.sqrt(data[2]) * 2
    }]
  })
})

export function useChartData() {
  const loadingStates = ref({})
  const errorStates = ref({})

  const loadChartData = async (chartConfig) => {
    const { id, type, dataSource } = chartConfig
    
    loadingStates.value[id] = true
    errorStates.value[id] = null
    
    try {
      // 加载数据
      const rawData = await chartDataLoaders[dataSource]()
      
      // 生成 ECharts 配置
      const option = chartOptionGenerators[type](rawData)
      
      return option
    } catch (error) {
      errorStates.value[id] = error.message
      console.error(`加载图表 ${id} 数据失败:`, error)
      
      // 返回错误状态的配置
      return {
        title: { text: chartConfig.title, textStyle: { color: '#ff4d4f' } },
        graphic: {
          type: 'text',
          left: 'center',
          top: 'middle',
          style: { text: '数据加载失败', fill: '#ff4d4f', fontSize: 14 }
        }
      }
    } finally {
      loadingStates.value[id] = false
    }
  }

  const refreshChartData = async (chartId) => {
    // 重新加载指定图表的数据
    // 在实际实现中,这里会触发对应图表的重新渲染
  }

  return {
    loadingStates,
    errorStates,
    loadChartData,
    refreshChartData
  }
}
三、高级特性实现

5. 布局模板系统

javascript 复制代码
// utils/layoutTemplates.js
export const layoutTemplates = {
  grid2x2: {
    name: '2x2网格',
    layout: [
      { colStart: 1, colEnd: 3, rowStart: 1, rowEnd: 2 },
      { colStart: 3, colEnd: 5, rowStart: 1, rowEnd: 2 },
      { colStart: 1, colEnd: 3, rowStart: 2, rowEnd: 3 },
      { colStart: 3, colEnd: 5, rowStart: 2, rowEnd: 3 }
    ]
  },
  focus: {
    name: '焦点布局',
    layout: [
      { colStart: 1, colEnd: 4, rowStart: 1, rowEnd: 2 },
      { colStart: 4, colEnd: 5, rowStart: 1, rowEnd: 2 },
      { colStart: 1, colEnd: 3, rowStart: 2, rowEnd: 3 },
      { colStart: 3, colEnd: 5, rowStart: 2, rowEnd: 3 }
    ]
  }
}

export function applyLayoutTemplate(charts, templateName) {
  const template = layoutTemplates[templateName]
  if (!template) return charts
  
  return charts.map((chart, index) => ({
    ...chart,
    position: template.layout[index] || chart.position
  }))
}
四、性能优化策略

7. 性能优化措施

javascript 复制代码
// hooks/useDashboardPerformance.js
import { debounce } from 'lodash-es'

export function useDashboardPerformance() {
  // 图表防抖重渲染
  const debouncedChartResize = debounce((chartInstance) => {
    if (chartInstance && !chartInstance._disposed) {
      chartInstance.resize()
    }
  }, 300)
  
  // 虚拟滚动(如果图表数量很多)
  const useVirtualCharts = (charts, containerRef) => {
    const visibleCharts = ref([])
    
    const updateVisibleCharts = () => {
      if (!containerRef.value) return
      
      const containerRect = containerRef.value.getBoundingClientRect()
      visibleCharts.value = charts.filter(chart => {
        // 计算图表是否在可视区域内
        return isElementInViewport(chart, containerRect)
      })
    }
    
    return { visibleCharts, updateVisibleCharts }
  }
  
  // 图表数据缓存
  const chartDataCache = new Map()
  
  const getCachedChartData = async (dataSource) => {
    const cacheKey = `${dataSource}-${new Date().toDateString()}`
    
    if (chartDataCache.has(cacheKey)) {
      return chartDataCache.get(cacheKey)
    }
    
    const data = await chartDataLoaders[dataSource]()
    chartDataCache.set(cacheKey, data)
    
    return data
  }
  
  return {
    debouncedChartResize,
    useVirtualCharts,
    getCachedChartData
  }
}
面试回答要点总结

"总结我的实现方案:

  1. 架构设计:采用组件化架构,分离布局、图表、数据逻辑

  2. 拖拽实现:使用 HTML5 Drag & Drop API 或第三方库实现流畅拖拽

  3. 布局系统:基于 CSS Grid 的灵活布局,支持位置交换和尺寸调整

  4. 状态持久化:本地存储 + 可选后端同步,确保布局持久化

  5. 数据加载:按需加载 + 缓存策略,优化性能体验

  6. 用户体验:拖拽反馈、加载状态、错误处理、布局模板

相关推荐
道可到3 小时前
写了这么多代码,你真的在进步吗??—一个前端人的反思与全栈突围路线
前端
洛克大航海3 小时前
Ajax基本使用
java·javascript·ajax·okhttp
用户916357440953 小时前
LeetCode热题100——11.盛最多水的容器
javascript·算法
凡大来啦4 小时前
v-for渲染的元素上使用ref
前端·javascript·vue.js
道可到4 小时前
前端开发的生存法则:如何从“像素工人”进化为价值创造者?
前端
eggcode4 小时前
Vue前端开发学习的简单记录
vue.js·学习
中微子4 小时前
TypeScript 泛型与 ReturnType 详解
前端
我叫张得帅4 小时前
从零开始的前端异世界生活--003--“探究Domain,DNS,Hosting”
前端
一大树4 小时前
H5在不同操作系统与浏览器中的兼容性挑战及全面解决方案
前端·ios