草稿PPT
自我介绍
面试官好,我是李白,23年毕业于南阳理工学院,有3年前端开发经验,目前在中山稳安特科技公司负责智慧停车业务线以及停车周边生态的前端开发,搭建了包含C端H5、小程序和B端管理系统的智慧管理平台,实现了从停车欠费到清缴结算完整的业务闭环。同时我是Wot UI组件库的核心成员,主导开发了高拓展性的瀑布流组件,以及搭建了以组件库为基础的通用脚手架,降低了开发者上手难度。我觉得我既熟练C端用户体验打磨,也能应对B端复杂业务逻辑拆分,对贵公司前端岗位很感兴趣,希望能有机会深入交流。
wot ui组件库
1. 介绍组件库(背景、优势)
Wot UI 是一个基于 Vue 3 + TS 构建的 UniApp 跨端组件库,项目提供了 70+ 个高质量组件,支持微信、支付宝、钉钉等 7 个小程序平台,以及 H5 和 App。我主要负责核心组件的开发维护,包括瀑布流、Toast 等基础组件,以及组件库的文档编写。优势是:我们提供了 70+ 个组件,覆盖了移动端 90% 的常见场景,TS类型支持友好,除了组件库本身,我们还构建了完整的开发生态,VS Code 插件 :提供代码补全和智能提示,脚手架模板:提供快速搭建项目的能力;AI支持友好,提供了 AI 工具访问的文档和skill;支持npm和easycom两种使用方式。
2. 为什么做组件库
做组件库的初衷,其实源于我们团队的真实痛点。
- 第一,市面上的 UniApp 组件库虽然多,但质量参差不齐——很多库要么文档缺失,要么vue3、TS支持不完善,要么跨端兼容性没做好,我们业务里经常遇到"看起来能用,实际埋坑"的情况。
- 第二,我们希望组建更有活力的技术团队。组件库是一个很好的抓手,它能让新人快速有产出、有成就感,也能让资深同学沉淀设计经验,形成技术传承。
- 第三,我们在构建完整的工具链生态,组件库是其中关键一环。配合我们做的脚手架、VSCode插件,能让开发者从 0 到 1 搭项目的体验更顺畅。
- 第四,对我个人而言,开源是锤炼技术的最好方式。你在写业务代码时,很多边界情况可以妥协;但开源出去,要面对各种场景的 issue 和社区 review,这会逼你把 API 设计、性能优化、兼容性处理都做到更严谨。我在 Wot UI 这一年多,对 Vue3 的响应式原理、组件通信模式的理解,比单纯做业务深得多。
3. 我的贡献
我主要负责四个方向:
- 第一,高拓展瀑布流组件 —— 这是我最核心的贡献。
- 第二,Toast 反馈组件 —— 重点突破了 UniApp 的上下文限制。因为 UniApp 没有 Vue 的 teleport 能力,我通过 Pinia 状态管理 + Layout 自动注入 实现了函数式全局调用,让开发者在路由拦截、请求封装等非组件场景也能直接 toast.show()。
- 第三,工具 Hooks —— 封装了 useCountdown、useLoading 等高频逻辑。useCountdown 特别处理了服务端时间校准,解决客户端系统时间不准导致的倒计时偏差,这个在支付、核销场景很关键。
- 第四,类型系统 —— 完善TS类型定义,让组件在使用时能有完整的类型提示和自动补全,降低上手成本。 另外也参与了文档建设,主要是把复杂组件的用法拆成渐进式示例,减少开发者的理解负担。
5. 设计组件库
设计组件库要分四个层面来考虑:架构设计、开发体验、交付质量、社区运营。
一、架构设计(技术底座)
- 技术选型
- 框架:Vue3 vs Vue2?Composition API 的兼容性
- 构建:Vite 还是 Rollup?要不要支持按需加载、Tree-shaking
- 样式:CSS 变量做主题?UnoCSS/Tailwind 还是传统预处理器
- 跨端:如果是 UniApp,要处理 H5/小程序/App 三端差异
- 组件规范
- 命名规范:前缀统一(如
w-)、Props 命名风格 - API 设计:参考 Ant Design 的"合理默认值"原则,减少必填项
- 可扩展性:插槽、作用域插槽、render 函数的多级暴露
二、开发体验(效率工程)
- 开发工具链
- 本地调试:怎么快速在真实项目里验证组件改动(link 模式?monorepo?)
- 文档站点:VitePress 还是 Storybook?要不要支持在线编辑预览
- 类型提示:完整的 TS 定义,甚至自动生成 API 表格
- 测试体系
- 单元测试:Vitest + Vue Test Utils,覆盖核心逻辑
- 视觉回归:Percy 或 Chromatic,防止样式意外变更
- 真机测试:UniApp 必须用真机验证,模拟器很多坑
三、交付质量(稳定性)
- 构建与发布
- 多格式输出:ESM、CJS、UMD,适配不同引入方式
- 版本管理:Semver 规范,changelog 自动化生成
- 兼容性:浏览器版本、小程序基础库版本声明
- 性能保障
- 包体积监控:CI 里加 bundlesize 检测,防止膨胀
- 渲染性能:长列表、大数据场景的虚拟滚动、懒加载
四、社区运营(生态活力)
- 文档与示例
- 不是 API 罗列,而是场景化示例:用户带着问题来,能直接复制代码
- 常见问题 FAQ:把 issue 里的高频问题沉淀下来
- 贡献机制
- 清晰的贡献指南:怎么提 PR、怎么写测试、代码规范
- 快速响应:issue 模板、自动回复机器人,让贡献者有正反馈
组件设计需要考虑哪些?
封装一个公用组件时,主要需要考虑以下几点:
- 通用性
- 组件的设计需要足够细致,保证在不同场景下都能通用。
- 提炼核心功能,避免组件过于特定于某个业务场景。
- 清晰的 API 设计
- 定义明确的 props、事件、插槽等接口,让其他团队成员可以快速理解每个参数的用途。
- 考虑默认值、类型校验和必要的错误提示,降低使用难度。
- 可定制性
- 提供简洁的扩展接口或者通过样式、插槽等方式允许自定义内部行为和样式。
- 保证在不修改组件内部实现的前提下,实现自定义效果。
- 内部逻辑和状态管理
- 尽量将组件内部的状态和逻辑封装起来,仅向外暴露必要的接口。
- 处理好异步数据、事件绑定等问题,防止组件过于臃肿。
- 兼容性与扩展性
- 考虑到版本更新和未来扩展,设计时尽量减少对外部组件的依赖或者耦合。
- 通过合理的封装模式,方便后续维护和升级。
- 性能优化
- 考虑组件在多次渲染时的性能问题,如合理使用防抖、节流、缓存等。
- 根据具体需求选择合适的渲染方式,确保组件在复杂场景下依然高效。
- 可维护性和文档
- 提供清晰的文档和使用示例,便于团队成员理解组件逻辑和调用方式。
- 定期整理代码和注释,确保代码在多人协作环境下易于维护。
瀑布流组件设计
一、 核心设计与高拓展性体现
这个瀑布流组件采用了 “职责分离与动态调度” 的设计理念。
父子组件解耦的设计 (高拓展性体现1:作用域插槽与通信)
- 架构:采用父子架构,主要容器负责全局调度和计算(列宽、坐标),子项目容器负责包裹内容和上报高度。它们通过 Vue 3 的
Provide/Inject建立上下文通信。 - 高拓展性:瀑布流不关心业务具体渲染什么。子项目利用作用域插槽 (Scoped Slots),把高度、错误状态、加载回调抛给使用者。不论里面是纯图片、复杂卡片、还是带视频的块,业务方随意定制,组件只管控制外层包裹的坐标(
transform: translate3d)。
- 架构:采用父子架构,主要容器负责全局调度和计算(列宽、坐标),子项目容器负责包裹内容和上报高度。它们通过 Vue 3 的
异步排版系统 (高拓展性体现2:秩序与灵活)
- 架构:引入了 排版队列 和 删除队列。
- 高拓展性:允许业务层通过传递排序属性实现无序插队、动态删除。当组件收到插队/删除指令时,并非简单粗暴刷新整个页面,而是有针对性地调用增量方法进行安全的重排。
策略模式的容错处理 (高拓展性体现3:多级配置)
- 架构:内部集成了一套灵活的异常策略引擎。支持四种模式:致命失败、图片占位、自动请求重试、强力兜底。业务方修改一个属性,就能一键无缝切换加载异常的处理方案。
二、 重点难点分析 (核心技术点)
阅读源码可以发现,80% 的代码并不是在算坐标,而是在处理异步操作和各种异常调度。这是该组件含金量最高的地方:
异步高度获取与“队列堵塞”防范 (最高含金量)
- 痛点:瀑布流必须知道上一个元素的高度,才能排布下一个元素(否则会重叠)。但图片是异步加载的,如果其中一张图片加载极慢或死链,整个计算链条就会卡死,下面几十个元素全出不来。
- 解法:组件设计了严格的异步等待机制。更绝的是引入了 超时熔断机制 配合延时任务。无论某个子项目内部图片渲染多么慢,只要超过阈值时间,组件就会强行熔断,赋予兜底高度,让出队列执行权,绝对不让单张坏图阻塞整个列表的展示。
页面失活与幽灵任务兜底 (端侧真实考验)
- 痛点:在微信/支付宝等小程序中,当用户切到后台或跳页时,DOM 查询方法会直接失效或挂起,导致大量未决的 Promise 堆积内存。
- 解法:设计了生命周期感知回调。在监听到容器不再活跃的瞬间,主动触发兜底清理逻辑:遍历所有挂起的任务,强行终止所有查询,并将未加载的项清空状态。活跃时再依靠内部机制恢复。这展示了极深的多端底层渲染理解。
动画与性能优化
- 通过将计算坐标赋于
transform: translate3d()开启 GPU 硬件加速。 - 排布完成改变可见状态后再延迟施加
transition过渡动画,避免初次渲染内容从四面八方飞过来的混乱感,做到了优雅入场。对计算操作进行了防抖保护。
- 通过将计算坐标赋于
三、 面试切入与话术模板
面试时,不要等面试官问你“瀑布流怎么写的”,你可以主动借由“性能优化”或“复杂组件设计”的话题把这个组件抛出来。
🌟 开场
在处理复杂的信息流渲染时,我(编写/深入研究)过一个非常契合多端业务的高性能瀑布流组件。一般的瀑布流只是算一下最短列和绝对定位,但我这个组件的核心亮点在于,它实现了一套基于发布订阅的异步排版引擎,彻底解决了瀑布流中最头痛的动态内容异步加载导致排版重叠和**单图加载失败导致整个队列渲染卡死(堵塞)**的核心痛点。
🛠️ 介绍架构设计
在设计上,我采用了父子解耦的架构,父组件只做全局列高维护和坐标收集,子组件完全不限内部业务内容,通过作用域插槽(Scoped Slots)把加载反馈机制抛给业务方。这种设计让瀑布流既能包裹纯图片,也能包裹复杂的电商卡片或者富文本。
🔥 攻克难点
开发中最难的一点就是异步调度的堵塞问题和生命周期中断问题。
- 为了防止某一张破损的图片一直等待导致后续所有的卡片排不了版,我维护了一个排版队列。在监听子组件高度状态时,我加了一个超时熔断器。当超过比如 1.5 秒拿不到高度,我会通过策略模式强制触发兜底高度并继续执行当前的 Promise,保证后续流式布局依然顺滑输出。
- 另外针对小程序环境下,如果页面切到后台、导致获取元素宽高的 DOM API 失效从而引发隐蔽的内存泄漏和渲染错误,我在系统探测到失活态的瞬间,主动清理了所有挂起的监听器并强制拦截,等页面重新唤醒后再安全地回放这些计算任务。这个底层细节为线上避免了很多玄学的渲染 Bug。
💡 总结收尾(高扩展性)
最后,通过暴露排序属性配合增量重排算法,这个瀑布流天然支持对卡片的灵活插入和无缝删除,同时将计算任务绑定在
translate3d使用 GPU 渲染。使得这一套组件既保证了高扩展性(业务方随便塞东西),又保障了极高的渲染帧率和容错能力。
H5 性能优化核心笔记
一、 核心战果
通过体系化治理,实现了性能与业务指标的双重突破:
- 性能跑分:Lighthouse 45 → 92 分。
- 核心指标:LCP 3.5s → 1.0s(↓71%),P95 指标 5.2s → 1.8s。
- 业务收益:跳出率 35% → 22%,关键转化率提升 15%。
二、 瓶颈诊断
使用 Chrome DevTools 与 Lighthouse 定位四大核心痛点:
- 包体积过载:首屏 JS 达 2.8MB(利用率仅 42%),由于 Vendor 模块耦合严重。
- 资源加载冗余:20+ 图片同步加载(总 1.5MB),缺乏懒加载与现代格式支持。
- 接口响应瓶颈:首页 6 个请求串行,后端单接口查询 7 张表,耗时均 >2s。
- 感知体验缺失:缺少骨架屏,白屏时间与接口耗时完全对等。
三、 优化矩阵
1. 链路与接口 (Network Layer) - ROI 最高
- 聚合请求:合并 6 个串行接口为 1 个聚合接口,后端并行查询,总耗时 2.1s → 0.5s。
- 数据库优化:首页改查“视图层冗余表”,避免 7 表联查,单接口从 1.8s → 0.3s。
- 优先级治理:核心数据优先返回,非核心属性延迟加载。
2. 工程化打包 (Bundle Delivery)
- 智能分包:利用
vite-plugin-chunk-split自动化拆分,配合路由懒加载,首屏 JS 2.8MB → 350KB。 - 极限压缩:利用
vite-plugin-compression2开启 Brotli + Gzip,传输体积再降 15-20%。 - 精简产物:引入
lodash-es替代 lodash (Tree-shaking),dayjs替代 moment,esbuild自动剥离 console。
3. 静态资源 (Asset Management)
- 现代格式:全量启用 WebP(体积 ↓60%),结合 CDN 动态裁剪(1000px → 400px),质量平衡在 80%。
- 按需加载:首屏图片加载数 20 → 5 张,启用原生
loading="lazy"。
4. 渲染与感知 (Runtime & UX)
- 首屏提速:
- 骨架屏:将沉闷的白屏转化为有感知的加载态 (白屏感知 2.1s → 0.5s)。
- 资源提示:对关键 JS/CSS 使用
Preload,CDN 域名Preconnect,下一页资源Prefetch。
- 长任务拆解:计算密集型任务移至 Web Worker,定时器任务利用
RequestIdleCallback分片执行。
四、 方法论:ROI 驱动优化顺序
在实际操作中,我遵循“先减法、再并发、后感知”的原则:
- 第一优先级 (低投入/高产出):后端接口降本与前端打包分包,解决总量问题。
- 第二优先级 (中投入/稳收益):图片格式转化与懒加载,解决网络传输压力的瓶颈。
- 第三优先级 (细节打磨):骨架屏与 Prefetch,提升用户的主观感知体验。
前端性能优化方法论
我会从三阶段、五层次、六指标来构建这套方法论。
一、三阶段方法论框架
┌─────────────────────────────────────────────────────────┐
│ 第一阶段:发现与诊断 → 第二阶段:优化执行 → 第三阶段:持续监控 │
└─────────────────────────────────────────────────────────┘📋 完整检查清单(按优先级排序)
【阶段一:发现与诊断】
Step 1:建立性能基线
| 检查项 | 工具/方法 | 判断标准 |
|---|---|---|
| ✅ 线上性能数据 | 监控平台(Sentry/ARMS) | P75 LCP ≤ 2.5s? |
| ✅ 实验室评分 | Lighthouse | Performance ≥ 80 分? |
| ✅ 用户反馈 | 客服记录、问卷 | 有无"慢"相关投诉? |
| ✅ 竞品对比 | WebPageTest | 是否落后竞品? |
核心指标:LCP P75、FCP P75、TTI P75
Step 2:定位瓶颈
诊断流程(按执行顺序):
1️⃣ Lighthouse 跑分,看整体评分和主要扣分项
↓
2️⃣ Network 面板,看资源加载
- 首屏 JS 体积 > 500KB?⚠️
- 首屏图片总大小 > 500KB?⚠️
- 关键资源串行加载?⚠️
- 接口响应时间 > 500ms?⚠️
↓
3️⃣ Performance 面板,看执行时间
- Parse/Execute JS > 500ms?⚠️
- 有 Long Tasks(>50ms)?⚠️
- 首次绘制时间 > 1.5s?⚠️
↓
4️⃣ Coverage 面板,看代码使用率
- 已加载代码使用率 < 60%?⚠️
↓
5️⃣ Waterfall 瀑布流,看加载顺序
- 关键资源被阻塞?⚠️
- 资源加载并发度不足?⚠️输出:问题清单 + 优先级排序
【阶段二:优化执行】
Step 3:按 ROI 从高到低优化
🔴 优先级 P0(必须做,ROI 最高)
1. 后端接口优化
└─ 检查项:
• 首屏接口数 > 3 个? → 接口合并
• 接口响应时间 > 500ms? → 数据库优化/缓存
• 有复杂关联查询? → 冗余表/预计算
效果预期:接口耗时减少 50-80%
2. 路由懒加载
└─ 检查项:
• 路由组件是否使用动态 import?
• 首屏 JS 体积 > 500KB?
效果预期:首屏 JS 体积减少 60-80%
3. 图片懒加载 + 格式优化
└─ 检查项:
• 首屏图片数 > 5 张? → 懒加载
• 使用 PNG? → 改 WebP(体积-60%)
• 图片尺寸未压缩? → CDN 裁剪
效果预期:图片体积减少 70-80%🟡 优先级 P1(建议做,ROI 较高)
4. 第三方库优化
└─ 检查项:
• 有 moment.js? → 换 day.js
• 有 lodash? → 换 lodash-es
• ECharts 全量引入? → 按需引入
• 可延迟加载的库? → 动态 import
效果预期:减少 100-300KB
5. 资源预加载
└─ 检查项:
• 关键 JS/CSS 是否 preload?
• CDN 域名是否 preconnect?
• 下一页资源是否 prefetch?
效果预期:FCP 提升 20-30%
6. 骨架屏/Loading 态
└─ 检查项:
• 首屏白屏时间 > 1s? → 加骨架屏
• 列表加载态? → 加骨架屏
效果预期:用户感知速度提升🟢 优先级 P2(可以做,ROI 中等)
7. 长任务优化
└─ 检查项:
• 有 Long Tasks(>50ms)? → 拆分到 Web Worker
• 大数据计算? → 分片执行
• 复杂渲染? → 虚拟滚动
效果预期:TTI 提升 30-50%
8. CSS 优化
└─ 检查项:
• CSS 体积 > 200KB? → 检查未使用样式
• 关键 CSS 是否内联?
• 非关键 CSS 是否异步加载?
效果预期:FCP 提升 10-20%
9. 缓存策略
└─ 检查项:
• 静态资源是否有 long-term cache?
• 是否开启 Service Worker?
• HTTP 缓存策略是否合理?
效果预期:二次访问速度提升 50-80%🔵 优先级 P3(锦上添花,ROI 较低)
10. 打包优化
└─ 检查项:
• 是否开启 Gzip/Brotli?(体积-60-80%)
• 是否开启 Tree-shaking?
• 代码分割是否合理?
11. SSR/预渲染
└─ 适用场景:
• SEO 要求高
• 首屏内容非常复杂
• 用户网络环境差
12. HTTP/2、HTTP/3
└─ 检查项:
• CDN 是否支持 HTTP/2?
• 多路复用是否生效?【阶段三:持续监控】
Step 4:建立监控体系
监控维度:
1️⃣ 核心指标监控
• LCP P75
• FCP P75
• TTI P75
• CLS P75
2️⃣ 资源级监控
• JS 体积趋势
• 图片加载失败率
• 接口响应时间
3️⃣ 告警机制
• LCP P75 > 2.5s → 告警
• JS 体积增长 > 20% → 告警
• 接口超时率 > 5% → 告警
4️⃣ 定期回归
• 每次大版本发布后跑 Lighthouse
• 每季度全面性能审计
• 每半年竞品对比📊 六大核心指标(重点关注)
| 指标 | 定义 | 良好阈值 | 优化手段 |
|---|---|---|---|
| LCP | 最大内容绘制 | ≤ 2.5s | 资源体积、预加载、CDN |
| FCP | 首次内容绘制 | ≤ 1.8s | 关键 CSS、预加载 |
| TTI | 可交互时间 | ≤ 3.8s | 长任务优化、代码分割 |
| TBT | 总阻塞时间 | ≤ 200ms | 长任务拆分 |
| CLS | 累积布局偏移 | ≤ 0.1 | 图片尺寸预留、异步加载 |
| FID | 首次输入延迟 | ≤ 100ms | 长任务优化、JS 体积 |
🎯 我的优化决策树
性能问题?
│
├─ 接口慢? → 后端优化(数据库/缓存/接口合并)
│
├─ JS 体积大? → 路由懒加载 → 依赖优化 → Tree-shaking
│
├─ 图片多/大? → 懒加载 → WebP → CDN 压缩
│
├─ 白屏时间长? → 骨架屏 → 关键 CSS 内联 → 预加载
│
├─ 交互卡顿? → 长任务优化 → 虚拟滚动 → Web Worker
│
└─ 二次访问慢? → Service Worker → 长期缓存💡 实战经验总结
1. 优化顺序的黄金法则
后端 → 前端资源 → 渲染 → 缓存 → 高级优化
原因:
• 后端优化收益最大(一次改,所有端受益)
• 前端资源优化性价比高(改动小,效果明显)
• 渲染优化提升感知体验
• 缓存优化影响二次访问
• 高级优化成本高,最后考虑2. 不要过度优化
❌ 为了减少 10KB 花一周时间
✅ P75 LCP 从 3.5s 降到 1.0s,花一周时间值
原则:
• 优化到符合 Web Vitals 标准即可
• 关注用户实际体验,不是极致数字
• ROI 低于阈值的优化不做倒计时方案
这个需求的背景主要是我们的订单支付和线下核销二维码都有严格的时效性,比如15分钟内必须支付,核销码30秒刷新一次。最初我们用setInterval做前端倒计时,但很快发现有问题,有些用户的手机时间不准,或者被手动调整过,导致倒计时提前结束或延迟,直接影响用户体验和业务逻辑。所以我们设计了服务端基准时间的方案。具体实现是,在需要倒计时的页面初始化时,会请求一个专门的时间接口,拿到服务端的当前时间戳。同时,前端记录下收到响应时的本地时间戳,两者相减得到一个初始时间差。后续的倒计时逻辑就不再依赖本地时间,而是用Date.now()加上这个时间差来模拟一个“校准后”的本地时间,再结合目标截止时间进行计算。这样即使客户端时间被修改,只要不刷新页面,倒计时依然是准确的。
弱网、网络延迟处理
这个问题我们确实仔细考虑过。首先,网络延迟是不可避免的,我们的接口会尽量轻量,只返回一个时间戳,减少传输和解析开销。为了更精确,我们借鉴了NTP协议的思想,会计算请求的往返时间(RTT)。虽然无法精确知道请求在路上花了多少时间,但我们可以用RTT的一半来近似估计单向延迟,并在计算初始差值时将这个延迟考虑进去,这样能进一步减少误差。对于弱网环境,我们做了两层保障:一是获取服务端时间这个请求本身有重试机制;二是即使获取失败,我们也有降级方案,比如使用最后一次成功获取的时间差进行倒计时,或者直接展示一个静态的过期时间文本,而不是动态倒计时。同时,我们给倒计时设置了一个合理的误差阈值(比如正负1秒),只要在这个范围内,用户体验就是可接受的,这样方案就具备了足够的鲁棒性。
Toast全局调用
UniApp的主要限制在于,它的UI组件系统是深度集成在Vue的组件树里的。像Toast这种需要渲染视图的组件,必须在Vue组件的模板中声明,或者通过组件实例的方法调用。但在实际开发中,我们经常需要在一些非Vue上下文中进行提示,比如在请求拦截器里处理登录过期,或者在路由守卫里跳转前给出提示,这些地方都没有可用的组件实例。我的方案核心是状态驱动。首先,我用Pinia创建了一个专门管理Toast状态(如显示/隐藏、内容、类型)的store。然后,我在整个应用唯一的根Layout组件里,放置了一个Toast组件,并让它监听这个Pinia store的状态。这样,在任何地方,包括纯JS文件,我只需要导入这个store,修改它的状态,Layout里的Toast组件就会自动响应并显示出来。这相当于把Toast的显示逻辑从组件实例调用解耦成了状态变化,完美绕过了UniApp的限制。
原理:UniApp基于Vue运行时,Toast等UI组件需在Vue组件实例上下文中调用,无法在JS文件或路由拦截等非Vue上下文中直接使用 对比:传统方案需在每个页面引入组件或使用全局EventBus,前者繁琐后者类型支持差且易导致混乱 具体实现:利用Pinia创建全局Toast状态库,在应用根布局(Layout)中注入并监听此状态,状态变化即触发UI显示
统一支付和集成sdk
这个设计的初衷主要是为了解决我们智慧社区业务中支付场景快速迭代带来的代码混乱问题。当时我们有普通停车缴费、商城购物车、优惠券核销等多种订单,每种订单的支付前置逻辑、参数组装都不一样,但最终都要调用微信或支付宝的SDK。如果每个场景都写一套支付代码,维护成本会非常高。我的做法是抽象出一个支付调度中心,把支付流程标准化为几个步骤:首先是订单预处理,根据订单类型计算金额、验证优惠券;然后是支付方式选择与参数组装;接着是调用对应的支付SDK;最后是结果统一处理。不同订单类型的差异,我通过一个配置对象来描述,在预处理阶段根据这个配置来走不同的分支逻辑。而不同的支付方式,我用策略模式封装成独立的类。这样当新增一种支付方式时,我只需要新增一个策略类并注册到调度中心,业务侧几乎无感,保持了很好的扩展性。
同时管理多个SDK确实需要良好的架构设计。我们的核心思路是封装和隔离。我们为每个核心SDK(如高德地图、微信JSSDK、广告SDK)都创建了独立的服务模块。比如mapService.js和wxService.js。这些模块内部负责处理SDK的异步加载、初始化配置、错误监听等脏活累活,然后对外暴露出一组简洁、统一的Promise风格API。例如,调用微信支付不再是直接写一堆wx.ready和wx.error,而是直接调用wxService.pay(order)。在架构上层,我们设计了一个统一的调度中心,它并不直接包含业务逻辑,而是负责协调这些服务模块的初始化时机和生命周期。对于冲突和兼容性问题,我们确实遇到过,比如某些UI库的样式可能会影响地图控件的渲染。我们的解决方法是利用CSS作用域隔离,并优先保证核心SDK的样式加载。所有SDK的调用都包裹了完善的错误处理和日志上报,这样一旦出问题,我们能快速定位是哪个环节出了错。
PageSpy远程调试
PageSpy最大的优势是突破了物理距离和环境的限制。传统vConsole必须手机和电脑在一起,而PageSpy通过一个调试服务器,可以让任何有权限的开发者远程查看用户手机上的Console日志、Network请求甚至Storage状态,这对复现线上用户的白屏或诡异问题帮助巨大。离线日志回放功能也很实用,它会把用户会话期间的关键操作和错误记录下来,即使当时没人在线调试,事后也能像看录像一样回放定位问题。需要注意的主要是安全和性能。我们只在测试环境和需要排查特定问题时才开启,并且会过滤掉敏感信息。性能上,它对应用本身影响很小,但会消耗一些网络流量。
原理:PageSpy基于WebSocket实现远程调试,能实时捕获并传输客户端运行时信息 对比:相比vConsole本地调试,PageSpy支持远程协作和离线日志回放 选型:针对移动端H5真机调试难、复现难的问题,PageSpy提供了更优解 趋势:远程调试工具是提升移动端开发效率和协作体验的重要方向
智能渲染组件
这个问题其实触及到了我们在面对高频更新的长列表或复杂卡片流时,做极致渲染优化的核心痛点。
我当时设计这个“智能渲染组件”,主要是为了解决这样一个性能问题: 在复杂的列表里,很多组件可能包含定时器、轮询请求,或者依赖了频繁变动的全局响应式数据(比如倒计时)。当用户往下滚动,这些卡片已经离开视口(看不见)了,但如果数据变化,Vue 默认依然会一丝不苟地去执行它的 render 函数、做 VNode 的 Diff 比对甚至 DOM 更新。这种看不见却还在拼命计算的行为,会严重抢占主线程执行时间,导致页面滚动卡顿。
传统的做法可能是用 v-if 滑出视口就销毁,滑入再重建,但这会导致两个致命问题:一是组件内部状态(如输入框内容、播放进度)会丢失;二是频繁触发 Component 的挂载/卸载(Mount/Unmount)本身开销就贼大,反而更卡。
所以我改用了这套离屏自动休眠机制加上强制复用旧VNode的方案。它具体是这么工作的:
1. 离屏触发“休眠”(渲染劫持
我们在外层包裹了一个非常轻量的容器,利用底层的 IntersectionObserver 监听它。当发现这个组件完全离开视口时,我就去获取它内部真实组件的实例(component),执行一个非常 Hack 的操作:劫持它的 render 函数。
我会把原生的 render 保存下来,临时替换成一个“傀儡 render”。不管这段时间里面响应式数据怎么变,触发了多少次更新,这个“傀儡 render”统统都会拦截下来,并设置一个变量(相当于打个标记 called = true 证明被更新过)。
2. 强制复用旧 VNode(跳过 Diff 那这个“傀儡 render”返回什么呢?它直接 return component.subTree。 这个 subTree 是什么?它是 Vue 实例上缓存的上一次更新好的老 VNode 树。 当 Vue 的渲染引擎拿到我们返回的老 VNode 发现:“咦?新旧 VNode 的引用地址完全一样啊!” Vue 自身的 patch 算法就会直接跳过后续所有的 Diff 比较和 DOM 打补丁操作,强制退出更新流程。 这就实现了**“即使数据在变,视图渲染也彻底冻结,CPU 消耗降为 0”**。
3. 回到视口“唤醒”(状态同步 当用户滚动页面,这个组件重新回到视口时,我们立刻把原本那个真实的 render 函数还给它。 接着,我会去检查休眠期间那个 called 标记。如果它是 true,说明在它离屏的那段时间里,数据确实发生过变化,它错过了更新。此时我就会主动调用一下 component.update(),让组件用最新的数据立刻重新渲染一次,把视图同步过来。
总结一下: 这套方案相当于给渲染引擎按下了暂停键。它完全保留了组件的内部实例和状态,避开了销毁/重建的昂贵操作;同时又利用底层 API,精妙地骗过了 Vue 的更新机制,把视口外组件的渲染性能损耗彻底抹平。最终实现了滑走即休眠,回来即最新的体验,大幅提升了极端场景下的帧率(FPS)。
插件化架构落地
当时公司有几个并行的停车业务项目,比如社区服务、追缴管理、充电运营,虽然业务不同,但都需要权限控制、统一的请求拦截、UI组件这些基础能力。如果每个项目都自己实现一遍,不仅开发效率低,后期维护和升级更是噩梦。所以我想通过插件化架构,把这些共性能力沉淀下来。具体实现上,我利用Vite的import.meta.glob这个特性,让它自动扫描src/plugins目录下所有插件的入口文件。每个插件都是一个独立的文件夹,里面必须有一个index.ts导出固定的元数据,比如插件ID、名称,以及像install、beforeRouterEnter这样的生命周期函数。主应用启动时,会根据配置决定加载哪些插件,并依次执行它们的install方法。举个例子,我们有一个auth权限插件,它的install方法会向Vue实例注册全局的v-auth指令和hasAuth方法,还会在路由守卫里注入权限判断逻辑。如果某个项目不需要这么复杂的权限,直接在配置里关掉这个插件就行,实现了真正的热插拔,也就是代码层面的可插拔,大大降低了耦合度。
双Token方案 和 文件下载流封装
在稳安特停车追缴管理系统中,我们选择双Token方案主要是基于安全性和用户体验的平衡。Access Token有效期较短,比如15-30分钟,用于日常API请求,即使泄露影响范围也有限。Refresh Token有效期较长,比如7天,存储在HttpOnly的Cookie中,专门用于获取新的Access Token,这样更安全。实现请求队列是为了处理并发场景。具体做法是,当拦截器发现某个请求因Token过期失败时,不会立即刷新,而是先检查是否已有刷新请求在进行。我们用一个布尔变量isRefreshing作为锁,如果没在刷新,就锁定并发送刷新请求,同时将当前失败请求放入一个队列。刷新成功后,用新Token重试队列中的所有请求,然后释放锁。如果已经有刷新在进行,后续的失败请求就直接加入队列等待,这样就保证了只刷新一次,避免了重复请求和潜在的并发问题。
解决了前后端分离架构下,二进制文件流(Blob/ArrayBuffer)下载和文件名解析的难题。
智能识别:通过 response.request.responseType === 'blob' || 'arraybuffer' 自动识别是否为文件流请求。 JSON 错误兼容: 痛点: 后端在文件下载出错时(如文件不存在),往往返回 JSON 格式的错误信息,而不是 Blob。但前端设置了 responseType: 'blob',导致 JSON 也被转成了 Blob。 解决: 代码通过 FileReader 尝试将 Blob 转回文本,解析 JSON 判断 code。如果发现是错误信息,弹出 Message 提示并 reject。 文件名解析:优先从 content-disposition 响应头中提取文件名。 统一返回:将 data (Blob), fileName, headers 封装成对象返回,供业务层直接调用下载工具。
打包优化
通过对 [vite.config.ts] 及其 vite/ 目录下插件配置的分析,项目确实在构建优化方面做了大量工作,主要体现在 代码分割 (Code Splitting) 和 资源压缩 (Compression) 两个维度。
- 代码分割 (Code Splitting)与按需加载
项目使用了 vite-plugin-chunk-split 插件和 Rollup 原生配置来实现精细化的分包策略。
智能分包插件: 在 [vite/chunk.ts] 中引入了 chunkSplitPlugin。 虽然代码中仅配置了 strategy: 'default',但这个插件默认会将 node_modules 中的依赖拆分成独立的 chunk,避免 vendor 包过大。 手动分包 (Manual Chunks): 在 [vite.config.ts] 中,显式地将体积巨大的图表库 ECharts 单独打包:
manualChunks: { echarts: ['echarts'], // 独立拆分,避免阻塞主包加载 }, 这对于首屏优化至关重要,因为后台管理系统往往只有部分页面需要图表,将其拆分后,首屏加载时无需下载庞大的 ECharts 代码。2. 双重压缩 (Gzip + Brotli)
项目在 [vite/compression.ts] 中配置了基于 vite-plugin-compression2 的高级压缩策略。
配置逻辑: 读取环境变量 VITE_BUILD_COMPRESS (通常在 .env.production 中配置)。 Gzip: 标准压缩,兼容性好,压缩速度快。 Brotli: Google 推出的新一代压缩算法,压缩率比 Gzip 高 15-20%,现代浏览器支持良好。 双管齐下: 代码逻辑允许同时开启 gzip 和 brotli。
if (compressList.includes("gzip")) {
plugin.push(compression());
}
if (compressList.includes("brotli")) {
plugin.push(
compression({
exclude: [/\.(br)$/, /\.(gz)$/], // 避免对已压缩文件重复压缩
algorithm: "brotliCompress",
})
);
}效果: 这种策略能确保服务器在支持 Brotli 时优先返回更小的 .br 文件,不支持时降级返回 .gz 文件,最大限度减少传输体积。3. 其他优化细节 Tree Shaking: 生产环境构建时,通过 esbuild 自动移除 console 和 debugger,减少无用代码。 Legacy 兼容: 使用 @vitejs/plugin-legacy 处理旧浏览器兼容性,确保新特性代码能自动降级运行。 按需自动导入: 使用 unplugin-auto-import 和 unplugin-vue-components,无需手动 import Vue API 和组件,Vite 会在编译时按需引入,减少样板代码并优化 Tree Shaking 效果。
扫码充电模块
这个流程主要是为了解决用户扫码后,界面‘卡住’的糟糕体验。具体来说,用户点击‘开始充电’后,我们前端会立即做三件事:一是调用后端接口下发启动指令;二是立刻在UI上显示一个‘正在启动,请等待...’的倒计时动画,比如从15秒开始倒计;三是启动一个定时器,每隔2秒去轮询一次充电桩的真实状态(比如‘待机’、‘充电中’、‘故障’)。选择乐观UI更新,核心是为了用户体验。硬件响应可能5-15秒,如果等收到成功响应再更新界面,用户会觉得手机卡了,可能反复点击。我们提前给出‘指令已发送’的视觉反馈,让用户感知到操作已生效,只是在等待硬件执行。一旦轮询到状态变为‘充电中’,我们就用这个真实状态替换掉乐观的倒计时UI。即使最终轮询到启动失败,我们也会明确提示失败原因,这样体验也比一直‘转圈圈’要清晰。这个方案本质是在通信不可靠的场景下,用前端的即时反馈来弥补后端响应的延迟。
MD5加密-硬件指令
在物联网硬件控制场景里,前端安全确实比普通Web应用要考虑得更深。除了我们已实现的MD5签名防止参数在传输中被篡改,还有几个关键点。首先是防重放攻击,攻击者可能截获一个合法的启动充电请求包,然后重复发送给服务器。所以我们会在签名参数里加入一个随机数(Nonce)或时间戳,服务器端会校验这个Nonce是否使用过,或时间戳是否在有效窗口内。其次是设备身份认证。我们的充电桩在激活入网时,会向后端注册一个唯一的设备标识和密钥,前端下发的指令里会包含这个标识,后端会验证该标识是否合法、是否有权限执行该操作,这相当于一个简单的设备级认证。最后是审计和日志。所有硬件控制指令,无论成功失败,前端都会在本地记录详细的操作日志(包括用户、时间、指令内容、响应结果),并尽可能上报到服务端,这样一旦出现纠纷或异常,可以有完整的操作链追溯。这些措施共同构成了一个更立体的硬件交互安全防线。
实时数据推送与渲染优化
高频推送的需求来自于我们智慧停车平台的‘城市停车热力图’大屏。运营人员需要实时看到全市各路段的占用情况,而高峰期一个大型商圈周边的车位状态可能每秒都在变,如果每个车位变化都单独推送一条消息,WebSocket的连接瞬间就会被塞满。我们的方案是分层处理的。首先在服务端做了消息聚合,不是一有变化就推,而是每100毫秒将这段时间内所有状态变化的车位ID和最新状态打包成一个数组,作为一条差量消息推下来。这样网络传输量大大减少。消息格式大概是 {type: ‘diff’, data: [{parkId: ‘A001’, status: 1}, {parkId: ‘A005’, status: 0}]}。前端收到消息后,并不会立即去更新DOM。我们设计了一个缓存队列,把差量包先放进去。然后利用requestAnimationFrame来执行一个更新函数,这个函数会在下一个浏览器渲染帧执行时,一次性取出队列里累积的所有差量包,合并计算出所有车位的最新状态,再一次性更新到Vue的响应式数据中,进而触发ECharts图表的重绘。这样就把可能在一瞬间发生的几十次数据变更和渲染,合并成一次,确保渲染能跟上数据变化,不会卡顿。requestAnimationFrame比setTimeout更适合,因为它与浏览器的渲染周期对齐,能避免不必要的渲染帧。
技术选型原则和架构原则
在项目初期的技术选型和架构设计,我认为最重要的几个原则是:业务匹配度、团队可维护性和技术前瞻性。首先,技术必须服务于业务。比如在智慧停车项目中,因为涉及大量地图和实时数据,我们选择了高德地图SDK和WebSocket。其次,要考虑团队的熟悉度和维护成本。我们在引入Vue 3和TypeScript时,并没有一刀切,而是先在新的基础建设项目中试点,让大家逐步熟悉,同时提供完善的脚手架和文档支持。最后,要平衡先进与稳定。像Vite和UnoCSS,我们评估后发现它们能显著提升开发体验和构建速度,且生态已经比较成熟,才决定引入。核心是,任何技术决策都不能脱离业务目标和团队现状。
推动技术方案落地确实会遇到阻力,主要来自两方面:一是改变习惯的惰性,二是对迁移成本和稳定性的担忧。我的做法是,首先自己先把方案做扎实,确保它确实能解决问题,比如新脚手架能一键生成项目、内置了最佳实践。然后,我会找一个试点项目,通常是技术氛围较好或痛点最明显的团队,和他们一起用新方案做一个新模块或小项目,用实际效果说话。比如,我们通过数据对比,展示新项目启动时间从半天缩短到十分钟。同时,我会组织内部技术分享,讲解设计思路和好处,并编写非常详细的接入文档和FAQ。对于迁移困难的老项目,我们不强求立即重构,而是提供渐进式升级的指南。关键是要让大家感受到新工具是为他们服务的,而不是增加负担。
AI术语
Prompt 给AI的指令,越具体越好 RAG AI先查资料再回答,避免瞎编 Skill AI的某种专项能力 / 你使用AI的能力 MCP AI与外部世界通信的协议标准
PageSpy远程调试
(情境 - 痛点是什么) 在之前的项目中,我们移动端 H5 和 Webview 业务占比很高。当时面临最大的痛点是 真机调试困难 。一旦测试或用户在真机上遇到白屏或功能异常,开发在本地 Chrome 模拟器往往无法复现。 传统的做法是让测试同学截图发给我,或者通过 vConsole 在手机那块小屏幕上查看日志,不仅由于屏幕太小容易看漏关键报错,而且无法看到完整的 Network 请求体和 Response,排查效率极低,像是在对着‘黑盒’猜谜。
(任务 - 目标是什么) “所以我当时的任务是,寻找并落地一套 高效的远程调试解决方案 。 目标是实现类似 Chrome DevTools 的体验,不仅要能 实时 在 PC 端查看真机的 Console、Network 和 Storage,还要支持 离线回放 ,以便在用户不在线时也能通过日志还原现场,彻底解决‘无法复现’的难题。
(行动 - 做了什么) “我主要做了三个层面的工作:
- 第一是技术选型 :我调研了 vConsole、Eruda 等工具,发现它们局限于本地展示。最终选择了 PageSpy ,因为它基于 WebSocket 实现了双向通信,支持远程协同。
- 第二是私有化部署 :为了数据安全,我没有使用官方的公有云服务,而是基于 Docker + Nginx 搭建了一套私有的调试服务。期间解决了 WebSocket 协议代理 、 HTTPS 证书配置 以及 跨域资源共享 (CORS) 等一系列基础设施问题,确保服务在各种网络环境下都能稳定连接。
- 第三是工程化集成 :在前端项目中集成了 SDK,并引入了 DataHarbor (数据离线缓存) 和 RRWeb (录屏) 插件。我还做了一些优化,比如通过环境变量控制,只在测试环境或通过特定手势触发时才加载调试器,保证生产环境的性能不受影响。
- (结果) 方案上线后,效果非常显著:
- 效率提升 :移动端问题的平均排查时间从 小时级缩短到了分钟级 。现在测试遇到问题,只需要发给我一个连接码,我就能在电脑上实时看到报错堆栈和接口数据。
- 质量改善 :我们成功定位并修复了几个长期的偶发性白屏 Bug,这些 Bug 之前因为无法复现一直挂起。
- 团队协作 :测试和开发之间的沟通成本降低了,不再需要反复截图传话,缺陷定位效率整体提升了 50% 以上 。
