面试题2

5 阅读21分钟

四,跨端架构设计(暂时不会)

  1. 我会这样回答:

    “好的。这个问题的核心目标是**‘写一套代码,跑在三端上’**,并且能灵活切换高德和百度地图。我的设计思路可以概括为 ‘定协议、做适配、包组件’ 三步。”

    第一步:定协议(统一接口层)

    • “协议”是什么? 就是不管底层用什么地图,它们都必须遵守的共同约定。就像手机充电口,不管是哪家厂商,都得遵循USB-C的协议才能通用。
    • 具体做法: 我会创建一个纯抽象的 MapService 类(在TypeScript里就是Interface)。它只定义方法名、参数和返回值,不包含任何具体实现。

    这样一来,所有业务代码,比如“添加一个店铺标记”,只需要调用 mapService.addMarker(),完全不用关心底下用的是高德还是百度,是在H5还是小程序里。

    第二步:做适配(适配层)

    • “适配”是什么? 就是为每个平台、每个地图SDK,写一个“翻译官”。这个“翻译官”严格遵循第一步定的“协议”,但内部去调用各自平台那套“方言”API。
    • 具体做法: 我会创建6个具体的“翻译官”(Adapter):
      • H5AmapAdapter: 在H5端,内部调用高德JS SDK。
      • H5BaiduMapAdapter:在H5端,内部调用百度JS SDK。
      • MiniAmapAdapter: 在小程序端,内部调用小程序的高德API。
      • MiniBaiduMapAdapter:在小程序端,内部调用小程序的百度API。
      • RNAmapAdapter: 在RN端,内部调用 react-native-amap3d 库。
      • RNBaiduMapAdapter:在RN端,内部调用百度地图的RN库。

    每个“翻译官”的工作就是“协议转方言”。 比如,协议里定义 addMarker({lat, lng, title}),高德的“方言”可能是 new AMap.Marker({position: [lng, lat], title}),而百度可能是 new BMap.Marker(new BMap.Point(lng, lat), {title})。这个转换逻辑就封装在各自的Adapter里。

    第三步:包组件(组件封装层)

    • “组件”是什么? 就是一个给业务方用的、开箱即用的React/Vue组件。它是对外暴露的唯一界面。
    • 具体做法: 我会创建一个 <UniversalMap> 组件。它接收统一的Props,比如 points(标记点数组)、onMarkerClick(点击回调)等。
    • 组件内部:
      1. 根据当前的平台(H5/小程序/RN)和地图供应商(高德/百度)的配置,动态地选择并实例化对应的那个“翻译官”(Adapter)。
      2. 然后把 points 这样的Props,“翻译”成对 mapService.addMarker 的调用。

一、包管理工具

  1. 相比于npm和yarn,pnpm的优势是什么?

    npm、yarn 和 pnpm 都是前端包管理工具,但实现和性能上有明显区别。npm 是最早的官方工具,安装简单但速度较慢,且在项目依赖多时容易产生重复包和磁盘占用大问题。yarn 引入了 缓存机制和锁文件,安装速度快于 npm,并且保证了依赖版本的一致性,但是还是没有解决npm的幽灵依赖问题,pnpm 则通过 全局内容寻址存储和硬链接/符号链接机制,让所有项目共享相同版本的依赖,极大节省磁盘空间,同时严格遵循依赖声明,防止模块访问未声明依赖,降低潜在冲突。此外,pnpm 对 monorepo 支持更友好,跨包依赖安装和更新更高效,因此在大型项目和多包管理场景中更具优势。

二、性能优化

2.1 页面加载优化

优化后回答: “在实际项目中,我遇到的白屏问题主要有这几类:

网络层面:比如我们项目初期发现首屏接口响应慢,用户要等2-3秒才能看到内容。通过接口并发请求CDN加速,把时间降到了800ms左右。

资源加载:有次发现打包后的vendor.js有1.2MB,导致下载和执行都很慢。我们用路由懒加载第三方库外部化,把首包体积减少了60%。

渲染阻塞:之前有个页面同步加载了多个JS,HTML解析被阻塞。后来改成async/defer,并加了骨架屏,用户感知快了很多。

