一、vue 相关
1. Pinia vs Vuex
相同点:
都是 Vue 项目全局状态管理工具:比如登录用户信息、购物车、全局弹窗状态,组件之间要共享的数据,不用父子一层层传,统一存在这里。
- Vuex:老版 Vue 官方工具(Vue2 时代主流)
- Pinia:新版官方推荐(Vue3 专用,Vue2 也能用,官方说以后主推 Pinia,Vuex 不再维护更新)
核心区别,一条一条说人话
代码结构:Vuex 繁琐,Pinia 清爽
Vuex 四件套强制拆分,写起来啰嗦
固定拆 4 块文件,必须分开写:
- state:存数据
- getters:计算属性(拿加工后的数据)
- mutations:唯一改数据的地方(必须同步)
- 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 强制规则(新手极易踩坑):
- 只能在 mutation 里修改 state,action 不能直接改
- mutation 必须同步,异步代码只能放 action违反规则调试器不追踪、数据变更难排查
Pinia 无强制约束:
- action 同步 / 异步随便写,直接修改 state
- 简单场景甚至组件里直接
store.xxx = 123修改数据(规范项目还是推荐放 action)规则更少,学习门槛低。
7. 体积与维护
- Vuex:官方已停止维护,不再更新新功能,只修严重 bug
- Pinia:Vue 官方团队持续维护,是未来标准,Vue3 项目首选
三、极简总结(一句话区分)
- 新项目 Vue3:直接用 Pinia,代码少、类型友好、上手简单,官方主推;
- 老项目 Vue2 遗留:还在用 Vuex 不用强行重构,新项目别再写 Vuex;
- 核心痛点对比: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,所有页面直接引入复用。
使用方式极简说明
- 新建混入文件,写通用 data、方法、钩子;
- 组件配置
mixins: [xxxMixin]数组,支持引入多个混入; - 组件会自动合并混入内所有逻辑,直接调用混入里的方法、变量。
合并规则(面试官必问核心考点)
- 生命周期钩子:全部执行混入的钩子 和 组件自身钩子 都会触发,先执行混入,再执行组件本身;多个混入按数组顺序依次执行。
- data、methods、计算属性:组件优先级更高如果混入和组件出现同名变量 / 函数,组件自身覆盖混入,不会报错。
- 多个混入之间冲突:数组靠后的混入覆盖前面的。
适用场景(面试举例)
- 通用表格页面:分页、重置搜索、批量删除、导出逻辑抽混入;
- 弹窗通用逻辑:打开关闭弹窗、表单清空、校验重置;
- 页面缓存:activated/deactivated 统一处理缓存刷新逻辑;
- 权限通用判断:封装按钮权限校验公共方法。
mixins 致命缺点(重点,必主动说)
-
命名污染、隐式依赖混入和组件变量重名会静默覆盖,没有任何提示;阅读代码时,不知道某个方法 / 变量来自哪个混入,代码溯源困难。
-
多混入嵌套逻辑混乱引入 3 个以上混入,多层逻辑叠加,钩子执行顺序难梳理,后期维护成本极高。
-
传参不直观混入无法显式接收入参,只能依赖组件内部约定字段,容易出现字段缺失 bug。
-
Vue3 不推荐,官方废弃Vue2 只有 mixins 一种复用方案;Vue3 setup 用组合式函数(useXxx)替代,解决所有混入缺陷。
Vue3 替代方案:组合式 Hooks(对比回答加分)
- 组合函数是显式导入、显式导出,变量方法必须手动解构,来源清晰;
- 无命名覆盖静默合并问题,重名变量需要手动起别名,强制区分;
- 可以传参,逻辑封装更灵活;
- 天然支持 TS,类型推导完整,mixins 类型提示很差。
3. 微信支付
前置基础(必看)
- 必须有 微信商户号(mch_id) + 公众号 / 小程序 APPID,两个绑定在一起
- 商户后台配置:支付授权目录、API 证书、支付密钥(APIv3 密钥)、商户证书
- 流程统一逻辑:前端发起支付请求 → 后端调用微信下单接口拿支付凭证 → 前端调微信拉起支付弹窗
重点:绝对不能前端直接调微信下单,所有支付核心接口必须走后端,防止泄露密钥、恶意刷单
整体流程
- 用户下单,前端把商品、金额传给自己后端
- 后端调用微信
JSAPI下单接口,拿到prepay_id预支付订单 - 后端按规则生成签名,返回支付参数给前端
- 小程序调用
wx.requestPayment唤起支付
4. OCR 识别
基础概念
OCR 全称 光学字符识别 ,简单说:把图片里的文字,转成可复制、可编辑的纯文本。举个例子:
- 拍一张身份证照片 → OCR 自动提取姓名、身份证号、地址;
- 拍照发票 → 自动识别金额、税号、开票信息;
- 截图文档、手写笔记 → 一键导出文字,不用手动打字。
前端纯浏览器识别(轻量小项目,离线可用)
代表库:Tesseract.js、PP-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
前置统一准备
-
分别去对应开放平台申请 Web 端 Key
-
页面地图容器必须固定宽高,否则地图渲染空白
-
通用业务能力:定位、打点标记、地址解析、路线规划、搜索 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 打通,微信内定位更稳定
- 社交、门店、商圈数据完善,本地生活、社交类项目首选
- 免费额度更高,中小企业成本更低
前置统一准备
-
分别去对应开放平台申请 Web 端 Key
-
页面地图容器必须固定宽高,否则地图渲染空白
-
通用业务能力:定位、打点标记、地址解析、路线规划、搜索 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)
- 数据层:
data维护完整表单对象,动态明细用数组存储,每条明细生成唯一 UID 作为循环 key,规避 index 渲染错乱; - 增删逻辑:封装
addRow/delRow方法,通过数组原生push/splice操作保证响应式;若需给明细新增字段,使用$set解决 Vue2 对象新增属性无响应问题; - 表单校验:动态行通过拼接字符串
prop="数组.下标.字段名"实现单行独立校验,区分全局固定项规则与动态行子规则; - 页面销毁:组件销毁时清空表单校验实例,防止内存残留
多层嵌套复杂表单方案(明细下带子表单、多级数组)
- 数据结构设计:采用嵌套数组结构,父明细数组每一项内部包含子明细数组,支持无限层级拓展;
- 渲染逻辑:v-for 循环嵌套,每一层循环独立维护新增、删除方法,精准操作对应层级数组;
- 校验适配:prop 多层拼接
父数组.下标.子数组.子下标.字段,实现嵌套字段精准报错定位; - 解耦优化:将单条嵌套明细抽为独立子组件,通过
defineProps传明细数据、defineEmits派发增删事件,拆分复杂逻辑。
大型表单全局状态方案(跨页面分步表单、多标签共享表单)
Vue2:Vuex 方案
- 将完整表单数据、动态明细数组存入 Vuex state;
- 增删行、修改字段、重置表单全部封装为 mutations/actions;
- 页面组件仅读取 state、调用 action,多页面共享同一份表单缓存。
9. Vue3 复杂动态表单解决方案
通用基础实现方案(中小复杂度表单,增删行 / 单层数组)
Vue3 方案(Composition API + Element Plus)
- 数据层:用
reactive统一托管完整表单数据源,明细数组同样采用唯一 ID 做 key; - 增删逻辑:封装独立
addRow/delRow函数,数组操作天然响应式,无需额外 API; - 表单校验:逻辑同 Vue2,prop 字符串拼接;可抽离校验规则为独立常量,复用性更强;
- 生命周期:
onUnmounted销毁表单 ref、清空校验缓存。
大型表单全局状态方案(跨页面分步表单、多标签共享表单)
Vue3:Pinia 方案
- 单独创建表单仓库,统一托管表单数据、所有动态行操作逻辑;
- 页面直接引入 store 调用 actions,天然类型推导,相比 Vuex 简化代码;
- 支持持久化插件,刷新页面保留已填写的动态表单数据。
通用性能优化方案(面试必提亮点)
- key 优化:禁用数组下标 index,使用 uuid / 时间戳唯一标识,删除中间行避免 DOM 渲染错位;
- 组件拆分:动态行抽离独立子组件,减少父组件页面重渲染范围;
- 防抖节流:输入、数字输入框绑定防抖,减少频繁赋值触发的校验重计算;
- 懒渲染:明细行数上千时,采用虚拟列表
vue-virtual-scroller只渲染可视区域 DOM; - 校验优化:提交时统一全量校验,输入时局部单字段校验,避免全局循环校验卡顿。
Vue2 与 Vue3 实现核心差异(面试对比回答)
- 响应式:Vue2 需
$set处理对象新增属性;Vue3 Proxy 天然支持,无额外 API; - 代码组织:Vue2 所有逻辑写 methods,大表单 methods 臃肿;Vue3 可按功能拆分多个独立函数,逻辑分层清晰;
- 全局状态:Vue2 Vuex 分层繁琐(state/mutation/action);Vue3 Pinia 扁平化 API,代码更少、TS 友好;
- 类型支持:Vue3 组合式 API 天然适配 TS,动态表单、嵌套字段类型自动推导;Vue2 类型提示薄弱,需额外声明类型。
10. 超大表单
适用场景:上千字段、可配置表单、运营后台自定义表单
form-making 是Vue2 专用的开源表单低代码组件库(完整 npm 包) ,属于第三方插件包,可 npm 安装引入使用。它不是简单表单校验插件(vee-validate/async-validator),是完整表单渲染解决方案包,包含编辑器、渲染器、动态数组内置能力一体化。
vform3 是 form-making 的 Vue3 升级配套 npm 插件包,专门适配 Vue3 + Element Plus,是同团队开发的低代码表单一体化组件库,完美承接 Vue2 项目升级后的动态表单需求。属于完整表单解决方案包,不只是单纯校验插件,包含「可视化拖拽编辑器 + JSON Schema 表单渲染器」两大核心模块。
- 核心思路:维护一份表单 JSON Schema 配置,通过递归组件自动渲染所有表单项、动态数组块、嵌套子表单;
- Vue2 选型:
form-making/el-form-renderer;Vue3 选型:vform3/element-plus-form-design; - 能力覆盖:JSON 配置控制字段显隐、动态数组增删、联动校验、自定义组件插槽;
- 优势:无需手写大量 v-for 循环,新增表单仅修改配置,大幅降低维护成本。
11. 大数据树形渲染
业务场景定义
后端一次性返回成千上万条层级嵌套树形数据(部门组织、菜单权限、商品分类),直接全量渲染会出现页面卡顿、白屏、滚动延迟、展开 / 勾选交互阻塞,统称大数据树形渲染性能问题。
场景选型标准(面试场景题标准答案)
- 几千条、需要全局搜索筛选:虚拟滚动树
- 十万级、百万级组织架构、权限菜单:懒加载树
- 一两千条简单分类树:前端预处理 + 默认折叠优化即可
- 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虚拟滚动属性
- Vue2:
-
配套能力:自带展开、勾选、搜索过滤、懒加载兼容,不用手动分页,体验流畅。
-
适用:需要一次性加载全量树、支持全局搜索筛选、频繁展开收起的场景。
方案 2:后端分片懒加载树(百万级海量数据,后台权限 / 组织架构常用)
-
核心原理:页面初始只渲染一级根节点;点击展开节点时,再单独请求当前节点的子级数据,按需加载。
-
实现逻辑:
- 树组件开启
lazy懒加载配置,绑定加载回调; - 回调中携带当前节点唯一 ID,请求接口返回该节点下级;
- 缓存已加载子节点,重复展开不再重复请求。
- 树组件开启
-
优点:前端初始 DOM 极少,首屏秒开,无内存压力;
-
短板:无法全局全树搜索,搜索只能后端分页查询匹配节点。
方案 3:前端数据分片预处理(中等数据量,几千条层级树)
-
数据裁剪:后端返回完整树形后,前端递归扁平化处理,只保留渲染必需字段,剔除冗余后端字段,降低内存占用;
-
折叠优化:默认全部折叠,仅展开选中路径,减少初始渲染节点;
-
过滤防抖:搜索框加防抖,不实时递归全树匹配,停止输入后再筛选;
-
节流监听:滚动、展开操作加节流,避免高频重复渲染。
-
局限:上万条以上数据仍会卡顿,仅作为辅助优化手段,不单独作为核心方案。
方案 4:分页树(极少用,扁平类树形)
将树形数据转为扁平列表,后端分页返回,仅展示当前页层级,交互割裂,一般只做兜底方案。
Vue2 / Vue3 实现差异(面试官必对比提问)
-
1. 递归树组件响应式
- Vue2:递归传参多层嵌套,修改子节点勾选 / 展开需
$set,否则无响应;大量节点修改会触发整棵树重渲染; - Vue3:Proxy 响应式,子节点修改精准更新,无需额外 API,局部更新粒度更细。
- Vue2:递归传参多层嵌套,修改子节点勾选 / 展开需
-
2. 虚拟滚动库适配
- Vue2 虚拟树插件生态老旧,部分需二次封装;
- Vue3 Element Plus 原生内置 tree 虚拟滚动,零第三方依赖,开箱即用。
-
3. 渲染性能
- Vue3 diff 算法优化,同量级节点渲染速度比 Vue2 提升一倍,大数据场景阻塞时间明显缩短。
-
4. TS 支持
- Vue3 可给树形节点、递归组件定义完整类型,避免层级字段匹配错误。
通用性能优化点(面试加分,深挖必说)
-
DOM 渲染优化
- 树节点循环 key 用唯一 id,禁止数组下标,避免节点复用错乱;
- 节点模板轻量化,移除多余嵌套 div、复杂样式;复杂自定义节点抽离子组件,缩小重渲染范围。
-
数据内存优化
- 树形数据扁平化缓存,避免每次筛选重复递归;
- 页面销毁清空树形缓存、取消滚动监听、销毁虚拟滚动实例,防止内存泄漏;
- 勾选 / 半选状态单独存储 Map,不污染原树形数据。
-
交互节流防抖
- 搜索输入框防抖;滚动、展开、勾选操作节流,限制渲染执行频率。
-
功能降级兜底低配置设备关闭虚拟滚动,自动切换懒加载模式;超大数量弹窗树默认只展示前 2 级。
高频踩坑 & 解决方案(面试官深挖痛点)
- 虚拟滚动树展开 / 勾选错位:缓存每个节点高度,固定行高或动态计算高度;
- 全量搜索卡顿:防抖 + 后端检索兜底,前端只做少量数据过滤;
- 懒加载树勾选半选状态丢失:缓存已加载子节点勾选状态,每次加载后回填;
- Vue2 多层递归树修改数据不更新:使用 $set 操作节点属性,或扁平化数据存储状态;
- 大数据树内存持续上涨:页面销毁清空树形数据源、解绑滚动事件。
12. 虚拟表格优化大数据表格卡顿
一、先说卡顿根源
普通表格是一次性把后端返回几万条数据全部渲染成 DOM,页面会生成上千个 tr、td 节点,DOM 数量巨多:
- DOM 渲染、回流重绘耗时久,页面打开白屏、滚动卡顿;
- 内存占用飙升,切换页面、弹窗操作会明显卡死,低配设备、PDA 机器更严重。虚拟表格核心思路:只渲染可视区域内的表格行,不在视口内的行不生成 DOM,始终维持几十条 DOM,和总数据量无关。
二、底层实现原理
- 计算单一行高度、表格容器可视高度,算出一屏最多展示多少行;
- 计算全部数据总高度,用空白占位撑开滚动条,滚动条长度和全量数据保持一致,用户感知和完整表格无区别;
- 监听滚动事件,实时计算当前滚动偏移量,动态截取当前可视区间的数据,替换渲染 DOM;
- 上下预留少量缓冲行,滚动时不会出现空白闪烁。
三、两种落地方案(业务区分)
方案 1:组件库自带虚拟表格(项目主流,开箱即用)
Element Plus、Ant Design Vue、vxe-table 都内置虚拟滚动 table,只需要开启属性,配置行高,传入完整大数据数组即可。适用场景:常规后台列表、订单、库存大数据页面,不用自己造轮子,维护成本低。
方案 2:自行封装虚拟表格(深度优化、定制化需求)
底层基于滚动容器监听 scroll,手动做切片截取、占位高度计算,适合高度不固定、合并单元格、复杂自定义单元格、PDA 端极简页面。
四、配套多层优化手段(加分,贴合你的业务场景)
- 后端分页 + 虚拟滚动结合
- 十万级数据不一次性全量拉取,后端分页,前端滚动到底懒加载下一页,再用虚拟表格渲染当前页数据,减少前端内存占用;
- 单元格轻量化
- 单元格去掉复杂嵌套组件、频繁重渲染指令,复杂操作按钮、图表抽离懒加载组件,缩小单 DOM 渲染开销;
- 滚动事件节流
- scroll 高频触发,加节流控制渲染执行频率,避免短时间大量计算阻塞主线程;
- 唯一 key 优化 diff
- 表格行绑定业务唯一 ID 做 key,不用数组下标,减少 Vue diff 算法开销;
- 离线场景兼容
- PDA 弱网缓存全量本地数据,虚拟表格读取本地缓存数据渲染,不依赖实时接口。
五、解决的业务痛点
- 上万条库存、订单、单据列表页面打开秒加载,不会长时间白屏;
- 上下滚动丝滑,无明显延迟、卡顿;
- 降低浏览器内存占用,长时间打开页面不会崩溃;
- 适配仓库 PDA 低配安卓设备,低端机器也能流畅展示大数据单据。
六、高频追问:定高虚拟表格和不定高虚拟表格区别
- 固定行高:计算逻辑简单,性能最好,绝大多数后台表格首选;
- 动态不固定行高:需要实时测量每行实际高度,维护高度缓存 Map,计算开销更大,仅用于单元格文本换行、内容高度不统一的场景。
七、收尾总结
大数据表格卡顿本质是 DOM 节点过多,虚拟表格通过只渲染可视区域行、占位模拟滚动条的方案,把 DOM 数量控制在常数级;实际项目中优先使用组件库内置虚拟表格,搭配后端分页懒加载、单元格轻量化、滚动节流等手段,彻底解决上万条数据表格打开、滚动卡顿问题,同时兼容 PDA 工业设备弱网本地大数据渲染场景。
13. 接口降级
什么是接口降级
简单说就是后端服务压力过大、或者第三方接口崩了的时候,主动关掉非核心接口的完整能力,返回兜底数据,保住下单、支付这类核心流程不垮,避免雪崩,属于高可用保障手段。
降级和熔断的简单区分(高频追问)
熔断是连续失败后,直接切断调用链路,过一段时间再尝试恢复;降级是主动舍弃非核心能力、返回兜底数据,两者一般搭配使用,熔断是手段,降级是最终兜底表现。
前端层面降级方案(前端岗位重点说)
-
缓存兜底降级
- 接口请求失败 / 超时,直接读取本地缓存、sessionStorage、Pinia/Vuex 里存的旧数据展示,不直接空白报错。比如商品列表接口崩了,展示上次打开页面缓存的列表,同时给用户弱提示 “数据加载延迟,展示历史内容”。
-
功能模块降级
- 后端返回降级标记,前端直接隐藏非核心模块。比如大促高峰期,关掉商品推荐、评价列表、足迹模块,只保留商品详情、下单支付核心模块,减少接口并发。
-
请求限流 / 节流降级
- 搜索框、筛选这类高频请求做防抖节流,限制短时间重复发请求;同一接口短时间连续失败,直接拦截一段时间不再发起请求,减轻后端压力。
-
静态兜底页面降级
- 核心接口完全挂掉时,渲染预设静态兜底模板,比如 “当前访问人数过多,请稍后重试”,不白屏、不疯狂重试轮询。
-
第三方接口单独降级
- 像地图、OCR、物流查询这类第三方接口,单独捕获异常,接口失败就隐藏对应区域,不阻塞页面主体功能。
项目落地完整流程(面试官最爱问实战)
- 前后端约定降级标识:后端返回特定 code,标记当前接口已降级;
- 请求统一拦截器封装降级逻辑:axios 请求拦截器统一捕获超时、500、熔断状态,自动走缓存兜底;
- 页面区分强弱提示:非核心模块降级只小字提示,核心功能降级弹窗友好引导;
- 开关可配置:后台配置降级开关,不用改代码上线,大促前一键开启降级策略;
- 监控告警:接口失败率上涨触发告警,人工介入判断是否手动降级。
举业务实战例子(口语化加分)
比如电商大促场景:高峰期服务器压力暴涨,后端开启降级,前端收到标识后,关掉商品实时评论、浏览足迹、猜你喜欢三个非核心接口,同时商品列表接口失败就展示上次缓存的商品数据,优先保证用户能正常浏览商品、提交订单、微信支付,不会因为次要功能拖垮整个下单流程。
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 分级 + 后台静默更新双保险
- "安全性?" → 坦诚说现在是明文存储,敏感场景可以加本地加密(这是已知短板)
判断网络我做了三层: 优先看强制离线开关(测试用),然后按平台分——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.getNetworkType,networkType === '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 = 基于角色的权限控制。不直接给用户绑权限,中间加一层角色:
- 后台先建一堆权限点:页面路由、新增按钮、编辑按钮、删除按钮、导出按钮,每一个操作都是单独权限标识;
- 创建角色(管理员、运营、普通员工),给角色勾选一堆权限;
- 给用户分配角色,用户自动拥有该角色下所有权限。
好处:改权限不用一个个改用户,只改角色就行,批量管理省事。
整套落地完整流程(从头到尾串一遍,面试必说)
- 登录阶段
- 用户输账号密码登录,后端返回用户信息、角色、该角色全部权限标识符数组。
- 存储权限
- 把权限数组存全局状态(Vuex/Pinia),全局任何页面都能读取。
- 生成动态路由
-
前端把完整路由表做权限过滤,过滤出用户能看的页面,动态挂载路由,渲染侧边菜单。
-
权限过滤执行位置在全局前置路由守卫 router.beforeEach,
beforeEach是每次跳转路由最先触发的全局守卫,在页面渲染、组件创建之前执行,适合统一做权限校验、动态路由挂载、无权限拦截,是实现 RBAC 动态路由过滤的唯一合适守卫。 -
完整执行逻辑
-
进入
router.beforeEach((to, from, next) => {}) -
判断本地是否存在用户登录态 / 权限数组:
-
未登录:直接
next('/login')跳转登录页; -
已登录,判断是否已经动态挂载过路由(加标记变量如
hasAddRoute):- 未挂载:用后端返回的权限数组过滤完整路由表,调用
router.addRoute()动态追加可访问路由,标记为已挂载,再执行next(to.fullPath)重新触发一次守卫; - 已挂载:直接校验当前
to页面是否在权限列表内,无权限则next('/403'),有权限next()放行。
- 未挂载:用后端返回的权限数组过滤完整路由表,调用
区分其他守卫为什么不能做这件事
- 独享守卫 beforeEnter / 组件内守卫 beforeRouteEnter执行时机太晚,路由已经匹配完成,无法提前拦截非法地址,也不能统一批量挂载全部动态路由,只适合单页面独立权限,不做全局 RBAC 过滤。
- 后置守卫 afterEach路由已经跳转完成,页面已经渲染,无法拦截跳转,仅用来做页面标题、埋点,不能处理权限过滤。
Vue3 + Vite 实操关键坑点(面试加分)
addRoute动态添加路由后,必须next(to.fullPath)重走一次守卫,否则第一次加载页面 404;- 刷新页面会重置路由实例,所以每次刷新进入守卫都要重新拉取权限、重新挂载动态路由;
- 必须加挂载标记,防止无限循环
beforeEach。
极简面试口述总结
动态路由的权限过滤、路由挂载统一在全局前置守卫 router.beforeEach 中处理,它是每次路由跳转最先执行的钩子,能在页面渲染前完成路由筛选、动态添加和无权限拦截;其他守卫执行时机滞后,不适合全局 RBAC 权限体系的统一过滤。
-
- 页面渲染按钮
- 页面上所有操作按钮绑定权限指令,自动根据权限数组判断显隐。
- 双重安全拦截
- 路由守卫:用户手动输入无权限页面地址,直接拦截跳转;
- 请求拦截器:发起接口前校验接口对应的权限,无权限直接拦截请求,弹窗提示无操作权限。
- 权限更新
- 后台给用户改角色、改权限后,前端重新拉取权限列表,重新过滤路由、更新页面按钮状态。
Vue2 和 Vue3 微小区别(面试官容易追问)
- 动态路由 API:Vue2/Vue3 都是
router.addRoute,写法基本一致; - 全局权限指令:Vue2 用自定义指令钩子,Vue3 composition API 指令写法微调;
- 状态存储: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 指令核心差异(面试口述重点)
-
注册方式
- Vue2:
Vue.directive()全局挂载; - Vue3:
app.directive()全局,支持<script setup>局部定义指令。
- Vue2:
-
空值兜底Vue3 DOM 操作需要加可选链
el.parentNode?.,防止父节点不存在报错,Vue2 无需。 -
响应更新Vue2 仅
inserted只会在首次渲染执行;如果用户切换角色、权限更新,按钮不会自动隐藏;Vue3 加update钩子,数据变化自动重新校验权限。 -
组合式状态Vue3 指令内可以直接
useStore()获取 Pinia;Vue2 指令只能读取全局 Vuex,无法在钩子内部引入组合式 API。 -
钩子细微调整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 改造。
老旧系统原生存在的核心痛点
- 架构老旧:无统一请求拦截器,接口分散,没法统一做 AI 接口降级、缓存;
- 无权限体系:没有 RBAC 动态路由、按钮级权限,AI 功能入口、AI 操作按钮无法管控;
- 无离线弱网方案:仓库 PDA、外勤场景断网时,AI 识别 / 表单任务直接丢失;
- 代码耦合严重:页面硬写死表单、树形组件,无法快速接入 AI 自动填充能力;
- 跨系统兼容:老系统和 AI 中台、仓储 PDA、支付系统接口不统一,数据格式差异大。
前端改造落地方案(面试分层次口述)
- 中间适配层封装 在老旧系统原有请求逻辑外层,封装统一 axios 桥接层,统一对接 AI 中台接口,统一处理 AI 接口超时、降级、缓存、签名校验;老页面不用大面积改业务代码,直接复用封装后的 AI 请求方法。
- 权限体系增量接入 不重构老系统原有登录逻辑,增量接入 RBAC 动态路由 + v-perm 按钮指令:新增 AI 菜单路由、AI 操作权限标识,管理员可单独控制角色是否能使用 OCR、AI 自动流程等功能,老页面原有权限逻辑完全保留不冲突。
- AI 能力组件化下沉 封装通用 AI 公共组件(OCR 上传识别、AI 智能填表、流程画布节点、AI 报表导出),以弹窗 / 嵌入组件形式嵌入老旧页面,老页面仅需几行代码引入,不用重构原有复杂动态表单、树形渲染逻辑。
- 弱网离线兼容增量改造 给老系统新增统一本地存储工具类,AI 识别任务、待提交表单存入离线队列,断网缓存 AI 识别结果,联网自动同步 AI 中台,适配外勤 PDA、地下室无网场景。
- 新旧系统数据兼容转换 封装数据格式化工具,把老旧系统的表单结构、树形数据转换成 AI 中台要求的入参格式,AI 返回结果反向适配老页面渲染结构,消除两边字段差异。
- 渐进式灰度上线 按模块分批改造,先给仓库、财务模块接入 AI 能力,其余老页面保持原有逻辑,避免一次性全量重构带来的线上风险。
17. 编辑器引入 AI 开发工具
Cursor(AI 原生编辑器,替代 VSCode), 基于 VSCode 二次开发的纯 AI 内置编辑器,底层搭载 GPT-4o / Claude 大模型,无需额外装插件,开箱即用。
Claude Code" 超大上下文窗口,通读整个项目相比 Copilot 单文件有限上下文,Claude 能一次性读取几十上百个文件,读懂完整项目架构。比如你让它改造老旧 Vue2 系统,它能通读全部 mixins、全局请求、权限代码,批量统一改成 Vue3 组合式 API+Pinia,不会改一处崩另一处。
核心提效优势(比 Copilot 更强的点)
- 全局工程对话:可以上传整个前端项目文件夹,直接提问全局需求,比如 “把项目所有页面改成 vform3 低代码表单、统一接入 RBAC 权限指令”,AI 批量修改多处文件。
- 整块代码生成 + 文件创建:直接描述需求,自动新建完整组件(动态表单、树形虚拟列表、PDA 扫码适配组件),一次性产出 template+script 完整代码。
- 代码批量重构:老旧 Vue2 项目批量迁移 Vue3 组合式 API、批量替换 mixins 为组合 hook,大幅减少重复改造工作。
- 内置对话面板,不用切浏览器查文档,边写代码边问业务方案。
3. 短板
- 重度大文件、超大项目打开会卡顿;
- 国内访问速度一般,免费版有调用次数限制。
面试加分:AI 编辑器实际产能提升点(贴合前端业务)
- 复杂动态表单、树形虚拟渲染、RBAC 权限指令这类模板化代码,AI 一键生成,省去手写重复模板;
- 老旧系统改造时,批量转换 Vue2 代码为 Vue3 组合式 hook,替代手动逐页面修改;
- 接口降级、弱网离线存储、JSBridge 适配 PDA 等通用工具类,写一句注释直接生成完整封装代码;
- 调试报错、业务逻辑梳理不用反复翻阅文档,AI 即时给出解决方案,减少查资料耗时;
- 自动生成接口入参校验、单元测试,减少自测、文档编写工作量。
18. form-create
基础定位
form-create 是轻量开源低代码表单生成器 npm 包,一套库同时兼容 Vue2、Vue3,核心两件事:
- 可视化拖拽表单设计器(可视化画布,拖拽生成表单 JSON 配置);
- JSON 驱动表单渲染器(传入配置 JSON 自动渲染完整表单,自带动态增删行、嵌套、校验)。和 form-making、vform3 是同类产品,但它轻量化、不强制绑定 ElementUI/Element Plus,支持多 UI 库。
两大核心模块
1. 可视化设计器组件
页面嵌入拖拽画布,左侧基础组件(输入框、下拉、日期、上传、动态子表),中间实时预览,右侧配置字段校验、默认值、联动显隐、动态明细规则;拖拽完成直接导出 JSON Schema,存到后端数据库。
2. 表单渲染核心引擎
传入设计器产出的 JSON 配置,自动渲染表单,原生内置能力:
- 单层 / 多层嵌套动态表单(明细增删、循环子表单);
- 全局、单行自定义校验规则;
- 字段联动、显隐控制、只读 / 禁用;
- 自定义插槽,可嵌入业务组件(OCR 上传、地图、PDA 扫码组件)。
实战落地流程(口述完整链路)
- 运营在设计器页面拖拽表单,配置动态明细、校验规则,导出 JSON 存入后端;
- 业务页面引入 form-create 渲染器,读取后端 JSON 自动渲染完整表单;
- 自定义全局组件,嵌入 OCR 识别、PDA 扫码,识别后自动赋值表单字段;
- 搭配 RBAC 按钮级权限,控制表单新增、删除、导出按钮显隐;
- 表单提交前统一做接口降级、离线缓存,适配仓库弱网环境。
19. 定制文件处理组件,修复历史项目图片预览、跨域下载疑难问题
一、背景痛点(历史老项目现存问题)
原有项目没有统一封装文件处理逻辑,图片预览、文件下载代码零散写在各个页面,暴露出两类疑难问题:
- 图片预览问题
- 不同业务图片来源不统一,有本地上传图、第三方 OCR 返回图、PDA 拍照图片、后端文件服务器图片;部分大图加载卡顿、超大图片溢出屏幕、长图无法滚动、图片加载失败无兜底占位;老代码没有统一的预览弹窗逻辑,每个页面重复写预览逻辑,维护混乱。
- 跨域下载疑难问题
- 一是后端文件资源存在跨域,直接 a 标签下载被浏览器跨域策略拦截;二是接口返回二进制流、oss 文件链接两种下载格式混用,老代码只能处理其中一种;三是权限校验场景,下载接口需要携带 token,原生 a 标签无法自动带上请求头,导致下载 401 无权限;四是 IE、老旧安卓 PDA 设备兼容性差,部分文件下载失效。
二、定制通用文件处理组件整体设计
我独立封装一套全局文件处理公共组件,统一接管图片预览、文件下载、附件在线查看能力,全项目页面复用,分两大模块解决问题:
1. 统一图片预览模块,修复预览各类 bug
- 封装全局预览弹窗组件,支持传入图片数组、单张图片链接,统一逻辑;
- 兼容多来源图片地址:OSS 资源、后端文件流、PDA 本地临时图片路径;
- 性能优化:大图懒加载、超大图自动等比缩放,长图支持滚动查看;
- 异常兜底:图片 404、加载失败自动展示默认占位图,不会页面崩溃;
- 拓展功能:支持放大缩小、拖拽、键盘左右切换多张图片,适配仓库单据、发票 OCR 预览业务场景;
- 全局挂载,任意页面一行代码调用,替换所有页面零散预览逻辑,减少重复代码。
2. 封装统一下载工具函数,根治跨域、token、多格式下载难题
针对跨域、鉴权、两种文件源做分层处理:
- OSS / 文件服务器跨域资源通过 axios 发带 token 的请求获取文件二进制 blob 流,再通过 Blob+a 标签模拟下载,绕过浏览器图片、文件跨域限制;不再直接赋值 a 标签 href,解决跨域拦截。
- 后端接口返回二进制流场景统一处理响应类型 responseType: blob,自动解析文件名、文件后缀,避免乱码、下载文件无名称;
- 自动携带鉴权信息复用项目全局 axios 请求拦截器,下载请求自动带上 token、业务请求头,解决下载接口 401 无权限问题;
- 老旧设备兼容:做 IE、安卓低版本 PDA 设备兼容判断,区分 blob 下载和原生下载逻辑,保证仓储手持设备附件正常下载;
- 增加下载加载状态、失败弹窗提示,网络异常、文件不存在给出友好提示,配合之前的接口降级策略。
一句话收尾总结
针对老项目分散的图片预览、跨域下载各类疑难问题,我封装了全局统一文件处理组件,通过 Blob 流式下载解决资源跨域、token 鉴权下载问题,同时统一兼容多来源图片预览、老旧 PDA 设备,增量改造原有业务页面,统一管理附件相关能力,彻底根治历史遗留 bug 并提升后续开发效率。
20. 分支管理的前置校验思想
以 Git 规范化分支流程为基础,在项目启动、打包构建这两个关键节点,强制增加分支合规校验拦截,不允许在错误分支上调试、打包发布,从流程底层规避线上故障、代码错提交问题,属于研发流程左移、事前防控的工程化思路。
通用企业分支规范(GitFlow / 简化 GitFlow):
main/master:生产环境稳定分支,仅合并上线代码,禁止直接改、直接打包;test:测试环境分支,给测试人员验收;dev:开发主分支,所有需求迭代合并到此;- 功能分支
feature/xxx:开发人员本地写需求; - 修复分支
hotfix/xxx:线上紧急 bug 修复。
规则约束:
- 开发只能在 feature 分支写代码;
- 测试打包只能打 test 分支;
- 生产上线打包只能合并至 main 分支再构建;禁止:直接在 main/dev 打包、跨分支构建、在生产分支调试代码。
启动 / 打包前置校验完整逻辑
1. 本地启动项目(npm run serve /dev)时校验
校验规则举例:
- 禁止在
main/master生产分支执行本地启动调试; - 禁止直接在
test测试分支改代码调试(test 只允许合并,本地修改会污染测试包); - 仅允许
feature/*、dev分支本地启动开发;实现:在 package.json 脚本前置执行 node 校验脚本,读取当前 git 分支名,不符合规则直接终止进程,弹窗 / 控制台提示切换正确分支。
2. 打包构建(npm run build 打包测试 / 生产包)时校验
区分打包环境,强绑定对应分支:
- 执行生产打包命令时:强制校验当前必须是
main/master分支,其他分支直接阻断打包,防止误把开发代码构建上线包; - 执行测试打包命令时:强制校验当前只能是
test分支,避免拿 dev/feature 代码打包丢到测试环境; - hotfix 紧急修复打包:仅允许 hotfix 开头分支;校验不通过直接退出构建流程,不生成 dist 产物。
简单实现思路(面试官追问实现时口述)
- 写小型 node 校验工具脚本,通过
git rev-parse --abbrev-ref HEAD命令获取当前分支名称; - 区分 dev/build/build-prod 三套命令,每套命令预设允许的白名单分支;
- 脚本判断当前分支是否匹配当前脚本允许范围,不匹配
process.exit(1)终止程序; - package.json 的 scripts 前置挂载校验脚本:
"build-prod": "node check-branch.js && vite build"。
21. 优化 http 接口请求,采用强缓存和协商缓存优化请求速度
核心思路
HTTP 缓存的核心思想:把后端接口 / 静态资源存在浏览器本地,后续相同请求优先读本地缓存,减少重复网络请求,降低接口等待耗时、减轻服务器压力,分为强缓存、协商缓存两套机制,配合使用分层优化请求速度。
二、两种缓存完整区分(面试官必问)
1. 强缓存(最优,完全不走后端,速度最快)
-
响应头标识:
Cache-Control: max-age=xxx(现代标准,优先使用),Cache-Control: max-age=秒数告诉浏览器:从本次拿到资源的这一刻开始,多少秒之内,不用再发请求给服务器,直接读取本地缓存,这段时间就是强缓存有效期。计时起点不是服务器系统时间,是浏览器收到响应的当下开始倒计时,不受时区影响,这是比旧字段 Expires 更可靠的原因。- 旧标准
Expires绝对过期时间,存在时区缺陷,现在只用 Cache-Control
-
逻辑:浏览器判断资源未过期,直接读取本地缓存,不发任何 http 请求,状态码 200 (from disk cache/memory cache)
-
适用资源:长期不变的静态资源(图片、js、css、字典枚举接口、固定基础配置接口)
-
配置示例:接口返回
Cache-Control: max-age=3600,1 小时内重复请求直接读缓存。
2. 协商缓存(走一次轻量请求,不返回完整数据)
强缓存过期后触发,浏览器携带资源标识去服务端校验:
-
两套标识成对使用:
- 标识 1:
Last-Modified+ 请求头If-Modified-Since文件修改时间,Last-Modified是协商缓存用的响应头,服务器返回给浏览器,记录这个资源 / 接口数据最后一次修改的标准时间戳,格式是 GMT 时间字符串。粒度只能到「秒」 - 标识 2:
ETag+ 请求头If-None-Match文件唯一哈希指纹(精度更高,推荐)
- 标识 1:
-
逻辑:
- 资源无修改:服务端返回 304 Not Modified,无响应体,浏览器继续复用本地缓存;
- 资源已更新:返回 200,携带新数据、新缓存标识,更新本地缓存。
-
适用资源:会定时更新、但访问频次极高的列表、菜单、树形数据接口。
三、前端层面落地优化方案(业务接口怎么配置)
- 静态资源打包缓存(vite/webpack
- 打包时给 js/css/img 加哈希文件名,配置 Nginx 强缓存;文件不变哈希不变,永久走本地缓存,大幅减少页面加载时的资源请求。
* 基础不常变接口(权限列表、字典、系统菜单、组织树形):后端配置长时长`max-age`强缓存;
* 高频更新列表(订单、库存):后端开启 ETag(ETag 是服务端返回的**资源唯一哈希指纹**,属于协商缓存标识,一串唯一字符串) 协商缓存,无数据变更返回 304;
* 实时性要求极高接口(支付状态、PDA 实时扫码):设置`Cache-Control: no-cache`,强制每次协商;
* 绝不缓存:提交、删除、新增等 POST 写接口,禁用缓存。
3. axios 请求层兜底本地缓存(补充 HTTP 缓存
- 部分老旧后端不支持设置缓存响应头,前端封装内存 + localStorage 缓存层,根据接口地址 + 参数做 key 缓存返回数据,设定过期时间,作为 HTTP 缓存补充优化弱网场景。
- 缓存精准刷新机制
- 新增 / 编辑 / 删除操作完成后,主动清除对应接口缓存,避免展示脏数据;比如修改商品后,删除商品列表缓存,下次请求拉取最新数据。
四、强弱缓存搭配使用标准流程
- 第一次请求接口:后端返回数据 + Cache-Control + ETag;浏览器存入本地;
- 短时间重复请求:命中强缓存,直接读本地,无网络开销;
- 超过 max-age 过期:自动发起协商请求,携带 ETag;
- 服务对比 ETag:无更新返回 304,复用缓存;有更新返回 200,刷新本地缓存。
六、高频踩坑 & 解决方案(加分项)
-
问题:POST 接口缓存失效
答:HTTP 规范默认不缓存 POST,查询类接口统一改成 GET,再配置缓存头。
为什么
POST请求体导致缓存难以实现?
- GET 参数拼接在 URL 里,
URL + 请求头就能唯一标识一次请求,缓存键简单可靠。 - POST 参数在请求体,标准缓存机制不把请求体纳入缓存键。即使部分代理强行支持,解析、比对请求体也会大幅增加性能开销,还容易泄露请求体内的隐私数据(账号、密码、表单信息)。
-
问题:缓存脏数据,修改后页面不更新
答:写操作后手动清除对应接口缓存;或缩短 max-age 缩短缓存周期;关键业务接口改用协商缓存。
-
问题:Expires 时区不一致缓存错乱
答:废弃 Expires,统一只用 Cache-Control。
-
问题:本地开发缓存导致看不到最新代码
答:开发环境关闭强缓存,
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 缓存刷新全流程,标准化上线流程、杜绝人工操作失误。
二、技术分工拆解
- Node.js:整体脚本调度核心,串联所有步骤,做流程判断、日志输出、异常捕获、命令执行;
- SCP(ssh2-sftp-client Node 库) :基于 SSH 协议,将本地打包后的 dist 静态资源,安全传输到云服务器指定站点目录;支持增量上传、覆盖旧资源、备份历史版本;
- 云厂商 API(阿里云 / 腾讯云 CDN OpenAPI) :调用官方接口自动提交 CDN 缓存刷新任务,替代人工登录控制台填 URL / 目录刷新。
三、完整流水线执行步骤(一键执行 npm run deploy 自动走完)
步骤 1:前置校验(流程左移,提前拦截错误)
- 调用 git 命令读取当前分支,匹配分支白名单:生产打包仅允许 main 分支、测试打包仅允许 test 分支,分支不符直接终止流程;
- 校验代码是否存在未提交变更,防止漏提交代码直接发布;
- 安装依赖、执行打包命令
vite build / webpack build,打包失败(编译报错、资源缺失)脚本直接退出,不执行后续上传。
步骤 2:SCP 安全上传部署
- 脚本读取配置文件(服务器 IP、SSH 账号密钥、站点目录、忽略文件);
- 连接服务器 SSH,先备份线上旧 dist 目录(重命名备份,发布出错可快速回滚);
- 通过 SCP 把本地打包产物增量上传至服务器站点目录;
- 上传完成后执行简单服务器校验:读取目标目录文件数量,判断资源是否完整上传,文件缺失抛出异常。
步骤 3:调用云 API 自动刷新 CDN 缓存
- 读取配置内 CDN 域名、密钥、账号信息,通过 Node 的 http/axios 调用云厂商开放 API;
- 传入站点根目录,提交目录级缓存刷新任务;
- 轮询 API 接口查询刷新任务状态,控制台打印刷新进度;
- 捕获 API 调用异常(密钥过期、权限不足、接口限流),给出清晰报错日志。
步骤 4:流程收尾与日志记录
- 全流程无报错,打印发布成功、服务器地址、CDN 刷新任务 ID;
- 存在任意步骤失败,自动输出完整错误日志,保留服务器旧版本代码,支持一键回滚脚本;
- 自动记录本次发布时间、操作人、分支、版本到本地日志文件。
五、关键落地细节与踩坑优化
- SCP 传输优化使用增量上传,仅传输变更文件,减少大资源上传耗时;配置文件黑名单,不上传 map 源码、临时缓存文件;采用密钥登录 SSH,摒弃明文密码,提升服务器安全。
- CDN API 兼容处理云厂商刷新接口有调用频次限制,脚本做限流延迟;区分 URL 刷新、目录刷新两种模式;密钥抽离环境变量,不硬编码提交代码仓库。
- 异常兜底机制打包失败、服务器连接超时、CDN 接口报错,任意节点中断不会覆盖线上现有代码;全程捕获 try-catch,打印可定位的详细日志。
- 结合之前分支校验思想脚本第一步强制读取 git 分支做校验,和你之前提到的「打包前分支校验」工程化规范打通,双重管控发布流程。
23. CDN 刷新功能
一句话面试总结
CDN 刷新用于清除边缘节点旧静态资源缓存,分 URL 单文件、目录全局两种刷新方式;我通过 Node 脚本对接云厂商开放 API,把 CDN 自动刷新整合进一键发布流水线,解决人工登录后台刷新易遗漏、操作繁琐的问题,同时搭配打包哈希文件名、HTTP 缓存策略,减少 CDN 刷新频次,保障用户访问到最新线上页面。
一、CDN 缓存基础铺垫(先讲原理)
CDN 就是全国各地的缓存节点服务器,用户访问静态资源(js/css/ 图片)时,不会每次都回源站拉文件,直接就近拿节点缓存文件,加载更快、减轻源服务器压力。
但有个问题:我们打包发布新版本后,CDN 节点还存着旧页面资源,用户浏览器会一直看到老页面,CDN 刷新就是强制让所有节点删除旧缓存、重新去源站拉最新代码。
二、两种刷新模式(云厂商统一标准,阿里云 / 腾讯云都一样)
- **URL 刷新(单文件刷新)**单独填某一个文件完整地址,只删除这一个资源的缓存;适合只改了一张图片、单个 js 文件的小更新。
- **目录刷新(整站 / 文件夹刷新,项目上线最常用)**填网站根目录
/,会清空这个域名下全部静态资源缓存;我们前端项目打包发布新版本,统一用目录刷新,一次性清除所有旧页面缓存。
三、人工刷新 VS 自动化 API 刷新(结合你 Node 自动化发布脚本)
1. 传统人工操作(痛点)
发布完代码后,手动登录云厂商后台,粘贴域名提交刷新任务,有大量缺陷:
- 容易忘记刷新 CDN,用户页面展示旧代码,线上出 bug;
- 复制粘贴容易输错目录 / 链接,刷新失效;
- 多环境(测试 / 生产)来回切换后台,操作繁琐耗时;
- 无法集成发布流程,发布、刷新割裂,流程不规范。
2. 自动化云 API 刷新(你项目落地方案)
用 Node 脚本调用云厂商开放 API,一键自动刷新,整合进发布流水线:
- 脚本携带云账号密钥、域名、刷新类型(目录刷新)发起请求;
- 提交刷新任务,控制台打印任务 ID;
- 轮询接口查询任务完成状态,告知开发者刷新是否成功;
- 任意步骤报错直接终止发布流程,输出日志提醒。
四、刷新机制限制(面试官高频追问)
- 刷新不是瞬间完成全国几百个 CDN 节点异步清理缓存,一般 5-10 分钟全部生效,部分偏远节点会慢一点;紧急更新可以搭配资源哈希文件名长期缓存 + 按需刷新。
- 接口调用有频次限额云厂商限制每日刷新次数、每秒请求数,脚本里加延时节流,避免调用超限被限流。
- 区分「刷新缓存」和「预热缓存」刷新:删旧缓存,用户下次访问才回源拉新文件;预热:主动让 CDN 节点提前拉取新资源,用户访问直接拿到新版,适合大流量活动提前部署。
五、配套前端优化方案(加分回答)
- 打包静态资源加哈希后缀(
app.abc123.js)文件内容一变哈希就变,全新 URL 天然不走旧缓存,大幅减少刷新依赖; - 静态资源配置长时效强缓存 Cache-Control max-age稳定图片、组件脚本设置长期缓存,只有页面入口 html 设置短缓存,每次发布只刷新 html 目录即可;
- 自动化流水线闭环Node 发布脚本:代码校验→打包→SCP 上传服务器→调用 CDN API 刷新→日志输出,一条命令完成全部上线,完全替代人工操作。
六、线上踩坑 & 解决方案
- 发布后部分用户依旧显示旧页面原因:浏览器本地强缓存 + CDN 缓存双重叠加;解决:html 不设置长 max-age,每次发布自动刷新 CDN 目录,同时提示用户强制清除浏览器缓存。
- API 密钥泄露风险解决:密钥存在服务器环境变量,不硬编码写死在前端代码、提交 git 仓库。
- 刷新任务失败无感知解决:脚本捕获 API 异常,中断发布流程并打印错误日志,不会出现代码更新了缓存没清的情况。
24. postMessage
一句话极简总结
postMessage 就是跨域名页面之间专用的聊天工具,让 iframe、弹窗、后台计算线程能安全互传数据,使用时一定要校验对方域名防攻击
要注意的坑(面试必问)
- 安全红线:收到消息第一件事核对对方域名
e.origin,陌生网站消息直接忽略,不然别人伪造消息篡改你页面数据; - 别传函数、DOM 元素、循环嵌套的对象,传不过去,普通数组 / 对象 / 字符串没问题;
- 数据是复制一份传过去,超大几万条数据来回传会有点慢;
- 生产环境绝对不能填
*(代表所有网站都能给你发消息),有泄露 token、表单数据风险。
五、优缺点
优点
- 突破同源限制,实现跨域页面通信;
- 异步通信,不会阻塞主线程;
- 支持复杂对象传递,API 简单易封装。
缺点
- 数据是拷贝传递,超大对象传输存在性能损耗;
- 无消息应答机制,需要自己封装回调 ID 实现请求 - 响应模式;
- 不支持传递函数、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 二进制帧传输。
安全考点
- 生产必须用
wss://TLS 加密,防止中间人抓包篡改消息; - 握手阶段 Origin 校验,拒绝非法域名跨域连接;
- 连接建立后携带 token 鉴权(url 参数 / 第一条消息携带 token),不能仅靠 cookie 鉴权;
- 限制单客户端最大连接数,防止恶意攻击。
26. Canvas
我项目里用到 Canvas 主要做页面水印、图片裁剪,都是快速上手开发的:
- 门店后台全局水印:循环绘制文字到画布,覆盖整个页面,基础 API 就能实现;
- 图片裁剪:结合
drawImage+ 选区绘制,配合裁剪框完成图片处理。这类基础场景不需要深入原理,看完示例代码就能直接开发。
27. Echarts
1. ECharts 核心组成结构是什么?
标准四部分:
- 初始化容器 dom:必须有宽高的 div 画布;
- 实例 echarts.init (dom) :唯一图表实例;
- option 配置项:图表核心配置(xAxis/yAxis/series/tooltip/legend 等);
- setOption(option) :渲染 / 更新图表的核心方法。
Vue 中使用 ECharts 专属面试题
- 为什么要在
onMounted初始化图表?只有挂载完成后 DOM 容器才存在,setup 阶段 dom 未渲染无法 init。 - 组件复用 / 切换页面出现多个图表重叠?组件销毁
onUnmounted必须调用dispose()销毁实例,全局缓存实例变量避免多次 init。 - 响应式数据更新图表怎么做?监听接口数据变化,数据更新后执行
setOption。
怎么实现图表自适应窗口大小?
- 监听窗口
resize事件; - 调用图表实例方法
myChart.resize(); - 优化防抖: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 有几种渲染模式?区别是什么?
- canvas(默认适合大数据量折线、散点、海量点位;渲染速度快,但导出图片清晰度受 DPR 影响,内存占用更高。
- svg矢量图,缩放无模糊,内存占用低;不适合上万条数据的海量图表,渲染会卡顿。
业务选型:大数据大屏用 Canvas;移动端、需要高清导出报表用 SVG。
简单说下 setOption 的作用与特性
- 接收完整 / 增量配置,渲染或更新图表;
- 默认合并配置,不会覆盖未传入的字段;
- 支持多次调用实现图表动态刷新(切换数据、切换图表类型);
- 大数据量频繁更新可搭配
notMerge: true强制清空旧配置。
柱状图、折线图怎么实现鼠标点击事件?
通过实例 on('click', callback) 绑定,回调参数 params 包含当前点击系列、下标、数值:
chart.on('click', (params)=>{
console.log('点击数据', params.name, params.value)
})
动态异步接口加载数据怎么渲染图表?
- 页面先初始化空图表;
- 请求接口拿到后端数据;
- 组装 xAxis.data、series.data;
- 调用
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 是阿里开源的完整可视化解决方案,不是单一图表库,包含多条产品线:
- G2:基于图形语法的统计图表库(折线、柱状、饼图、业务大屏,对标 ECharts)
- G2Plot:G2 上层封装,开箱即用的极简图表(低代码、快速开发业务图)
- G6:关系图、拓扑、流程图、树图(流程图 / 组织架构 / 图谱)
- L7:地理空间可视化(地图、点位、热力、轨迹,对标 ECharts Map)
- F2:移动端轻量图表
- X6:流程图 / 画布编辑器
G2 核心设计思想:图形语法(Graphic Grammar)
这是 G2 和 ECharts 最本质区别,面试必背:一套完整图表由 7 层语法构成:
- 数据 Data:原始数据集
- 标记 Mark:图形元素(point 点、line 线、interval 柱状、pie 饼图)
- 视觉通道 Encoding:把数据映射到图形属性(x/y 位置、color 颜色、size 大小、shape 形状)
- 坐标系 Coordinate:直角 / 极坐标 / 笛卡尔(饼图 = 极坐标)
- 比例尺 Scale:数据值 → 画布像素的映射规则
- 视图 View:多图层、分面、多子图
- 交互 Interaction:tooltip、框选、图例、缩放
一句话区分 ECharts vs G2
- ECharts:配置驱动,按图表类型写配置项(xAxis/series);
- G2:图形语法驱动,不区分图表类型,通过「数据 + 标记 + 映射」自由组合生成任意图表,扩展性更强。
29. web3.js
web3.js 是以太坊及兼容 EVM 公链的前端 JS 开发库,专门用来让网页、客户端和区块链节点、智能合约打交道,相当于区块链世界的「请求工具库」,类似传统开发里的 axios。
传统前端用 axios 调后端接口拿数据,而 web3.js 负责对接区块链节点:查询钱包余额、发起转账、调用链上智能合约、监听链上事件等。日常开发中常搭配小狐狸(MetaMask)钱包使用,实现网页连接钱包、链上交互的功能。
补充一个关键现状:该库官方已经停止维护归档,现在新项目业内更推荐使用 ethers.js、viem 等替代方案Ethereum。
补充追问应答(备用)
- 问:web3.js 如何连接浏览器钱包(MetaMask)?
- 答:MetaMask 会在页面注入
window.ethereum作为 provider,直接传入即可初始化:const web3 = new Web3(window.ethereum),再调用eth_requestAccounts向用户申请连接钱包。
- 问:调用智能合约必须要有 ABI 吗?
- 答:必须要有。ABI 相当于合约的 “接口文档”,web3.js 需要依靠 ABI 解析合约的函数名、入参、返回值,没有 ABI 无法正常调用合约方法。
- 问: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.form、paginationState.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() 把 search、tableState、searchState 等吐出去,父组件能手动刷新、能拿当前搜索条件去做导出。
六、还顺手封了"跨页勾选"这种脏活
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 核心区别是什么?(必问)
- 语法层面:Vue 分模板 template + 脚本,更贴近传统 HTML;React 用 JSX,JS 和标签写在一起,纯 JS 驱动。
- 响应式机制:Vue2 Object.defineProperty、Vue3 Proxy 自动监听数据变化,修改数据自动更新视图;React 无自动响应,必须 setState 触发重渲染,靠手动更新状态。
- 数据流:Vue 支持 v-model 双向绑定,简化表单;React 严格单向数据流,子组件不能直接修改父 Props。
- 逻辑复用:Vue2 mixin(有命名冲突缺陷)、Vue3 组合式 API;React 全靠 Hooks 复用逻辑。
- 编译: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 底层响应式原理
-
采用
Object.defineProperty()劫持对象已有属性; -
缺陷:
- 无法监听新增 / 删除属性,需要
$set/this.$delete; - 无法监听数组下标、长度修改,重写 7 个数组
push、pop、shift、unshift、splice、sort、reverse方法实现拦截,因为这7个是变异方法(会改变原数组); - 递归遍历对象深层属性,初始化开销大,大对象卡顿。
- 无法监听新增 / 删除属性,需要
Vue3 底层响应式原理
-
采用
Proxy代理整个对象,搭配Reflect; -
优势:
- 原生支持监听新增、删除属性、数组下标、length;
- 惰性递归:只在访问时才递归深层对象,初始化更快;
- 支持 Map/Set 等复杂数据类型。
二、API 写法:Options API vs Composition API
Vue2:Options API(选项式)
- 代码按功能拆分:
data/methods/computed/watch/mounted分散; - 大型页面中,同一业务逻辑散落多处,阅读、复用困难;
- 逻辑复用靠 mixin,存在命名冲突、变量来源不清晰、多 mixin 层级混乱问题。
Vue3 双支持:Options + Composition API(setup)
-
组合式 API setup(推荐)
- 按业务功能聚合代码,同一个逻辑(如分页、搜索)写在一起;
- 抽离自定义 hooks 实现逻辑复用,无命名冲突,依赖清晰;
-
<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 Options | Vue3 setup Hooks |
|---|---|
| beforeCreate / created | setup(直接替代) |
| beforeMount | onBeforeMount |
| mounted | onMounted |
| beforeUpdate | onBeforeUpdate |
| updated | onUpdated |
| beforeDestroy | onBeforeUnmount |
| destroyed | onUnmounted |
2. 废弃更名
beforeDestroy/destroyed→onBeforeUnmount/onUnmounted- 移除
this.$on/this.$emit全局事件总线,推荐 mitt/pinia 替代。
六、响应式 API:ref /reactive
-
Vue2:只有 data 自动响应式,基础类型无法单独监听;
-
Vue3:
ref:处理基础类型(字符串、数字、布尔),.value读写;reactive:处理对象、数组;- 配套工具:
toRef/toRefs/computed/watch/watchEffect。
七、指令与模板
-
v-model 语法升级
- Vue2:组件只能单个 v-model,
.sync修饰符实现多双向绑定; - Vue3:移除
.sync,支持多参数 v-model:v-model:title="val"。
- Vue2:组件只能单个 v-model,
-
v-if 和 v-for 优先级
- Vue2:v-for 优先级高于 v-if(性能隐患);
- Vue3:v-if 优先级更高,先判断再循环,优化性能。
八、状态管理 Vuex → Pinia
-
Vue2 标配 Vuex:嵌套 modules、mutation 必须同步、写法繁琐;
-
Vue3 主推 Pinia:
- 无 mutations,state 直接修改;
- 天然 TS 友好、模块化扁平化、代码简洁;
- 去除繁琐模板代码,支持自定义 hooks 抽离逻辑。