简历

58 阅读8分钟

一、vue 相关

1. Pinia vs Vuex

相同点

都是 Vue 项目全局状态管理工具:比如登录用户信息、购物车、全局弹窗状态,组件之间要共享的数据,不用父子一层层传,统一存在这里。

  • Vuex:老版 Vue 官方工具(Vue2 时代主流)
  • Pinia:新版官方推荐(Vue3 专用,Vue2 也能用,官方说以后主推 Pinia,Vuex 不再维护更新

核心区别,一条一条说人话

代码结构:Vuex 繁琐,Pinia 清爽

Vuex 四件套强制拆分,写起来啰嗦

固定拆 4 块文件,必须分开写:

  1. state:存数据
  2. getters:计算属性(拿加工后的数据)
  3. mutations:唯一改数据的地方(必须同步)
  4. actions:异步操作(请求接口)

改个数据流程:组件 → dispatch (action) → commit (mutation) → 修改 state哪怕简单赋值,也要绕两层,代码拆分多、文件碎。

Pinia 一锅端,不用强制拆分

一个 store 文件搞定全部,没有强制四件套:

  • state 直接定义变量
  • getters 就是普通函数
  • actions 既能同步改数据,又能异步请求,不用区分 mutation改数据流程:组件 → 直接调用 action / 甚至直接改 state少写一半模板代码,不用新建一堆文件。

2. TypeScript 类型支持:Pinia 天生舒服,Vuex 巨难写类型

  • Vuex:TS 支持拉胯,写类型要额外加大量辅助代码,嵌套多类型容易丢失,新手写起来头疼。
  • Pinia:原生为 TS 设计,自动推导类型,写代码自带提示,不用额外定义复杂类型。

3. 模块命名空间:Vuex 容易踩坑,Pinia 天然隔离

Vuex

多模块时默认全局,不写 namespaced: true 就会命名冲突;写了之后调用要加超长前缀,代码冗长。

Pinia

每个 defineStore 就是独立模块,自带隔离,互不干扰,直接导入就能用,不用配置命名空间。

4. 使用方式:Vuex 一堆辅助函数,Pinia 原生简单

uex 组件内使用

要记一堆辅助函数:mapState/mapGetters/mapMutations/mapActions,选项式还好,setup 语法写起来更绕。

Pinia 组件内使用

直接导入 store 实例,点 . 就能拿数据、调用方法;搭配 storeToRefs 轻松解构,没有复杂辅助函数。

5. 调试工具 Devtools

  • Vuex:功能够用,但模块嵌套后查看不方便
  • Pinia:Devtools 体验更好,直接看每个 store 变化,时间旅行、记录修改一目了然

6. 冗余规则:Vuex 一堆硬性限制,Pinia 自由

Vuex 强制规则(新手极易踩坑):

  1. 只能在 mutation 里修改 state,action 不能直接改
  2. mutation 必须同步,异步代码只能放 action违反规则调试器不追踪、数据变更难排查

Pinia 无强制约束:

  1. action 同步 / 异步随便写,直接修改 state
  2. 简单场景甚至组件里直接 store.xxx = 123 修改数据(规范项目还是推荐放 action)规则更少,学习门槛低。

7. 体积与维护

  • Vuex:官方已停止维护,不再更新新功能,只修严重 bug
  • Pinia:Vue 官方团队持续维护,是未来标准,Vue3 项目首选

三、极简总结(一句话区分)

  1. 新项目 Vue3:直接用 Pinia,代码少、类型友好、上手简单,官方主推;
  2. 老项目 Vue2 遗留:还在用 Vuex 不用强行重构,新项目别再写 Vuex;
  3. 核心痛点对比:Vuex 强制四分层、规则多、TS 难用;Pinia 简化 API、去掉冗余限制、天生适配 Vue3+TS。

简单示例直观感受

Vuex 修改数据(繁琐)

// store/mutations.js
export function setUser(state, val) {
  state.user = val
}
// store/actions.js
export async function getUser({ commit }) {
  const res = await api.login()
  commit('setUser', res.data)
}

// 组件调用
this.$store.dispatch('getUser')

Pinia 修改数据(简洁)

// user.js store
export const useUserStore = defineStore('user', {
  state: () => ({ user: null }),
  actions: {
    async getUser() {
      const res = await api.login()
      this.user = res.data // 直接改,不用mutation
    }
  }
})

// 组件调用
const userStore = useUserStore()
userStore.getUser()

2. mixins混入(Vue2 专属,Vue3 已废弃)

基础概念(大白话)

mixins 就是代码片段复用方案,把多个页面重复的 data、methods、生命周期钩子抽成单独 js 文件,页面引入后自动合并到自身组件逻辑,不用重复写一模一样的代码。比如多个列表页都有分页、搜索重置、导出按钮逻辑,抽成一个 listMixin,所有页面直接引入复用。

使用方式极简说明

  1. 新建混入文件,写通用 data、方法、钩子;
  2. 组件配置 mixins: [xxxMixin] 数组,支持引入多个混入;
  3. 组件会自动合并混入内所有逻辑,直接调用混入里的方法、变量。

合并规则(面试官必问核心考点)

  1. 生命周期钩子:全部执行混入的钩子 和 组件自身钩子 都会触发,先执行混入,再执行组件本身;多个混入按数组顺序依次执行。
  2. data、methods、计算属性:组件优先级更高如果混入和组件出现同名变量 / 函数,组件自身覆盖混入,不会报错。
  3. 多个混入之间冲突:数组靠后的混入覆盖前面的。

适用场景(面试举例)

  1. 通用表格页面:分页、重置搜索、批量删除、导出逻辑抽混入;
  2. 弹窗通用逻辑:打开关闭弹窗、表单清空、校验重置;
  3. 页面缓存:activated/deactivated 统一处理缓存刷新逻辑;
  4. 权限通用判断:封装按钮权限校验公共方法。

mixins 致命缺点(重点,必主动说)

  • 命名污染、隐式依赖混入和组件变量重名会静默覆盖,没有任何提示;阅读代码时,不知道某个方法 / 变量来自哪个混入,代码溯源困难。

  • 多混入嵌套逻辑混乱引入 3 个以上混入,多层逻辑叠加,钩子执行顺序难梳理,后期维护成本极高。

  • 传参不直观混入无法显式接收入参,只能依赖组件内部约定字段,容易出现字段缺失 bug。

  • Vue3 不推荐,官方废弃Vue2 只有 mixins 一种复用方案;Vue3 setup 用组合式函数(useXxx)替代,解决所有混入缺陷。

Vue3 替代方案:组合式 Hooks(对比回答加分)

  1. 组合函数是显式导入、显式导出,变量方法必须手动解构,来源清晰;
  2. 无命名覆盖静默合并问题,重名变量需要手动起别名,强制区分;
  3. 可以传参,逻辑封装更灵活;
  4. 天然支持 TS,类型推导完整,mixins 类型提示很差。

3. 微信支付

前置基础(必看

  1. 必须有 微信商户号(mch_id) + 公众号 / 小程序 APPID,两个绑定在一起
  2. 商户后台配置:支付授权目录、API 证书、支付密钥(APIv3 密钥)、商户证书
  3. 流程统一逻辑:前端发起支付请求 → 后端调用微信下单接口拿支付凭证 → 前端调微信拉起支付弹窗

重点:绝对不能前端直接调微信下单,所有支付核心接口必须走后端,防止泄露密钥、恶意刷单

整体流程

  1. 用户下单,前端把商品、金额传给自己后端
  2. 后端调用微信 JSAPI下单 接口,拿到 prepay_id 预支付订单
  3. 后端按规则生成签名,返回支付参数给前端
  4. 小程序调用 wx.requestPayment 唤起支付

4. OCR 识别

基础概念

OCR 全称 光学字符识别 ,简单说:把图片里的文字,转成可复制、可编辑的纯文本。举个例子:

  • 拍一张身份证照片 → OCR 自动提取姓名、身份证号、地址;
  • 拍照发票 → 自动识别金额、税号、开票信息;
  • 截图文档、手写笔记 → 一键导出文字,不用手动打字。

前端纯浏览器识别(轻量小项目,离线可用)

代表库:Tesseract.jsPP-OCR.jsVue3 / 小程序可直接引入,图片在浏览器本地完成识别,不用传后端。

// Vue3极简示例 tesseract.js
import Tesseract from 'tesseract.js'
async function ocrImg(file) {
  const res = await Tesseract.recognize(file, 'chi_sim+eng')
  console.log('识别文字:', res.data.text)
}

5. 高德地图

高德核心优势

  • 城市道路、POI 数据精度更高,物流 / 外卖 / 门店选址首选
  • 定位精度强,Geolocation 插件兼容性好
  • 官方 Loader 异步加载,无全局污染,适配 Vite/Vue3

前置统一准备

  1. 分别去对应开放平台申请 Web 端 Key

    • 高德:高德开放平台,需要额外配置安全密钥
    • Key 域名白名单:两个地图都需要在后台配置项目域名,本地localhost单独放行,否则报 key 非法
  2. 页面地图容器必须固定宽高,否则地图渲染空白

  3. 通用业务能力:定位、打点标记、地址解析、路线规划、搜索 POI

Vue3 接入高德地图(推荐后台管理、高精度定位场景)

安装依赖

npm install @amap/amap-jsapi-loader

完整单文件组件 MapAmap.vue

<template>
  <!-- 地图容器固定宽高 -->
  <div id="amap-container" style="width:100%;height:600px;"></div>
</template>

<script setup>
import { onMounted, onUnmounted } from 'vue'
import AMapLoader from '@amap/amap-jsapi-loader'

// 全局配置安全密钥(必填,否则定位失效)
window._AMapSecurityConfig = {
  securityJsCode: '你的高德安全密钥'
}

let map = null // 地图实例
const amapKey = '你的高德Web端Key'

// 初始化地图
function initAmap() {
  AMapLoader.load({
    key: amapKey,
    version: '2.0',
    plugins: ['AMap.Geolocation', 'AMap.Marker'] // 按需引入插件
  }).then(AMap => {
    // 创建地图实例,中心点杭州
    map = new AMap.Map('amap-container', {
      zoom: 13,
      center: [120.1551, 30.2741], // 经纬度 [经度,纬度]
    })

    // 1. 添加点位标记
    new AMap.Marker({
      position: [120.1551, 30.2741],
      title: '当前点位',
      map
    })

    // 2. 获取浏览器定位
    const geo = new AMap.Geolocation({
      zoomToAccuracy: true,
      buttonPosition: 'RB'
    })
    map.addControl(geo)
    geo.getCurrentPosition((status, res) => {
      if(status === 'complete') {
        console.log('定位成功:', res.position)
      }
    })
  })
}

onMounted(() => initAmap())
// 页面销毁销毁地图,释放内存
onUnmounted(() => map?.destroy())
</script>

6. 腾讯地图

腾讯地图核心优势

  • 和微信小程序、公众号 H5 打通,微信内定位更稳定
  • 社交、门店、商圈数据完善,本地生活、社交类项目首选
  • 免费额度更高,中小企业成本更低

前置统一准备

  1. 分别去对应开放平台申请 Web 端 Key

    • 腾讯地图:腾讯位置服务,仅需 Key
    • Key 域名白名单:两个地图都需要在后台配置项目域名,本地localhost单独放行,否则报 key 非法
  2. 页面地图容器必须固定宽高,否则地图渲染空白

  3. 通用业务能力:定位、打点标记、地址解析、路线规划、搜索 POI

Vue3 接入腾讯地图(微信生态小程序 / H5 优先) 两种引入方式

方式 1:全局 script 引入(最简单,index.html 配置)

<!-- public/index.html 底部引入 --> 
<script charset="utf-8" src="https://map.qq.com/api/gljs?v=1.exp&key=你的腾讯地图Key"></script>

方式 2:完整业务组件 MapTencent.vue

<template>
  <div id="tmap-container" style="width:100%;height:600px;"></div>
</template>

<script setup>
import { onMounted, onUnmounted } from 'vue'
let tmap = null

function initTMap() {
  // 全局TMap已通过index.html脚本注入
  const TMap = window.TMap
  // 中心点杭州经纬度
  const center = new TMap.LatLng(30.2741, 120.1551)
  tmap = new TMap.Map('tmap-container', {
    center,
    zoom: 13
  })

  // 添加标记点
  const marker = new TMap.Marker({
    position: center,
    map: tmap
  })

  // 地址解析/POI搜索示例
  const service = new TMap.service.Search()
  service.search({ keyword: '杭州西湖' }).then(res => {
    console.log('搜索结果', res.data)
  })
}

onMounted(() => initTMap())
onUnmounted(() => tmap?.destroy())
</script>

7. 腾讯地图 LBS 能力

一、基础定义

LBS 是基于地理位置的服务,腾讯地图 JS API 提供一整套前端可调用的地图能力,适配 PC 后台、H5、uni-app/PDA 移动端,用于外勤打卡、仓库网点管理、配送路线规划、地址解析等业务

腾讯地图 LBS 落地做了哪些工作

封装地图单例工具类,整合正向 / 逆向地址解析、门店点位聚合渲染、配送路线规划能力;解决上千门店标记渲染卡顿、多坐标系坐标偏移、用于门店选址、配送轨迹溯源。

8. Vue2 复杂动态表单解决方案

通用基础实现方案(中小复杂度表单,增删行 / 单层数组), Vue2 方案(Options API + ElementUI)

  1. 数据层data 维护完整表单对象,动态明细用数组存储,每条明细生成唯一 UID 作为循环 key,规避 index 渲染错乱;
  2. 增删逻辑:封装addRow/delRow方法,通过数组原生push/splice操作保证响应式;若需给明细新增字段,使用$set解决 Vue2 对象新增属性无响应问题;
  3. 表单校验:动态行通过拼接字符串prop="数组.下标.字段名"实现单行独立校验,区分全局固定项规则与动态行子规则;
  4. 页面销毁:组件销毁时清空表单校验实例,防止内存残留

多层嵌套复杂表单方案(明细下带子表单、多级数组)

  1. 数据结构设计:采用嵌套数组结构,父明细数组每一项内部包含子明细数组,支持无限层级拓展;
  2. 渲染逻辑:v-for 循环嵌套,每一层循环独立维护新增、删除方法,精准操作对应层级数组;
  3. 校验适配:prop 多层拼接 父数组.下标.子数组.子下标.字段,实现嵌套字段精准报错定位;
  4. 解耦优化:将单条嵌套明细抽为独立子组件,通过defineProps传明细数据、defineEmits派发增删事件,拆分复杂逻辑。

大型表单全局状态方案(跨页面分步表单、多标签共享表单)

Vue2:Vuex 方案

  1. 将完整表单数据、动态明细数组存入 Vuex state;
  2. 增删行、修改字段、重置表单全部封装为 mutations/actions;
  3. 页面组件仅读取 state、调用 action,多页面共享同一份表单缓存。

9. Vue3 复杂动态表单解决方案

通用基础实现方案(中小复杂度表单,增删行 / 单层数组)

Vue3 方案(Composition API + Element Plus)

  1. 数据层:用reactive统一托管完整表单数据源,明细数组同样采用唯一 ID 做 key;
  2. 增删逻辑:封装独立addRow/delRow函数,数组操作天然响应式,无需额外 API;
  3. 表单校验:逻辑同 Vue2,prop 字符串拼接;可抽离校验规则为独立常量,复用性更强;
  4. 生命周期onUnmounted销毁表单 ref、清空校验缓存。

大型表单全局状态方案(跨页面分步表单、多标签共享表单)

Vue3:Pinia 方案

  1. 单独创建表单仓库,统一托管表单数据、所有动态行操作逻辑;
  2. 页面直接引入 store 调用 actions,天然类型推导,相比 Vuex 简化代码;
  3. 支持持久化插件,刷新页面保留已填写的动态表单数据。

通用性能优化方案(面试必提亮点)

  1. key 优化:禁用数组下标 index,使用 uuid / 时间戳唯一标识,删除中间行避免 DOM 渲染错位;
  2. 组件拆分:动态行抽离独立子组件,减少父组件页面重渲染范围;
  3. 防抖节流:输入、数字输入框绑定防抖,减少频繁赋值触发的校验重计算;
  4. 懒渲染:明细行数上千时,采用虚拟列表vue-virtual-scroller只渲染可视区域 DOM;
  5. 校验优化:提交时统一全量校验,输入时局部单字段校验,避免全局循环校验卡顿。

Vue2 与 Vue3 实现核心差异(面试对比回答)

  1. 响应式:Vue2 需$set处理对象新增属性;Vue3 Proxy 天然支持,无额外 API;
  2. 代码组织:Vue2 所有逻辑写 methods,大表单 methods 臃肿;Vue3 可按功能拆分多个独立函数,逻辑分层清晰;
  3. 全局状态:Vue2 Vuex 分层繁琐(state/mutation/action);Vue3 Pinia 扁平化 API,代码更少、TS 友好;
  4. 类型支持:Vue3 组合式 API 天然适配 TS,动态表单、嵌套字段类型自动推导;Vue2 类型提示薄弱,需额外声明类型。

10. 超大表单

适用场景:上千字段、可配置表单、运营后台自定义表单

form-makingVue2 专用的开源表单低代码组件库(完整 npm 包) ,属于第三方插件包,可 npm 安装引入使用。它不是简单表单校验插件(vee-validate/async-validator),是完整表单渲染解决方案包,包含编辑器、渲染器、动态数组内置能力一体化。

vform3form-making 的 Vue3 升级配套 npm 插件包,专门适配 Vue3 + Element Plus,是同团队开发的低代码表单一体化组件库,完美承接 Vue2 项目升级后的动态表单需求。属于完整表单解决方案包,不只是单纯校验插件,包含「可视化拖拽编辑器 + JSON Schema 表单渲染器」两大核心模块。

  1. 核心思路:维护一份表单 JSON Schema 配置,通过递归组件自动渲染所有表单项、动态数组块、嵌套子表单;
  2. Vue2 选型:form-making / el-form-renderer;Vue3 选型:vform3 / element-plus-form-design
  3. 能力覆盖:JSON 配置控制字段显隐、动态数组增删、联动校验、自定义组件插槽;
  4. 优势:无需手写大量 v-for 循环,新增表单仅修改配置,大幅降低维护成本。

11. 大数据树形渲染

业务场景定义

后端一次性返回成千上万条层级嵌套树形数据(部门组织、菜单权限、商品分类),直接全量渲染会出现页面卡顿、白屏、滚动延迟、展开 / 勾选交互阻塞,统称大数据树形渲染性能问题。

场景选型标准(面试场景题标准答案)

  1. 几千条、需要全局搜索筛选:虚拟滚动树
  2. 十万级、百万级组织架构、权限菜单:懒加载树
  3. 一两千条简单分类树:前端预处理 + 默认折叠优化即可
  4. Vue3 新项目优先用 Element Plus 原生虚拟树;Vue2 老项目选用 vue-virtual-scroller 封装虚拟树

四大落地解决方案(按优先级排序,面试分层讲)

方案 1:虚拟滚动树(主流最优方案,十万级数据首选)

  • 核心原理:只渲染可视区域内 DOM,滚动时动态替换节点,DOM 数量始终维持几十条,不随总数据量上涨。

  • 技术选型

    • Vue2:vue-virtual-scroller + 递归树组件 / el-tree-virtual
    • Vue3:vue-virtual-scroller@2.x / Element Plus 内置 el-tree 虚拟滚动属性
  • 配套能力:自带展开、勾选、搜索过滤、懒加载兼容,不用手动分页,体验流畅。

  • 适用:需要一次性加载全量树、支持全局搜索筛选、频繁展开收起的场景。

方案 2:后端分片懒加载树(百万级海量数据,后台权限 / 组织架构常用)

  • 核心原理:页面初始只渲染一级根节点;点击展开节点时,再单独请求当前节点的子级数据,按需加载。

  • 实现逻辑:

    • 树组件开启lazy懒加载配置,绑定加载回调;
    • 回调中携带当前节点唯一 ID,请求接口返回该节点下级;
    • 缓存已加载子节点,重复展开不再重复请求。
  • 优点:前端初始 DOM 极少,首屏秒开,无内存压力;

  • 短板:无法全局全树搜索,搜索只能后端分页查询匹配节点。

方案 3:前端数据分片预处理(中等数据量,几千条层级树)

  • 数据裁剪:后端返回完整树形后,前端递归扁平化处理,只保留渲染必需字段,剔除冗余后端字段,降低内存占用;

  • 折叠优化:默认全部折叠,仅展开选中路径,减少初始渲染节点;

  • 过滤防抖:搜索框加防抖,不实时递归全树匹配,停止输入后再筛选;

  • 节流监听:滚动、展开操作加节流,避免高频重复渲染。

  • 局限:上万条以上数据仍会卡顿,仅作为辅助优化手段,不单独作为核心方案。

方案 4:分页树(极少用,扁平类树形)

将树形数据转为扁平列表,后端分页返回,仅展示当前页层级,交互割裂,一般只做兜底方案。

Vue2 / Vue3 实现差异(面试官必对比提问)

  • 1. 递归树组件响应式

    • Vue2:递归传参多层嵌套,修改子节点勾选 / 展开需$set,否则无响应;大量节点修改会触发整棵树重渲染;
    • Vue3:Proxy 响应式,子节点修改精准更新,无需额外 API,局部更新粒度更细。
  • 2. 虚拟滚动库适配

    • Vue2 虚拟树插件生态老旧,部分需二次封装;
    • Vue3 Element Plus 原生内置 tree 虚拟滚动,零第三方依赖,开箱即用。
  • 3. 渲染性能

    • Vue3 diff 算法优化,同量级节点渲染速度比 Vue2 提升一倍,大数据场景阻塞时间明显缩短。
  • 4. TS 支持

    • Vue3 可给树形节点、递归组件定义完整类型,避免层级字段匹配错误。

通用性能优化点(面试加分,深挖必说)

  1. DOM 渲染优化

    • 树节点循环 key 用唯一 id,禁止数组下标,避免节点复用错乱;
    • 节点模板轻量化,移除多余嵌套 div、复杂样式;复杂自定义节点抽离子组件,缩小重渲染范围。
  2. 数据内存优化

    • 树形数据扁平化缓存,避免每次筛选重复递归;
    • 页面销毁清空树形缓存、取消滚动监听、销毁虚拟滚动实例,防止内存泄漏;
    • 勾选 / 半选状态单独存储 Map,不污染原树形数据。
  3. 交互节流防抖

    • 搜索输入框防抖;滚动、展开、勾选操作节流,限制渲染执行频率。
  4. 功能降级兜底低配置设备关闭虚拟滚动,自动切换懒加载模式;超大数量弹窗树默认只展示前 2 级。

高频踩坑 & 解决方案(面试官深挖痛点)

  1. 虚拟滚动树展开 / 勾选错位:缓存每个节点高度,固定行高或动态计算高度;
  2. 全量搜索卡顿:防抖 + 后端检索兜底,前端只做少量数据过滤;
  3. 懒加载树勾选半选状态丢失:缓存已加载子节点勾选状态,每次加载后回填;
  4. Vue2 多层递归树修改数据不更新:使用 $set 操作节点属性,或扁平化数据存储状态;
  5. 大数据树内存持续上涨:页面销毁清空树形数据源、解绑滚动事件。

12. 虚拟表格优化大数据表格卡顿

一、先说卡顿根源

普通表格是一次性把后端返回几万条数据全部渲染成 DOM,页面会生成上千个 tr、td 节点,DOM 数量巨多:

  1. DOM 渲染、回流重绘耗时久,页面打开白屏、滚动卡顿;
  2. 内存占用飙升,切换页面、弹窗操作会明显卡死,低配设备、PDA 机器更严重。虚拟表格核心思路:只渲染可视区域内的表格行,不在视口内的行不生成 DOM,始终维持几十条 DOM,和总数据量无关。

二、底层实现原理

  1. 计算单一行高度、表格容器可视高度,算出一屏最多展示多少行;
  2. 计算全部数据总高度,用空白占位撑开滚动条,滚动条长度和全量数据保持一致,用户感知和完整表格无区别;
  3. 监听滚动事件,实时计算当前滚动偏移量,动态截取当前可视区间的数据,替换渲染 DOM;
  4. 上下预留少量缓冲行,滚动时不会出现空白闪烁。

三、两种落地方案(业务区分)

方案 1:组件库自带虚拟表格(项目主流,开箱即用)

Element Plus、Ant Design Vue、vxe-table 都内置虚拟滚动 table,只需要开启属性,配置行高,传入完整大数据数组即可。适用场景:常规后台列表、订单、库存大数据页面,不用自己造轮子,维护成本低。

方案 2:自行封装虚拟表格(深度优化、定制化需求)

底层基于滚动容器监听 scroll,手动做切片截取、占位高度计算,适合高度不固定、合并单元格、复杂自定义单元格、PDA 端极简页面。

四、配套多层优化手段(加分,贴合你的业务场景)

  1. 后端分页 + 虚拟滚动结合
    • 十万级数据不一次性全量拉取,后端分页,前端滚动到底懒加载下一页,再用虚拟表格渲染当前页数据,减少前端内存占用;
  2. 单元格轻量化
    • 单元格去掉复杂嵌套组件、频繁重渲染指令,复杂操作按钮、图表抽离懒加载组件,缩小单 DOM 渲染开销;
  3. 滚动事件节流
    • scroll 高频触发,加节流控制渲染执行频率,避免短时间大量计算阻塞主线程;
  4. 唯一 key 优化 diff
    • 表格行绑定业务唯一 ID 做 key,不用数组下标,减少 Vue diff 算法开销;
  5. 离线场景兼容
    • PDA 弱网缓存全量本地数据,虚拟表格读取本地缓存数据渲染,不依赖实时接口。

五、解决的业务痛点

  1. 上万条库存、订单、单据列表页面打开秒加载,不会长时间白屏;
  2. 上下滚动丝滑,无明显延迟、卡顿;
  3. 降低浏览器内存占用,长时间打开页面不会崩溃;
  4. 适配仓库 PDA 低配安卓设备,低端机器也能流畅展示大数据单据。

六、高频追问:定高虚拟表格和不定高虚拟表格区别

  1. 固定行高:计算逻辑简单,性能最好,绝大多数后台表格首选;
  2. 动态不固定行高:需要实时测量每行实际高度,维护高度缓存 Map,计算开销更大,仅用于单元格文本换行、内容高度不统一的场景。

七、收尾总结

大数据表格卡顿本质是 DOM 节点过多,虚拟表格通过只渲染可视区域行、占位模拟滚动条的方案,把 DOM 数量控制在常数级;实际项目中优先使用组件库内置虚拟表格,搭配后端分页懒加载、单元格轻量化、滚动节流等手段,彻底解决上万条数据表格打开、滚动卡顿问题,同时兼容 PDA 工业设备弱网本地大数据渲染场景。

13. 接口降级

什么是接口降级

简单说就是后端服务压力过大、或者第三方接口崩了的时候,主动关掉非核心接口的完整能力,返回兜底数据,保住下单、支付这类核心流程不垮,避免雪崩,属于高可用保障手段。

降级和熔断的简单区分(高频追问)

熔断是连续失败后,直接切断调用链路,过一段时间再尝试恢复;降级是主动舍弃非核心能力、返回兜底数据,两者一般搭配使用,熔断是手段,降级是最终兜底表现。

前端层面降级方案(前端岗位重点说

  1. 缓存兜底降级

    • 接口请求失败 / 超时,直接读取本地缓存、sessionStorage、Pinia/Vuex 里存的旧数据展示,不直接空白报错。比如商品列表接口崩了,展示上次打开页面缓存的列表,同时给用户弱提示 “数据加载延迟,展示历史内容”。
  2. 功能模块降级

    • 后端返回降级标记,前端直接隐藏非核心模块。比如大促高峰期,关掉商品推荐、评价列表、足迹模块,只保留商品详情、下单支付核心模块,减少接口并发。
  3. 请求限流 / 节流降级

    • 搜索框、筛选这类高频请求做防抖节流,限制短时间重复发请求;同一接口短时间连续失败,直接拦截一段时间不再发起请求,减轻后端压力。
  4. 静态兜底页面降级

    • 核心接口完全挂掉时,渲染预设静态兜底模板,比如 “当前访问人数过多,请稍后重试”,不白屏、不疯狂重试轮询。
  5. 第三方接口单独降级

    • 像地图、OCR、物流查询这类第三方接口,单独捕获异常,接口失败就隐藏对应区域,不阻塞页面主体功能。

项目落地完整流程(面试官最爱问实战)

  1. 前后端约定降级标识:后端返回特定 code,标记当前接口已降级;
  2. 请求统一拦截器封装降级逻辑:axios 请求拦截器统一捕获超时、500、熔断状态,自动走缓存兜底;
  3. 页面区分强弱提示:非核心模块降级只小字提示,核心功能降级弹窗友好引导;
  4. 开关可配置:后台配置降级开关,不用改代码上线,大促前一键开启降级策略;
  5. 监控告警:接口失败率上涨触发告警,人工介入判断是否手动降级。

举业务实战例子(口语化加分)

比如电商大促场景:高峰期服务器压力暴涨,后端开启降级,前端收到标识后,关掉商品实时评论、浏览足迹、猜你喜欢三个非核心接口,同时商品列表接口失败就展示上次缓存的商品数据,优先保证用户能正常浏览商品、提交订单、微信支付,不会因为次要功能拖垮整个下单流程。

14. 小程序弱网/离线设计

一句话概括

我做的是一个医废收运的 PDA 应用,用户是开车在外面跑的司机和押运员。痛点很明确——他们经常在地下车库、偏远医院这种弱信号甚至没信号的环境下作业,如果断网就没法填表、没法提交任务,业务直接卡住。所以我做了一套离线方案,核心目标是:没网也能正常干活,有网了数据自动补回去。

主体:讲设计思路,强调"读写分离"(1-2分钟)

"我把问题拆成了两块——,因为它们的策略完全不同。

指的是填表用的下拉数据,比如车辆、人员、医废类型这些。我做了一个缓存管理器,核心是分级过期策略:动态数据像车辆人员设 24 小时过期,静态字典设 7 天。取数据时缓存没过期直接用,过期了有网就刷新,没网就算过期也用旧的——我的原则是坏数据也比没数据强,先保证用户能填表。

指的是司机新建的收运任务。提交时先检测网络:有网直接调接口;没网就存到本地队列,标记成 pending 待同步状态,列表上显示一个红色角标提醒。等网络恢复回到列表页,检测到有待同步数据就弹窗问用户要不要立即同步,然后逐条补传,传成功的就清理掉。"

讲两三个体现工程深度的细节

  • "防并发:同一份枚举正在请求时,再调用会复用同一个 Promise,避免重复请求。
  • 多端网络检测兼容:H5 用 navigator.onLine,App 和小程序用 uni.getNetworkType,用条件编译区分,还留了个强制离线开关方便测试——在浏览器控制台直接敲命令就能模拟断网,不用真拔网线。
  • 后台静默更新:App 启动或网络恢复时,悄悄把缓存刷一遍,用户无感知,失败也不影响使用。"

主动讲短板和取舍(这是最关键的加分项)

"这套方案也有边界,我是有意识做了取舍的。比如目前同步失败的数据是直接丢弃的,没做重试队列——因为业务上失败大多是数据校验问题,重传也没用,不如让用户重填。另外没做冲突处理,因为是单向补传的场景。如果要继续优化,我会加重试队列同步进度反馈,数据量大了还可以考虑从 KV 存储换成 IndexedDB。"

  • "怎么判断断网?"  → 三端检测方式 + 强制开关那段
  • "同步到一半又断网了怎么办?"  → 有 syncInProgress 锁防并发;逐条同步,已成功的标记后清理,没传的还是 pending,下次继续
  • "为什么不用 IndexedDB / SQLite?"  → 数据量小(枚举<1MB,单条记录 1-2KB),uni 的同步 KV 存储够用且简单;数据量大了才需要升级。讲"按需选型"而不是"我不会"
  • "缓存数据怎么保证不过期太久?"  → TTL 分级 + 后台静默更新双保险
  • "安全性?"  → 坦诚说现在是明文存储,敏感场景可以加本地加密(这是已知短板)
image.png

判断网络我做了三层: 优先看强制离线开关(测试用),然后按平台分——H5 用 navigator.onLine,App 和小程序用 uni.getNetworkType,通过条件编译在打包时裁剪。这样一套代码多端都能正确判断网络。强制开关这块我还把管理器挂到了 window,在浏览器控制台一行命令就能模拟断网,不用真的拔网线,调试离线逻辑特别方便。

强制开关(forceOffline)单独说

这是整段里最值得讲的"工程细节",因为它解决一个实际痛点:离线功能怎么测?总不能真跑到没信号的地方,或者拔网线吧。

setForceOffline(true)  // 一开,checkNetwork 永远返回 false

而且它在 H5 下把整个 manager 挂到了 window:

// #ifdef H5
window.offlineDataManager = offlineDataManager
// #endif

所以测试时直接在浏览器控制台敲 offlineDataManager.setForceOffline(true),不用改代码、不用断网,就能模拟离线提交,再 setForceOffline(false) 测同步。这个开关还存进了本地存储,刷新页面也保持。

"navigator.onLine 准吗?"——可以坦诚说它只能判断"网卡有没有连上",连上但断流(连了 wifi 但没有外网)它还是 true,所以它只是第一道粗筛,真正的兜底是请求失败时降级用缓存。这么答就很完整了。

app 是给收运司机/押运员在外面跑的,信号经常不好。这套设计的核心思路就两条:读数据靠缓存兜底,写数据先存本地、有网再补传。读和写分别由两个文件管,互不干扰。 整体分两块

第一块:读(枚举缓存)—— enum-cache-manager.js

管的是那些"填表用的下拉选项":车辆、押运员、医废企业、医废类型、门诊类型。

为什么要缓存它们? 因为离线时你填表得有车牌号、人员名单可选,没网这些拉不回来表单就废了。所以提前存在本地。

怎么决定用缓存还是请求网络,是一套"分级新鲜度"策略:

  • 每条缓存存的时候带个时间戳,配了过期时间(TTL)
  • 动态数据(车辆、人员、企业)→ 24 小时过期,因为这些会变
  • 静态数据(医废类型、门诊类型枚举)→ 7 天过期,因为基本不变
  • 取数据时:缓存没过期就直接用;过期了且有网就重新拉并刷新缓存;没网就算过期也照样用旧的(坏数据总比没数据强)

还有几个细节挺实用:

  • loadingMap 防并发——同一个枚举正在请求时,再次调用会复用同一个 Promise,不会发两次请求
  • silentUpdateAll() 后台静默刷新——App 启动/回到页面/网络恢复时,悄悄把所有枚举更新一遍,用户无感知,失败也不影响用
  • 网络判断用 uni.getNetworkTypenetworkType === 'none' 就算离线

第二块:写(离线数据)—— offline-data-manager.js

管的是司机在外面新建的收运任务(提交配置表单)。

核心流程就是 README 里那张图:

提交表单 → 检测网络
              ├─ 有网:直接调 medicalWasteServer.start() 提交
              └─ 没网:存本地,标记 pending(待同步),列表角标 +1

存在本地的每条数据长这样(关键字段):

{
  id: 'local_' + Date.now(),  // 本地临时ID,区别于服务器ID
  ...表单数据,
  status: 'pending',          // pending待同步 / syncing同步中 / synced已同步 / failed失败
  createTime, isOffline: true
}

同步syncAllData)的逻辑:

  • 网络恢复后,回到列表页会检测到"有 pending 数据",弹窗问你要不要立即同步
  • 逐条提交,提交前把 id/status 这些本地私有字段删掉再发给服务器
  • syncInProgress 锁防止重复点击触发并发同步
  • 成功的标 synced,最后统一清理掉(本地不留)
  • 失败的这版是直接删掉(代码里 deleteOfflineData,不是 README 说的"保留 failed")——这点 README 已经过时了,实际现在失败就丢弃并把错误信息汇总弹给用户

一个很贴心的测试开关setForceOffline(true) 强制离线模式,存在本地。H5 环境还把整个 manager 挂到了 window.offlineDataManager,浏览器控制台直接敲命令就能测,不用真的拔网线。

15. RBAC 按钮级权限 + 动态路由

RBAC 是什么(大白话)

RBAC = 基于角色的权限控制。不直接给用户绑权限,中间加一层角色

  1. 后台先建一堆权限点:页面路由、新增按钮、编辑按钮、删除按钮、导出按钮,每一个操作都是单独权限标识;
  2. 创建角色(管理员、运营、普通员工),给角色勾选一堆权限;
  3. 给用户分配角色,用户自动拥有该角色下所有权限。

好处:改权限不用一个个改用户,只改角色就行,批量管理省事。

整套落地完整流程(从头到尾串一遍,面试必说)

  1. 登录阶段
  • 用户输账号密码登录,后端返回用户信息、角色、该角色全部权限标识符数组。
  1. 存储权限
  • 把权限数组存全局状态(Vuex/Pinia),全局任何页面都能读取。
  1. 生成动态路由
  • 前端把完整路由表做权限过滤,过滤出用户能看的页面,动态挂载路由,渲染侧边菜单。

  • 权限过滤执行位置在全局前置路由守卫 router.beforeEach,beforeEach 是每次跳转路由最先触发的全局守卫,在页面渲染、组件创建之前执行,适合统一做权限校验、动态路由挂载、无权限拦截,是实现 RBAC 动态路由过滤的唯一合适守卫。

  • 完整执行逻辑

    1. 进入 router.beforeEach((to, from, next) => {})

    2. 判断本地是否存在用户登录态 / 权限数组:

    • 未登录:直接 next('/login') 跳转登录页;

    • 已登录,判断是否已经动态挂载过路由(加标记变量如 hasAddRoute):

      • 未挂载:用后端返回的权限数组过滤完整路由表,调用 router.addRoute() 动态追加可访问路由,标记为已挂载,再执行 next(to.fullPath) 重新触发一次守卫;
      • 已挂载:直接校验当前 to 页面是否在权限列表内,无权限则 next('/403'),有权限 next() 放行。

    区分其他守卫为什么不能做这件事

    1. 独享守卫 beforeEnter / 组件内守卫 beforeRouteEnter执行时机太晚,路由已经匹配完成,无法提前拦截非法地址,也不能统一批量挂载全部动态路由,只适合单页面独立权限,不做全局 RBAC 过滤。
    2. 后置守卫 afterEach路由已经跳转完成,页面已经渲染,无法拦截跳转,仅用来做页面标题、埋点,不能处理权限过滤。

    Vue3 + Vite 实操关键坑点(面试加分)

    1. addRoute 动态添加路由后,必须 next(to.fullPath) 重走一次守卫,否则第一次加载页面 404;
    2. 刷新页面会重置路由实例,所以每次刷新进入守卫都要重新拉取权限、重新挂载动态路由;
    3. 必须加挂载标记,防止无限循环 beforeEach

    极简面试口述总结

    动态路由的权限过滤、路由挂载统一在全局前置守卫 router.beforeEach 中处理,它是每次路由跳转最先执行的钩子,能在页面渲染前完成路由筛选、动态添加和无权限拦截;其他守卫执行时机滞后,不适合全局 RBAC 权限体系的统一过滤。

  1. 页面渲染按钮
  • 页面上所有操作按钮绑定权限指令,自动根据权限数组判断显隐。
  1. 双重安全拦截
  • 路由守卫:用户手动输入无权限页面地址,直接拦截跳转;
  • 请求拦截器:发起接口前校验接口对应的权限,无权限直接拦截请求,弹窗提示无操作权限。
  1. 权限更新
  • 后台给用户改角色、改权限后,前端重新拉取权限列表,重新过滤路由、更新页面按钮状态。

Vue2 和 Vue3 微小区别(面试官容易追问)

  1. 动态路由 API:Vue2/Vue3 都是 router.addRoute,写法基本一致;
  2. 全局权限指令:Vue2 用自定义指令钩子,Vue3 composition API 指令写法微调;
  3. 状态存储:Vue2 用 Vuex 存权限数组,Vue3 用 Pinia,读取更简单。

Vue2 自定义权限指令 v-perm 完整示例(Options API 指令钩子)

1. 全局注册指令 main.js

import Vue from 'vue'
import router from './router'
import store from './store'

// 权限自定义指令 v-perm
Vue.directive('perm', {
  // 钩子:元素插入父DOM时触发
  // `inserted`:元素插入页面 DOM 时执行,适合权限按钮销毁;
  inserted(el, binding) {
    // binding.value 传进来的权限标识,如 'user:delete'
    const permCode = binding.value
    // 从Vuex取出当前用户拥有的全部权限数组
    const permList = store.state.user.permissionList

    // 判断是否包含权限,没有权限直接删除按钮DOM
    if (!permList.includes(permCode)) {
      el.parentNode.removeChild(el)
    }
  }
})

2. 页面中使用

<template>
  <!-- 需要删除权限才显示 -->
  <el-button v-perm="'user:delete'" type="danger">删除用户</el-button>
  <!-- 需要新增权限才显示 -->
  <el-button v-perm="'user:add'" type="primary">新增用户</el-button>
</template>

Vue3 Composition API 两种指令写法(对比 Vue2 差异)

方式 1:main.js 全局注册(最常用,和 Vue2 写法接近,仅钩子参数 / API 微调)


import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import { useUserStore } from '@/stores/user'

const app = createApp(App)

// Vue3 全局指令
app.directive('perm', {
  // Vue3 钩子名和Vue2一致,但参数结构、响应式处理有区别
  inserted(el, binding) {
    const permCode = binding.value
    const userStore = useUserStore()
    const permList = userStore.permissionList

    if (!permList.includes(permCode)) {
      el.parentNode?.removeChild(el)
    }
  },
  // 新增:权限列表更新后,页面刷新重新判断显隐(Vue2需要额外监听)
  update(el, binding) {
    const permCode = binding.value
    const userStore = useUserStore()
    const permList = userStore.permissionList
    if (!permList.includes(permCode) && el.parentNode) {
      el.parentNode.removeChild(el)
    }
  }
})

app.use(router).mount('#app')

方式 2:setup 内局部注册指令(纯 Composition API 写法,单文件内部使用)

<template>
  <el-button v-perm="'order:export'">导出订单</el-button>
</template>

<script setup>
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()

// 局部自定义指令,Vue3 setup专属写法
const vPerm = {
  inserted(el, binding) {
    const code = binding.value
    if (!userStore.permissionList.includes(code)) {
      el.parentNode?.removeChild(el)
    }
  }
}
</script>

Vue3 模板使用(和 Vue2 完全一样,无改动)

<el-button v-perm="'order:edit'">编辑订单</el-button>

Vue2 / Vue3 指令核心差异(面试口述重点)

  1. 注册方式

    • Vue2:Vue.directive() 全局挂载;
    • Vue3:app.directive() 全局,支持 <script setup> 局部定义指令。
  2. 空值兜底Vue3 DOM 操作需要加可选链 el.parentNode?.,防止父节点不存在报错,Vue2 无需。

  3. 响应更新Vue2 仅 inserted 只会在首次渲染执行;如果用户切换角色、权限更新,按钮不会自动隐藏;Vue3 加 update 钩子,数据变化自动重新校验权限。

  4. 组合式状态Vue3 指令内可以直接 useStore() 获取 Pinia;Vue2 指令只能读取全局 Vuex,无法在钩子内部引入组合式 API。

  5. 钩子细微调整Vue3 废弃部分老旧钩子,统一对齐生命周期;权限场景影响不大,核心 inserted 两者通用。

16. 适配老旧系统 AI 改造、数字员工前端定制

里的 AI 不是大模型聊天机器人,是业务自动化 AI 能力集合,分 4 类前端能摸到的落地功能

1. OCR 文字识别(最常用)

发票、营业执照、快递单、纸质单据拍照上传,AI 自动提取文字、金额、税号、地址,不用手动填表单。前端要做:图片上传组件、识别结果回填动态表单、识别失败降级兜底。

2. 智能自动填表 / 数据匹配 AI

AI 根据单据、历史数据、库存信息,自动把数据填进系统复杂表单;比如扫商品条码,AI 自动带出名称、规格、成本价,一键填充入库明细。

3. 流程自动化 AI(数字员工核心)

AI 机器人自动跑重复工作:定时拉取对账数据、批量导出报表、自动提交审批、异常单据自动标记预警,替代人工重复操作。

4. 辅助决策 AI

后台页面展示 AI 计算后的统计分析、库存预警、销量预测、异常订单筛选,给运营做参考。

举业务实例方便面试说

原来老旧仓储系统:仓管拿到纸质送货单,手动一条一条敲商品名称、数量、价格到表单里,效率低还容易输错;AI 改造后:拍照送货单上传,OCR AI 识别全部商品信息,自动回填页面动态明细表单;还能配置数字员工 AI 机器人,每天凌晨自动核对入库单和库存,生成对账报表,不用财务手动导出核对。

业务背景

公司里跑了多年的老后台,大多是 Vue2 / 原生 jQuery、无分层架构、没有统一接口层、无缓存 / 弱网兼容、没有权限体系;现在要接入 AI 能力(OCR 识别、智能表单填充、AI 自动对账、数字机器人),但不能推翻重写,只能在原有老旧系统上做兼容改造,就是老旧系统 AI 改造。

老旧系统原生存在的核心痛点

  1. 架构老旧:无统一请求拦截器,接口分散,没法统一做 AI 接口降级、缓存;
  2. 无权限体系:没有 RBAC 动态路由、按钮级权限,AI 功能入口、AI 操作按钮无法管控;
  3. 无离线弱网方案:仓库 PDA、外勤场景断网时,AI 识别 / 表单任务直接丢失;
  4. 代码耦合严重:页面硬写死表单、树形组件,无法快速接入 AI 自动填充能力;
  5. 跨系统兼容:老系统和 AI 中台、仓储 PDA、支付系统接口不统一,数据格式差异大。

前端改造落地方案(面试分层次口述)

  1. 中间适配层封装 在老旧系统原有请求逻辑外层,封装统一 axios 桥接层,统一对接 AI 中台接口,统一处理 AI 接口超时、降级、缓存、签名校验;老页面不用大面积改业务代码,直接复用封装后的 AI 请求方法。
  2. 权限体系增量接入 不重构老系统原有登录逻辑,增量接入 RBAC 动态路由 + v-perm 按钮指令:新增 AI 菜单路由、AI 操作权限标识,管理员可单独控制角色是否能使用 OCR、AI 自动流程等功能,老页面原有权限逻辑完全保留不冲突。
  3. AI 能力组件化下沉 封装通用 AI 公共组件(OCR 上传识别、AI 智能填表、流程画布节点、AI 报表导出),以弹窗 / 嵌入组件形式嵌入老旧页面,老页面仅需几行代码引入,不用重构原有复杂动态表单、树形渲染逻辑。
  4. 弱网离线兼容增量改造 给老系统新增统一本地存储工具类,AI 识别任务、待提交表单存入离线队列,断网缓存 AI 识别结果,联网自动同步 AI 中台,适配外勤 PDA、地下室无网场景。
  5. 新旧系统数据兼容转换 封装数据格式化工具,把老旧系统的表单结构、树形数据转换成 AI 中台要求的入参格式,AI 返回结果反向适配老页面渲染结构,消除两边字段差异。
  6. 渐进式灰度上线 按模块分批改造,先给仓库、财务模块接入 AI 能力,其余老页面保持原有逻辑,避免一次性全量重构带来的线上风险。

17. 编辑器引入 AI 开发工具

Cursor(AI 原生编辑器,替代 VSCode), 基于 VSCode 二次开发的纯 AI 内置编辑器,底层搭载 GPT-4o / Claude 大模型,无需额外装插件,开箱即用。

Claude Code" 超大上下文窗口,通读整个项目相比 Copilot 单文件有限上下文,Claude 能一次性读取几十上百个文件,读懂完整项目架构。比如你让它改造老旧 Vue2 系统,它能通读全部 mixins、全局请求、权限代码,批量统一改成 Vue3 组合式 API+Pinia,不会改一处崩另一处。

核心提效优势(比 Copilot 更强的点)

  1. 全局工程对话:可以上传整个前端项目文件夹,直接提问全局需求,比如 “把项目所有页面改成 vform3 低代码表单、统一接入 RBAC 权限指令”,AI 批量修改多处文件。
  2. 整块代码生成 + 文件创建:直接描述需求,自动新建完整组件(动态表单、树形虚拟列表、PDA 扫码适配组件),一次性产出 template+script 完整代码。
  3. 代码批量重构:老旧 Vue2 项目批量迁移 Vue3 组合式 API、批量替换 mixins 为组合 hook,大幅减少重复改造工作。
  4. 内置对话面板,不用切浏览器查文档,边写代码边问业务方案。

3. 短板

  • 重度大文件、超大项目打开会卡顿;
  • 国内访问速度一般,免费版有调用次数限制。

面试加分:AI 编辑器实际产能提升点(贴合前端业务)

  1. 复杂动态表单、树形虚拟渲染、RBAC 权限指令这类模板化代码,AI 一键生成,省去手写重复模板;
  2. 老旧系统改造时,批量转换 Vue2 代码为 Vue3 组合式 hook,替代手动逐页面修改;
  3. 接口降级、弱网离线存储、JSBridge 适配 PDA 等通用工具类,写一句注释直接生成完整封装代码;
  4. 调试报错、业务逻辑梳理不用反复翻阅文档,AI 即时给出解决方案,减少查资料耗时;
  5. 自动生成接口入参校验、单元测试,减少自测、文档编写工作量。

18. form-create

基础定位

form-create 是轻量开源低代码表单生成器 npm 包,一套库同时兼容 Vue2、Vue3,核心两件事:

  1. 可视化拖拽表单设计器(可视化画布,拖拽生成表单 JSON 配置);
  2. JSON 驱动表单渲染器(传入配置 JSON 自动渲染完整表单,自带动态增删行、嵌套、校验)。和 form-making、vform3 是同类产品,但它轻量化、不强制绑定 ElementUI/Element Plus,支持多 UI 库。

两大核心模块

1. 可视化设计器组件

页面嵌入拖拽画布,左侧基础组件(输入框、下拉、日期、上传、动态子表),中间实时预览,右侧配置字段校验、默认值、联动显隐、动态明细规则;拖拽完成直接导出 JSON Schema,存到后端数据库。

2. 表单渲染核心引擎

传入设计器产出的 JSON 配置,自动渲染表单,原生内置能力:

  • 单层 / 多层嵌套动态表单(明细增删、循环子表单);
  • 全局、单行自定义校验规则;
  • 字段联动、显隐控制、只读 / 禁用;
  • 自定义插槽,可嵌入业务组件(OCR 上传、地图、PDA 扫码组件)。

实战落地流程(口述完整链路)

  1. 运营在设计器页面拖拽表单,配置动态明细、校验规则,导出 JSON 存入后端;
  2. 业务页面引入 form-create 渲染器,读取后端 JSON 自动渲染完整表单;
  3. 自定义全局组件,嵌入 OCR 识别、PDA 扫码,识别后自动赋值表单字段;
  4. 搭配 RBAC 按钮级权限,控制表单新增、删除、导出按钮显隐;
  5. 表单提交前统一做接口降级、离线缓存,适配仓库弱网环境。

19. 定制文件处理组件,修复历史项目图片预览、跨域下载疑难问题

一、背景痛点(历史老项目现存问题)

原有项目没有统一封装文件处理逻辑,图片预览、文件下载代码零散写在各个页面,暴露出两类疑难问题:

  1. 图片预览问题
    • 不同业务图片来源不统一,有本地上传图、第三方 OCR 返回图、PDA 拍照图片、后端文件服务器图片;部分大图加载卡顿、超大图片溢出屏幕、长图无法滚动、图片加载失败无兜底占位;老代码没有统一的预览弹窗逻辑,每个页面重复写预览逻辑,维护混乱。
  2. 跨域下载疑难问题
    • 一是后端文件资源存在跨域,直接 a 标签下载被浏览器跨域策略拦截;二是接口返回二进制流、oss 文件链接两种下载格式混用,老代码只能处理其中一种;三是权限校验场景,下载接口需要携带 token,原生 a 标签无法自动带上请求头,导致下载 401 无权限;四是 IE、老旧安卓 PDA 设备兼容性差,部分文件下载失效。

二、定制通用文件处理组件整体设计

我独立封装一套全局文件处理公共组件,统一接管图片预览、文件下载、附件在线查看能力,全项目页面复用,分两大模块解决问题:

1. 统一图片预览模块,修复预览各类 bug

  1. 封装全局预览弹窗组件,支持传入图片数组、单张图片链接,统一逻辑;
  2. 兼容多来源图片地址:OSS 资源、后端文件流、PDA 本地临时图片路径;
  3. 性能优化:大图懒加载、超大图自动等比缩放,长图支持滚动查看;
  4. 异常兜底:图片 404、加载失败自动展示默认占位图,不会页面崩溃;
  5. 拓展功能:支持放大缩小、拖拽、键盘左右切换多张图片,适配仓库单据、发票 OCR 预览业务场景;
  6. 全局挂载,任意页面一行代码调用,替换所有页面零散预览逻辑,减少重复代码。

2. 封装统一下载工具函数,根治跨域、token、多格式下载难题

针对跨域、鉴权、两种文件源做分层处理:

  1. OSS / 文件服务器跨域资源通过 axios 发带 token 的请求获取文件二进制 blob 流,再通过 Blob+a 标签模拟下载,绕过浏览器图片、文件跨域限制;不再直接赋值 a 标签 href,解决跨域拦截。
  2. 后端接口返回二进制流场景统一处理响应类型 responseType: blob,自动解析文件名、文件后缀,避免乱码、下载文件无名称;
  3. 自动携带鉴权信息复用项目全局 axios 请求拦截器,下载请求自动带上 token、业务请求头,解决下载接口 401 无权限问题;
  4. 老旧设备兼容:做 IE、安卓低版本 PDA 设备兼容判断,区分 blob 下载和原生下载逻辑,保证仓储手持设备附件正常下载;
  5. 增加下载加载状态、失败弹窗提示,网络异常、文件不存在给出友好提示,配合之前的接口降级策略。

一句话收尾总结

针对老项目分散的图片预览、跨域下载各类疑难问题,我封装了全局统一文件处理组件,通过 Blob 流式下载解决资源跨域、token 鉴权下载问题,同时统一兼容多来源图片预览、老旧 PDA 设备,增量改造原有业务页面,统一管理附件相关能力,彻底根治历史遗留 bug 并提升后续开发效率。

20. 分支管理的前置校验思想

以 Git 规范化分支流程为基础,在项目启动、打包构建这两个关键节点,强制增加分支合规校验拦截,不允许在错误分支上调试、打包发布,从流程底层规避线上故障、代码错提交问题,属于研发流程左移、事前防控的工程化思路。

通用企业分支规范(GitFlow / 简化 GitFlow):

  1. main/master:生产环境稳定分支,仅合并上线代码,禁止直接改、直接打包;
  2. test:测试环境分支,给测试人员验收;
  3. dev:开发主分支,所有需求迭代合并到此;
  4. 功能分支 feature/xxx:开发人员本地写需求;
  5. 修复分支 hotfix/xxx:线上紧急 bug 修复。

规则约束:

  • 开发只能在 feature 分支写代码;
  • 测试打包只能打 test 分支;
  • 生产上线打包只能合并至 main 分支再构建;禁止:直接在 main/dev 打包、跨分支构建、在生产分支调试代码。

启动 / 打包前置校验完整逻辑

1. 本地启动项目(npm run serve /dev)时校验

校验规则举例:

  1. 禁止在 main/master 生产分支执行本地启动调试;
  2. 禁止直接在 test 测试分支改代码调试(test 只允许合并,本地修改会污染测试包);
  3. 仅允许 feature/*dev 分支本地启动开发;实现:在 package.json 脚本前置执行 node 校验脚本,读取当前 git 分支名,不符合规则直接终止进程,弹窗 / 控制台提示切换正确分支。

2. 打包构建(npm run build 打包测试 / 生产包)时校验

区分打包环境,强绑定对应分支:

  1. 执行生产打包命令时:强制校验当前必须是 main/master 分支,其他分支直接阻断打包,防止误把开发代码构建上线包;
  2. 执行测试打包命令时:强制校验当前只能是 test 分支,避免拿 dev/feature 代码打包丢到测试环境;
  3. hotfix 紧急修复打包:仅允许 hotfix 开头分支;校验不通过直接退出构建流程,不生成 dist 产物。

简单实现思路(面试官追问实现时口述)

  1. 写小型 node 校验工具脚本,通过 git rev-parse --abbrev-ref HEAD 命令获取当前分支名称;
  2. 区分 dev/build/build-prod 三套命令,每套命令预设允许的白名单分支;
  3. 脚本判断当前分支是否匹配当前脚本允许范围,不匹配 process.exit(1) 终止程序;
  4. package.json 的 scripts 前置挂载校验脚本:"build-prod": "node check-branch.js && vite build"

21. 优化 http 接口请求,采用强缓存和协商缓存优化请求速度

核心思路

HTTP 缓存的核心思想:把后端接口 / 静态资源存在浏览器本地,后续相同请求优先读本地缓存,减少重复网络请求,降低接口等待耗时、减轻服务器压力,分为强缓存、协商缓存两套机制,配合使用分层优化请求速度。

二、两种缓存完整区分(面试官必问)

1. 强缓存(最优,完全不走后端,速度最快)

  1. 响应头标识:

    • Cache-Control: max-age=xxx(现代标准,优先使用), Cache-Control: max-age=秒数告诉浏览器:从本次拿到资源的这一刻开始,多少秒之内,不用再发请求给服务器,直接读取本地缓存,这段时间就是强缓存有效期。计时起点不是服务器系统时间,是浏览器收到响应的当下开始倒计时,不受时区影响,这是比旧字段 Expires 更可靠的原因。
    • 旧标准 Expires 绝对过期时间,存在时区缺陷,现在只用 Cache-Control
  2. 逻辑:浏览器判断资源未过期,直接读取本地缓存,不发任何 http 请求,状态码 200 (from disk cache/memory cache)

  3. 适用资源:长期不变的静态资源(图片、js、css、字典枚举接口、固定基础配置接口)

  4. 配置示例:接口返回 Cache-Control: max-age=3600,1 小时内重复请求直接读缓存。

2. 协商缓存(走一次轻量请求,不返回完整数据)

强缓存过期后触发,浏览器携带资源标识去服务端校验:

  1. 两套标识成对使用:

    • 标识 1:Last-Modified + 请求头 If-Modified-Since 文件修改时间, Last-Modified协商缓存用的响应头,服务器返回给浏览器,记录这个资源 / 接口数据最后一次修改的标准时间戳,格式是 GMT 时间字符串。粒度只能到「秒」
    • 标识 2:ETag + 请求头 If-None-Match 文件唯一哈希指纹(精度更高,推荐)
  2. 逻辑:

    • 资源无修改:服务端返回 304 Not Modified,无响应体,浏览器继续复用本地缓存;
    • 资源已更新:返回 200,携带新数据、新缓存标识,更新本地缓存。
  3. 适用资源:会定时更新、但访问频次极高的列表、菜单、树形数据接口。

三、前端层面落地优化方案(业务接口怎么配置)

  1. 静态资源打包缓存(vite/webpack
    • 打包时给 js/css/img 加哈希文件名,配置 Nginx 强缓存;文件不变哈希不变,永久走本地缓存,大幅减少页面加载时的资源请求。
image.png 2. **业务接口分层缓存策略**
*   基础不常变接口(权限列表、字典、系统菜单、组织树形):后端配置长时长`max-age`强缓存;
*   高频更新列表(订单、库存):后端开启 ETag(ETag 是服务端返回的**资源唯一哈希指纹**,属于协商缓存标识,一串唯一字符串) 协商缓存,无数据变更返回 304;
*   实时性要求极高接口(支付状态、PDA 实时扫码):设置`Cache-Control: no-cache`,强制每次协商;
*   绝不缓存:提交、删除、新增等 POST 写接口,禁用缓存。

3. axios 请求层兜底本地缓存(补充 HTTP 缓存

  • 部分老旧后端不支持设置缓存响应头,前端封装内存 + localStorage 缓存层,根据接口地址 + 参数做 key 缓存返回数据,设定过期时间,作为 HTTP 缓存补充优化弱网场景。
  1. 缓存精准刷新机制
  • 新增 / 编辑 / 删除操作完成后,主动清除对应接口缓存,避免展示脏数据;比如修改商品后,删除商品列表缓存,下次请求拉取最新数据。

四、强弱缓存搭配使用标准流程

  1. 第一次请求接口:后端返回数据 + Cache-Control + ETag;浏览器存入本地;
  2. 短时间重复请求:命中强缓存,直接读本地,无网络开销;
  3. 超过 max-age 过期:自动发起协商请求,携带 ETag;
  4. 服务对比 ETag:无更新返回 304,复用缓存;有更新返回 200,刷新本地缓存。

六、高频踩坑 & 解决方案(加分项)

  1. 问题:POST 接口缓存失效

    答:HTTP 规范默认不缓存 POST,查询类接口统一改成 GET,再配置缓存头。

    为什么POST请求体导致缓存难以实现?

  • GET 参数拼接在 URL 里,URL + 请求头 就能唯一标识一次请求,缓存键简单可靠。
  • POST 参数在请求体,标准缓存机制不把请求体纳入缓存键。即使部分代理强行支持,解析、比对请求体也会大幅增加性能开销,还容易泄露请求体内的隐私数据(账号、密码、表单信息)。
  1. 问题:缓存脏数据,修改后页面不更新

    答:写操作后手动清除对应接口缓存;或缩短 max-age 缩短缓存周期;关键业务接口改用协商缓存。

  2. 问题:Expires 时区不一致缓存错乱

    答:废弃 Expires,统一只用 Cache-Control。

  3. 问题:本地开发缓存导致看不到最新代码

    答:开发环境关闭强缓存,Cache-Control: no-cache,生产环境开启长时效缓存。

七、简短收尾总结

我会分层搭配强缓存 + 协商缓存优化 HTTP 请求:静态资源靠哈希文件名 + Nginx 长时效强缓存;不常变更的基础接口配置 max-age 强缓存;高频更新列表接口开启 ETag 协商缓存,同时在 axios 封装一层前端本地缓存兜底,大幅减少重复网络请求,提升页面渲染速度,减轻后端服务压力。

22. 使用 Node+SCP + 云 API 搭建自动化发布流程

一句话面试总结

我基于 Node.js 开发自动化发布脚本,通过 ssh2-sftp-client 实现 SCP 服务器文件上传,对接云厂商 CDN 开放 API 自动刷新静态缓存,内置分支校验、代码检测、打包、资源备份、一键回滚整套流程,实现前端项目一键自动化部署,完全替代人工登录服务器、手动刷新 CDN 的操作,标准化上线流程、降低线上发布故障概率。

整体方案核心目标

摒弃传统人工操作:本地打包→手动传服务器文件→登录服务器替换代码→手动调用 CDN 后台刷新缓存;基于 Node 脚本整合 SCP 文件传输、厂商云服务 API,搭建一键发布流水线,执行单条命令即可完成打包、上传、部署、CDN 缓存刷新全流程,标准化上线流程、杜绝人工操作失误。

二、技术分工拆解

  1. Node.js:整体脚本调度核心,串联所有步骤,做流程判断、日志输出、异常捕获、命令执行;
  2. SCP(ssh2-sftp-client Node 库) :基于 SSH 协议,将本地打包后的 dist 静态资源,安全传输到云服务器指定站点目录;支持增量上传、覆盖旧资源、备份历史版本;
  3. 云厂商 API(阿里云 / 腾讯云 CDN OpenAPI) :调用官方接口自动提交 CDN 缓存刷新任务,替代人工登录控制台填 URL / 目录刷新。

三、完整流水线执行步骤(一键执行 npm run deploy 自动走完)

步骤 1:前置校验(流程左移,提前拦截错误)

  1. 调用 git 命令读取当前分支,匹配分支白名单:生产打包仅允许 main 分支、测试打包仅允许 test 分支,分支不符直接终止流程;
  2. 校验代码是否存在未提交变更,防止漏提交代码直接发布;
  3. 安装依赖、执行打包命令 vite build / webpack build,打包失败(编译报错、资源缺失)脚本直接退出,不执行后续上传。

步骤 2:SCP 安全上传部署

  1. 脚本读取配置文件(服务器 IP、SSH 账号密钥、站点目录、忽略文件);
  2. 连接服务器 SSH,先备份线上旧 dist 目录(重命名备份,发布出错可快速回滚);
  3. 通过 SCP 把本地打包产物增量上传至服务器站点目录;
  4. 上传完成后执行简单服务器校验:读取目标目录文件数量,判断资源是否完整上传,文件缺失抛出异常。

步骤 3:调用云 API 自动刷新 CDN 缓存

  1. 读取配置内 CDN 域名、密钥、账号信息,通过 Node 的 http/axios 调用云厂商开放 API;
  2. 传入站点根目录,提交目录级缓存刷新任务;
  3. 轮询 API 接口查询刷新任务状态,控制台打印刷新进度;
  4. 捕获 API 调用异常(密钥过期、权限不足、接口限流),给出清晰报错日志。

步骤 4:流程收尾与日志记录

  1. 全流程无报错,打印发布成功、服务器地址、CDN 刷新任务 ID;
  2. 存在任意步骤失败,自动输出完整错误日志,保留服务器旧版本代码,支持一键回滚脚本;
  3. 自动记录本次发布时间、操作人、分支、版本到本地日志文件。

五、关键落地细节与踩坑优化

  1. SCP 传输优化使用增量上传,仅传输变更文件,减少大资源上传耗时;配置文件黑名单,不上传 map 源码、临时缓存文件;采用密钥登录 SSH,摒弃明文密码,提升服务器安全。
  2. CDN API 兼容处理云厂商刷新接口有调用频次限制,脚本做限流延迟;区分 URL 刷新、目录刷新两种模式;密钥抽离环境变量,不硬编码提交代码仓库。
  3. 异常兜底机制打包失败、服务器连接超时、CDN 接口报错,任意节点中断不会覆盖线上现有代码;全程捕获 try-catch,打印可定位的详细日志。
  4. 结合之前分支校验思想脚本第一步强制读取 git 分支做校验,和你之前提到的「打包前分支校验」工程化规范打通,双重管控发布流程。

23. CDN 刷新功能

一句话面试总结

CDN 刷新用于清除边缘节点旧静态资源缓存,分 URL 单文件、目录全局两种刷新方式;我通过 Node 脚本对接云厂商开放 API,把 CDN 自动刷新整合进一键发布流水线,解决人工登录后台刷新易遗漏、操作繁琐的问题,同时搭配打包哈希文件名、HTTP 缓存策略,减少 CDN 刷新频次,保障用户访问到最新线上页面。

一、CDN 缓存基础铺垫(先讲原理)

CDN 就是全国各地的缓存节点服务器,用户访问静态资源(js/css/ 图片)时,不会每次都回源站拉文件,直接就近拿节点缓存文件,加载更快、减轻源服务器压力。

但有个问题:我们打包发布新版本后,CDN 节点还存着旧页面资源,用户浏览器会一直看到老页面,CDN 刷新就是强制让所有节点删除旧缓存、重新去源站拉最新代码

二、两种刷新模式(云厂商统一标准,阿里云 / 腾讯云都一样)

  1. **URL 刷新(单文件刷新)**单独填某一个文件完整地址,只删除这一个资源的缓存;适合只改了一张图片、单个 js 文件的小更新。
  2. **目录刷新(整站 / 文件夹刷新,项目上线最常用)**填网站根目录 /,会清空这个域名下全部静态资源缓存;我们前端项目打包发布新版本,统一用目录刷新,一次性清除所有旧页面缓存。

三、人工刷新 VS 自动化 API 刷新(结合你 Node 自动化发布脚本)

1. 传统人工操作(痛点)

发布完代码后,手动登录云厂商后台,粘贴域名提交刷新任务,有大量缺陷:

  • 容易忘记刷新 CDN,用户页面展示旧代码,线上出 bug;
  • 复制粘贴容易输错目录 / 链接,刷新失效;
  • 多环境(测试 / 生产)来回切换后台,操作繁琐耗时;
  • 无法集成发布流程,发布、刷新割裂,流程不规范。

2. 自动化云 API 刷新(你项目落地方案)

用 Node 脚本调用云厂商开放 API,一键自动刷新,整合进发布流水线:

  1. 脚本携带云账号密钥、域名、刷新类型(目录刷新)发起请求;
  2. 提交刷新任务,控制台打印任务 ID;
  3. 轮询接口查询任务完成状态,告知开发者刷新是否成功;
  4. 任意步骤报错直接终止发布流程,输出日志提醒。

四、刷新机制限制(面试官高频追问)

  1. 刷新不是瞬间完成全国几百个 CDN 节点异步清理缓存,一般 5-10 分钟全部生效,部分偏远节点会慢一点;紧急更新可以搭配资源哈希文件名长期缓存 + 按需刷新。
  2. 接口调用有频次限额云厂商限制每日刷新次数、每秒请求数,脚本里加延时节流,避免调用超限被限流。
  3. 区分「刷新缓存」和「预热缓存」刷新:删旧缓存,用户下次访问才回源拉新文件;预热:主动让 CDN 节点提前拉取新资源,用户访问直接拿到新版,适合大流量活动提前部署。

五、配套前端优化方案(加分回答)

  1. 打包静态资源加哈希后缀(app.abc123.js)文件内容一变哈希就变,全新 URL 天然不走旧缓存,大幅减少刷新依赖;
  2. 静态资源配置长时效强缓存 Cache-Control max-age稳定图片、组件脚本设置长期缓存,只有页面入口 html 设置短缓存,每次发布只刷新 html 目录即可;
  3. 自动化流水线闭环Node 发布脚本:代码校验→打包→SCP 上传服务器→调用 CDN API 刷新→日志输出,一条命令完成全部上线,完全替代人工操作。

六、线上踩坑 & 解决方

  1. 发布后部分用户依旧显示旧页面原因:浏览器本地强缓存 + CDN 缓存双重叠加;解决:html 不设置长 max-age,每次发布自动刷新 CDN 目录,同时提示用户强制清除浏览器缓存。
  2. API 密钥泄露风险解决:密钥存在服务器环境变量,不硬编码写死在前端代码、提交 git 仓库。
  3. 刷新任务失败无感知解决:脚本捕获 API 异常,中断发布流程并打印错误日志,不会出现代码更新了缓存没清的情况。

24. postMessage

一句话极简总结

postMessage 就是跨域名页面之间专用的聊天工具,让 iframe、弹窗后台计算线程能安全互传数据,使用时一定要校验对方域名防攻击

要注意的坑(面试必问)

  1. 安全红线:收到消息第一件事核对对方域名e.origin,陌生网站消息直接忽略,不然别人伪造消息篡改你页面数据;
  2. 别传函数、DOM 元素、循环嵌套的对象,传不过去,普通数组 / 对象 / 字符串没问题;
  3. 数据是复制一份传过去,超大几万条数据来回传会有点慢;
  4. 生产环境绝对不能填 *(代表所有网站都能给你发消息),有泄露 token、表单数据风险。

五、优缺点

优点

  1. 突破同源限制,实现跨域页面通信;
  2. 异步通信,不会阻塞主线程;
  3. 支持复杂对象传递,API 简单易封装。

缺点

  1. 数据是拷贝传递,超大对象传输存在性能损耗;
  2. 无消息应答机制,需要自己封装回调 ID 实现请求 - 响应模式;
  3. 不支持传递函数、DOM 元素、循环引用对象。

代码示例如下: 父页面(发送)

<iframe id="childIfr" src="https://demo.com/child.html"></iframe>
<script>
const ifr = document.getElementById('childIfr')
ifr.onload = () => {
  // 给iframe发数据,限定目标域名
  ifr.contentWindow.postMessage({msg:'你好子页面'}, 'https://demo.com')
}

// 接收子页面返回的消息
window.addEventListener('message', e => {
    // e.data 传递过来的数据 
    // e.origin 发送方域名,业务中必须校验,防恶意攻击 
    // e.source 发送窗口引用,可以反向回传数据
    
  // 安全校验域名
  if(e.origin !== 'https://demo.com') return
  console.log('收到子页面:', e.data)
})
</script>

iframe 子页面(接收 + 回复)

<script>
window.addEventListener('message', e => {
  if(e.origin !== 'https://main.com') return
  console.log('父页面消息:', e.data)
  // 回复父页面
  window.parent.postMessage({msg:'收到啦'}, 'https://main.com')
})
</script>

25. WebSocket

WebSocket什么

WebSocket 是基于 HTTP 1.1 升级的全双工长连接协议,客户端与服务端建立一次连接后,双方可随时主动互相发消息,不用每次建立 / 断开连接。

解决什么痛点

  • HTTP 是单向短轮询:只能客户端主动请求,服务端无法主动推送;

  • 轮询 / 长轮询开销大:频繁 HTTP 请求,大量重复请求头、握手,延迟高;WebSocket 一次握手后持续通信,头部极小、实时性强、性能更好。

适用场景

实时聊天、消息推送、在线协同、实时数据大屏、股票行情、游戏、IM、设备实时状态监控。

握手流程

第一步:HTTP 升级请求(客户端发)

本质先发一次普通 GET 请求,携带固定头部告知服务端:我要升级 WebSocket关键请求头:

//告知代理服务器、Nginx、后端服务,**当前 HTTP 连接不结束,需要升级协议**。
Connection: Upgrade
//明确声明**要升级到的目标协议是 websocket**。
Upgrade: websocket
//标准 HTTP 请求头,标识当前请求访问的域名 / 端口。
Host: xxx
//**作用**:携带发起请求的页面域名(如`https://xxx.com`),用于**服务端跨域安全校验**。
Origin: xxx
//安全校验,防止缓存代理错误复用连接
Sec-WebSocket-Key: 随机base64字符串(客户端密钥)
//-   WebSocket 协议标准版本号,**13 是目前浏览器统一固定版本**。
-   服务端只识别版本 13,若客户端传其他数字,会返回 400 错误,握手直接失败。
Sec-WebSocket-Version: 13(固定版本)

第二步:服务端响应 101 Switching Protocols

状态码 101 代表协议切换成功,返回响应头:

Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Accept: 密钥处理结果(Sec-WebSocket-Key拼接固定盐值后SHA1再base64)

校验通过,TCP 通道保留,从此不再走 HTTP,走 WebSocket 二进制帧传输。

安全考点

  1. 生产必须用 wss:// TLS 加密,防止中间人抓包篡改消息;
  2. 握手阶段 Origin 校验,拒绝非法域名跨域连接;
  3. 连接建立后携带 token 鉴权(url 参数 / 第一条消息携带 token),不能仅靠 cookie 鉴权;
  4. 限制单客户端最大连接数,防止恶意攻击。

26. Canvas

我项目里用到 Canvas 主要做页面水印、图片裁剪,都是快速上手开发的:

  1. 门店后台全局水印:循环绘制文字到画布,覆盖整个页面,基础 API 就能实现;
  2. 图片裁剪:结合drawImage+ 选区绘制,配合裁剪框完成图片处理。这类基础场景不需要深入原理,看完示例代码就能直接开发。

27. Echarts

1. ECharts 核心组成结构是什么?

标准四部分:

  1. 初始化容器 dom:必须有宽高的 div 画布;
  2. 实例 echarts.init (dom) :唯一图表实例;
  3. option 配置项:图表核心配置(xAxis/yAxis/series/tooltip/legend 等);
  4. setOption(option) :渲染 / 更新图表的核心方法。

Vue 中使用 ECharts 专属面试题

  1. 为什么要在 onMounted 初始化图表?只有挂载完成后 DOM 容器才存在,setup 阶段 dom 未渲染无法 init。
  2. 组件复用 / 切换页面出现多个图表重叠?组件销毁 onUnmounted 必须调用 dispose() 销毁实例,全局缓存实例变量避免多次 init。
  3. 响应式数据更新图表怎么做?监听接口数据变化,数据更新后执行 setOption

怎么实现图表自适应窗口大小?

  1. 监听窗口 resize 事件;
  2. 调用图表实例方法 myChart.resize()
  3. 优化防抖:resize 频繁触发,加 300ms 防抖,避免频繁重绘消耗性能。
const chart = echarts.init(dom)
window.addEventListener('resize', debounce(()=>{
  chart.resize()
},300))

图表容器隐藏 / 切换 Tab 后,图表变形、空白怎么解决?

原因:dom 隐藏时宽高为 0,ECharts 获取不到尺寸。解决方案:Tab 切换显示后,延迟执行 resize () ;或每次显示时重新 init。

// tab切换激活后 
setTimeout(()=> chart.resize(), 0)

ECharts 有几种渲染模式?区别是什么?

  1. canvas(默认适合大数据量折线、散点、海量点位;渲染速度快,但导出图片清晰度受 DPR 影响,内存占用更高。
  2. svg矢量图,缩放无模糊,内存占用低;不适合上万条数据的海量图表,渲染会卡顿。

业务选型:大数据大屏用 Canvas;移动端、需要高清导出报表用 SVG。

简单说下 setOption 的作用与特性

  1. 接收完整 / 增量配置,渲染或更新图表;
  2. 默认合并配置,不会覆盖未传入的字段;
  3. 支持多次调用实现图表动态刷新(切换数据、切换图表类型);
  4. 大数据量频繁更新可搭配 notMerge: true 强制清空旧配置。

柱状图、折线图怎么实现鼠标点击事件? 通过实例 on('click', callback) 绑定,回调参数 params 包含当前点击系列、下标、数值:

chart.on('click', (params)=>{
  console.log('点击数据', params.name, params.value)
})

动态异步接口加载数据怎么渲染图表?

  1. 页面先初始化空图表;
  2. 请求接口拿到后端数据;
  3. 组装 xAxis.data、series.data;
  4. 调用 chart.setOption(option) 更新渲染。

页面多个 ECharts 图表内存泄漏怎么处理?

必做销毁操作:页面销毁 / 组件卸载时,调用 chart.dispose() 释放实例,清空画布内存;不执行 dispose 会持续占用内存,多次切换页面后页面卡死。

Vue 示例:

onUnmounted(()=>{
  if(chart) chart.dispose()
})

按需引入减少打包体积(工程化优化)

不引入完整 echarts.js,按需导入模块:

// 只引入核心+需要的图表
import * as echarts from 'echarts/core'
import { LineChart } from 'echarts/charts'
import { GridComponent, TooltipComponent } from 'echarts/components'
echarts.use([LineChart, GridComponent, TooltipComponent])

28. AntV

AntV 是什么?旗下有哪些可视化套件?

AntV 是阿里开源的完整可视化解决方案,不是单一图表库,包含多条产品线:

  1. G2:基于图形语法的统计图表库(折线、柱状、饼图、业务大屏,对标 ECharts)
  2. G2Plot:G2 上层封装,开箱即用的极简图表(低代码、快速开发业务图)
  3. G6:关系图、拓扑、流程图、树图(流程图 / 组织架构 / 图谱)
  4. L7:地理空间可视化(地图、点位、热力、轨迹,对标 ECharts Map)
  5. F2:移动端轻量图表
  6. X6:流程图 / 画布编辑器

G2 核心设计思想:图形语法(Graphic Grammar)

这是 G2 和 ECharts 最本质区别,面试必背:一套完整图表由 7 层语法构成:

  1. 数据 Data:原始数据集
  2. 标记 Mark:图形元素(point 点、line 线、interval 柱状、pie 饼图)
  3. 视觉通道 Encoding:把数据映射到图形属性(x/y 位置、color 颜色、size 大小、shape 形状)
  4. 坐标系 Coordinate:直角 / 极坐标 / 笛卡尔(饼图 = 极坐标)
  5. 比例尺 Scale:数据值 → 画布像素的映射规则
  6. 视图 View:多图层、分面、多子图
  7. 交互 Interaction:tooltip、框选、图例、缩放

一句话区分 ECharts vs G2

  • ECharts:配置驱动,按图表类型写配置项(xAxis/series);
  • G2:图形语法驱动,不区分图表类型,通过「数据 + 标记 + 映射」自由组合生成任意图表,扩展性更强。

29. web3.js

web3.js以太坊及兼容 EVM 公链的前端 JS 开发库,专门用来让网页、客户端和区块链节点、智能合约打交道,相当于区块链世界的「请求工具库」,类似传统开发里的 axios

传统前端用 axios 调后端接口拿数据,而 web3.js 负责对接区块链节点:查询钱包余额、发起转账、调用链上智能合约、监听链上事件等。日常开发中常搭配小狐狸(MetaMask)钱包使用,实现网页连接钱包、链上交互的功能。

补充一个关键现状:该库官方已经停止维护归档,现在新项目业内更推荐使用 ethers.jsviem 等替代方案Ethereum。

补充追问应答(备用)

  1. 问:web3.js 如何连接浏览器钱包(MetaMask)?
  • 答:MetaMask 会在页面注入 window.ethereum 作为 provider,直接传入即可初始化:const web3 = new Web3(window.ethereum),再调用 eth_requestAccounts 向用户申请连接钱包。
  1. 问:调用智能合约必须要有 ABI 吗?
  • 答:必须要有。ABI 相当于合约的 “接口文档”,web3.js 需要依靠 ABI 解析合约的函数名、入参、返回值,没有 ABI 无法正常调用合约方法。
  1. 问:call 和 send 核心区别?
  • 答:call 用于查询数据,无 Gas 消耗、无需签名;send 用于提交交易、修改链数据,需要钱包签名并支付 Gas,交易会上链永久保存。

30. pageList 组件分封装思想

它本身不渲染任何 UI,而是个"组装工厂",把三个独立 hook 拼成一个完整列表页。

一、封装的核心思路:组合而非继承

PageList 自己几乎不写表格、表单、搜索的具体实现,而是把活儿分给三个 hook,自己只做三件事:调用 hook、串联回调、按布局拼装

PageList (组装层)
  ├─ useSearch(searchConfig)   → renderSearch()    搜索栏
  ├─ useTable(tableConfig)     → renderTable()     表格
  ├─ usePagination(...)        → renderPagination() 分页
  └─ useUpload(props)          → 导入功能
  + 自己管:新增/编辑弹窗、删除确认、保存逻辑、跨页勾选

每个 hook 返回 renderXxx() 渲染函数 + xxxState 状态。PageList 把这些渲染函数按布局摆好就行。

二、最关键的设计:用回调把 hook 串成闭环

这是整个封装的精髓。三个 hook 本来是独立的,PageList 通过回调注入把它们连起来,形成"搜索→查询→渲染"的数据流:

// 搜索 hook:把 search 作为 onSearch 回调传进去
const { searchState, onSearchClick } = useSearch({
  onSearch: search,          // ← 用户点查询时,调 PageList 的 search()
  ...props.searchConfig
})

// 表格 hook:把编辑/删除回调传进去
const { renderTable, tableState } = useTable({
  editRow, deleteRow, sortEnd,   // ← 操作列按钮点击时回调
  ...props.tableConfig
})

// 分页 hook:把翻页回调传进去
const { renderPagination, paginationState } = usePagination({
  onCurrentChange: currentChange,  // ← 翻页时调 getTableData()
  onSizeChange: sizeChange,
})

search / getTableData 这些函数,反过来又读 searchState.formpaginationState.page 来拼请求参数。hook 提供状态和触发时机,PageList 提供"拿到触发后干什么"的逻辑,双向咬合。

三、查询逻辑是怎么收口的

所有查询最终都汇到 getTableData() 一个函数,它做了三件聪明事:

function getTableData(hiddenLoading) {
  // ① 分页 vs 不分页 两种接口形态统一处理
  if (props.apiNoPagination) {
    return props.api.list({ ...extraParams, ...searchState.form })
      .then(data => { tableState.data = data || [] })
  }
  // ② 分页:自动拼 current/size,自动取 records/total
  return props.api.list({
    ...props.extraParams,      // 固定附加参数
    ...searchState.form,       // 搜索条件
    current: paginationState.page,
    size: paginationState.pageSize
  }).then(data => {
    tableState.data = data.records || []
    paginationState.total = data.total
    props.onChangeData?.({ tableState, data })  // ③ 给外部留钩子
  })
}
  • search() = 重置到第 1 页 + 查询(搜索、删除、新增后用)
  • getTableData() = 保持当前页查询(翻页、编辑后用)

这俩的区别就是要不要把 page 重置成 1,这个细节封装得很到位。

四、增删改也全部内置

PageList 把 CRUD 的标准动作都收了,业务页面不用自己写:

addRow()    → 打开弹窗,isEdit=false
editRow()   → 打开弹窗回填,isEdit=true(支持 editDataFromDetail 先拉详情)
save()      → 根据 isEdit 自动选 api.create / api.update,成功后刷新
deleteRow() → 内置 ElMessageBox 确认 → api.batchDelete → 刷新
batchDelete()→ 取勾选项批量删

save 这段尤其体现封装意图——它自己判断该走新增还是修改接口:

if (isEdit || form.id) {
  return props.api.update(_params).then(...)  // 编辑后 getTableData(留在当前页)
}
return props.api.create(_params).then(...)    // 新增后 search(回第一页)

连"新增完回第一页、编辑完留在当前页"这种交互细节都替你想好了。

五、灵活性靠"插槽 + ref 暴露"兜底

封装得越死,越要留口子,否则特殊需求就用不了。PageList 留了两层口子:

插槽——operate(操作栏)、content(内容区)、default(挂自定义弹窗)等,而且很巧妙地把内部方法透传出去:

slots.operate?.({ addRow, batchDelete, search, checkedList, renderBtns })
// ↑ 默认按钮也给你,
// 你可以 renderBtns() 后再追加自己的

ref 暴露——通过 expose()searchtableStatesearchState 等吐出去,父组件能手动刷新、能拿当前搜索条件去做导出。

六、还顺手封了"跨页勾选"这种脏活

enableCrossPageSelection 开启后,用 allCheckedMap 这个对象按行 key 存全局勾选状态,翻页时 saveCurrentPageChecked / restorePageChecked 保存和恢复。这种容易写错的逻辑封在组件里,业务方一个 prop 就用上了。


面试 / 总结时怎么说封装价值

"PageList 的封装本质是用组合模式把 search/table/pagination 三个独立 hook 编排成一个标准列表页。它通过回调注入把三者的数据流串成闭环——hook 负责状态和触发时机,组件负责查询、增删改的业务逻辑。对外用声明式 config(tableConfig/formConfig/searchConfig)降低使用成本,再用插槽和 ref 暴露兜住灵活性。好处是一个 CRUD 页面从一两百行降到几十行配置,且交互细节(翻页保持、新增回首页、跨页勾选)全部统一;代价是抽象层偏厚,新人要先理解这套约定。"

这套设计和请求层的 CreateService(自动生成 page/create/update/batchDelete)是配套的——一个把接口标准化,一个把页面标准化,接上就是完整 CRUD。这点提一句会让面试官觉得你看到了整体架构。

31. react相关

平常react用得多吗

面试官您好,我日常业务开发主要深耕 Vue2/Vue3 完整技术栈,90% 以上项目都是 Vue 体系,包括 Vue2 Options、Vue3 组合式 API、Pinia、VueRouter、Vite/Webpack 工程化、自定义指令 / 路由守卫、RBAC 权限系统这些都有完整落地经验。React 我只做过简单基础学习,了解它组件化、单向数据流、虚拟 DOM、JSX这些和 Vue 相通的核心思想,也清楚 Vue 和 React 的设计差异:Vue 是模板驱动、双向绑定,React 是 JSX 函数式、纯单向数据流;基础的函数组件、hooks 概念能看懂简单代码,但没有独立负责过 React 业务项目,所以求职更专注 Vue 方向岗位,我的实战积累全部集中在 Vue 生态。

那 Vue 和 React 核心区别是什么?(必问)

  1. 语法层面:Vue 分模板 template + 脚本,更贴近传统 HTML;React 用 JSX,JS 和标签写在一起,纯 JS 驱动。
  2. 响应式机制:Vue2 Object.defineProperty、Vue3 Proxy 自动监听数据变化,修改数据自动更新视图;React 无自动响应,必须 setState 触发重渲染,靠手动更新状态。
  3. 数据流:Vue 支持 v-model 双向绑定,简化表单;React 严格单向数据流,子组件不能直接修改父 Props。
  4. 逻辑复用:Vue2 mixin(有命名冲突缺陷)、Vue3 组合式 API;React 全靠 Hooks 复用逻辑。
  5. 编译:Vue 分编译时、运行时;React JSX 仅编译为 createElement 函数。

为什么不深耕 React,选择 Vue?

之前任职公司业务全部采用 Vue 技术栈,长期做企业 B 端管理系统,Vue 上手快、模板开发效率高,更贴合业务快速迭代需求,长期积累了完整 Vue 落地经验;我职业规划持续深耕 Vue 生态,所以优先投递 Vue2/3 相关岗位。

32. vue2和vue3的区别

一句话: Vue3 相比 Vue2 核心升级:底层改用 Proxy 解决响应式缺陷;新增组合式 API 与 script setup,大型项目逻辑更好复用;编译层通过静态提升、PatchFlags 优化 diff 性能;原生完美支持 TS;配套 Vite、Pinia 生态,同时新增 Teleport、多根节点等实用特性,整体性能、开发体验、工程化全面优于 Vue2。

Teleport 是 Vue3 内置内置组件,直译传送门;作用:把组件内部的 DOM 节点,“传送” 到页面任意 DOM 位置挂载,脱离当前父组件 DOM 层级,但逻辑、作用域、数据响应式、事件依然保留在原组件内。

一、底层响应式原理(最核心考点)

Vue2 底层响应式原理

  1. 采用 Object.defineProperty() 劫持对象已有属性

  2. 缺陷:

    • 无法监听新增 / 删除属性,需要 $set / this.$delete
    • 无法监听数组下标、长度修改,重写 7 个数组pushpopshiftunshiftsplicesortreverse方法实现拦截,因为这7个是变异方法(会改变原数组);
    • 递归遍历对象深层属性,初始化开销大,大对象卡顿。

Vue3 底层响应式原理

  1. 采用 Proxy 代理整个对象,搭配 Reflect

  2. 优势:

    • 原生支持监听新增、删除属性、数组下标、length;
    • 惰性递归:只在访问时才递归深层对象,初始化更快;
    • 支持 Map/Set 等复杂数据类型。

二、API 写法:Options API vs Composition API

Vue2:Options API(选项式)

  • 代码按功能拆分:data/methods/computed/watch/mounted 分散;
  • 大型页面中,同一业务逻辑散落多处,阅读、复用困难;
  • 逻辑复用靠 mixin,存在命名冲突、变量来源不清晰、多 mixin 层级混乱问题。

Vue3 双支持:Options + Composition API(setup)

  1. 组合式 API setup(推荐)

    • 按业务功能聚合代码,同一个逻辑(如分页、搜索)写在一起;
    • 抽离自定义 hooks 实现逻辑复用,无命名冲突,依赖清晰;
  2. <script setup> 语法糖(开发主流):

    • 无需 return、无需手动引入 defineProps/defineEmits,更简洁;
    • 编译期优化,性能更好。

三、虚拟 DOM & 编译优化(性能重点)

1. Diff 算法优化

  • Vue2:全量对比节点;
  • Vue3:PatchFlags 静态标记编译时标记节点类型(纯静态文本 / 动态文本 / 绑定 class / 绑定子组件),diff 时跳过完全静态节点,只对比动态部分。

2. 静态提升

Vue2:每次渲染函数都重新创建静态 VNode;Vue3:把不会变化的静态节点提升到渲染函数外部,只创建一次,减少内存占用。

3. 事件缓存 v-on

Vue3 内联事件 @click="fn" 自动缓存,避免每次渲染生成新函数,减少 diff 开销。

4. Fragment 多根节点

Vue2 模板只能有单个根标签,必须套 div;Vue3 支持多根节点(Fragment),不生成多余 DOM,减少层级冗余。

5. Teleport / Suspense 内置组件

Vue2 无内置,需自己封装弹窗传送;Vue3 新增:

  • Teleport:将 DOM 挂载到 body 任意位置(弹窗、遮罩);
  • Suspense:异步组件加载占位、骨架屏。

五、生命周期、钩子变化

1. 组合式 API 生命周期对应

表格

Vue2 OptionsVue3 setup Hooks
beforeCreate / createdsetup(直接替代)
beforeMountonBeforeMount
mountedonMounted
beforeUpdateonBeforeUpdate
updatedonUpdated
beforeDestroyonBeforeUnmount
destroyedonUnmounted

2. 废弃更名

  • beforeDestroy / destroyedonBeforeUnmount / onUnmounted
  • 移除 this.$on / this.$emit 全局事件总线,推荐 mitt/pinia 替代。

六、响应式 API:ref /reactive

  1. Vue2:只有 data 自动响应式,基础类型无法单独监听;

  2. Vue3:

    • ref:处理基础类型(字符串、数字、布尔),.value 读写;
    • reactive:处理对象、数组;
    • 配套工具:toRef/toRefs/computed/watch/watchEffect

七、指令与模板

  1. v-model 语法升级

    • Vue2:组件只能单个 v-model,.sync 修饰符实现多双向绑定;
    • Vue3:移除 .sync,支持多参数 v-model:v-model:title="val"
  2. v-if 和 v-for 优先级

    • Vue2:v-for 优先级高于 v-if(性能隐患);
    • Vue3:v-if 优先级更高,先判断再循环,优化性能。

八、状态管理 Vuex → Pinia

  1. Vue2 标配 Vuex:嵌套 modules、mutation 必须同步、写法繁琐;

  2. Vue3 主推 Pinia:

    • 无 mutations,state 直接修改;
    • 天然 TS 友好、模块化扁平化、代码简洁;
    • 去除繁琐模板代码,支持自定义 hooks 抽离逻辑。