排查时我们一般这样操作:先用DevTools的Performance面板看关键路径,再用Lighthouse打分,最后针对性地优化瓶颈点。”

  1. 导致页面加载白屏时间长的原因有哪些,怎么进行优化? 页面白屏时间长主要原因包括:第一个是网络请求慢,请求接口耗时,或者静态资源加载过大;然后是资源体积过大或依赖多,比如 JS、CSS 打包过大,或者未使用压缩和缓存;其次阻塞渲染的脚本,例如同步加载大量 JS 文件阻塞 HTML 渲染;还有首屏渲染复杂,DOM 节点过多或渲染计算量大;最后后端响应慢,页面需要依赖接口返回数据才能渲染

  2. 排查问题:首先可以用 浏览器开发者工具(DevTools) 的 Network 面板和 Performance 面板查看加载时间和关键请求。首先是通过 Network 面板可以分析 DNS 解析、TCP/TLS 建立、静态资源下载时间和接口请求耗时;通过 Performance 面板可以查看 HTML 解析、CSS 渲染、JS 执行、首次绘制(FCP)和首次内容绘制(FCP)时间,判断是否被阻塞脚本或大计算量渲染拖慢。其次,可以检查 资源体积和依赖,确认是否存在过大 JS/CSS、同步加载的阻塞脚本或第三方库导致的延迟。然后,排查 接口响应,如果首屏渲染一定要使用慢接口,必要时进行接口并发或缓存优化。最后,可使用 Lighthouse、WebPageTest 或 Chrome Trace 等工具做全面性能分析,结合水平方向(网络、资源、渲染、数据)定位瓶颈,从而制定针对性的优化方案。

解决方案:可以从网络、资源、渲染和数据几个方面优化。首先,在网络层面,可以使用 CDN 加速静态资源,开启 浏览器缓存和压缩,减少 DNS 解析、TCP/TLS 握手和资源下载时间。其次,在资源层面,对 JS 和 CSS 进行 按需加载、代码分割、Tree Shaking 和压缩,减少首屏需要加载的体积,同时将阻塞渲染的 JS 设置为 async 或 defer。第三,在渲染层面,可以使用 SSR(服务端渲染)或静态预渲染 提前生成首屏内容,或者用 骨架屏和占位元素 提升用户感知速度。最后,在数据层面,优化接口响应速度、减少首屏请求量或使用 缓存和并发请求

2.2 构建工具性能优化

优化后回答: “我们项目用Webpack优化时,主要从这几个方面入手:

构建速度:之前完整构建要3分钟,加了cache-loaderthread-loader后降到1分钟。生产环境还用Terser多进程压缩进一步提速。

包体积优化:通过webpack-bundle-analyzer分析发现lodash占用很大,改用按需引入;对react等基础库做externals,通过CDN引入。

分包策略:我们按业务路由做动态import,用户访问时才加载对应模块。还用了SplitChunks把node_modules单独打包,利用浏览器缓存。”

  1. Webpack打包优化的常用方法有哪些? Webpack 打包优化的常用方法主要包括:首先对代码进行 Tree Shaking 去掉未使用的模块,减少最终包体积;使用 代码压缩(TerserPlugin)压缩 JS,CSS 使用 MiniCssExtractPlugin + cssnano 压缩;对依赖库进行 外部化处理(externals),通过 CDN 引入,减少打包体积;使用 多线程/缓存构建(thread-loader、cache-loader、持久化缓存)加快构建速度;同时开启 Source Map 分析,排查包体积过大或冗余依赖。

    **Webpack 分包(Code Splitting)**一般有三种方式:入口分包(Entry Points)、动态 import 异步分包和 SplitChunks 插件拆分第三方库及公共模块。分包后,运行时 Webpack 会在首次加载主 bundle 时生成一个 manifest 或 runtime,记录各个分包对应的文件名和哈希。浏览器在遇到动态 import 时,会根据 manifest 加载对应分包的 JS 文件,并执行模块初始化;第二次加载时,浏览器会利用 缓存和文件 hash 判断资源是否变更,如果 hash 一致,就直接使用缓存文件,从而避免重复下载,提高性能。

  2. Webpack中分包是怎么处理的?分包后在运行时怎么加载的?第二次加载这个包怎么知道还是这个包? 三种主要方式:入口分包、动态 import() 异步分包,以及使用 SplitChunksPlugin 拆分公共库或第三方依赖。Webpack 在打包时会为每个分包生成独立的 bundle 文件,并在主 bundle 中生成一个 runtime 或 manifest,记录各分包对应的文件名和模块映射。运行时,当代码执行到动态 import() 时,Webpack 会根据 manifest 请求对应分包的 JS 文件,加载并执行初始化模块,实现按需加载。第二次加载同一个分包时,浏览器会根据 文件 hash 判断资源是否变化,如果 hash 未变,则直接使用浏览器缓存文件,从而避免重复下载,提高性能和加载速度。

