前言
大家好!我是【前端大大大】。金三银四,秋招春招,前端面试的“战火”似乎从未停歇。面对日益激烈的竞争,面试官们的问题也越来越深入,不仅考察候选人的知识广度,更注重底层原理和实践经验的深度。
一份某大厂外包一面的面试题清单。
1. 移动端 Web 开发与 PC 端 Web 开发的主要差异点有哪些
面试官意图: 考察你是否深刻理解移动端的特殊性,包括但不限于屏幕、交互、性能、环境等方面的差异,以及你是否掌握了相应的解决方案和最佳实践。这不仅仅是技术层面的对比,也包括对用户行为和开发策略的理解。
回答思路:
可以从以下几个核心维度展开,并针对每个维度深入探讨其差异、带来的挑战以及常用的解决策略:
- 屏幕与布局 (Screen & Layout)
- 交互方式 (Interaction & Input)
- 性能与网络 (Performance & Network)
- 硬件与 API (Hardware & APIs)
- 浏览器环境与碎片化 (Browser Environment & Fragmentation)
- 用户行为与使用场景 (User Behavior & Context)
详细解答:
移动端 Web 开发和 PC 端 Web 开发虽然都基于 Web 技术栈(HTML, CSS, JavaScript),但在实际开发中存在显著差异,需要采取不同的策略和技术来应对。
1. 屏幕与布局 (Screen & Layout)
- 差异点:
- 屏幕尺寸多样性: 移动端设备屏幕尺寸范围极广(小屏手机、大屏手机、折叠屏、平板),远超 PC 端相对固定的几种分辨率。
- 物理像素与逻辑像素: 移动端普遍采用高密度(Retina / HiDPI)屏幕,一个 CSS 像素可能对应多个物理像素 (Device Pixel Ratio, DPR)。
- 屏幕方向: 移动端存在横屏和竖屏切换,布局需要能适应变化。
- 可视区域: 移动端屏幕更小,可展示内容有限,信息需要更精简、更聚焦。
- 挑战:
- 如何在不同尺寸、不同密度的屏幕上都提供良好的阅读和操作体验?
- 如何处理屏幕旋转?
- 如何在有限空间内有效展示核心信息?
- 解决方案/策略:
- 响应式网页设计 (Responsive Web Design - RWD): 使用流式布局(Fluid Grids)、弹性图片(Flexible Images)和媒体查询(Media Queries)使页面适应不同屏幕尺寸。
- Viewport 设置: 通过
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">控制视口宽度、初始缩放比例,并通常禁止用户缩放(需谨慎,考虑可访问性)。 - CSS 单位选择: 多使用相对单位如
rem,em,vw,vh代替固定像素px来实现弹性布局。rem基于根元素字体大小,vw/vh基于视口宽高,非常适合移动端适配。 - 移动优先 (Mobile First) 策略: 先为小屏幕设计和开发,然后通过媒体查询逐步增强大屏幕的体验。这有助于优先保证核心功能在移动端的可用性,并简化 CSS。
- 图片适配: 使用
<picture>元素或srcset属性提供不同分辨率的图片,根据 DPR 和屏幕宽度加载最合适的图片,节省带宽、提升加载速度。 - 处理屏幕旋转: 使用
@media (orientation: portrait)和@media (orientation: landscape)媒体查询为不同方向提供特定样式。
2. 交互方式 (Interaction & Input)
- 差异点:
- 主要输入: 移动端是触摸(Tap, Swipe, Pinch, Long Press),PC 端是鼠标(Click, Hover, Double Click, Right Click, Scroll Wheel)和键盘。
- Hover 状态缺失: 移动端没有鼠标悬停 (Hover) 状态,依赖 Hover 显示的信息或交互在移动端不可用。
- 精确度: 手指触摸的精确度远低于鼠标指针,容易误触(“胖手指”问题)。
- 虚拟键盘: 输入时会弹出虚拟键盘,可能遮挡部分屏幕内容。
- 挑战:
- 如何设计易于触摸操作的界面?
- 如何替代 Hover 效果?
- 如何避免虚拟键盘遮挡重要输入框或按钮?
- 解决方案/策略:
- 增大触摸目标: 按钮、链接等可点击元素的尺寸要足够大,并保持足够的间距(例如,苹果建议 44x44pt,谷歌建议 48x48dp)。
- 明确视觉反馈: 为触摸操作提供即时、清晰的视觉反馈(如
:active伪类样式、短暂高亮)。 - 避免依赖 Hover: 将原本通过 Hover 触发的功能改为点击触发(如展开菜单),或提供其他显式交互方式。
- 处理触摸事件: 使用
touchstart,touchmove,touchend,touchcancel事件。注意与click事件的 300ms 延迟问题(现代浏览器已通过viewport设置或 FastClick.js 解决,或推荐使用Pointer EventsAPI 统一处理)。 - 手势支持: 对滑动(Swiper)、缩放(Pinch)等常见手势提供支持,可以使用 Hammer.js 等手势库。
- 表单优化: 使用合适的
input type(如tel,email,number)调起最优虚拟键盘;当输入框获得焦点时,确保其可见,避免被键盘遮挡(可能需要 JS 辅助滚动)。
3. 性能与网络 (Performance & Network)
- 差异点:
- 硬件性能: 移动设备通常 CPU、GPU 性能和内存容量低于 PC。
- 网络环境: 移动网络速度不稳定(2G/3G/4G/5G/Wi-Fi 切换),延迟较高,且流量通常更昂贵。
- 电池续航: 移动设备依赖电池,高性能消耗会加速电量耗尽。
- 挑战:
- 如何在性能受限的设备和不稳定的网络下保证页面快速加载和流畅运行?
- 如何节省用户流量和电量?
- 解决方案/策略:
- 性能预算 (Performance Budget): 设定严格的资源大小(JS, CSS, 图片)、加载时间、请求数等目标。
- 资源优化:
- 图片: 压缩图片、使用 WebP/AVIF 等现代格式、懒加载 (Lazy Loading)、响应式图片。
- 代码: 代码分割 (Code Splitting)、摇树优化 (Tree Shaking)、按需加载 (On-demand Loading)、压缩 JS/CSS (Minification)、启用 Gzip/Brotli 压缩。
- 减少 HTTP 请求: 合并文件(现代 HTTP/2 下重要性降低,但仍需适度)、使用 CSS Sprites 或 Icon Fonts/SVG Sprites。
- 利用缓存: 设置合理的 HTTP 缓存头、使用 Service Worker 实现离线缓存和更精细的缓存控制(PWA 核心技术之一)。
- 优化渲染性能: 减少重绘 (Repaint) 和回流 (Reflow)、利用 CSS Transforms/Opacity 实现硬件加速动画、虚拟滚动 (Virtual Scrolling) 处理长列表。
- JavaScript 执行: 避免长时间运行的 JS 阻塞主线程,使用 Web Workers 处理复杂计算。
- 首屏加载优化: 关键 CSS 内联、服务端渲染 (SSR) 或预渲染 (Prerendering)。
4. 硬件与 API (Hardware & APIs)
- 差异点:
- 设备能力: 移动端可以访问更多硬件传感器和系统功能,如 GPS 定位、摄像头、麦克风、加速度计、陀螺仪、振动、通讯录、指纹识别等。PC 端相对受限。
- 挑战:
- 如何利用这些移动端独有的能力提升用户体验?
- 如何优雅地处理 API 不可用或用户拒绝授权的情况?
- 解决方案/策略:
- Web APIs: 使用 Geolocation API 获取位置、Media Capture and Streams API (getUserMedia) 访问摄像头/麦克风、Device Orientation API 获取方向信息、Vibration API 控制振动等。
- 渐进式增强 (Progressive Enhancement): 首先保证核心功能可用,然后检测并利用可用的高级 API 增强体验。
- 权限处理: 明确告知用户为何需要权限,并在合适的时机请求。优雅处理用户拒绝授权的情况。
- 渐进式 Web 应用 (Progressive Web Apps - PWA): 利用 Service Worker、Manifest 等技术,让 Web 应用具备类似原生应用的体验,如离线访问、添加到主屏幕、推送通知等。
5. 浏览器环境与碎片化 (Browser Environment & Fragmentation)
- 差异点:
- 浏览器内核: 移动端主要是 WebKit (iOS Safari) 和 Blink (Android Chrome 及众多基于 Chromium 的浏览器/WebView)。PC 端种类更多(Chrome, Firefox, Safari, Edge, 旧版 IE)。
- WebView: 大量移动应用内嵌 WebView 来展示网页内容,其版本、性能和对 Web 标准的支持可能与独立浏览器不同,且更新不及时。
- 系统版本碎片化: 尤其是 Android 系统版本众多,不同版本内置的 WebView 可能存在差异。
- 厂商定制: 部分手机厂商会对浏览器或 WebView 进行定制,可能引入特定 Bug 或行为差异。
- 挑战:
- 如何确保 Web 应用在不同移动浏览器和 WebView 环境下表现一致?
- 如何处理特定环境下的兼容性问题?
- 解决方案/策略:
- 跨浏览器测试: 在主流移动浏览器(Safari on iOS, Chrome on Android)以及常见的 WebView 环境(如微信内置浏览器)进行充分测试。使用真机测试最为可靠,也可结合模拟器/仿真器和云测试平台(如 BrowserStack, Sauce Labs)。
- 特性检测 (Feature Detection): 使用 Modernizr 或直接编写 JS 代码检测浏览器是否支持某个特性,而不是依赖 User Agent 字符串进行判断。
- Polyfills/Shim: 为不支持某些新标准 API 的旧环境提供兼容实现。
- CSS 前缀: 使用 Autoprefixer 等工具自动添加所需的 CSS 浏览器引擎前缀。
- 了解 WebView 限制: 意识到 WebView 可能存在的性能瓶颈、API 支持差异和渲染怪癖,并进行针对性调试。
- 远程调试: 使用 Chrome DevTools 远程调试 Android 设备/WebView,使用 Safari Web Inspector 远程调试 iOS 设备/WebView。
6. 用户行为与使用场景 (User Behavior & Context)
- 差异点:
- 使用场景: 移动端用户可能在任何地方、任何时间使用,环境多变(户外强光、嘈杂环境、移动中),且经常是碎片化时间。PC 端通常在固定场所(办公室、家)使用,环境相对稳定。
- 任务目标: 移动端用户通常目标明确,希望快速完成特定任务(查找信息、社交、购物、导航)。PC 端可能进行更复杂、更长时间的操作。
- 注意力: 移动端用户注意力更容易被打断,耐心相对较低。
- 挑战:
- 如何适应多变的使用环境?
- 如何满足用户快速完成任务的需求?
- 如何抓住短暂的用户注意力?
- 解决方案/策略:
- 内容优先,简化设计: 突出核心内容和功能,去除不必要的干扰元素,保持界面简洁直观。
- 优化导航: 提供清晰、易于单手操作的导航(如底部 Tab Bar)。
- 提升可读性: 保证足够的字体大小和对比度,适应不同光线环境。
- 性能至上: 快速加载和响应是留住用户的关键。
- 精简流程: 简化注册、登录、购买等关键流程。
- 考虑单手操作: 将常用操作按钮放置在屏幕底部或侧边易于触达的区域。
2. uniapp wgt 热更新原理
想象一下你的 App 是一个房子(就是用户手机上安装的那个 .apk 或 .ipa 文件)。
这个房子里面,有很多房间和家具(比如页面、按钮、图片、业务逻辑代码等),这些主要是用网页技术(HTML, CSS, JavaScript)写的。UniApp 把这些网页技术相关的东西打包成一个叫 WGT 的资源包(你可以想象成一个装着所有家具和装修图纸的包裹)。
现在,你想给房子里的某个家具换个颜色(比如改个按钮样式),或者添个小摆设(比如加个小功能、改句提示文字)。
传统方式(App 整包更新):
- 你得把整个房子重新设计装修一遍(重新打包整个 App)。
- 然后把这个新房子交给物业/房管局(苹果 App Store / 安卓应用市场)审核。
- 审核通过后,业主(用户)才能收到通知,去下载这个全新的房子来替换旧的。
- 这个过程很慢,还要等审核,很麻烦。
WGT 热更新方式(UniApp 的玩法):
- 只改家具/装修图纸: 你发现只是某个按钮不好看或有 Bug,你只修改这部分对应的网页代码/图片。
- 打包新包裹: 你把这些修改后的新家具/新图纸(新的 HTML, CSS, JS 文件等)重新打成一个新的 WGT 资源包(一个小包裹)。
- 放到指定地方: 你把这个新包裹上传到你自己的服务器上,并告诉服务器:“嘿,这是最新版的家具包裹(版本号 1.1)!”
- App 自己检查: 用户打开 App(旧房子)时,App 会自动(或者你手动触发)去你的服务器问:“喂,我这里是版本 1.0 的家具包裹,你有更新的吗?”
- 服务器回应: 服务器一看,“哎呀,有啊!最新的 1.1 包裹在这里,快去下载吧!”
- App 下载新包裹: App 就偷偷把这个新 WGT 包裹下载下来。
- 替换旧家具: App 下载完后,验证一下包裹没问题,就把房子里旧的家具和图纸(旧的 WGT 资源)用新包裹里的替换掉。
- 刷新/重启生效: App 提示用户“更新好了,重启下应用吧”,或者下次启动时,App 加载的就是这些新的家具和图纸了。用户就能看到你修改后的效果了。
总结一下原理:
UniApp 的 WGT 热更新,就像是给你的 App(房子)只更换内部的“软装”(网页部分的代码和资源),而不用重新盖房子、也不用通过应用市场的审核。
它通过检查服务器上的新资源包 (WGT),下载下来,然后替换掉 App 内部原来的旧资源,让用户在不重新安装 App 的情况下,就能用到你修改后的界面或功能。
好处:
- 快! 绕过应用市场审核,修改立刻能推给用户。
- 方便! 修复 Bug、上线小功能特别灵活。
- 用户体验好! 用户不用每次都去应用市场下载一个大大的安装包。
限制:
- 只能更新网页技术写的部分(HTML, CSS, JS, 图片等)。
- 涉及到原生代码(比如需要调用新的手机硬件功能、修改了原生插件)的部分,还是得老老实实发新版 App 去应用市场。
3. 你做过的性能优化
一、 加载性能优化 (让页面更快地展示出来)
这部分的目标是减少白屏时间,让用户尽快看到内容(关注 FCP, LCP, TTI 等指标)。
-
资源体积优化:
- 图片优化: 这是效果最显著的优化之一。
- 格式选择: 对非透明图片优先使用
WebP或AVIF格式(需要兼容性处理,使用<picture>标签),对简单图标使用SVG。对于老旧格式,选择JPG还是PNG要看场景(照片用 JPG,透明或线条图用 PNG)。 - 压缩: 使用
TinyPNG/JPG或构建工具插件(如imagemin-webpack-plugin)对图片进行有损或无损压缩。 - 尺寸适配: 使用
srcset属性或 JS 判断,根据设备 DPR 和屏幕宽度加载不同尺寸的图片,避免在小屏设备上加载超大图片。 - 懒加载 (Lazy Loading): 对视口外的图片使用懒加载,
<img>标签原生支持loading="lazy",或使用Intersection Observer API配合占位图实现。
- 格式选择: 对非透明图片优先使用
- 代码压缩与混淆: 使用
Webpack的TerserWebpackPlugin(JS) 和CssMinimizerWebpackPlugin(CSS) 或类似工具,在生产环境构建时移除空格、注释,并缩短变量名。 - 代码分割 (Code Splitting):
- 按路由分割: 利用
React.lazy+Suspense或Vue的异步组件,实现访问特定路由时才加载对应页面的代码。 - 按功能/组件分割: 将不常用的、体积较大的库或组件(如图表库、富文本编辑器)单独打包,在需要时才异步加载。
Webpack的dynamic import()语法是关键。
- 按路由分割: 利用
- 摇树优化 (Tree Shaking): 确保使用 ES Module 语法,并配置
Webpack的mode: 'production',它会自动移除未被引用的代码。检查并移除无用的console.log或debugger语句。 - 字体优化:
- 字体裁剪/子集化: 只打包项目中实际用到的字符,可以使用
font-spider等工具。 - 使用 WOFF2 格式: 压缩率更高。
- 控制 FOIT/FOUT: 使用
font-display: swap;或optional;策略,避免字体加载阻塞渲染或导致文本长时间不可见。
- 字体裁剪/子集化: 只打包项目中实际用到的字符,可以使用
- 图片优化: 这是效果最显著的优化之一。
-
网络传输优化:
- 启用 Gzip/Brotli 压缩: 在服务器端(如 Nginx)配置,对文本类资源(HTML, CSS, JS, JSON)进行压缩,能大幅减少传输大小。Brotli 压缩率通常更高。
- 使用 HTTP/2 或 HTTP/3: 相比 HTTP/1.1,支持多路复用、头部压缩等特性,能显著减少网络延迟,尤其是在请求数较多的情况下。
- CDN 加速: 将静态资源(JS, CSS, 图片, 字体)部署到 CDN,利用其边缘节点缓存和就近访问,加速资源下载。
- 减少 HTTP 请求数:
- 合并资源: 虽然 HTTP/2 下合并的收益降低,但对于小图标,合并成 CSS Sprites 或 SVG Sprites 仍然有效。
- 合理使用内联 (Inline): 将首屏必需的、体积很小的 CSS 或 JS 内联到 HTML 中(注意权衡,过大会阻塞 HTML 解析)。
- 利用缓存:
- HTTP 缓存 (强缓存与协商缓存): 合理设置
Cache-Control(max-age, s-maxage, no-cache, no-store) 和Expires(强缓存),以及ETag和Last-Modified(协商缓存)。对不常变动的库文件使用长期强缓存(文件名带 Hash)。 - Service Worker 离线缓存: 对于 PWA 或需要更精细缓存控制的场景,使用 Service Worker 拦截请求,优先从缓存读取资源,实现离线访问和更快的二次加载。
- HTTP 缓存 (强缓存与协商缓存): 合理设置
-
关键渲染路径优化:
- CSS 阻塞: 将关键 CSS(首屏渲染必需的样式)内联到
<head>中,或使用<link rel="preload" href="styles.css" as="style" onload="this.rel='stylesheet'">异步加载非关键 CSS,避免 CSS 下载阻塞页面渲染。 - JavaScript 阻塞:
- 将
<script>标签放在</body>前。 - 使用
defer或async属性。defer保证按顺序执行且在 DOMContentLoaded 前,async不保证顺序且可能在 DOMContentLoaded 前后执行。通常推荐defer。
- 将
- 预加载与预连接:
dns-prefetch:<link rel="dns-prefetch" href="//example.com">提前解析域名。preconnect:<link rel="preconnect" href="//example.com">提前建立 TCP 连接 + TLS 握手。preload:<link rel="preload" href="critical.js" as="script">提前加载当前页面必需的关键资源。prefetch:<link rel="prefetch" href="next-page.js" as="script">提前加载未来可能用到的资源(浏览器空闲时)。
- 服务端渲染 (SSR) / 静态站点生成 (SSG): 对于 SPA 应用,SSR/SSG 可以让浏览器直接接收到包含内容的 HTML,极大地缩短 FCP 和 LCP 时间,对 SEO 也更友好。我使用过
Next.js(React) 和Nuxt.js(Vue) 实现 SSR。
- CSS 阻塞: 将关键 CSS(首屏渲染必需的样式)内联到
二、 渲染性能优化 (让页面交互更流畅)
这部分关注页面滚动、动画、用户输入响应的流畅度,避免卡顿(关注 FPS, FID/INP 等指标)。
-
减少重绘 (Repaint) 与回流 (Reflow/Layout):
- 分离读写操作: 避免在循环中频繁读取会触发回流的属性(如
offsetTop,clientWidth),可以先批量读取,再批量写入。 - 使用 CSS Transform 和 Opacity 实现动画: 这两个属性通常能被 GPU 加速,不触发回流,只触发合成 (Composite),性能最好。避免使用
top,left,width,height等触发布局的属性做动画。 - 对频繁变化的元素使用
will-change: 提前告知浏览器该元素可能要进行变换,让浏览器优化(但不要滥用,会消耗额外内存)。 - 使用
contain属性: 限制元素的布局、绘制和大小计算的影响范围。 - 批量 DOM 操作: 使用
DocumentFragment存储多个 DOM 更改,然后一次性插入文档,减少回流次数。框架(React/Vue)的 Virtual DOM 机制本身就在做类似的事情。
- 分离读写操作: 避免在循环中频繁读取会触发回流的属性(如
-
优化 JavaScript 执行:
- 节流 (Throttle) 与防抖 (Debounce): 对高频触发的事件(如
scroll,resize,input)的回调函数进行节流或防抖处理,减少函数执行次数。 - 避免长时间任务 (Long Tasks): 将耗时长的 JS 计算任务拆分成小块,使用
requestIdleCallback或setTimeout分散执行,避免阻塞主线程。 - 使用 Web Workers: 将复杂的、CPU 密集型的计算(如数据处理、加解密)放到 Worker 线程中执行,不影响主线程的响应。
- 优化循环: 缓存数组长度,避免在循环条件中重复计算。选择合适的循环方式(
forvsforEachvsfor...of,虽然现代 JS 引擎优化很多,但在极端性能敏感场景下仍可关注)。 - 内存管理: 注意及时解除事件监听器、清除定时器、释放不再使用的对象引用,避免内存泄漏导致页面越来越卡顿甚至崩溃。
- 节流 (Throttle) 与防抖 (Debounce): 对高频触发的事件(如
-
框架相关优化 (以 React/Vue 为例):
- React:
- 使用
React.memo或PureComponent避免不必要的组件重新渲染。 - 使用
useMemo和useCallback缓存计算结果和函数实例,配合React.memo使用。 - 合理设计
key,尤其是在列表渲染中,确保key的稳定和唯一。 - 使用虚拟滚动(如
react-window或react-virtualized)处理长列表。
- 使用
- Vue:
- 使用
v-once指令用于只渲染一次的静态内容。 - 使用
keep-alive缓存组件状态。 - 合理使用
computed属性利用其缓存特性。 - 路由懒加载和组件异步加载。
- 同样,注意列表渲染中的
:key。 - 使用虚拟滚动库处理长列表。
- 使用
- React:
三、 监控与分析工具
空谈优化没有意义,必须结合工具进行度量和分析:
- 浏览器开发者工具 (DevTools):
- Lighthouse: 全面的性能、可访问性、最佳实践、SEO 审计工具,提供优化建议。
- Performance Tab: 录制页面运行过程,分析火焰图,找出 JS 长任务、渲染瓶颈、内存问题。
- Network Tab: 查看资源加载时间线、大小、缓存情况、请求瀑布流。
- WebPageTest: 在全球不同地点、不同网络条件下测试网站性能,提供详细报告。
- Bundle 分析工具: 如
webpack-bundle-analyzer,可视化分析打包后的文件构成,找出体积过大的模块或重复依赖。 - 真实用户监控 (RUM - Real User Monitoring): 如 Sentry、Datadog 等服务,收集真实用户访问时的性能数据,了解实际体验。
4. 你认为你做过的项目难点在哪里
面试官问这个,其实是想了解你解决复杂问题的能力,以及你对技术的理解深度。就是想知道:
- 你碰到的最让你头疼、花了大力气才搞定的事情是啥?
- 你是怎么琢磨这个问题,怎么一步步把它解决掉的?
- 这事儿让你学到了啥?
你可以从这几个方面挑一两个你印象最深的来说:
举例说明(选一个你熟悉的来讲):
场景一:页面卡得像 PPT,用户体验贼差
- 难点是啥? "之前做过一个 XX 项目(比如数据看板、或者有复杂交互的页面),里面有个列表/图表要显示超级多的数据,或者用户操作(比如拖拽、筛选)特别频繁。结果呢,页面动不动就卡死,或者操作反应慢半拍,用户用起来很难受。"
- 怎么搞定的? "一开始我也不知道具体是哪里卡,就用浏览器自带的工具 (Performance Tab) 录了一下,发现是某段代码(比如每次更新都重新计算整个列表 / DOM 操作太多)执行时间太长,把浏览器给卡住了。或者发现是内存一直在涨,可能是哪里没释放掉(内存泄漏)。
- (针对渲染卡顿) 后来我就想办法,比如用 '懒加载' / '虚拟滚动' 的技术,就是只显示用户当前能看到的那一小部分数据,滚动的时候再加载新的,扔掉旧的,这样浏览器就不用一次处理那么多东西了。
- (针对计算卡顿) 或者把一些复杂的计算任务,想办法拆开,或者放到后台线程 (Web Worker) 去做,不耽误主界面的响应。
- (针对内存泄漏) 就仔细检查代码,看看是不是事件监听没移除,或者定时器没清掉,把这些 '水龙头' 关好。"
- 结果怎么样? "改完之后,那个页面就算数据再多,或者操作再快,也基本不卡了,用户体验好多了。"
- 学到了啥? "这事儿让我明白,写代码不能光图省事,还得时刻想着性能,特别是处理大量数据或者复杂交互的时候。学会用工具去定位问题也很重要。"
场景二:兼容各种奇奇怪怪的手机/浏览器,样式乱七八糟
- 难点是啥? "有个项目需要在各种手机上跑,老的安卓机、iPhone、甚至一些平板。结果发现在某些手机上,页面布局乱了,或者某个功能点不了,样式也不对,找问题特别费劲。" (或者可以说兼容某个特定浏览器,比如旧版 IE,或者某些国产浏览器的内置 WebView)
- 怎么搞定的? "这就得一个个去试了。先用 '开发者工具' 模拟不同设备看看大概情况。真机有问题的话,就得想办法在真机上调试(比如连电脑远程调试)。发现问题后,
- 如果是 CSS 样式问题,可能得用一些 '兼容性写法' (比如加浏览器前缀,用 Autoprefixer 工具自动加),或者换一种实现方式。有时候得用些 'hack' 技巧(虽然不推荐,但有时没办法)。
- 如果是 JS 功能问题,可能是用了太新的语法或 API,那些老手机浏览器不认识,就得用 'Babel' 把新代码转成旧代码,或者用 'Polyfill' 补上缺少的功能。"
- 结果怎么样? "最后总算在大部分主流设备上看起来、用起来都正常了。"
- 学到了啥? "做前端真的要考虑 '碎片化' 问题,不能只在自己电脑上跑通就完事。测试很重要,而且要了解不同浏览器/环境的差异,写代码时就要注意规避一些坑。"
场景三:跟后端/其他团队接口对接,老对不上
- 难点是啥? "做一个需要前后端分离的项目,后端大哥给了接口文档,但我调的时候老是出错。要么是 '数据格式' 对不上(他给我的是字符串,我这边当数字用了),要么是 '接口参数' 理解错了,要么是 '异常情况' 没考虑到(比如网络不好,或者后端服务挂了,我这边没处理,页面直接崩了)。来回沟通调试特别花时间。"
- 怎么搞定的? "首先是 '仔细看文档',不确定的地方提前沟通清楚。然后用 'Postman' 或类似工具先单独测试接口,确保接口本身没问题。代码里要做好 '数据校验' 和 '错误处理',不能想当然觉得后端给的数据一定是对的。对于接口可能失败的情况,要给用户友好的提示,或者设计 '重试' 机制。后来我们还约定了更详细的 '接口规范',甚至用了
TypeScript这种能提前检查类型的语言,减少了很多这类问题。" - 结果怎么样? "后面接口对接顺畅多了,联调效率高了不少,线上因为接口问题导致的 Bug 也少了。"
- 学到了啥? "前后端协作,'沟通' 和 '规范' 太重要了。不能各写各的,要提前对好 '暗号'(数据结构、参数、异常处理方式)。前端也要有 '防御性编程' 的意识,不能完全相信外部数据。"
说的时候注意:
- 挑你真正做过的、感受深的,这样细节才能说出来。
- 说清楚 “难” 在哪里,是技术本身复杂?还是调试困难?还是沟通成本高?
- 重点说你是 “怎么解决” 的,体现你的思考过程和动手能力。
- 最好有个 “结果”,说明你的努力是有成效的。
- 拔高一下,说说 “学到了什么”,体现你的成长和总结能力。
5. 说一下 SPA 页面实现更新通知
常见的几种“通知”方法,说白了就是让浏览器里的旧代码能发现服务器上有新代码了:
-
定时“体检”(轮询 Polling):
- 怎么做: 浏览器里跑着的代码(JavaScript)定个闹钟,比如每隔 10 分钟或者半小时,就去服务器上问一下:“喂,服务器大哥,你那最新的版本号是多少啊?” 它会请求服务器上的一个特定文件(比如
version.json),里面存着最新的版本号。 - 发现更新: 如果代码发现服务器上的版本号比自己当前运行的版本号要新,就知道“哦豁,有更新了!”
- 通知用户: 这时候它就在页面上弹个小提示条或者对话框,告诉你:“发现新版本,点这里更新!”
- 更新操作: 你一点那个按钮,通常就是执行
window.location.reload(true),强制浏览器重新、彻底地加载整个页面,这样就把服务器上的新代码给拉下来了。 - 缺点: 不停地问,有点浪费资源,而且发现更新可能不及时(取决于你多久问一次)。
- 怎么做: 浏览器里跑着的代码(JavaScript)定个闹钟,比如每隔 10 分钟或者半小时,就去服务器上问一下:“喂,服务器大哥,你那最新的版本号是多少啊?” 它会请求服务器上的一个特定文件(比如
-
服务器主动“喊话”(Server-Sent Events / WebSockets):
- 怎么做: 页面加载后,跟服务器建立一个“长连接”,就像一直开着对讲机。服务器那边一旦部署了新版本,就通过这个对讲机“喊一嗓子”:“全体注意,有新版本了!”
- 发现更新: 浏览器里的代码收到这个“喊话”,就知道有更新了。
- 通知用户 & 更新: 后面步骤就跟上面类似,弹窗提示,用户点击刷新。
- 优点: 发现更新很及时。
- 缺点: 对服务器有要求,需要服务器支持这种“喊话”技术,而且一直连着也消耗点资源。
-
利用“快递员”小弟(Service Worker):
- 这是目前比较现代和推荐的方式,尤其是在 PWA (渐进式 Web 应用)里。
- 怎么做: Service Worker 是一个在你浏览器后台运行的“小脚本”,像个独立的“快递员”。它可以拦截你的网页发出的网络请求。当浏览器再次访问这个网站时,Service Worker 会先去服务器检查一下自己(以及它管理的资源)是不是最新的。
- 发现更新: 如果 Service Worker 发现服务器上有更新版本的资源文件(比如新的 JS、CSS),它会先把新的下载到后台。
- 通知用户: 当新的 Service Worker 准备好接替旧的“快递员”时,它可以给页面发送一个消息:“嘿,新版本已经下载好了,准备安装!”
- 更新操作: 页面收到消息后,同样可以弹窗提示用户。用户点击“更新”后,可以告诉 Service Worker “立即激活新版本”,然后刷新页面。Service Worker 就能确保下次加载时用的是全新的资源。
- 优点: 更智能,可以在后台完成下载,更新体验更平滑,还能做离线缓存等高级功能。
- 缺点: 设置起来比轮询复杂一点。
6. H5 跟 原生怎么做通信
想象一下:
- 你的原生 App (比如用 Swift/Objective-C 写的 iOS 应用,或者用 Kotlin/Java 写的 Android 应用) 是一个本地居民,住在手机这个“国家”里,能直接使用手机的各种设施(摄像头、GPS、通讯录等),说的是“本地话”。
- 你的H5 页面 (用 HTML, CSS, JavaScript 写的网页) 是一个外来游客,住在 App 里的一个叫 WebView (或 WKWebView) 的“旅馆”里。这个游客说的是“网页话”(JavaScript),他自己没法直接去用手机的摄像头,得通过“旅馆前台”帮忙。
这个 WebView “旅馆” 就是关键的沟通桥梁或翻译官。
H5 (游客) 想让 原生 App (本地居民) 帮忙做事: (比如 H5 想调用摄像头扫码)
-
方法一:通过“旅馆前台”直接喊话 (JavaScript Bridge / JSBridge)
- 原生 App (本地居民) 会提前在 WebView 这个“旅馆”里安装一个特殊的“内部电话” (注入一个全局的 JavaScript 对象,比如叫
nativeBridge或者webkit.messageHandlers.xxx等)。 - H5 页面 (游客) 想扫码时,就拿起这个“内部电话” (调用这个 JS 对象的方法),说:“喂,前台 (
nativeBridge),帮我调用一下扫码功能 (scanQRCode),扫完结果告诉我哈!” (nativeBridge.scanQRCode(callbackFunction))。 - WebView (旅馆前台) 听到这个请求,就转告给原生 App (本地居民)。
- 原生 App 去调用手机的摄像头,扫码。扫完了,再通过“旅馆前台”把结果 (比如二维码内容) 回传给 H5 页面的那个
callbackFunction。H5 拿到结果就能显示了。 - 这是最常用、最标准的方式。
- 原生 App (本地居民) 会提前在 WebView 这个“旅馆”里安装一个特殊的“内部电话” (注入一个全局的 JavaScript 对象,比如叫
-
方法二:发出“特殊暗号” (URL Scheme 拦截)
- H5 页面 (游客) 假装要去访问一个特别格式的网址,比如
myapp://scanQRCode?param=someValue。这个myapp://就是约好的“暗号开头”。 - WebView (旅馆前台) 看到游客要去这个“奇怪”的地址,它不会真的去访问,而是拦截下来。
- 它一看这“暗号”,就知道:“哦,这是游客想让本地居民做
scanQRCode这件事,还带了个参数someValue”。 - 于是 WebView 就通知原生 App 去做对应的事。
- 这种方法比较老,或者在某些特定场景下用,没有 JSBridge 灵活。 获取返回值也麻烦一些。
- H5 页面 (游客) 假装要去访问一个特别格式的网址,比如
原生 App (本地居民) 想通知 H5 页面 (游客) 一些事: (比如 App 登录成功了,想让 H5 页面更新状态)
- 怎么做: 原生 App (本地居民) 直接走到 WebView 这个“旅馆”,对着 H5 页面 (游客) 喊话。
- 具体操作: 原生 App 可以直接执行一段 JavaScript 代码在 H5 页面里。比如,原生 App 调用 WebView 提供的类似
evaluateJavaScript("updateLoginStatus('loggedIn')")这样的方法。 - H5 页面里只要有一个叫
updateLoginStatus的全局 JavaScript 函数,它就会被执行,然后 H5 页面就知道自己该更新登录状态了。
总结一下:
H5 和原生 App 通信,主要靠 WebView 这个中间人。
- H5 调用原生: 要么通过原生预留在 H5 里的 JS 对象 (JSBridge) 来调用原生方法,要么通过发送特定格式的 URL (暗号) 让原生拦截处理。
- 原生调用 H5: 原生直接在 WebView 里执行 H5 的 JavaScript 代码。
7. 说-下 Http2
以前咱们用那个老的 HTTP/1.1 上网看网页:
- 那感觉就像你去餐厅吃饭(请求网页上的图片、样式、代码文件啥的)。用 HTTP/1.1,就好比餐厅给你派了好几个服务员(就是好几个连接),但每个服务员一次只能端一道菜(一个文件),而且得等这道菜送到你桌上,他才能回去拿下一道。要是某道菜(比如一个大图片)在厨房做得特别慢,这个服务员就卡在那儿了,他本来该送的其他菜(比如样式文件)也就耽误了。这就叫 “队头阻塞” (Head-of-Line Blocking),效率很低。
- 而且,你每次点菜(发请求),都得给服务员说一堆要求(比如“我要这种格式的”、“我用的是这个浏览器”等等,这些叫“请求头” Headers)。用 HTTP/1.1 的话,你基本上是每次都得把这套要求完整说一遍,就算大部分内容都一样。挺浪费口舌的吧?
现在,HTTP/2 来了,它把事情搞得聪明多了:
- 一个超级服务员(单一连接 & 多路复用): 不再需要好几个服务员了,现在只需要一个超级能干的服务员,通过一个连接就搞定所有事。这个服务员厉害在哪呢?他能同时处理(端)好几道菜!他会把你的大订单(大文件)拆成一小块一小块的,然后交错着一起送过来。所以,就算某个大图片的一小块儿卡住了,其他文件(比如样式、代码)的小块儿照样能嗖嗖地送达。再也不会因为一个慢菜卡住整个队伍了!这个技术叫 “多路复用”(Multiplexing) ,是 HTTP/2 最大的优点。
- 指令速记(头部压缩): 还记得每次都重复说要求吗?HTTP/2 有个妙招。它会记住你之前提过的要求(请求头)。下次你再要东西,它就不用把所有话再说一遍了,可能就简单说一句“跟上次差不多,就改这一点点”,或者用一些特殊的“暗号”(编码)来代表常用的要求。这就压缩了请求头(Header Compression),省了不少沟通成本(传输的数据量),尤其在手机上,流量又慢又贵的时候特别有用。
- 说“暗号”(二进制协议): 以前 HTTP/1.1 是用咱们能直接读懂的文字(文本格式)来沟通,而 HTTP/2 改用了一种更简洁、更像计算机内部指令的**“暗号”(二进制格式)**。电脑处理这种二进制码比读文本快得多,也更不容易出错,整个沟通效率就上去了。
- (可选功能)会“读心术”的服务员(服务器推送): 有时候,服务器(厨房)知道,你要是点了主菜(HTML 页面),那八成也得要餐具和餐巾纸(CSS 和 JS 文件)。有了“服务器推送”(Server Push),服务器就能主动地,“先知先觉”地,把这些你很可能马上就要的东西,在你开口要之前,就先推给你。这样能进一步加快速度。不过这功能得用得巧,不然服务员可能会硬塞给你一堆你根本不需要的东西。
8. http2 特性原理
| 特性 (Feature) | 解决的 HTTP/1.1 问题 | 原理/怎么做的 (How it Works) | 主要好处 (Benefit) |
|---|---|---|---|
| 多路复用 (Multiplexing) | 队头阻塞 (Head-of-Line Blocking):一个慢请求会卡住后续请求。 需要开多个 TCP 连接,浪费资源。 | 一个 TCP 连接就够了。把请求和响应都拆成很多小的、带标签的“帧”(Frames),这些帧可以在这一个连接上混在一起传输,到了目的地再根据标签拼回去。哪个请求先准备好,它的帧就可以先发,不会被别的卡住。 | 消除队头阻塞,提高并发性,减少连接开销,网页加载更快。 |
| 头部压缩 (Header Compression - HPACK) | 请求头 (Headers) 重复内容多,体积大,浪费带宽,尤其在移动端。 | 客户端和服务器维护一个共享的“字典” (Header Table)。对于重复的头部信息,只发送一个**“索引号”**;对于新的或稍微不同的,只发送变化的部分,并用霍夫曼编码压缩。 | 大幅减小请求头体积,节省带宽,加快请求发送速度。 |
| 二进制分帧 (Binary Framing) | HTTP/1.1 是文本协议,解析起来相对麻烦,容易出错(比如处理换行符)。 | 不再用纯文本,而是把所有传输的信息(头部、数据体)都打包成二进制格式的“帧”。这种格式对计算机来说解析更快、更不容易出错。每个帧都有类型、长度、标识符等信息。 | 解析效率高,不易出错,是实现多路复用、优先级等功能的基础。 |
| 服务器推送 (Server Push) | 浏览器先请求 HTML,解析后才发现需要 CSS、JS 等资源,再去请求,浪费了往返时间。 | 服务器**“猜到”** 浏览器可能马上就需要某个资源(比如和 HTML 关联的 CSS),于是在浏览器还没开口要的时候,就主动把这个资源推给浏览器,放在缓存里备用。 | 减少请求次数,降低延迟,让关键资源更快到位,提升首屏速度。 |
| 流优先级 (Stream Prioritization) | 无法明确告诉服务器哪些请求更重要,服务器只能大致按顺序处理。 | 允许浏览器给每个“流”(也就是一个请求/响应对)设定一个优先级(权重)和依赖关系。服务器可以根据这个信息,优先处理更重要的流(比如先发送关键 CSS,再发送图片)。 | 更合理地分配服务器资源,优化资源加载顺序,改善用户感知性能。 |
9. 说-下 vite 跟 webpack 的区别
你在开发一个网站(应用):
- Webpack 像一个老派但非常全能的管家。你每次要看效果(启动开发服务器)或者要正式发布(打包上线),他都得把你家所有的东西(代码、图片、样式等)仔仔细行地从头到尾整理打包一遍,确保万无一失,才拿给你。这在项目小的时候还行,项目一大,每次启动或者改一点东西等他整理好,就得花点时间。但他经验丰富,几乎什么定制化的要求都能帮你搞定。
- Vite 像一个反应极快的年轻助手。你启动开发服务器时,他几乎是秒开。因为他不急着打包所有东西,而是先把“门面”(入口 HTML)给你,然后你需要哪个房间(模块/代码文件),浏览器就直接问他要,他再快速地把那个房间的东西递给你(利用浏览器自带的 ES Module 加载能力)。改动代码后,他也只更新你改动的那一小块,所以热更新(HMR)也飞快。不过,真到要正式发布时,他还是会请出他信赖的打包专家(Rollup)来帮你做一次彻底的优化打包。
下面是用表格总结的主要区别:
| 对比方面 | Vite | Webpack | 关键区别(大白话) |
|---|---|---|---|
| 开发服务器启动速度 | 极快 (Near-instant) | 相对较慢 (Slower, 尤其项目大时) | Vite 基本不用等,Webpack 要先打包,项目越大等越久。 |
| 开发时 HMR 速度 (热模块替换) | 极快 (Very Fast) | 较快,但可能随项目增大而变慢 (Fast, but can slow down) | Vite 更新几乎是瞬间的,Webpack 可能需要重新计算和构建更多东西。 |
| 开发时工作方式 | 基于原生 ES 模块 (Native ESM),按需编译和提供源码,不打包 | 需要先将整个项目打包 (Bundle) 成一个或多个文件再提供服务 | Vite 让浏览器自己去要代码,Webpack 是先打包好再给浏览器。 |
| 生产环境打包 | 使用 Rollup 进行打包优化 | 使用 Webpack 自身 进行打包优化 | 最后上线都要打包,Vite 默认请 Rollup 大佬帮忙,Webpack 自己干。 |
| 配置复杂度 | 相对简单,开箱即用度高,配置项较少 | 相对复杂,需要较多配置,但非常灵活,可定制性强 | Vite 省心,常用功能都帮你弄好了;Webpack 需要你多操心配置,但自由度极高。 |
| 生态系统/插件 | 快速发展中,基于 Rollup 插件体系,常见需求基本满足 | 非常成熟、庞大,拥有海量的 Loader 和 Plugin,几乎无所不能 | Webpack 的工具箱更大更全(历史悠久),Vite 的工具箱在快速扩充中。 |
| 浏览器兼容性 (开发时) | 依赖现代浏览器 (需支持 Native ESM) | 兼容性更好,打包后的代码不直接依赖 Native ESM,可在稍旧浏览器运行 | Vite 开发时需要新一点的浏览器支持,Webpack 没这个限制。(注意:生产打包后都能做兼容处理) |
| 诞生背景/理念 | 旨在利用现代浏览器特性(Native ESM)提升开发体验和速度 | 最初为解决模块化打包问题而生,构建能力全面且强大 | Vite 是“开发体验优先”,Webpack 是“构建能力优先”(虽然也在优化体验)。 |
总结一下:
- 如果你想追求极致的开发速度和体验,尤其是新项目或者中小型项目,并且团队使用的浏览器都比较现代,Vite 是个非常棒的选择,能让你爽到飞起。
- 如果你的项目非常庞大且复杂,或者需要大量深度定制化的构建流程,或者需要兼容非常老的浏览器进行开发调试,Webpack 那套成熟、强大的生态和配置能力可能更稳妥,更能满足你的特殊需求。
不过,Vite 发展很快,生态也在迅速完善,越来越多的项目开始转向 Vite。
10. 为什么他 HMR 那么快
这主要得归功于它在开发模式下根本不打包,而是直接利用浏览器自己就能读懂的新式 JavaScript 模块(叫 ES Module,简称 ESM)。
Webpack 那边,它在开发时,当你改了一个文件:
- 它得先找出这个文件和哪些其他文件有关系(构建依赖图)。
- 然后,它需要把相关的这一堆文件重新打包成浏览器能运行的一坨或几坨代码(Bundle)。
- 最后才把这更新后的“代码包”发给浏览器去替换。
这个“重新打包”的过程,在项目变大之后,就会越来越慢。 就像你改了一块积木,Webpack 可能得把那一大片相关的积木都拆了重新拼一遍。
而 Vite 这边呢,它聪明得多:
- 启动时根本不打包。 它直接把你的源代码交给浏览器。现代浏览器很厉害,能直接理解 ESM,知道怎么按需去加载各个模块(JS 文件)。
- 当你改了一个文件(比如
A.js):- Vite 通过 WebSocket (一种实时通信技术) 立刻就知道了是
A.js这个文件变了。 - 它只需要把
A.js这一个文件重新处理一下(比如,如果它是 Vue 或 React 文件,就编译一下;如果是普通 JS,可能转译一下语法)。这个处理过程非常快,因为只处理一个文件。 - 然后,Vite 通知浏览器:“喂,
A.js更新了,这是新的代码,你把它替换掉旧的就行了!” - 浏览器接到通知,利用 ESM 的特性,只需要把内存里旧的
A.js换成新的,并且只更新那些直接或间接依赖了A.js的、并且能够接受热更新的部分。
- Vite 通过 WebSocket (一种实时通信技术) 立刻就知道了是
关键就在于:
- 粒度极小: Vite 的更新是基于单个文件的,而不是像 Webpack 那样基于“代码块”或“包”。改哪个,就只处理哪个。
- 没有打包负担: 开发时省去了耗时的打包步骤。编译(如果需要的话)也是按需、单个文件进行的。
- 利用浏览器原生能力: 它把很多模块管理的工作直接交给了浏览器本身(通过 ESM),自己就轻松多了。
简单打个比方:
- Webpack 的 HMR: 你家墙上掉了一块砖,Webpack 可能得把整面墙,甚至和这面墙相连的其他结构都检查一遍,再重新砌好。
- Vite 的 HMR: 同样是掉了一块砖,Vite 直接拿一块新砖,“啪”一下塞回那个洞里,搞定!
所以,Vite 的 HMR 才能做到那么快,几乎是即时响应,大大提升了咱们前端开发的幸福感!
11. vue3 跟 vue2 的区别
| 对比方面 | Vue 3 | Vue 2 | 关键区别(大白话) |
|---|---|---|---|
| 核心性能 | 更快 (渲染、更新都更快),更小 (核心库体积减小) | 相对较慢,体积较大 | Vue 3 跑得更快,占地儿更小,对用户和开发者都更友好。 |
| 响应式系统 | 基于 Proxy,能监测到更多类型的变化(如属性增删、数组索引修改),性能更好 | 基于 Object.defineProperty,有些限制(对新增/删除属性、数组索引修改等需要特殊 API $set/$delete) | Vue 3 能自动“看到”几乎所有数据变化,不需要你用特殊方法去“提醒”它;Vue 2 有些情况需要你手动“戳”一下才行。而且 Vue 3 “看”得更快。 |
| 组件写法 (API) | 主推 Composition API (组合式 API),也兼容 Options API (选项式 API) | 主要是 Options API (data, methods, computed 分开写) | Vue 2 代码像按类别整理(数据放一起,方法放一起);Vue 3 推荐一种新方法,可以按功能/逻辑把相关代码放一起(比如处理用户信息的代码全在 setup 函数附近),代码多了也不乱,更容易维护和复用。当然,你习惯 Vue 2 的写法,Vue 3 也支持。 |
| TypeScript 支持 | 非常好,从底层就对 TS 做了优化,类型推导更智能,结合 Composition API 体验极佳 | 支持,但相对没那么完美,有时需要额外配置或类型体操 | Vue 3 和 TypeScript 是天生一对,配合起来非常顺畅;Vue 2 和 TS 也能搭伙,但默契度稍差一点。 |
| 新特性 | Composition API, Teleport (任意门,把组件内容渲染到 DOM 其他地方), Fragments (组件可以有多个根节点), Suspense (处理异步组件加载状态) | 无 | Vue 3 多了几个“神兵利器”,比如 Composition API 让代码组织更灵活,Teleport 可以“乾坤大挪移”,Fragments 不再强制套个根 div。 |
| 虚拟 DOM (Virtual DOM) | 优化得更好,使用静态标记 (Patch Flags),diff 算法更快,只对比可能变化的部分 | 需要对比整个虚拟 DOM 树 | Vue 3 在更新界面时更聪明,它会提前“标记”好哪些地方可能变、哪些地方肯定不变,更新时只关注可能变的地方,大大减少了对比的工作量,所以更快。 |
| 打包体积 (Tree-Shaking) | 做得更好,核心功能按需引入,没用到的功能不会打包进去,最终包体积更小 | Tree-Shaking 效果相对有限 | Vue 3 像个模块化家具,你只需要的部分才会被打包带走,整体更轻便;Vue 2 可能有些你没用上的功能也得一起带上。 |
| 开发工具/生态 | 推荐使用 Vite 作为构建工具,开发体验极速;Vue Devtools 等工具也已适配 | 主要使用 Vue CLI (基于 Webpack) | Vue 3 配上 Vite 这个新搭档,开发启动和热更新速度快得飞起,写代码的幸福感飙升。 |
总结:
Vue 3 就是 Vue 2 的全面升级版,主要亮点在于:
- 更快、更小: 底层性能优化,用户体验更好。
- Composition API: 提供了一种更灵活、更利于维护大型项目的代码组织方式。
- 更好的 TypeScript 支持: 对 TS 开发者更友好。
- 更强的响应式系统:
Proxy解决了Object.defineProperty的一些痛点。 - 更棒的开发体验: 尤其是配合 Vite 使用时。
对于新项目,强烈推荐直接上 Vue 3。对于老项目,可以根据情况考虑是否升级,Vue 官方也提供了一些迁移工具和指南。
12. 说一下 hook 理念
Hook 的理念就像是给你的函数组件(就是那种用 function 写的简单组件)配上了一个“工具腰带”。
以前(比如在 React 的类组件里),组件想拥有自己的状态 (state) 或者想在特定生命周期(比如刚显示出来时、更新时、消失时)做点事情,就得写成一个“类 (Class)”,用 this.state、componentDidMount 这些比较“重”的东西。
Hook 来了之后,它说:
“嘿,简单的函数组件,我知道你很轻便,但你也想拥有那些复杂功能对吧?来,我给你提供一堆‘钩子’(就是这些
use开头的函数,比如useState,useEffect)。你想用状态?用useState()这个钩子勾一下就行。想在组件渲染后做点事?用useEffect()这个钩子勾一下就行。”
所以,Hook 的核心理念就是:
- 让你在函数组件里也能用上类似 state 和生命周期的特性,不用非得去写复杂的类组件。
- 把“有状态的逻辑”从组件里抽出来,变成可复用的“钩子”。比如你可以自己写一个
useFetchData的自定义 Hook,哪个组件想发请求拿数据,直接用这个 Hook 就行,代码干净又好复用。 - 让相关的逻辑能聚合在一起。以前在类组件里,一个功能的逻辑可能分散在
componentDidMount,componentDidUpdate,componentWillUnmount好几个地方。用了 Hook(特别是useEffect),你可以把同一个功能的设置和清理代码放在同一个useEffect里,代码更内聚、更好懂。
简单比喻: 函数组件是个“普通工人”,Hook 就是给他配备的“高级工具”(状态管理工具、副作用处理工具等),让他也能干“工程师”(类组件)的活儿,而且可能更灵活、更方便。
13. 说一下 vue3 跟 vue2 diff 算法原理 跟具体实现
Vue 2 和 Vue 3 在 Diff 算法上的区别。这块是 Vue 性能提升的关键之一。
核心思想转变(大白话):
- Vue 2 的 Diff: 像个老实但有点笨的侦探。拿到新旧两份“蓝图”(Virtual DOM),它会从头到尾仔仔细细地对比每一个节点,找出哪里不一样,然后告诉施工队(真实 DOM 操作)去修改。即使很多地方压根没变,它也得去瞅一眼。特别是对比一长串子节点列表时,它用的是一种叫**“双端比较”**的方法,就是从列表的两头往中间凑,尽量减少移动操作,但也挺费劲。
- Vue 3 的 Diff: 像个有备而来的高科技侦探。它在编译阶段(把你的模板代码变成渲染函数时)就已经做了很多功课。
- 静态标记 (Patch Flags): 它会提前给动态的部分(就是可能变化的地方,比如绑定了变量的属性、文本)打上**“嫌疑标签”**。比如标记“这个节点的文本可能变”、“这个节点的 class 可能变”、“这堆子节点的顺序可能变”等等。
- 静态提升 (Static Hoisting): 对于那些完全不会变的静态内容(比如纯文本、没有绑定的属性),它直接“拎出来”存好,更新时压根不看它们。
- 靶向更新: 到了真正对比的时候,侦探直接看“嫌疑标签”。如果标签说“只有文本可能变”,那它就只对比文本,其他属性直接跳过。如果标签说“子节点顺序可能变”,它才会去用一个更高效的方法(基于最长递增子序列 LIS)去对比子节点列表。
所以,Vue 3 的 Diff 快在哪里?
- 跳过静态内容: 大量不需对比的工作直接省略了。
- 靶向更新动态内容: 对比时目标明确,只比较标记了可能变化的部分,大大减少了对比次数。
- 更优的列表 Diff: 对比
v-for列表时,使用了更适合复杂移动场景的 LIS 算法,移动操作更少、更高效。
具体实现原理简介:
Vue 2 Diff (双端比较 - Double Ended Diff)
- 同层比较: 只比较同一层级的节点。
- 节点对比:
- 类型不同:直接销毁旧的,创建新的。
- 类型相同 (Element):复用节点,对比属性并更新。然后递归对比子节点。
- 子节点列表对比 (双端比较核心):
- 维护四个指针:
oldStartIdx,oldEndIdx,newStartIdx,newEndIdx。 - 进行五种比较尝试,尽可能复用节点:
oldStartVnodevsnewStartVnode:相同则 patch,指针都后移。oldEndVnodevsnewEndVnode:相同则 patch,指针都前移。oldStartVnodevsnewEndVnode:相同则 patch,将oldStartVnode对应的 DOM 移动到末尾,oldStartIdx后移,newEndIdx前移。oldEndVnodevsnewStartVnode:相同则 patch,将oldEndVnode对应的 DOM 移动到开头,oldEndIdx前移,newStartIdx后移。- 以上都不匹配:
- 用
newStartVnode的key去旧 VNode 列表的key -> index映射中查找。 - 找到: patch 节点,并将该旧节点对应的 DOM 移动到开头。将旧 VNode 数组中该位置设为
undefined(表示已处理)。 - 没找到 (或 key 不存在): 这是新节点,创建并插入到开头。
newStartIdx后移。
- 用
- 循环结束后,处理剩余节点:
- 若
oldStartIdx > oldEndIdx:说明旧节点处理完了,新节点列表还有剩余,批量创建这些新节点。 - 若
newStartIdx > newEndIdx:说明新节点处理完了,旧节点列表还有剩余,批量删除这些旧节点。
- 若
- 维护四个指针:
Vue 3 Diff (静态标记 + 最长递增子序列 - Patch Flags + LIS)
- 编译时分析:
- 静态提升: 完全静态的节点或属性被提升,更新时跳过。
- Patch Flags: 为动态节点打上标记,指示需要对比的类型(如
TEXT,CLASS,STYLE,PROPS,FULL_PROPS,KEYED_FRAGMENT,UNKEYED_FRAGMENT等)。
- 运行时 Patching:
- 根据 Patch Flag 进行靶向更新:
- 如果
flag指示只需更新 TEXT,则只对比textContent。 - 如果
flag指示只需更新 CLASS,则只对比className。 - ...以此类推,避免不必要的对比。
- 如果
- 子节点列表对比 (v-for):
- 有 Key (KEYED_FRAGMENT):
- 头尾预处理: 从头到尾、从尾到头,比较相同
key的节点,能复用的先 patch 掉(类似双端,但更简化)。 - 处理剩余的中间乱序部分:
- 为剩余的新 VNode 创建
key -> newIndex映射。 - 遍历剩余的旧 VNode,尝试在新 VNode 映射中查找。
- 找不到:删除旧节点。
- 找到:patch 节点,并记录下新节点在旧节点序列中的相对位置 (存入一个数组,比如
newIndexToOldIndexMap,找不到的标记为 0)。
- 计算
newIndexToOldIndexMap的最长递增子序列 (LIS)。这个子序列代表了那些不需要移动的节点。 - 移动和创建: 倒序遍历新 VNode 列表(或 LIS 结果),将在 LIS 中的节点不动,不在 LIS 中的节点移动到正确位置,新 VNode 在
newIndexToOldIndexMap中标记为 0 的是需要创建的。
- 为剩余的新 VNode 创建
- 头尾预处理: 从头到尾、从尾到头,比较相同
- 无 Key (UNKEYED_FRAGMENT): 对比新旧列表长度,多删少补,尽可能原地复用(不保证节点稳定)。
- Fragment (部分 Key): Vue 3 也能处理部分节点有 Key,部分没有的情况。
- 有 Key (KEYED_FRAGMENT):
- 根据 Patch Flag 进行靶向更新:
总结表格:
| 对比方面 | Vue 2 Diff | Vue 3 Diff | 主要提升 |
|---|---|---|---|
| 核心策略 | 全量 Diff (Full Tree Diff) | 静态标记 + 靶向更新 (Compile-time Informed Diff) | 避免了大量不必要的对比工作。 |
| 编译时作用 | 较小,主要是生成渲染函数 | 非常关键,进行静态分析、标记 Patch Flags、静态提升 | 为运行时 Diff 提供大量优化信息。 |
| 对比范围 | 需要遍历整个 VNode 树 | 大大缩减,跳过静态节点,动态节点按需对比 | 运行时开销显著降低。 |
| 列表 Diff 算法 (Keyed) | 双端比较 (Double Ended Diff) | 预处理 + 最长递增子序列 (LIS) | 在复杂列表重排、移动场景下,LIS 通常更高效,DOM 移动次数可能更少。 |
| 性能表现 | 相对较慢,尤其在大型组件或复杂列表更新时 | 显著提升,更新速度更快,内存占用可能更低 | 用户体验更好,应用性能更高。 |
| 实现复杂度 | Diff 逻辑相对集中在运行时 | 运行时 Patching 逻辑更清晰,但依赖编译时信息,整体系统复杂度可能更高 | 对开发者透明,最终效果是性能提升。 |
14. vue 路由的模式
好的,用表格来总结一下 Vue Router 的几种主要模式:
| 对比方面 | Hash 模式 (createWebHashHistory) | History 模式 (createWebHistory) | Memory 模式 (createMemoryHistory) |
|---|---|---|---|
| URL 样子 | 带 # 号,例如 https://example.com/#/user/1 | 不带 #,看起来像普通 URL,例如 https://example.com/user/1 | 不改变浏览器地址栏 URL,路由状态只存在内存里。 |
| 工作原理 | 利用 URL 的 hash (# 后面的部分) 和 hashchange 事件。hash 变化不会让浏览器向服务器发请求。 | 利用 H5 History API (pushState, replaceState, popstate 事件)。URL 变化像真的一样,但 JS 会阻止浏览器发请求,而是自己更新页面内容。 | 不依赖浏览器 API,自己维护一个内存中的历史记录栈。 |
| 服务器配置 | 不需要 特殊配置。服务器始终认为访问的是根目录 (/) 或 index.html。 | 需要 服务器配置!对于所有路由(例如 /user/1),服务器都必须返回同一个 index.html 文件,否则刷新页面或直接访问会 404。 | 不需要 服务器配置 (因为它不跟 URL 互动)。 |
| SEO 友好度 | 相对较差。虽然现代搜索引擎能处理一些 hash,但普遍认为不如 history 模式。 | 好。URL 结构清晰,对搜索引擎友好。 | 不适用 / 无 SEO。地址栏不体现路由,搜索引擎无法抓取。 |
| 优点 | 部署简单,兼容性好 (甚至能在 file:// 协议下工作)。 | URL 美观、标准,SEO 友好。 | 可以在任何 JS 环境中使用(不限于浏览器),适合测试、SSR 等场景。 |
| 缺点 | URL 中带 # 不够美观,SEO 相对差。 | 需要后端配合配置 fallback 路由,否则会 404。 | 没有浏览器历史记录,用户无法通过前进/后退按钮或地址栏导航。 |
| 主要场景 | 简单项目、纯前端项目托管 (如 GitHub Pages)、无法配置服务器的环境。 | 绝大多数现代单页应用 (SPA),需要干净 URL 和 SEO 的场景。 | 非浏览器环境 (如 Node.js 服务端渲染 SSR)、嵌入式 WebView (若不想改动原生 URL)、测试环境。 |
| 简单说 | 带 #,部署省事但 URL 不好看。 | URL 好看,像普通网站,但服务器得配合一下。 | 只在代码里跑,地址栏看不见,适合特殊环境或测试。 |
总结:
- 想省事、对 URL 美观度要求不高、或者服务器没法配置,就用 Hash 模式。
- 追求用户体验、希望 URL 干净、重视 SEO,并且能配置服务器,那就用 History 模式(这是目前的主流选择)。
- 如果你的应用不在浏览器里跑,或者是在一些特殊的嵌入式环境,或者只是做测试,Memory 模式 就派上用场了。