简历配套面试题 — v1.5
基于
v1.5.mdx简历中的具体技术栈与项目经历整理。 答案默认折叠,点击展开。建议面试前自测,能用自己的话说出来再展开核对。
一、Vue 深度
Q1: Vue 2 和 Vue 3 响应式实现的核心差异是什么?为什么 Vue 3 选择重写?
📖 参考答案 / 解析
Vue 2 使用 Object.defineProperty 劫持对象属性的 getter/setter:
- 必须递归遍历对象所有属性进行劫持(初始化慢)
- 无法监听新增 / 删除属性(需要
Vue.set/Vue.delete) - 无法监听数组下标修改 / length 变化(要重写 7 个数组方法)
Vue 3 改用 Proxy:
- 拦截整个对象的访问,懒代理(访问到才递归),性能更好
- 天然支持新增 / 删除属性、数组下标
- 支持
Map/Set/WeakMap等集合类型
为什么重写:Object.defineProperty 的局限是语言层面的,无法绕过;而 Proxy 是 ES6 新特性,Vue 3 不再兼容 IE 11,可以放心用。
注意点:Proxy 是浅层的,访问深层属性时才会再次代理 → Vue 3 通过这个实现了懒代理。
Q2: Composition API 解决了 Options API 哪些问题?什么时候不该用?
📖 参考答案 / 解析
Options API 的痛点:
- 逻辑碎片化:一个功能的代码分散在
data/computed/methods/watch中 - 复用差:Mixin 有命名冲突、来源不清的问题
- TypeScript 友好度差:
this类型推导复杂
Composition API 的优势:
- 按功能组织代码(
useXxx一个 hook 集中相关逻辑) - 复用通过普通函数实现,类型推导自然
- 更适合大型项目
什么时候不该用:
- 简单组件(< 50 行)用 Options API 反而更直观
- 团队没 TS 基础时,Composition API 的优势不明显
- 老项目大面积迁移成本高,不必为了用而用
陷阱:ref vs reactive 的选择、toRefs 解构问题、watch vs watchEffect。
Q3: 你在简历里提到「定位并解决 Vue 插件引起的重复渲染问题」,具体怎么排查的?
📖 参考答案 / 解析
排查路径:
- 复现:Vue DevTools 打开"Highlight Updates",肉眼观察哪些组件在不该重渲染时高亮
- 定位触发源:
- 是 props 变化?用
watch监听 props 看是不是引用变了 - 是 state 变化?看响应式数据是不是被插件意外触发
- 是父组件 re-render?用
React.memo类比手法定位
- 是 props 变化?用
- 常见原因:
- 插件在
setup中创建了响应式对象,每次渲染都重新创建 → 引用不稳定 - 插件提供的
provide值是新对象引用 → 子组件inject后认为变了 - 全局状态 mutate 而不是 replace
- 插件在
- 解决:
- 把对象提到
setup外或用shallowRef/markRaw - 用
computed缓存 - 改用
Pinia的storeToRefs而不是解构
- 把对象提到
总结:核心是"引用稳定性"和"响应式追踪范围",DevTools 是最快的工具。
Q4: Pinia 比 Vuex 好在哪?项目里你是怎么组织 store 的?
📖 参考答案 / 解析
Pinia 优势:
- 去除 mutation:直接改 state,更符合 Composition API 直觉
- TS 友好:自动推导 state / getters / actions 类型,零类型注解
- 模块化天然:每个 store 是独立函数,不需要
modules - 轻量:~1KB
- DevTools 集成:time-travel、热重载
组织方式(个人实践):
stores/
user.ts // 用户身份、权限
app.ts // 全局配置、主题
modules/
chat.ts // 实时聊天状态
game.ts // 游戏运行时状态最佳实践:
- 小 store 优于大 store(单一职责)
- Action 中
await调用其他 store 是允许的 - 跨 store 引用用
useOtherStore()而不是直接 import 实例(避免循环依赖) - 持久化用
pinia-plugin-persistedstate
Q5: Vue 3 有哪些性能优化点?你用过哪些?
📖 参考答案 / 解析
框架层面:
- Patch Flag:编译时标记动态部分,运行时只 diff 动态节点
- Block Tree:跳过静态子树
- 静态提升 (hoistStatic):静态节点提到 render 外
- 预字符串化:连续静态节点编译为字符串
应用层面(项目实践):
v-memo缓存大列表项shallowRef/shallowReactive处理大对象markRaw标记不需要响应式的对象(如 Phaser 实例、Cesium 实例)- 组件懒加载
defineAsyncComponent KeepAlive缓存不常切换的页面- 长列表用虚拟滚动(
vue-virtual-scroller)
调试工具:Vue DevTools Performance 面板 + Chrome Performance。
二、React 生态
Q6: React Hooks 闭包陷阱怎么理解?有哪些典型场景?
📖 参考答案 / 解析
闭包陷阱本质:每次 render 都创建新的 props/state 快照,函数体内的变量绑定到当时那次 render 的值;如果在 setTimeout / setInterval / useEffect 中引用,可能用到过期数据。
典型场景
const [count, setCount] = useState(0)
useEffect(() => {
const id = setInterval(() => {
console.log(count) // 永远是 0
}, 1000)
return () => clearInterval(id)
}, []) // 空依赖 → count 被锁定在初始值解决方案:
- 加依赖:
useEffect(..., [count])—— 但会反复创建定时器 - 函数式更新:
setCount(c => c + 1) - useRef 持有最新值:
const countRef = useRef(count) useEffect(() => { countRef.current = count }) // 在 setInterval 中读 countRef.current - 自定义 useEvent / useEventCallback hook 包装
Q7: React Query 解决了什么问题?和 Redux / Zustand 是什么关系?
📖 参考答案 / 解析
React Query 定位:服务端状态(Server State)管理库,不是通用状态管理。
它解决的问题:
- 接口请求的 loading / error / data 状态样板代码
- 缓存(同一接口短时间内不重复请求)
- 自动重试、轮询
- 失效(mutation 后让相关 query 重新拉取)
- 后台刷新(refetchOnWindowFocus)
- 分页 / 无限滚动
与 Redux / Zustand 关系:
- Redux / Zustand 管的是客户端状态(UI 状态、表单、用户偏好)
- React Query 管的是服务端状态(拉来的数据)
- 两者互补不冲突,大型项目通常同时存在
项目实践(针对简历过敏性疾病随访项目):
- 后台列表 / 详情用 Vue Query 自动管理缓存与失效
- 全局用户身份用 Zustand
- 即时通讯状态用 Zustand + 本地持久化
Q8: Zustand 的设计哲学是什么?为什么比 Redux 简洁那么多?
📖 参考答案 / 解析
核心哲学:
- No Provider:不需要
<Provider>包裹,全局单例 - Hook-first:通过
useStore直接订阅 - 极简 API:一个
create函数搞定 state + actions - 选择性订阅:组件只订阅自己关心的 slice,避免无关 re-render
核心代码示意:
const useBearStore = create((set) => ({
bears: 0,
increase: () => set((state) => ({ bears: state.bears + 1 })),
}))
// 组件中
const bears = useBearStore((s) => s.bears)对比 Redux:
- Redux:action / reducer / dispatch / selector 四件套,模板代码多
- Zustand:直接
set更新,函数式或对象式都行
适用场景:中小型应用 / 局部状态(聊天室、游戏状态)。大型有复杂中间件需求(撤销重做、time-travel debug)的,Redux Toolkit 仍有优势。
三、TypeScript
Q9: 你在 uni-app 项目中怎么引入 TypeScript?踩过什么坑?
📖 参考答案 / 解析
接入步骤:
- 升级
uni-app到支持 TS 的版本(CLI 项目用 vite 模板) - 添加
tsconfig.json,关键配置:{ "compilerOptions": { "target": "ESNext", "module": "ESNext", "moduleResolution": "node", "strict": true, "jsx": "preserve", "types": ["@dcloudio/types", "@types/wechat-miniprogram"] } } - 安装类型声明:
@dcloudio/types(uni 全局 API)、@types/wechat-miniprogram(wx 全局 API) vue文件添加<script setup lang="ts">
踩过的坑:
- 多端条件编译类型不一致:
#ifdef MP-WEIXIN在 H5 端类型缺失 → 用// @ts-ignore或自定义条件类型 - 小程序组件 props 类型:自定义组件
defineProps需要手动声明 - uni.xxx API 在新版本类型不全:自己补充
.d.ts - Pinia + uni 持久化:小程序没 localStorage,需自定义 storage adapter,类型需对齐
收益:类型安全 + IDE 自动补全,大型项目几乎必备。
Q10: 写一个 TS 工具类型 DeepReadonly<T>,把对象所有嵌套属性变为只读。
📖 参考答案 / 解析
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object
? T[K] extends Function
? T[K]
: DeepReadonly<T[K]>
: T[K]
}关键点:
- 用映射类型
[K in keyof T]遍历 - 加
readonly修饰 - 用条件类型判断是否为 object,递归
- 排除 Function(函数也是 object,但不需要递归)
进阶:还要排除 Date / RegExp / Map / Set:
type Primitive = string | number | boolean | bigint | symbol | null | undefined
type Builtin =
| Primitive
| Function
| Date
| Error
| RegExp
| Map<any, any>
| Set<any>
type DeepReadonly<T> = T extends Builtin
? T
: { readonly [K in keyof T]: DeepReadonly<T[K]> }实战用途:配置对象、状态快照防篡改。
四、工程化 / Monorepo
Q11: 简历提到「20+ 游戏包 / 5min → 2min」,构建优化具体怎么做的?
📖 参考答案 / 解析
优化路径(按收益排序):
- Monorepo 工具链选型:从 Lerna 改为
pnpm workspace+ 增量构建工具(Turborepo / Nx)- pnpm 用硬链接,安装速度 + 磁盘占用都改善
- 构建缓存:
- Turborepo / Nx 的远端缓存:未改动的包直接命中缓存,跳过构建
- Vite 的依赖预构建 + 缓存
- 并行构建:
- Turborepo 自动按依赖图并行
- CI 中按 affected packages 只跑增量
- 抽取公共依赖:
- 把
Vue3/Pinia/ 公共 UI 库放到packages/shared - 避免每个游戏包重复打包同一份 vendor
- 把
- Source Map 策略:
- 开发用
cheap-module-source-map,生产用hidden-source-map
- 开发用
- Tree-shaking:
- ESM-only 依赖、
sideEffects: false - 按需引入 Vuetify / Element-Plus
- ESM-only 依赖、
衡量:用 vite-plugin-inspect / webpack-bundle-analyzer 看产物分布。
Q12: Babel 插件开发流程是怎样的?你做过哪些插件?
📖 参考答案 / 解析
流程:
- AST 入门:用 AST Explorer 看代码对应的 AST 树
- Babel 插件结构:
module.exports = function ({ types: t }) { return { name: 'my-plugin', visitor: { ImportDeclaration(path, state) { // 操作 path.node }, }, } } - 常用 API:
path.replaceWith/path.remove/t.callExpression... - 测试:用
babel-plugin-tester
实战插件类型:
- 按需加载:Element-Plus / Vuetify 等组件库自动导入对应 css
- 国际化:把硬编码字符串提取到 i18n 资源文件
- 自定义指令转换:把
v-permission编译为函数调用 - 生产环境去 log:删除
console.log - autoImport:分析模板,自动 import 用到的组件
踩坑:
- Babel 7 vs 6 不兼容
path.traverse容易死循环(要用path.skip()或state.processed = true)- 遇到
import('xxx')动态导入要特殊处理
Q13: Vite 比 Webpack 快在哪?有哪些场景 Vite 不适合?
📖 参考答案 / 解析
Vite 快的原因:
- 开发模式用 ESM:浏览器原生支持 ESM,不需要打包,按需编译
- 依赖预构建用 esbuild:把 CommonJS 转 ESM、合并依赖,esbuild 是 Go 写的,比 babel/webpack 快 10-100 倍
- HMR 精准:模块图清晰,改一个文件只重编译这个文件
Webpack 快的地方:
- 生态成熟、配置灵活
- 老项目兼容性好
- 复杂 SSR / 多入口场景配置更完善
Vite 不适合的场景:
- 需要兼容老浏览器(IE 11 等):Vite 默认输出现代 ES,需要
@vitejs/plugin-legacy - 特别复杂的构建逻辑:Vite 的 Rollup 插件生态比 Webpack 弱
- SSR 高度定制:Vite SSR 模板还在演进
- 微前端 qiankun 等:Vite 5 + qiankun 仍有兼容问题,需要
vite-plugin-qiankun
实际选择:
- 新项目 → Vite
- 老项目 + 复杂构建 → 保持 Webpack
- 工具库 → Rollup(更纯净)
Q14: ESLint + Prettier + Husky + lint-staged + Commitlint 完整工具链怎么搭?
📖 参考答案 / 解析
分工:
- ESLint:代码质量(未使用变量、潜在 bug)
- Prettier:代码格式(缩进、引号、分号)
- Husky:Git Hooks 触发器
- lint-staged:只检查 staged 文件,避免全量扫描
- Commitlint:提交信息规范(Conventional Commits)
搭建步骤:
# 1. 安装
pnpm add -D eslint prettier eslint-config-prettier eslint-plugin-prettier \
husky lint-staged @commitlint/cli @commitlint/config-conventional// package.json
{
"scripts": {
"prepare": "husky install"
},
"lint-staged": {
"*.{ts,tsx,vue}": ["eslint --fix", "prettier --write"],
"*.{json,md}": ["prettier --write"]
}
}# 2. 初始化 husky
npx husky install
npx husky add .husky/pre-commit "npx lint-staged"
npx husky add .husky/commit-msg "npx --no -- commitlint --edit ${1}"// commitlint.config.cjs
module.exports = { extends: ['@commitlint/config-conventional'] }冲突处理:
- ESLint 与 Prettier 规则冲突 →
eslint-config-prettier关闭冲突规则 - 性能:大项目 lint 慢 → 用
--cache选项
五、跨端开发
Q15: uni-app 和 Taro 怎么选?各自的局限是什么?
📖 参考答案 / 解析
uni-app:
- 基于 Vue
- 编译目标多:H5 / 小程序(微信、支付宝、抖音、QQ、百度、京东等)/ App(基于 weex 或 Vue+原生混合)
- 国内生态强,文档全,社区活跃
- DCloud HBuilderX 体验好
- 局限:App 端基于 weex 有性能瓶颈,复杂动画不流畅;TypeScript 支持后期才完善
Taro 3+:
- 基于 React(也支持 Vue 3)
- 运行时方案:把 React 组件树转成小程序的视图层
- TypeScript 支持原生友好
- React 生态可以直接复用(react-query、antd-mobile 等)
- 局限:性能略低于 uni-app(运行时方案有开销);国内社区不如 uni-app 火
选择标准:
- 团队 Vue 栈 + 主攻小程序 → uni-app
- 团队 React 栈 / TS 重度依赖 → Taro
- 需要原生体验的复杂 App → 都不太合适,建议 RN 或 Flutter
项目实战(针对简历):
- 医科大学过敏性项目用 Taro + React(团队 React 栈)
- 云上托育用 uni-app + Vue3(团队 Vue 栈)
- 商米 ERP 用 uni-app + Vue3(商米设备兼容性好)
Q16: Electron 主进程 / 渲染进程怎么通信?安全风险有哪些?
📖 参考答案 / 解析
通信方式:
-
IPC(推荐):
- 渲染 → 主:
ipcRenderer.send/ipcRenderer.invoke - 主 → 渲染:
ipcMain.on/webContents.send invoke配合handle返回 Promise,比send更现代
- 渲染 → 主:
-
contextBridge(安全做法):
// preload.js contextBridge.exposeInMainWorld('electronAPI', { readFile: (path) => ipcRenderer.invoke('read-file', path), }) -
remote 模块(已废弃,不要用)
安全风险:
- nodeIntegration: true + 加载远程 URL → XSS 直接拿到 Node 权限,可执行任意命令
- contextIsolation: false → preload 与 web 共享上下文,攻击者可篡改 API
- 未验证 IPC 参数 → 渲染进程构造恶意路径读取系统文件
最佳实践:
nodeIntegration: falsecontextIsolation: true- 用
contextBridge暴露白名单 API - IPC 参数严格校验
- 加载的 URL 校验来源
实战(简历的称重系统):
- 主进程持有
SerialPort实例,处理底层串口通信 - 渲染进程通过
invoke请求称重数据 - preload 桥接
electronAPI.weigh()
六、可视化 / GIS
Q17: 简历提到「100W+ 格点数据秒级渲染」,技术方案是怎样的?
📖 参考答案 / 解析
总体思路:CPU 计算 → WebWorker,渲染 → Canvas 分块 + 离屏 + 抽帧
具体方案:
- 数据传输优化:
- 服务端用二进制(
Float32Array/ Protobuf)而非 JSON pakogzip 解压前端做(节省带宽 70%+)jszip处理打包文件
- 服务端用二进制(
- 计算分离:
- 主线程:DOM + 用户交互
- WebWorker:插值、网格化、色斑映射计算
transferable对象转移所有权,避免拷贝
- 渲染优化:
- Canvas 分块(tile):把大画布切成 256×256 的小块
- 离屏 Canvas (
OffscreenCanvas) 预渲染 requestAnimationFrame调度- 只重绘视口可见区域
- 视觉欺骗:
- 大数据先渲染降采样版(10W 点)让用户看到东西
- 后台加载完整精度,无感切换
- 抗锯齿与色彩:
chroma-js做颜色插值- 双线性 / 三次样条插值算法
性能指标(要能说清):
- 100W 点首次渲染 < 3s
- 平移 / 缩放保持 30+ FPS
- 内存占用 < 200MB
陷阱:
- WebWorker 与主线程通信序列化有开销,频繁通信反而慢
- Canvas 太大(> 16384×16384)部分浏览器崩溃
Q18: WebWorker 使用边界?什么时候不该用?
📖 参考答案 / 解析
适合用 WebWorker:
- CPU 密集型计算:加密、压缩、图像处理、地理网格化
- 大数据处理:> 1W 条数据的解析、排序、过滤
- 算法密集:路径规划、物理模拟、机器学习推理
不适合 / 用了反而慢的场景:
- DOM 操作:Worker 中没有
document/window - 小数据频繁通信:序列化 / 反序列化开销可能比直接计算还大
- 简单同步任务:< 50ms 的任务不值得 Worker
通信成本:
- 默认是结构化克隆(深拷贝),大对象拷贝慢
Transferable对象(ArrayBuffer/MessagePort/ImageBitmap)转移所有权,零拷贝
实战陷阱:
- Worker 文件路径在 Vite / Webpack 中需要特殊处理(
new Worker(new URL('./worker.ts', import.meta.url))) - 错误处理用
worker.onerror,错误信息可能被脱敏 - 调试用 Chrome DevTools 的 Sources → Workers 面板
Q19: Cesium 三维地形渲染原理?大数据场景怎么优化?
📖 参考答案 / 解析
核心原理:
- 瓦片金字塔:地球被切成多个 LOD(细节层级),近处用高精度瓦片,远处用低精度
- 四叉树调度:相机移动时,根据距离与角度计算需要加载哪些瓦片
- 请求合并:瓦片以 HTTP 请求异步加载,浏览器并发 6 个连接
- WebGL 渲染:地形 + 影像 + 矢量 + 模型层叠渲染
简历项目(航空气象)的优化点:
- GeoServer 提供 WMTS 高程瓦片:避免每次请求计算
- 缓存 + 预加载:常用区域提前预热瓦片缓存
- 降采样三维云图:远景用低分辨率纹理
- 裁剪策略:屏幕外瓦片不渲染(视锥裁剪由 Cesium 内置)
- 请求节流:相机快速移动时只请求最终位置的瓦片
踩坑:
- Cesium 包体积大(5MB+),需要按需引入或 CDN
- 内存泄漏:自定义 Primitive 要手动
destroy() - iOS / 低端机性能堪忧
Q20: 风场流线动画的实现思路?
📖 参考答案 / 解析
算法核心:
- 数据准备:风场数据是格点上的 U / V 分量(U=东西分量,V=南北分量),组成向量场
- 粒子初始化:在画布上随机散布 N 个粒子(典型 N = 5000~10000)
- 每帧更新:
- 对每个粒子,根据位置在风场中插值(双线性)出当前位置的风速 (u, v)
- 粒子位置加上
(u * dt, v * dt) - 粒子寿命 -1,到 0 重生
- 渲染:
- 不要 clear 整个 canvas,而是用半透明矩形
globalAlpha=0.95覆盖 - 这样粒子轨迹有"拖尾"效果
- 用粒子速度大小映射颜色(chroma-js)
- 不要 clear 整个 canvas,而是用半透明矩形
性能优化:
- WebWorker 计算粒子位置,主线程只画
- Canvas 用 GPU 加速:
willReadFrequently: false - 粒子数量自适应:根据 FPS 动态调整
进阶:可以用 WebGL 着色器在 GPU 上跑,性能 10 倍以上提升(参考 Mapbox 的 windgl 实现)。
七、游戏开发
Q21: Phaser.js 游戏循环(Game Loop)原理?怎么和 Vue 3 集成?
📖 参考答案 / 解析
游戏循环原理:
- Phaser 内部用
requestAnimationFrame维护一个主循环 - 每帧调用
Scene.update(time, delta):time:累计时间delta:与上一帧的时间差(ms)
- 物理引擎 / 动画 / 输入都基于
delta计算,保证不同帧率下行为一致
与 Vue3 集成方式:
// PhaserGame.vue
<template><div ref="container"></div></template>
<script setup lang="ts">
import Phaser from 'phaser'
import { ref, onMounted, onBeforeUnmount, markRaw } from 'vue'
const container = ref<HTMLDivElement>()
let game: Phaser.Game
onMounted(() => {
game = markRaw(
new Phaser.Game({
parent: container.value,
type: Phaser.AUTO,
scene: [BootScene, MainScene],
}),
)
})
onBeforeUnmount(() => game?.destroy(true))
</script>关键点:
markRaw:Phaser 内部状态非常多,不要被 Vue 响应式劫持,否则性能崩溃- Vue 与 Phaser 通信:通过
EventBus或 Phaser 的EventEmitter - HMR:开发时
destroy+recreate,否则会有多个实例
性能陷阱:
- 大量 sprite 用
Container批量管理 - 文本对象慢 → 用
BitmapText代替 - 帧率掉 → 用 Phaser DevTools 看哪个 system 占时间
Q22: 精灵图(Sprite Sheet)合并工具的实现思路?
📖 参考答案 / 解析
核心步骤(针对简历提到的自研工具):
- 扫描资源:读取目录下所有 PNG
- 打包算法:用 MaxRects / Skyline / Guillotine 算法把多个小图塞进大图
- 推荐用现成库 maxrects-packer
- 生成图集:把所有小图绘制到一张大 Canvas
- 生成元数据:JSON 描述每个 sprite 在大图中的 (x, y, w, h)
- 输出:PNG + JSON(Phaser / Pixi 都能直接消费)
优化点:
- 旋转:把竖图旋 90° 节省空间
- Padding:小图之间留 1-2px,避免渲染时采样到邻居
- 多张图集:超过 2048×2048 拆分(兼容老 GPU)
- TinyPNG / pngquant 压缩
业务价值:
- 减少 HTTP 请求数(10 张图 → 1 张)
- GPU 纹理切换次数减少(同图集的 sprite 不切换 batch)
- 节省内存(合并后总体积小)
八、硬件集成
Q23: SerialPort 串口通信的关键流程?有哪些坑?
📖 参考答案 / 解析
流程:
import { SerialPort } from 'serialport'
import { ReadlineParser } from '@serialport/parser-readline'
const port = new SerialPort({
path: 'COM3',
baudRate: 9600,
dataBits: 8,
parity: 'none',
stopBits: 1,
})
const parser = port.pipe(new ReadlineParser({ delimiter: '\r\n' }))
parser.on('data', (data) => {
console.log('Recv:', data)
})
port.write('READ\n', (err) => {
if (err) console.error(err)
})关键点:
- 波特率必须和设备一致(9600 / 19200 / 115200 是常见值)
- 数据帧用 Parser:原始流是字节流,要按协议分帧(换行符 / 字节长度 / 自定义分隔符)
- 设备路径:Windows 是
COM3,Mac/Linux 是/dev/tty.usbserial-xxx
坑:
- 设备不存在 / 占用:要 catch
Error: Opening COM3: Access denied - 断线重连:监听
close事件,定时器重试open - 字符编码:电子磅秤常用 GB2312,需要
iconv-lite转码 - 数据丢包:Buffer 满了不及时读取
- 多设备识别:用 USB VID/PID 而不是 COM 号(COM 号会变)
进阶:Web Serial API 在 Chrome 89+ 支持,无需 Node 也能调串口(但兼容性差,只能用 Chromium 内核)。
Q24: Koffi(C 动态库调用)你怎么用的?相比 ffi-napi 优势在哪?
📖 参考答案 / 解析
Koffi 简介:Node.js 调用 C/C++ 动态库(.dll / .so / .dylib)的工具,是 ffi-napi 的现代替代。
基本用法:
import koffi from 'koffi'
const lib = koffi.load('myhardware.dll')
// 声明函数原型
const readCard = lib.func('int ReadCard(char *buffer, int len)')
const buffer = Buffer.alloc(256)
const result = readCard(buffer, buffer.length)
console.log(buffer.toString('utf8'))相比 ffi-napi 优势:
- Node.js 版本兼容性好:ffi-napi 在 Node 18+ 经常编译失败,Koffi 用纯 JS + N-API,无需编译
- Electron 友好:electron-rebuild 噩梦没了
- 性能更好:Koffi 自带内联汇编优化
- API 更现代:类型声明用 C 语言字符串,更直观
- 维护活跃:ffi-napi 已基本不更新
简历项目(屠宰场 ERP)应用:
- 调用厂商提供的 IC 卡读写 SDK(C dll)
- 调用身份证识别 SDK
- 二维码扫码模块 SDK
踩坑:
- 32 位 vs 64 位 dll 要匹配 Node 架构
- 字符串编码(很多国产 dll 用 GBK)
- 回调函数:Koffi 支持
callback注册,但生命周期要小心
九、实时通信
Q25: WebSocket 心跳保活与断线重连怎么设计?
📖 参考答案 / 解析
心跳保活:
class WSClient {
private ws: WebSocket
private heartbeatTimer: any
private reconnectTimer: any
private reconnectCount = 0
connect() {
this.ws = new WebSocket(this.url)
this.ws.onopen = () => {
this.reconnectCount = 0
this.startHeartbeat()
}
this.ws.onmessage = (e) => this.handleMessage(e)
this.ws.onclose = () => this.reconnect()
this.ws.onerror = () => this.ws.close()
}
private startHeartbeat() {
this.heartbeatTimer = setInterval(() => {
if (this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify({ type: 'ping' }))
}
}, 30_000) // 30s
}
private reconnect() {
clearInterval(this.heartbeatTimer)
if (this.reconnectCount >= 5) return // 放弃
const delay = Math.min(1000 * 2 ** this.reconnectCount, 30_000) // 指数退避
this.reconnectTimer = setTimeout(() => {
this.reconnectCount++
this.connect()
}, delay)
}
}关键设计点:
- 心跳间隔:30s 是常见值,太短浪费带宽,太长可能被中间代理踢
- 指数退避:
1s → 2s → 4s → 8s → 16s,避免雪崩 - 重连上限:5-10 次后放弃,避免无限重试
- 应用层 ACK:服务端收到 ping 要回 pong,超时未收到说明链路坏了
- 状态机:connecting / connected / disconnected / reconnecting,避免重复连接
- 消息队列:断开期间的消息缓存,重连后批量发送
实战陷阱:
- 网络切换(WiFi → 4G)时
onclose可能不触发,需要setTimeout主动检测 - 浏览器后台标签页定时器被限流,心跳可能漏发
- 移动端进入后台时主动关闭连接,避免被系统杀
Q26: MQTT 和 WebSocket 区别?什么时候用 MQTT?
📖 参考答案 / 解析
协议层面:
- WebSocket:基于 TCP,是一个通用的全双工通道,没有规定上层语义
- MQTT:基于 TCP(或 WebSocket),是一个发布/订阅协议,专为 IoT 设计
核心差异:
| 特性 | WebSocket | MQTT |
|---|---|---|
| 通信模式 | 点对点 | 发布订阅(Topic) |
| 消息保证 | 无内置 | QoS 0/1/2 三档 |
| 离线消息 | 无 | 持久 Session |
| 数据格式 | 无规定 | 二进制头部,紧凑 |
| 适用场景 | Web 实时交互 | IoT、传感器、物联网 |
MQTT 适合用的场景(针对简历的云上托育项目):
- 多对多通信:温度传感器发布到
dorm/temperature,所有教师端订阅 - 离线消息:传感器掉线后再上线,收到积压消息
- 海量设备:MQTT broker 能扛百万级连接
- 低带宽:协议头部小(最小 2 字节)
浏览器端:用 mqtt.js,底层走 WebSocket(因为浏览器不能开 raw TCP)
踩坑:
- Topic 设计:用
/分层,支持通配符+和# - QoS 1 / 2 是端到端的,broker 不保证业务层处理成功
- 公网 broker 要加 TLS(mqtts://)
Q27: Agora 实时音视频集成关键点?延迟怎么优化?
📖 参考答案 / 解析
集成关键点:
- Token 鉴权:服务端用 App Certificate 签发临时 Token,前端不能硬编码 App ID 的明文
- 频道(Channel):所有进入同一频道的用户互通
- 角色:主播(host) vs 观众(audience),权限不同
- 轨道(Track):
LocalAudioTrack/LocalVideoTrack:本地采集RemoteAudioTrack/RemoteVideoTrack:订阅远端
典型流程:
const client = AgoraRTC.createClient({ mode: 'rtc', codec: 'vp8' })
await client.join(appId, channel, token, uid)
const localAudio = await AgoraRTC.createMicrophoneAudioTrack()
const localVideo = await AgoraRTC.createCameraVideoTrack()
await client.publish([localAudio, localVideo])
client.on('user-published', async (user, mediaType) => {
await client.subscribe(user, mediaType)
if (mediaType === 'video') user.videoTrack.play(`remote-${user.uid}`)
})延迟优化:
- codec 选择:H.264 兼容性好,VP8/VP9 压缩率高,H.265 最优但兼容性差
- 分辨率自适应:弱网降到 480p
- 带宽探测:Agora SDK 内置,自动调整
- 接入点就近:Agora 全球节点,主播 / 观众都连最近的边缘节点
- 抗丢包:FEC 前向纠错 + ARQ 重传,移动端必开
针对简历游戏直播场景:
- 主播端用游戏画面采集(
createScreenVideoTrack)+ 麦克风 - 观众端只订阅,不发布
- 互动用
RTM(实时消息)补充,做礼物 / 弹幕
十、浏览器 / 性能
Q28: 浏览器渲染流程?哪些 CSS 操作会触发 Reflow?
📖 参考答案 / 解析
渲染流程:
HTML → DOM Tree
CSS → CSSOM Tree
↓
Render Tree (合并)
↓
Layout (布局/回流 Reflow)
↓
Paint (绘制/重绘 Repaint)
↓
Composite (合成图层)触发 Reflow 的操作:
- 修改
width/height/padding/margin/border - 修改
position/top/left - 修改
font-size/font-weight - 读取
offsetWidth/scrollTop/getComputedStyle()等(强制同步布局) - 增删 DOM 节点
- 修改 class 影响布局
只触发 Repaint(不 Reflow):
- 修改
color/background-color/visibility
只触发 Composite(不 Repaint):
transform/opacity(前提是元素已提升为合成层)
优化建议:
- 批量修改:
document.createDocumentFragment()或display: none后修改 - 避免强制同步布局:读写分离(先读 → 一次性写)
- 用 transform 替代 left/top:动画专用
- will-change 提示:提前提升合成层(注意不要滥用)
- CSS Containment:
contain: layout/paint/strict
面试常考:
- 强制同步布局是什么?读
offsetWidth时浏览器必须先完成 pending 的布局 - 合成层提升的代价是什么?内存(每层都是独立位图)
Q29: JS 事件循环?宏任务 / 微任务有哪些?
📖 参考答案 / 解析
事件循环模型:
┌──────────────────────────┐
│ 执行同步代码(Call Stack) │
└──────────────────────────┘
↓
┌──────────────────────────┐
│ 执行所有微任务(清空队列) │
└──────────────────────────┘
↓
┌──────────────────────────┐
│ 渲染(如果需要) │
└──────────────────────────┘
↓
┌──────────────────────────┐
│ 执行一个宏任务 │
└──────────────────────────┘
↓ 循环宏任务(每次循环执行一个):
setTimeout/setIntervalsetImmediate(Node 独有)- I/O(文件、网络)
- UI 事件(click、scroll)
MessageChannel
微任务(一次循环清空全部):
Promise.then / catch / finallyqueueMicrotaskMutationObserverprocess.nextTick(Node,优先级比 Promise 还高)
经典面试题:
console.log('1')
setTimeout(() => console.log('2'))
Promise.resolve().then(() => console.log('3'))
console.log('4')
// 输出:1 4 3 2进阶:
- Node.js 事件循环分 6 个阶段(timer / pending / idle / poll / check / close),与浏览器不完全一样
await等价于Promise.then,后续代码进微任务队列- 浏览器渲染时机:每次微任务清空后,但具体时机取决于浏览器(不一定每帧都渲)
Q30: 内存泄漏常见场景与排查工具?
📖 参考答案 / 解析
常见场景:
-
未清理的定时器:
setInterval(() => { /* ... */ }, 1000) // 组件卸载时未 clearInterval -
未解绑的事件监听:
window.addEventListener('resize', handler) // 组件卸载时未 removeEventListener -
闭包持有大对象:
function createHandler() { const bigData = new Array(1e6) return () => console.log(bigData.length) // bigData 永不释放 } -
DOM 引用:
const cache = {} cache.el = document.getElementById('foo') document.body.removeChild(cache.el) // cache.el 仍持有 DOM -
WebSocket / WebWorker 没关
-
Vue / React 组件中 ref 第三方实例(Phaser / Cesium)未 destroy
-
全局变量越来越大(缓存策略不当)
排查工具:
-
Chrome DevTools Memory 面板:
- Heap Snapshot:拍快照,对比两次操作前后的对象数量
- Allocation timeline:录制内存分配过程,找出热点
- Allocation sampling:低开销采样,长期监控
-
Performance 面板:JS Heap 曲线,看是否单调递增(典型泄漏)
-
Memory tab 中的 Retainers:看对象被谁引用了,找到泄漏源头
实战流程:
- 复现操作(如反复打开关闭弹窗)
- 拍 baseline Heap Snapshot
- 操作 10 次
- 再拍一次 Heap Snapshot,Comparison 模式
- 找 Delta 异常增长的对象(如 Detached HTMLDivElement)
- 看 Retainers 链路,找到根因
Q31: 首屏性能优化你做过哪些?怎么衡量?
📖 参考答案 / 解析
关键指标(Core Web Vitals):
- LCP(Largest Contentful Paint):最大内容绘制,反映首屏体感,目标 < 2.5s
- FID / INP(Interaction to Next Paint):首次输入延迟 / 交互响应,目标 < 200ms
- CLS(Cumulative Layout Shift):累计布局偏移,目标 < 0.1
- FCP(First Contentful Paint):首次内容绘制
- TTFB(Time to First Byte):服务器响应时间
优化手段(按收益排序):
- 资源加载:
- CDN 加速
- HTTP/2 多路复用
- 关键资源
preload,非关键prefetch - 字体子集化(fonttools)
- 图片:WebP / AVIF + 响应式
srcset
- 代码分割:
- 路由级懒加载
- 第三方库按需引入
- 抽离 vendor chunk
- 服务端:
- SSR / SSG
- 边缘渲染(Vercel Edge / Cloudflare Workers)
- 接口聚合,减少串行请求
- 运行时:
- 骨架屏代替白屏
- 渐进式加载(先低质量图,再替换高清)
- 关键 CSS 内联
defer/async控制脚本执行时机
- 缓存:
- HTTP 强缓存(不变资源)+ 协商缓存(HTML)
- Service Worker 离线缓存
衡量工具:
- Lighthouse(命令行 + Chrome 面板)
- WebPageTest(多地区测试)
- PerformanceObserver API(线上 RUM 上报)
- Chrome DevTools Performance Insights
实战案例(简历的星光华人通项目):
- 公众号网页首屏 LCP 优化:骨架屏 + 图片懒加载 + 接口缓存
- 部署到 CDN
- 关键 CSS 内联
十一、AI 编码助手
Q32: Claude Code / Copilot / Cursor 你都怎么用?哪些场景不该用?
📖 参考答案 / 解析
实际使用场景(按效果排序):
- 模板代码生成:CRUD 表单、表格、列表页 → 节省 60%+ 时间
- 类型声明:从 JSON 生成 TypeScript interface
- 测试用例:从函数签名生成 Vitest 用例
- 重构辅助:把 Options API 转 Composition API、提取 hook
- 文档生成:README、JSDoc、变更日志
- 复杂正则:自然语言描述 → 正则表达式
- 代码 Review:让 AI 先看一遍,找明显问题
- 算法实现:经典算法直接生成
- 配置文件:Webpack、Vite、ESLint 配置
不该用 / 慎用的场景:
- 业务核心逻辑:AI 不懂上下文,容易写出"看起来对"的代码
- 安全敏感:鉴权、加密、SQL 拼接 → AI 可能引入漏洞
- 性能关键路径:AI 倾向于"通用解",性能不一定最优
- 数据库迁移 / 删除操作:风险太大,必须人工 review
- 不熟悉的领域:AI 可能编造(hallucination)API / 库
- 公司机密代码:要选择不上传训练的工具(Claude Code 是较好选择)
Claude Code 独特价值(vs Copilot / Cursor):
- 多文件 agent 能力强,能跨文件理解项目
- 长上下文,可以读完整文档
- 工具调用(执行 bash、读写文件)— 适合自动化任务
心得:AI 是"加速器"不是"代驾"。让它写,然后逐行 review。
十二、项目深挖
Q33: Spinman 平台 20+ 游戏怎么管理?新游戏怎么接入?
📖 参考答案 / 解析
架构示意:
spinman-monorepo/
├── packages/
│ ├── shared/ # 共享 UI 组件、工具函数、类型
│ ├── core/ # 游戏引擎封装(Phaser / Pixi)
│ ├── auth/ # 鉴权 SDK
│ ├── socket/ # WebSocket / Agora 封装
│ └── live/ # 直播 UI 模块
├── games/
│ ├── game-slot/ # 游戏 1
│ ├── game-dice/ # 游戏 2
│ ├── game-poker/ # 游戏 3
│ └── ... (20+)
├── shell/ # 主应用壳子(路由、布局、鉴权入口)
└── tools/
└── create-game/ # 新游戏脚手架新游戏接入流程:
pnpm create-game game-newxxx→ 脚手架生成模板- 自动注入到 shell 的路由表(动态扫描
games/*) - 共享 UI / 鉴权 / 通信,开发者只关注游戏本身逻辑
- CI 自动按依赖图增量构建(Turborepo)
- 发布:每个游戏独立构建产物 + CDN 部署
核心收益:
- 新游戏从 0 到能跑:~30 分钟
- 公共 bug 修一处所有游戏受益
- 多游戏可同时迭代不冲突
踩坑:
- 共享库版本:用
workspace:*内部依赖 - Vite 多入口构建:要配置
optimizeDeps.entries - 类型共享:
tsconfig用 path 映射,编辑器支持好
Q34: 灵创你带 3 人团队,怎么排期 / 协作 / Review?
📖 参考答案 / 解析
排期方式:
- 需求评估会:产品讲需求 → 技术分解为 task → 估时(1d / 3d / 5d 三档)
- 进 Jira / 飞书项目,每周冲刺 5d
- 每日站会 15min:昨天做了什么、今天做什么、有什么 blocker
协作机制:
- Git Flow:
main/develop/feature/*/hotfix/* - 强制 PR Review:至少 1 人 approve 才能合并
- PR 描述模板:变更原因、影响范围、测试方式、截图
- Conventional Commits + 自动生成 CHANGELOG
Code Review 重点:
- 命名是否清晰
- 是否有重复代码(参考已有 utils)
- 类型是否完整
- 边界 case(空数组、null、网络错误)
- 性能(循环里调函数、大对象 watch)
- 安全(XSS、SQL 注入、敏感信息硬编码)
培养机制:
- 每周技术分享(30min),轮流主讲
- 推荐高质量文章 / 视频,建知识库
- 复杂任务结对编程
- 季度做技术总结,输出文档沉淀
实际产出(针对简历):
- 工程化规范从 0 到 1(ESLint + CI/CD + 自动打包)
- 线上 Bug 率降低(事故复盘 + 单测要求)
- 团队能独立 take 跨行业项目
Q35: 100W+ 格点数据"秒级展示",秒级具体多少秒?瓶颈在哪?
📖 参考答案 / 解析
真实指标(要能讲清):
- 数据传输:< 1s(gzip 压缩后 ~3MB)
- WebWorker 计算插值 / 着色:~0.5s
- Canvas 渲染:~0.3s
- 总计:< 3s 首屏可见
主要瓶颈(按优化顺序):
- 网络传输(瓶颈 1):
- 原始 JSON 30MB+ → 用 Float32Array 二进制 + pako gzip → 3MB
- HTTP/2 多路复用
- JS 计算(瓶颈 2):
- 主线程做计算会卡死 → WebWorker 异步
- 算法选择:双线性插值 vs 三次样条 → 双线性够用且快
- Canvas 绘制(瓶颈 3):
- 单个 fillRect 100W 次太慢 → 用 ImageData 一次性写入
ctx.putImageData()是 GPU 加速的
- DOM 阻塞(瓶颈 4):
- 大批操作放在
requestIdleCallback/requestAnimationFrame - 避免长任务(> 50ms)
- 大批操作放在
剩余优化方向:
- WebGL Shader 直接渲染(10 倍性能,但开发复杂度高)
- WASM 计算(适合更复杂的算法)
- WebGPU(未来方向,浏览器支持中)
坦诚说明:3 秒不算极致,对气象决策场景已够用。如果是高频交互(如游戏),还需进一步优化到 < 1s。
Q36: 三家托育机构 50% 复用率怎么测算?怎么实现的?
📖 参考答案 / 解析
复用率测算(粗略口径):
- 总代码量:约 30K 行(园长端 + 教师端 + 家长端)
- 共享代码:约 15K 行(业务逻辑、UI 组件、工具函数)
- 复用率 = 15K / 30K = 50%
实现方式:
miniapp-monorepo/
├── packages/
│ ├── shared/
│ │ ├── components/ # 通用 UI(按钮、表单、列表)
│ │ ├── hooks/ # 通用 composables
│ │ ├── utils/ # 工具函数
│ │ ├── api/ # 接口封装
│ │ └── types/ # 类型定义
│ ├── biz-shared/
│ │ ├── auth/ # 鉴权(不同角色共用)
│ │ ├── upload/ # 文件上传
│ │ └── canvas-draw/ # 跨平台 Canvas 绘制(课程表、海报)
├── apps/
│ ├── principal/ # 园长端
│ ├── teacher/ # 教师端
│ └── parent/ # 家长端关键决策:
- 角色相关逻辑放在各 app(不强行共享)
- 跨角色一致的业务抽到
biz-shared(如鉴权、上传) - 纯 UI / 工具全放
shared - 用
pnpmworkspace 软链接,改一处所有端实时生效
带来的收益:
- 修 bug 一处生效
- 三端 UI 风格统一
- 新增功能(如海报分享)一次开发三端可用
踩坑:
- 小程序对
node_modules引用方式不一样,要配subPackages - 不同角色权限差异:用
provide / inject注入角色信息,组件按角色裁剪显示
十三、软实力 / 团队管理
Q37: 你怎么做技术选型?举一个具体决策的例子。
📖 参考答案 / 解析
我的方法论:
- 明确约束:业务场景、团队栈、时间预算、未来扩展
- 列候选:通常 2-4 个,避免只有一个"信仰选择"
- 多维评估:
- 学习曲线
- 社区活跃度(npm 周下载、GitHub issue 响应)
- 生态完整度
- 性能 benchmark
- 文档质量
- 长期维护(避免选个人项目)
- 公司投入(背靠大厂的更稳)
- POC 验证:用 1-2 天写小 demo,验证核心难点
- 决策记录:写 ADR(Architecture Decision Record),未来 review
具体例子:跨端框架选 uni-app vs Taro
约束:
- 团队 Vue 栈
- 目标:微信 + 抖音 + 支付宝小程序 + H5
- 时间:1.5 个月上线
评估:
- uni-app:Vue 生态、文档全、HBuilderX 友好
- Taro:React 栈(团队不熟)、TS 友好但生态略弱
决策:选 uni-app,理由:
- 团队上手快
- 多端支持成熟
- DCloud 商业支持靠谱
结果:1.5 个月按时上线,多端体验一致。
反思:如果是 5 年规划的项目 + React 团队,Taro + TS 可能更适合。
Q38: 跨部门 / 客户协作中遇到的最大挑战是什么?
📖 参考答案 / 解析
典型场景(针对简历"作为技术代表对接合作伙伴和客户"):
最大挑战:客户对技术不懂,但有非常具体的"业务直觉",两者经常冲突。
举例:
- 客户:"这个页面再快一点" → 实际首屏已经 1.5s
- 客户:"数据要实时" → 实际场景 10s 延迟也可接受
- 客户:"手机也要看" → 实际后台系统在 PC 端用得多
应对方式:
- 翻译需求:把客户的"快"翻译为"FCP < 1s + 首屏 < 2s",给具体指标
- 数据说话:埋点统计实际场景,用数据反驳"直觉"(如:实时改 10s 轮询,看用户感知差异)
- AB 测试:争议时上线两版本对比
- 优先级管理:建 backlog,公开排期,避免临时插需求
- 教育客户:偶尔讲讲技术原理,提升对方理解
关键心态:
- 不要把客户当"对立面",他们的需求往往有真实业务痛点,只是表达不准
- 技术人多问几个"为什么",往往能挖到本质需求
- 不要用技术术语压人,用对方能理解的话沟通
反例(不该做的):直接说"这不可能" / "我们做不了" → 应该说"做这件事的成本是 X,如果调整成 Y 方案,成本能降到 Z"。
Q39: 你对自己未来 3-5 年的规划是什么?
📖 参考答案 / 解析
这题答案要"真诚"+"匹配岗位",给个框架:
短期(1 年):
- 在新公司站稳,深度参与至少 1 个核心项目,沉淀有影响力的技术产出
- 补齐当前的短板(如:大规模 C 端经验、性能监控体系、SSR 深度)
- 持续输出博客 / npm 工具
中期(2-3 年):
- 在某个细分方向(前端基建 / 可视化 / 跨端)成为团队公认的"专家"
- 主导 1-2 个具有公司级影响力的技术项目(如基建升级、性能优化专项)
- 参与开源社区,至少 1 个项目 1k+ star
长期(3-5 年):
- 技术专家路线:前端架构师 / Principal Engineer,主导技术方向
- 或:技术管理路线:带 10+ 人团队,负责业务线技术体系建设
- 持续保持动手能力,不要变成"PPT 工程师"
核心目标:在 35 岁前,建立独特的技术影响力(开源 / 文章 / 演讲 / 公司内 case),而不是只有"工作年限"。
实诚版:现阶段最关注的是找一个能让我深度成长的平台。当前已经做过广度,下一步想专精深度。
避免的回答:
- "我想成为 CTO" → 太大,没法验证
- "随便,看公司安排" → 缺乏主动性
- "想做管理" / "想做技术" 二选一 → 过于绝对,应该是双向开放
学习建议
每天 1 题深挖
不要追求"题目数量",重点是每题都能用自己的话讲 5 分钟,能举例、能反驳、能扩展。
知识体系图
把所有题目按"技术栈 → 子领域 → 具体知识点"画成思维导图,找出自己的薄弱区。
模拟面试
找朋友 / 同行模拟 1-2 次,重点训练:
- 表达清晰度(不能磕巴)
- 引导节奏(不能被追问到死胡同)
- "我不知道" 的优雅回答
项目复盘准备
简历每个项目都准备 3 个深挖问题 + 1 个失败案例(HR 喜欢问挫折)。
祝面试顺利 🚀