2.3 渲染性能优化

  1. Canvas性能为何会比HTML/CSS好? 主要是因为 渲染机制不同。HTML/CSS 的渲染依赖浏览器的 DOM 和布局引擎,每次元素增删、样式变化或重排重绘都会触发 浏览器的布局(Layout)和渲染(Paint/Reflow)流程,开销较大,尤其是元素数量多或动画频繁时性能下降明显。而 Canvas 是基于 位图的逐像素绘制,渲染时直接操作画布像素,不依赖 DOM,浏览器只需要把画布整体渲染到屏幕上,避免了频繁的重排重绘开销,因此在复杂图形绘制、游戏动画或高频更新场景中,Canvas 的性能优势明显。另外,Canvas 可以利用 GPU 加速渲染(WebGL),进一步提升绘制效率,而 HTML/CSS 的 GPU 加速受限于特定属性和浏览器实现。

  2. 图片压缩和图片格式转换怎么实现的? 前端方面,可以使用 Canvas 或第三方库(如 compress.jsbrowser-image-compression)对图片进行压缩和格式转换:先通过 FileReader 读取图片,然后用 CanvasdrawImage 绘制图片,再通过 canvas.toDataURL('image/jpeg', 0.7)canvas.toBlob 输出指定格式和质量的图片,从而实现 尺寸压缩、质量压缩以及格式转换(如 PNG → JPEG)。同时,还可以结合 Web Worker 异步处理,避免阻塞主线程。 可以说一下uniapp的压缩 或者是裁剪框

  3. WebP是有损转换还是无损?原理是什么?

    WebP 支持 有损和无损两种压缩方式。有损 WebP 基于 预测编码 + 离散余弦变换(DCT),类似 JPEG,通过去掉人眼不敏感的高频信息实现压缩;无损 WebP 则使用 局部像素块内的预测和字典压缩(类似 PNG 的 LZ77 + 霍夫曼编码),在保证图像质量不变的情况下压缩文件大小。相比 JPEG 和 PNG,WebP 在同等质量下通常能获得更小的体积,同时支持透明通道(alpha channel)和动画,因此在现代网页中广泛用于提高加载性能和减少带宽消耗。

2.4 动画性能

优化后回答: “根据项目经验,我的选择原则是:

CSS动画:适合简单的交互动效,比如按钮hover、侧边栏展开。我们有个商品卡片动画就用transform+transition,GPU加速很流畅。

JS动画:复杂路径或需要精确控制时用,比如有个购物车抛物线动画,必须用requestAnimationFrame逐帧计算位置。

性能对比:曾经测试过,200个元素同时移动时,CSS动画帧率保持在50fps以上,纯JS操作DOM的只有20fps左右。”

  1. JS动画与CSS动画区别及相应实现?

    JS 动画与 CSS 动画的区别主要体现在渲染机制、控制方式和性能上。CSS 动画(通过 transition@keyframes 实现)由浏览器的 渲染引擎(Compositor/GPU) 直接处理,通常可以利用 GPU 加速,性能较高,适合处理简单的过渡、位移、透明度、缩放等动画,而且写法简洁,浏览器可以自动优化。缺点是 灵活性有限,不适合复杂逻辑控制或依赖动态数据的动画。

    JS 动画则通过 定时器(setTimeout/setInterval)或 requestAnimationFrame 逐帧修改 DOM 或 CSS 属性实现,优点是 逻辑可控性强,可以随时响应用户交互、改变动画路径或速度。缺点是过多操作 DOM 会导致 回流和重绘(reflow/repaint)开销大,性能低于 CSS 动画,尤其是在大量元素同时动画时。

    实现方式

    • CSS 动画:transition 用于状态变化过渡,@keyframes 配合 animation 属性可实现复杂帧动画。
    • JS 动画:常用 requestAnimationFrame 循环更新 DOM 或元素样式,也可以使用第三方库如 GSAP、Anime.js 来管理动画帧和缓动效果,提升性能和易用性。

三、系统设计与架构

3.1 业务系统设计

  1. 假如让你负责一个商城系统的开发,现在需要统计商品的点击量,你有什么样设计与实现的思路?

    这一点之前做的主要还是用户的行为捕获,埋点上报方面,首先用户行为捕获的话,那可以在商品的点击事件中做处理,这边的话我做了一个统一的时间委托处理 然后是埋点上报,之前是在接口去上报点击数据,其实后面也有了解到专门做事件埋点的厂家 好像是后端接口那边统一去做的 然后其实很重要的一点是对于用户来说可能会发生的事情就是频繁点击该商品,这个的话可以在前端把这些频繁点击的操作进行一个批量和并上报,减少非必要请求,像我在这家公司做的帖子的数据埋点还需要实时显示点击量,这里我其实用了一种欺骗手法,在用户点击的时候我只是在前端给浏览数量加上去,实际真实的数据或者去重还是由后端去负责

3.2 工程化与部署

  1. 如何做一个前端项目工程的自动化部署?有哪些规范和流程设计?

  2. 前端项目的自动化部署,核心目标是 代码提交 → 构建 → 测试 → 部署 → 发布 的全流程自动化,减少人工操作,提高效率和可靠性。

    流程设计

    1. 代码管理规范
      • 使用 Git 进行版本管理,统一分支策略(如 Git Flow 或 trunk-based development);
      • 规范提交信息(Commitlint),保证可追踪性和可回滚性。
    2. 构建与测试
      • 使用构建工具(Webpack、Vite 等)打包项目;
      • 集成自动化测试(单元测试、集成测试、E2E 测试)保证代码质量;
      • 可以通过 lint、type check、prettier 校验代码规范。
    3. CI/CD 流程
      • 使用 GitLab CI、GitHub Actions、Jenkins 等工具,实现持续集成(CI)和持续部署(CD);
      • CI 阶段:代码提交 → 自动构建 → 运行测试 → 生成构建产物;
      • CD 阶段:将构建产物上传到服务器或 CDN,支持灰度发布、回滚策略;
      • 对于多环境(开发、测试、生产),可通过环境变量或不同配置文件进行区分。
    4. 部署规范
      • 保证构建产物可回滚,每次发布生成唯一版本号或 hash;
      • 对静态资源使用 CDN 分发,提高访问速度;
      • 部署完成后可通过自动化监控或 Smoke Test 验证功能正常。
    5. 优化与扩展
      • 对大项目,可使用 分阶段部署、灰度发布,减少风险;
      • 可结合 自动化通知(如 Slack/钉钉)提醒团队构建和部署状态;
      • 可以集成 性能检测(Lighthouse、PageSpeed)作为发布前质量门控。

    总结:前端自动化部署不仅是“自动上传代码”,而是一个完整的 持续集成、测试、构建、发布、监控和回滚闭环,规范化的流程保证高效、可控、可靠的上线体验。

  3. 你做的脚手架是用了什么工具?为什么没有把CI/CD封装进去?

    我做的脚手架主要是用 Node.js + npm / yarn / pnpm + 现代构建工具(Webpack、Vite 等) 来初始化项目模板、管理依赖和脚本命令,实现了快速生成项目骨架、目录结构、基础配置和常用功能(如 ESLint、Prettier、TypeScript 配置等)。

    没有把 CI/CD 封装进脚手架的原因主要是:CI/CD 通常涉及环境、服务器、部署策略和权限等信息,这些属于项目或团队级别的配置,而非每个新项目比如我们做的app都通用。每个团队可能使用 GitLab CI、Jenkins 或其他工具,流程和规范差异很大,所以在脚手架中直接封装 CI/CD 会降低灵活性,反而可能增加维护成本。

    通常的做法是:脚手架生成 可扩展的模板和构建脚本,让团队可以在此基础上轻松集成自己的 CI/CD 流程,保证项目初始化速度,同时保持部署流程的灵活性和可控性。

七、虚拟滚动

  1. 虚拟滚动是怎么实现的? 首先是实现原理吧,通过 容器固定高度 + 内部内容总高度占位,只渲染当前可视区域及部分缓冲区域的数据;怎么样去之渲染当前可视区呢 使用 scroll 事件监听滚动,通过计算 startIndex 和 endIndex 来确定渲染哪些数据。这个的话要比你所看到的更多一点 比如可视区只有十条其实应该渲染的是30条 不然快速向下或者快速向上滚动会出现暂时无数据的状态

  2. 虚拟滚动要不要加缓冲区域?虚拟滚动优化

    缓冲区域其实前面也有讲到,提前渲染可视区上下的少量元素,避免快速滚动时出现空白或闪烁,提高用户体验;

    虚拟滚动优化包括:缓冲区渲染(也就是之前说的缓冲区域)、requestAnimationFrame 处理滚动、滚动事件 防抖/节流、复用 DOM 节点。 requestAnimationFrame 处理滚动,使用 requestAnimationFrame 可以把渲染任务排入浏览器下一帧执行队列,保证每帧只渲染一次,避免卡顿; 滚动事件防抖/节流 复用 DOM 节点

    虚拟滚动时可创建固定数量的 DOM 元素,通过 更新元素内容和位置 来复用,而不是每次滚动都销毁和创建节点;

    对于长列表,这种方式能显著减少 DOM 操作和内存占用,提升渲染性能

  3. 虚拟滚动中怎么计算startIndex?白屏怎么处理的? 虚拟滚动中,计算startIndex分为固定高度和不固定高度 首先固定高度 ,startIndex 表示当前可视区第一个需要渲染的列表项索引,计算方法取决于列表项高度:startIndex = Math.floor(scrollTop / itemHeight); 其中 scrollTop 是滚动容器的滚动距离,itemHeight 是单个列表项高度。

    然后是不固定高度

    不固定高度比较复杂 一般来说可以提前计算每个元素的累计高度数组 cumHeight[i] = height[0] + height[1] + ... + height[i]

    通过 二分查找 找到第一个满足 cumHeight[i] >= scrollTop 的索引作为 startIndex。 白屏处理

    占位元素**:在容器中设置一个总高度占位 div(或 padding/margin 占位),保证滚动条和布局正常,即使数据还未渲染,也不会出现空白区域。

    骨架屏**:可在可视区先渲染 骨架占位元素(灰色条或加载动画),等数据加载完成再替换为真实内容。

    还有就是缓冲区渲染了

  4. 虚拟滚动中数据加载,在滑动事件中使用防抖后计算显示,如果显示还没有计算完,第二次滑动出现了,怎么处理?

    在虚拟滚动中,如果 数据加载或渲染耗时,而用户快速连续滚动,防抖/节流后上一次渲染还没完成,可能会出现 渲染延迟或显示错误。处理方法主要有以下几种:

    1. 最新渲染覆盖旧渲染
      • 每次滚动事件计算可视区 startIndex/endIndex 后,保存最新任务标识(如一个版本号或时间戳);
      • 渲染完成前,判断任务标识是否仍然是最新,如果不是则放弃旧任务的渲染,只渲染最新可视区域。
      • 这样可以保证 最终显示始终与最新滚动位置一致,避免白屏或错位。
    2. 渲染队列或任务调度
      • 使用一个渲染队列,把滚动事件产生的渲染任务依次入队;
      • 渲染完成后再处理下一条任务,或者只保留队列中最后一个任务,丢弃中间冗余任务,提高效率。
    3. 分块渲染
      • 将可视区和缓冲区的渲染拆分为小块,通过 requestAnimationFrame 或微任务分批渲染,避免一次渲染阻塞。
      • 即使用户快速滚动,也能保证页面及时响应,且最终结果正确。

    总结:核心思想是 永远以最新滚动位置为准进行渲染,结合任务标识、队列或分块渲染,可以解决防抖导致的渲染延迟问题,同时保证虚拟滚动的性能和用户体验。

十、组件库设计

  1. 为H5、微信小程序、React Native三端设计一套通用组件库架构,如何保证组件在不同平台下的一致性体验,同时充分利用各平台的原生能力?

    分为通用逻辑组件渲染两个部分来说,首先是通用逻辑这一块 使用 TypeScript 编写所有与UI渲染无关的业务逻辑和状态管理。我们将组件的核心逻辑(如:表单验证、数据加载)抽象成纯JavaScript的 HooksUtilsService。这样无论在哪一个平台,组件的内部状态、交互逻辑和数据处理方式都是完全一样的。 然后是组件渲染,首先要做到的是样式或者说是视觉统一 其次是渲染适配这边可以用uniapp的条件编译的思想来做,比如在h5端的话 button会解析成div 小程序端去渲染成view

    然后就是原生方面,uniapp比如调用相册权限在app和小程序和h5用了不同的api,我们也可以沿用这种思维,比如h5直接调用浏览器的api小程序调用小程序的api等

  2. 如何设计一个高复用性的组件?单一职责原则在组件设计中如何应用?

    高复用性的组件的话其实要考虑的点比较多,首先一个是样式覆盖,像wot-design-ui会提供一个className来给你做样式,第二个是暴露必要的传参,像botton的size之类的,最后是支持内容自定义,通过插槽来实现

    单一原则 我想一下,比如说拿一个checkbox来做吧,首先是拆分 一个组件里面拆分成两个 checkbox做checkbox_item的一个容器,然后只负责事件的交互去进行选择item,而item的话是只负责渲染内容物,这样的话box是负责他们的一个状态管理 两者不会对对方造成任何影响

  3. 组件库的TypeScript类型定义是如何设计的?如何保证类型安全?

tsconfig.json开启严格模式选项吧 然后一个是定义泛型 把接受所有参数的类型申明 其他的我想不到了

十六、项目经验

  1. 说一下你的项目