一、从输入 URL 到页面渲染完整链路,请分阶段解释关键环节与可观测点。
一句话概括:
浏览器将用户输入的URL,通过网络请求获取资源,然后经过一系列解析、计算和绘制步骤,最终将页面内容呈现到屏幕上。
详细解析:
整个过程可以分为以下几个关键阶段:
1.导航阶段(Navigation)
1)用户输入与解析:用户在地址栏输入URL,浏览器会解析这个URL,判断是搜索内容还是一个合法的网址。
2)DNS解析:浏览器首先会查找各级缓存(浏览器缓存、操作系统缓存、路由器缓存、ISP缓存),看是否有该域名对应的IP地址。如果都没有,则会向根域名服务器发起地柜查询,最终获取到目标服务器的IP地址。
3)建立TCP连接:浏览器利用IP地址和端口号,与服务器进行“三次握手”,建立一个可靠的TCP连接。
4)TLS握手:如果是HTTPS协议,还需要在TCP连接之上进行TLS/SSL握手,协商加密密钥,建立安全的加密通道。
5)发送HTTP请求:浏览器构建一个HTTP请求报文(包含请求行、请求头、请求体),通过建立好的连接发送给服务器。
6)可观测点:浏览器开发者工具的NetWork面板可以清晰地看到这个阶段的耗时,包括DNS Lookup,Initial connection,SSL/TLS handshake以及Time to First Byte(TTFB)。TTFB是一个关键指标,它衡量了从请求发出到收到服务器第一个字节相应的时间。
2.响应与解析阶段(Response&Parsing)
1)服务器处理与响应:服务器接收到请求后,进行处理(查询数据库、执行业务逻辑等),然后返回一个HTTP响应报文(包含状态码、响应头、响应体)。响应体通常就是HTML文档。
2)解析HTML构建DOM树:浏览器接收到HTML后,渲染引擎会自上而下逐行解析,生成一个树状结构的DOM(Document Object Model)对象。
3)解析CSS构建CSSOM树:在解析过程中,如果遇到CSS链接或样式代码,会去加载并解析CSS,生成CSSOM(CSS Object Model)树。CSS的解析不会阻塞DOM的解析。
4)JavaScript的执行:如果遇到
5)可观测点:开发者工具的Performance面板可以录制加载过程,观察到Parse HTML,Parse Stylesheet等事件。
3.渲染阶段(Rendering)
1)构建渲染树(Render Tree):将DOM树和CSSOM树结合起来,生成渲染树。渲染树只包含需要显示在页面上的节点及其样式信息(例如display:none的节点不会出现在渲染树中)。
2)布局(Layout/Reflow):根据渲染树,计算出每个节点在屏幕上的精确位置和大小。这个过程也称为“回流”或“重排”。
3)绘制(Paint/Repaint):根据布局阶段计算出的信息,将每个节点绘制到屏幕上,包括文本、颜色、边框、阴影等。这个过程也称为“重绘”。
4)合成(Compositing):浏览器会将页面的不同部分(特别是涉及动画、transform等属性的元素)提升到独立的“层”中。当这些层发生变化时,浏览器只需重新绘制该层,然后将所有层合并(合成)到屏幕上,而无需对整个页面进行重排和重绘,极大地提高了性能。
5)可观测点:Performance面板中的Recalculate Style,Layout,Paint,Composite Layers事件详细记录了这一阶段的开销,频繁的Layout(回流)是前端性能优化的重点关注对象。
二、浏览器渲染流水线:构建 DOM/CSSOM、回流与重绘的触发与优化。
概括:
浏览器通过解析HTML和CSS构建DOM和CSSOM树,然后结合它们生成渲染树,接着通过回流(计算布局)和重绘(绘制像素)将页面呈现出来,而性能优化的关键在于最小化回流和重绘的次数与范围。
详细解析:
1.构建DOM/CSSOM:
1)DOM(文档对象模型):浏览器接收到HTML文档后,会将其解析成一个由节点构成的树形结构,即DOM。这个过程是逐行进行的,并且是“流式”的,意味着浏览器无需等待整个HTML,下载完毕就可以开始解析和构建DOM。
2)CSSOM(CSS对象模型):当解析器遇到CSS(无论是标签、标签还是内联样式)时,会开始解析CSS,并构建一个CSSOM树。与DOM不同,CSS的解析是阻塞渲染的,因为浏览器需要收集所有的样式规则来确定每个节点的最终样式。在CSSOM构建完成之前,浏览器不会进入下一步的渲染流程。
3)渲染树(Render Tree):DOM和CSSOM都构建完毕后,浏览器会将两者结合起来,生成渲染树。渲染树只包含需要被渲染的节点(例如,display:none的节点不会在渲染树中),以及它们的计算后样式。
2.回流(Reflow/Layout)
1)定义:当渲染树中部分或全部元素的尺寸、结构或位置发生改变时,浏览器需要重新计算元素的几何属性(位置和大小),这个过程称为回流。
2)触发条件:
1、页面首次渲染。
2、DOM节点的新增或删除。
3、元素尺寸或位置的改变(width,height,padding,margin,border,top,left等)。
4、浏览器窗口resize。
5、内容改变,如文本改变或图片大小改变,导致布局变化。
6、获取特定的布局信息,例如读取offsetTop,offsetLeft,offsetWidth,offsetHeight,scrollTop,scrollLeft,scrollWidth,scrollHeight,clientTop,clientWidth,getComputedStyle()等。为了返回最精确的值,浏览器必须立即执行一次回流操作,这被称为“强制同步布局”。
3.重绘(Repaint/Paint)
1)定义:当元素的视觉表现发生变化(如颜色、背景、可见性等),但其几何属性没有改变时,浏览器会重新绘制该元素,这个过程称为重绘。
2)关系:回流必须导致重绘,但重绘不一定需要回流。回流的成本远高于重绘,是性能优化的重点关注对象。
4.优化策略:
1)读写分离,避免强制同步布局:在修改样式的代码块中,避免穿插读取布局信息的代码。最佳实践是先集中读取所有需要的值,然后再一次性地集中修改样式。
// Bad: 读写穿插,触发多次回流
div.style.left = div.offsetLeft + 10 + 'px';
div.style.top = div.offsetTop + 10 + 'px';
// Good: 读写分离
const left = div.offsetLeft;
const top = div.offsetTop;
div.style.left = left + 10 + 'px';
div.style.top = top + 10 + 'px';
2)批量处理DOM操作:对于需要多次操作DOM的场景,可以先将元素display:none,操作完毕后再显示出来。或者使用DocumentFragment作为临时容器,在内存中完成所有DOM修改,最后一次性地追加到真实DOM中。
3)使用CSStransform和opacity实现动画:这两个属性的变化通常可以由GPU直接处理(通过合成层Compositing),从而绕开回流和重绘,实现更流畅的动画效果。这也是所谓的硬件加速。
4)避免使用table布局:table布局中,一个微小的改动就可能导致整个表格的回流,开销较大。应优先使用Flexbox或Grid布局。
5)将频繁回流的元素提升为独立图层:使用will-change或transform:translateZ(0)等hack手段,可以提示浏览器为该元素创建一个独立的合成层。这样,该元素的回流和重绘将被限制在该图层内部,不会影响到其他元素。但需注意,滥用图层会消耗大量内存。
三、前端性能指标(FCP/LCP/CLS/TTI/TBT),如何采集、解读与优化?
一句话概括:
这些核心Web指标分别从加载速度、交互性和视觉稳定性三个维度量化用户体验,我们可以通过PerformanceObserverAPI或Lighthouse等工具采集数据,并进行针对性优化。
详细解析:
FCP(First Contentful Paint)-首次内容绘制
解读:衡量第一个DOM内容(文本、图片等)出现在屏幕上的时间,是用户感知页面开始加载的标志。
优化:减少服务器响应时间(TTFB)、消除渲染阻塞的JS/CSS。
LCP(Largest Contentful Paint)-最大内容绘制
解读:衡量视口内最大内容元素(通常是图片或文本)渲染完成的时间,代表页面核心内容加载完毕。
优化:优化并加载LCP元素(如压缩图片)、使用服务端渲染(SSR)。
CLS(Cumulative Layout Shift)-累积布局偏移
解读:衡量页面加载过程中视觉元素的非预期移动程度,代表视觉稳定性。
优化:为图片、视频等元素设置固定尺寸或宽高比,为动态内容预留空间。
TTI(Time to Interactive)-可交互时间
解读:为衡量页面加载到完全可交互(能可靠响应用户操作)的时间。
优化:代码拆分(Code Splitting)、延迟加载非关键JS、拆分长任务。
TBT(Total Blocking Time)-总阻塞时间
解读:衡量FCP到TTI之间,主线程被长任务阻塞的总时间,反映页面加载期间的响应迟钝程度。
优化:减少主线程工作量、使用Web Worker、优化或异步加载第三方脚本。
四、防抖与节流的差异、实现与典型业务场景。
一句话概括:
防抖(Debounce)是“延迟执行”,在连续触发事件后,只在最后一次触发的N秒后执行一次;节流(Throttle)是“固定频率执行”,在连续触发事件时,保证每隔N秒最多执行一次。
详细解析:
我们可以把这两个概念想象成游戏里的技能:防抖就像“回城”,任何打断都会重新读条;节流就像“普通攻击”,有固定的攻击间隔(冷却时间)。
1)防抖(Debounce)-“回城”
核心逻辑:当一个事件被连续触发时,防抖函数会重置一个定时器,而不是立即执行。只有当事件停止触发,并且经过了指定的延迟时间后,回调函数才会被执行。
实现要点:
利用setTimeout创建一个定时器。
每次函数被调用时,先用clearTimeout清除上一个定时器。
然后设置一个新的定时器,延迟执行真正的逻辑。
使用apply或call保证this指向和参数传递正确。
// 防抖函数实现
function debounce(fn, delay) {
let timer = null; // 维护一个定时器
return function(...args) {
// 如果定时器已存在,则清除它
if (timer) {
clearTimeout(timer);
}
// 重新设置一个新的定时器
timer = setTimeout(() => {
fn.apply(this, args); // delay时间后执行真实函数
}, delay);
};
}
典型业务场景:
输入框搜索联想(Search Suggestion):用户在输入框中连续输入时,我们不希望每次按键都发送请求,而是等待用户停止输入的一小段时间后再发。
窗口大小调整(window.resize):避免在窗口调整过程中频繁地重新计算布局,只在调整结束后计算一次。
2)节流(Throttle)-“技能冷却”
核心逻辑:当一个事件被连续触发时,节流函数会确保在指定的时间间隔内,回调函数最多只被执行一次。
实现要点:
通常使用一个标志位(flag)来判断当前是否可以执行。
当函数被调用时,如果标志位为true,则执行逻辑,并将标志位置为false。
同时设置一个setTimeout,在延迟时间结束后,将标志位重新设为true,运行下一次执行。
// 节流函数实现
function throttle(fn, delay) {
let canRun = true; // 通过闭包保存一个标志位
return function(...args) {
if (!canRun) {
return; // 如果标志位为false,则直接返回
}
canRun = false; // 立即将标志位设为false
setTimeout(() => {
fn.apply(this, args);
canRun = true; // delay时间后,将标志位设为true
}, delay);
};
}
典型业务场景:
页面滚动事件(scroll):监听滚动位置,实现图片加载、”返回顶端“按钮的显隐等功能时,没必要对每一个像素的滚动都做出响应。
DOM拖拽(mousemove):在拖拽过程中,高频触发mousemove事件,节流可以保证移动动画的平滑性,避免过度计算。
高频点击:防止用户快速点击按钮,导致表单重复提交或其他意外行为。
五、图片懒加载的实现方式(IntersectionObserver/占位/预加载)与体验收益。
一句话概括:
图片懒加载是一种延迟加载页面可视区域外图片的技术,它通过减少首屏需加载的资源,来显著提升页面加载速度、节省用户流量并降低服务器压力。
详细解析:
图片懒加载的核心思想是:只加载用户当前看得到的图片。当页面加载时,我们不把标签的src属性设置为真实的图片地址,而是存放在一个自定义属性(如data-src)中。当图片滚动到可视区域时,再将data-src的值赋给src,触发图片加载。
1)实现方式:
现代方式:IntersectionObserverAPI
原理:这是浏览器原生提供的交叉观察器API,可以高效地、异步地监视一个元素(如图片)是否进入了另一个元素(如视口)的可见区域,而无需进行任何计算或监听滚动事件。
实现流程:
1、创建一个IntersectionObserver实例,并定义一个回调函数。
2、让这个观察器去”观察“所有需要懒加载的图片元素。
3、当图片进入视口时,回调函数将被触发。
4、在回调函数中,将图片的data-src属性值赋给src属性。
5、图片加载完成后,可以停止对该图片的观察,以节省资源。
优点:性能极好,因为它将所有计算都交给了浏览器高效处理,避免了scroll事件监听可能带来的性能抖动和计算延迟。这是当前实现懒加载的最佳实践。
传统方式:scroll事件监听 +getBoundingClientRect()
原理:通过监听scroll和resize事件,在事件处理函数中判断图片是否进入可视区域。
实现流程:
1、在处理函数中,遍历所有待加载的图片。
2、使用element.getBoundingClientRect().top获取图片顶部相对于视口的位置。
3、如果top值小于视口高度(window.innerHeight),则认为图片可见,进行加载。
缺点:高频触发scroll事件会导致大量计算,容易引发性能问题,因此必须配合节流(throttle)函数调用。
2)体验优化策略
这些策略通常与上述实现方式结合使用,以提供更流畅的体验。
使用占位符(Placeholder)
目的:在真实图片加载完成前,先用一个低质量的占位符(如纯色背景、SVG图标、骨架屏或极小的模糊缩略图)占据图片位置。
收益:
1、防止布局偏移(CLS):提前占据了空间,避免图片加载后页面内容”跳动“,提升了视觉稳定性。
2、提供加载反馈:让用户知道这里即将显示一张图片,体验更友好。
预加载(Preloading)
目的:为了让用户在滚动时感觉不到加载延迟,可以提前加载视口下方”即将进入“的图片。
实现:在使用IntersectionObserver时,可以通过设置rootMargin属性来扩大”视口“的判定范围。例如rootMargin:‘200px’会让距离视口底部200px的图片也视为”可见“,从而提前触发加载。
收益:滚动体验更平滑,图片仿佛在进入视口时就已经加载完毕。
3)核心体验收益总结:
1、提升页面加载速度:显著减少了首屏加载所需的资源数量和大小,使页面内容更快地呈现给用户,直接优化FCP和LCP指标。
2、节省用户流量:只加载用户浏览到的图片,对于未滚动到底部的用户,可以节省大量不必要的网络流量,在移动端尤其重要。
3、降低服务器压力:减少了并发请求数,降低了服务器的负载和带宽成本。
六、讲一下Web 安全:XSS/CSRF 的攻击面与前端侧防护。
一句话概括:
XSS(跨站脚本攻击)是攻击者将恶意脚本注入到你的网站,在用户浏览器上执行;CSRF(跨站请求伪造)是引诱已登录的用户,在他们不知情的情况下,以他们的名义向你的网站发送恶意请求。
详细解析:
1.XSS(Cross-Site Scripting)-跨站脚本攻击
攻击面:核心是”代码注入“。攻击者利用你的网站漏洞,将恶意JavaScript代码植入到页面中。当其他用户访问这个页面时,这段恶意代码就会在他们的浏览器中执行。
存储型XSS:恶意代码被存储在服务器数据库中(如文章评论、用户昵称)。所有访问该数据的用户都会受到攻击,危害最大。
反射型XSS:恶意代码存在于URL参数中,用户需要点击一个特制的链接才会触发。常用于钓鱼攻击。
DOM型XSS:通过恶意修改页面的DOM结构实现,整个过程可能不与服务器交互,是纯前端的安全漏洞。
前端侧防护:核心原则是”不信任任何用户输入“。
1)输入转义(Escaping):对所有展示在页面上的用户内容(如
2)设置HttpOnlyCookie:通过在Set-Cookie响应头中加入HttpOnly标志,可以禁止JavaScript读取Cookie,能有效防止XSS攻击者窃取用户的会话Cookie。
3)内容安全策略(CSP):通过设置HTTP响应头Content-Security-Policy,可以定义一个可信的脚本来源白名单。浏览器将只执行来自这些来源的脚本,从而阻止恶意注入的内敛脚本或外部脚本执行。
2.CSRF(Cross-Site Request Forgery)-跨站请求伪造
攻击面:核心是”身份伪造“。攻击者在自己的恶意网站(如evil.com)上,设置一个指向你网站(如bank.com)的请求。当一个已经登录bank.com的用户访问了evil.com,浏览器会自动携带bank.com的Cookie发起这个请求(例如转账),而bank.com的服务器会误以为这是用户的真实操作。
整个攻击过程,用户是无感知的,攻击者也拿不到Cookie,只是”借用“了用户的登录状态。
前端侧防护:核心原则是”验证请求来源“。
1)使用SameSiteCookie属性:在设置Cookie时,将SameSite属性设置为Strict或Lax(目前是许多浏览器的默认值)。这会禁止浏览器在跨站请求中携带Cookie,从而从根源上阻断CSRF攻击。这是目前最有效、最简单的防御方式。
2)使用Anti-CSRF Token:
1、原理:在用户访问页面时,服务器生成一个随机的、不可预测的Token,并将其返回给前端。
2、实践:前端在发起所有状态变更的请求时(如POST,PUT,DELETE),必须在请求体或请求头中带上这个Token。
3、验证:服务器在接收到请求后,会验证这个Token是否与session中保存的一致。由于攻击者的网站无法获取到这个Token,所以伪造的请求会因验证失败而被拒绝。
3)验证Referer/OriginHeader:服务器可以检查HTTP请求头中的Referer或Origin字段,判断请求是否来自合法的源。这是一种辅助手段,因为这些头部信息可能被用户浏览器设置或代理所移除。
七、讲一下跨域的成因与方案:CORS、代理、JSONP、同源策略细节。
一句话概括:
跨域问题的根本原因是浏览器的同源策略(Same-Origin Policy),这是一种安全机制,它限制了从一个源加载的文档或脚本如何与来自另一个源的资源进行交互。主流解决方案是通过CORS让服务器授权、通过代理在服务端绕过限制,或是使用JSONP等历史技巧。
详细解析:
1.成因:同源策略(Same-Origin Policy)
什么是“同源”?
当两个URL的协议(protocol)、域名(domain)和端口(port)完全相同时,它们才属于“同源”。例如example.com:80和https://example.co…
为什么需要这个策略?
同源策略是浏览器最核心、最基本的安全功能。如果没有它,你在浏览器标签页A登录了网上银行,恶意网站标签页B就可以轻易地通过脚本发送请求到网上银行的接口,并读取你的个人信息,造成严重的安全风险。
它限制了什么?
它主要限制了三方面的行为:
1.AJAX请求:无法通过XMLHttpRequest或FetchAPI发送跨域请求并读取响应。这是最常见到的跨域问题。
2.DOM访问:无法通过脚本操作不同源的DOM(例如,一个页面无法操作另一个源的iframe内部DOM)。
3.Cookie,LocalStorage,IndexedDB:无法读取或写入其他源的数据。
2.解决方案:
方案一:CORS(Cross-Origin Resource Sharing)-跨域资源共享
原理:这是W3C标准,也是目前解决跨域问题的主流和推荐方案。它允许服务器在HTTP响应头中添加一些字段,来声明哪些外部源有权限访问其资源。
工作流程:
1.浏览器在发起跨域AJAX请求时,会自动在请求头中添加一个Origin字段,表明请求的来源。
2.服务器收到请求后,会根据自身的跨域配置,在响应头中返回一个Access-Control-Allow-Origin字段。
3.服务器接收到响应后,会检查Access-Control-Allow-Origin的值是否包含了当前的Origin,如果包含了,就允许JavaScript读取响应;否则,浏览器会拦截响应,并在控制台报错。
预检请求(Preflight Request):对于一些“非简单请求”(例如PUT/DELETE方法,或Content-Type为application/json的请求),浏览器会先发送一个OPTIONS方法的“预检”请求,询问服务器是否允许接下来的实际请求。服务器同意后,才会发送真实的请求。
方案二:代理(Proxy)
原理:利用服务器之间通信不受同源策略限制的特点。前端不直接请求目标API,而是请求自己的同源服务器,再由这个服务器作为“代理”,去请求真正的目标API,最后将数据返回给前端。
应用场景:
开发环境:像webpack-dev-server或Vite都内置了代理功能,可以方便地将API请求转发到后端服务器,解决开发时的跨域问题。
生产环境:可以通过Nginx反向代理,或在自己的后端服务(Node.js,Java等)中封装一个接口来实现。这种方式还可以用于隐藏真实API地址、添加认证等。
方案三:JSONP(JSON with Padding)
原理:一个“古老”的hack方法。它利用了、和
工作流程:
1.前端动态创建一个
2.将其src指向目标API地址,并通过URL参数传递一个全局回调函数的名字(例如?callback=handleResponse)。
3.服务器收到请求后,会将要返回的JSON数据包裹在这个回调函数调用中,例如handleResponse({"data":...}),并作为JavaScript脚本返回。
4.浏览器执行这个脚本,从而调用了前端预先定义好的handleResponse函数,数据就通过参数传递过来了。
缺点:
只支持GET请求:因为
安全性差:容易遭受XSS攻击,因为你请求并执行的是一段来自外部的脚本。
目前已基本被CORS取代,仅在需要兼容非常古老的系统时才可能用得到。
八、HTTP 缓存:强缓存/协商缓存、Cache-Control/ETag 的策略设计。
一句话概括:
HTTP缓存通过强缓存(直接使用本地副本,不发请求)和协商缓存(发请求询问服务器本地副本是否仍有效)两种机制,来减少不必要的网络传输,提升加载速度;其中Cache-Control主要控制强缓存策略,而ETag是协商缓存的核心。
详细解析:
浏览器缓存机制的决策流程是:先判断强缓存,如果失败,再发起协商缓存。
1.强缓存(Strong Cache)-”别来烦我“
核心:在指定的时间内,浏览器如果再次请求同一资源,会直接从本地缓存中读取,不会向服务器发送任何请求。状态码通常是200OK(from memory/disk cache)。
实现方式(HTTP响应头):
Cache-Control(HTTP/1.1,优先级更高):
max-age=:设置一个相对时间,告诉浏览器在接下来多少秒内,资源都是有效的。这是最常用的指令。
public:资源可以被任何缓存(如CDN、代理服务器)缓存。
private:资源只能被用户的私有缓存(即浏览器)缓存。
no-cache:跳过强缓存,强制进入协商缓存阶段。名字有误导性,它不是”不缓存“,而是”每次都得问问服务器“。
no-store:禁止任何形式的缓存,资源每次都会被完整地请求和下载。
Expires(HTTP/1.0):设置一个绝对的过期时间点。由于它依赖客户端的本地时间,可能因时间不准而出错,现已基本被Cache-Control取代。
2.协商缓存(Negotiation Cache)-”我这有个旧的,还能用吗?“
核心:当强缓存失效后,浏览器会向服务器发送一个请求,但这个请求会携带一些缓存标识。服务器根据这些标识来判断浏览器本地的缓存是否仍然有效。
如果有效,服务器返回304Not Modified状态码,响应体为空。浏览器继续使用本地缓存。
如果无效,服务器返回200OK状态码,并携带新的资源。
实现方式(两队Request/Response头):
ETag/if-None-Match(推荐使用)
1.首次响应:服务器在响应头中提供一个ETag(Entity Tag)字段,它是根据当前资源内容生成的一个唯一标识符(类似文件内容的哈希值)。
2.后续请求:浏览器在请求头的if-None-Match字段中带上这个ETag值。
3.服务器验证:服务器比较收到的ETag和当前资源的ETag。如果一致,说明资源未改变,返回304;如果不一致,返回新资源和新的ETag。
优点:比Last-Modified更精确,能感知到文件内容级别的变化。
Last-Modified/if-Modified-Since
1.首次响应:服务器在响应头中提供一个Last-Modified字段,表示资源的最后修改时间。
2.后续请求:浏览器在请求头的if-Modified-Since字段中带上这个时间。
3.服务器验证:服务器比较这个时间和资源的实际最后修改时间。如果时间一致,返回304
缺点:时间戳的精度只能到秒,无法感知到一秒内的多次修改;有时文件内容没变,但修改时间变了,也会导致缓存失效。
3.策略设计
一个好的缓存策略能极大提升用户体验和服务器性能。
不常变动的资源(JS/CSS/图片):
通常在文件名加入内容哈希值(如bundle.[contenthash].js)。
策略:开启长期缓存,Cache-Control:max-age=31536000,immutable。
当文件内容变更时,哈希值改变,URL也就变了,浏览器会将其视为一个全新的资源发起请求。immutable告诉浏览器这个资源绝对不会变,连协商缓存都无需发起。
经常变动的资源(HTML文件):
index.html是应用的入口,需要及时更新以引用新的JS/CSS资源。
策略:使用协商缓存。Cache-Control:no-cache。
这样可以确保用户每次都能获取到最新的HTML文件,同时如果HTML内容没有变化(例如只是重新部署),服务器会返回304,避免了不必要的下载。
敏感数据或实时性要求极高的API请求:
策略:禁用缓存。Cache-Control:no-store。
确保数据永远是最新的,且不会在本地或中间代理留下任何副本。
九、HTTP 缓存:强缓存/协商缓存、Cache-Control/ETag 的策略设计。
一句话概括:
Cookie会随HTTP请求发送到服务器且容量小,主要用于身份认证;localStorage用于跨会话的持久化本地存储;而sessionStorage的数据则仅存在于当前标签页的会话期间,关闭即清。
详细解析:
这三者都是浏览器提供的客户端存储方案,但它们在生命周期、存储容量和与服务器的交互方式上有着本质的区别。
特性
Cookie
localStorage
sessionStorage
生命周期
可设置过期时间,否则随浏览器关闭而失效
永久性,除非用户手动清除
会话级,标签页关闭后即销毁
存储大小
约 4KB,非常小
约 5-10MB,较大
约 5-10MB,较大
与服务器通信
自动发送,每次 HTTP 请求都会携带
从不发送,纯客户端存储
从不发送,纯客户端存储
作用域
同源,且在所有标签页共享
同源,且在所有标签页共享
仅限当前标签页,不同标签页不共享
API 易用性
需手动封装,原生 API 不友好
友好,setItem,getItem,removeItem
与 localStorage API 相同
使用边界与典型场景
Cookie:”会话身份证“
核心特征:它最大的特点是会自动附加在同源的HTTP请求头中,发送给服务器。这是服务器能够识别用户身份的关键。
使用边界:
1.会话管理(Session Management):保存用户的登录状态。服务器通过验证Cookie来确认用户身份。
2.需要与服务器共享的数据:例如用户偏好设置(主题、语音),服务器可以根据Cookie直接渲染出用户喜欢的页面。
注意事项:由于容量小且会增加网络传输开销,不应该用Cookie存储大量业务数据。应通过设置HttpOnly防止脚本窃取。设置SameSite防御CSRF攻击。
localStorage:”本地永久仓库“
核心特征:数据会一直存在,除非被用户手动清除或代码主动删除。它在同源的所有标签页之间是共享的。
使用边界:
1.持久化用户设置:如”记住我“功能、网站的主题偏好等不需要让服务器知道的配置。
2.缓存应用数据:存储一些不常变动但需要频繁使用的数据(如用户信息、字典数据),以减少网络请求。
3.草稿箱:在富文本编辑器中自动保存用户输入的内容,防止意外关闭导致数据丢失。
sessionStorage:”临时记事本“
核心特征:生命周期与标签页绑定,关闭标签页后数据自动清除。最重要的是,它的数据在不同标签页之间是隔离的。
使用边界:
1.单次会话的临时数据:例如,在多步骤的表单中,用户填写前几步的数据可以暂存在sessionStorage中,刷新页面不会丢失,但关闭页面后就没了。
2.防止数据在多窗口间”串扰“:如果你打开了两个购物网站的标签页,想在每个页面独立进行结算操作,用sessionStorage存储各自的订单信息就非常合适,它们不会互相影响。
十、讲一下axios/fetch 的差异、二次封装规范与错误重试策略。
一句话概括:
fetch是浏览器原生的底层API,功能基础,使用时需手动处理很多细节;axios是一个功能更丰富、封装更完善的第三方库。对它们进行二次封装,主要是为了统一配置、实现请求/响应拦截和设计如错误重Test等通用策略,从而提升代码的健壮性和可维护性。
详细解析:
1.axios vs fetch的核心差异
特性
fetch (原生 API)
axios (第三方库)
API 易用性
底层 API,相对繁琐。请求体需手动JSON.stringify,响应需调用.json()解析。
上层封装,开箱即用。自动进行 JSON 的转换。
错误处理
只有当网络失败时才会 reject。对于404/500等 HTTP 错误状态,它依然会 resolve,需要通过response.ok或response.status手动判断。
自动 reject 任何4xx或5xx的错误状态码,更符合直觉。
拦截器
原生不支持,需要自己封装实现。
内置请求和响应拦截器,非常适合进行全局的 token 注入、加载动画、错误处理等。
请求取消
通过AbortController实现,需要额外代码。
内置CancelToken机制(新版已支持AbortController),使用更方便。
浏览器兼容性
现代浏览器原生支持,IE 等旧浏览器需要 Polyfill。
基于XMLHttpRequest,兼容性更好。
其他特性
功能基础。
内置客户端 XSRF 防护、请求超时设置、上传进度监控等高级功能。
小结:在复杂的项目中,axios通常是更优选择,因为它提供了更多企业级功能,减少了大量重复的模板代码。
2.二次封装规范
二次封装的目标是提供一个统一、可配置、易于维护的请求模块。通常我们会基于axios进行封装。
封装要点:
1.创建实例:使用axios.create()创建一个独立的实例,避免全局配置污染。
2.统一配置:在实例中配置baseURL、timeout(请求超时)和通用headers(如Content-Type)。
3.请求拦截器(Request Interceptor):
1.注入Token:从localStorage或Vuex/Pinia等状态管理库中读取Token,并统一添加到Authroization请求头中。
2.添加Loading:在请求开始时显示全局加载动画。
4.响应拦截器(Response Interceptor)
1.数据解构:通常后端返回的数据会包裹一层,如{code:0,data:{...},message:‘success’}。拦截器可以判断code,如果成功,直接返回data部分,让业务代码更纯粹。
2.全局错误处理:
1.如果code不为0,则根据message弹出全局错误提示。
2.处理HTTP状态码,如401(未授权)则清空token并跳转到登录页,403(无权限)提示用户,500(服务器错误)给出统一的友好提示。
3.关闭Loading:在请求结束(无论成功或失败)时关闭加载动画。
// request.js - 一个典型的 axios 封装示例
import axios from 'axios';
const service = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL, // 环境变量
timeout: 5000,
});
// 请求拦截器
service.interceptors.request.use(
config => {
const token = localStorage.getItem('token');
if (token) {
config.headers['Authorization'] = `Bearer ${token}`;
}
// showLoading();
return config;
},
error => Promise.reject(error)
);
// 响应拦截器
service.interceptors.response.use(
response => {
// hideLoading();
const res = response.data;
if (res.code !== 0) {
// 全局错误提示
console.error(res.message || 'Error');
return Promise.reject(new Error(res.message || 'Error'));
}
return res.data; // 直接返回数据
},
error => {
// hideLoading();
if (error.response?.status === 401) {
// 处理 token 失效
// redirectToLogin();
}
// 全局错误提示
console.error('Request Error:', error.message);
return Promise.reject(error);
}
);
export default service;
3.错误重试策略
目标:当发生瞬时网络错误或服务器临时不可用(如503)时,自动重新发起请求,提升应用的健壮性。
实现思路:
在响应拦截器的错误处理部分实现:
1.判断错误类型:只对特定的错误进行重试,如网络超时(ECONNABORTED)、网络中断,或502/503/504等服务器临时性故障。不应重试业务错误(code!=0)或客户端错误(4xx)。
2.设置重试次数和间隔:
1.在请求的config对象中附加一个重试计数器config_retryCount。
2.每次重试前,检查是否超过最大重试次数。
3.为了避免短时间内大量请求冲击服务器,最好设置一个重试延迟,可以使用指数退避(Exponential Backoff)策略(如第一次延迟1s,第二次2s,第三次4s)。
3.重新发起请求:通过setTimeout延迟后,再次调用axios(config)来重新执行原始请求。
十一、前端异常监控与埋点体系:采集、上报、回放与灰度。
一句话概括:
前端监控体系通过采集用户行为(埋点)和程序异常(监控),利用sendBeacon等方式上报数据,结合会话回放技术还原用户场景以精准定位问题,并最终服务于灰度发布等发布策略,以数据驱动的方式来保障线上应用的质量和稳定性。
详细解析:
1.采集(Data Collection)
采集是整个体系的基础,主要分为两类数据:
异常监控(Error Monitoring):被动地捕获程序运行中的非预期错误。
JS运行时错误:通过window.onerror和window.addEventListener('unhandledrejection')捕获全局的同步/异步脚本错误。
资源加载错误:通过window.addEventListen('error',true)在捕获阶段监听,
API请求错误:在封装的请求库(如axios)的响应拦截器中,捕获4xx/5xx状态码或网络异常。
白屏/卡顿:通过定时检测DOM关键节点是否存在,或使用PerformanceObserver监听长任务(Long Task)来判断页面是否无响应。
框架内部错误:利用React的ErrorBoundary或Vue的errorHandler来捕获组件渲染层面的错误。
行为埋点(Behavior Tracking):主动记录用户与应用的关键交互,以分析用户流失、转化率等。
页面浏览(PV):监听路由变化事件(hashchange,popstate或路由库的钩子)来记录页面访问。
用户点击(Click):通过事件代理在根节点监听点击事件,并根据data-*属性或元素路径来识别被点击的业务模块。
曝光(Exposure):使用IntersectionObserver来高效地判断某个模块/广告是否进入了用户可视区域。
自定义事件:在关键业务流程中(如“加入购物车”、“支付成功”)手动调用上报函数。
2.上报(Data Reporting)
采集到的数据需要可靠且无感地发送回服务器。
上报方式:
navigator.sendBeacon():首选方式。浏览器提供的标准API,用于在页面卸载(关闭、刷新)前发送数据。它能保证请求被发出,且是异步的,不阻塞页面关闭。
Image Beacon(GIF):创建一个1x1的Image对象,将其src指向带有一系列参数的上报接口地址。这是兼容性最好的传统方案。
AJAX:不推荐在页面卸载时使用,因为请求可能在页面关闭前被中断。
上报策略:
合并上报(Batching):将短时间内采集到的多条数据暂存在一个队列中,当达到一定数量或时间间隔时,合并成一个请求发送,以减少请求频率。
采样(Sampling):对于PV、点击等高频事件,可以设置一个采样率(如10%),只上报一部分用户的数据,以降低服务器和数据处理的压力。
3.回放(Session Replay)
回放是解决“用户说有问题,但我无法复现”的终极武器。
原理:通过引入一个记录库(如开源的rrweb),它可以将页面上所有的DOM变动、鼠标移动、点击、移动、输入等行为,序列化成一个可存储的JSON数据流并上报。
价值:当监控系统捕获到一个异常时,可以关联到该用户此次会话的回放记录。开发人员可以像看视频一样,完整地、像素级地还原出Bug发生前用户的每一步操作和页面状态,极大地提升了Debug效率。
4.灰度(Grayscale Release & Feature Flagging)
灰度发布是监控体系价值最大化的体现。
定义:一种平滑过渡的发布方式,新版本或新功能先发布给一小部分特定用户(如内部员工、VIP用户、或按用户ID百分比划分的群体),运行一段时间,观察数据。
与监控体系的结合:
1.数据打标:所有上报的异常和埋点数据,都必须携带版本号或功能开关(Feature Flag)的标识。
2.数据对比:在发布新版本后,可以实时对比新旧版本在错误率、性能指标(LCP/CLS)、业务指标(转化率)等方面的差异。
3.决策与回滚:
1.如果新版本的错误率飙升或关键业务指标下跌,可以立即关闭功能开关或回滚发布,将影响范围控制在最小。
2.如果数据表现平稳或优于旧版,则可以逐步扩大灰度范围,直到全量发布。
十二、前端异常监控与埋点体系:采集、上报、回放与灰度。
一句话概括:
REST是一种基于URL和HTTP方法的、面向资源的架构风格,返回固定的数据解构;而GraphQL是一种API查询语音,通过单个端点,运行客户端精确地声明其所需数据,从而解决了REST中常见的Over-fetching(数据过载)和Under-fetching(数据不足)问题。
详细解析:
我们可以用一个比喻来理解:REST像一家自动售货机,每个按钮(URL)对应一个固定的商品(资源);而GraphQL则像一个自助餐厅,你可以按需取用,只拿自己想吃的菜。
1.核心差异对比
特性
RESTful API
GraphQL
数据获取
被动、固定:由后端定义每个端点返回的数据结构。
主动、灵活:由前端通过查询语句精确定义需要哪些字段。
端点 (Endpoint)
多个端点:每个资源通常对应一个或多个 URL(如/users,/users/1,/users/1/posts)。
单个端点:通常只有一个/graphql端点,所有请求都发往此处。
数据冗余
常见:容易出现 Over-fetching(返回了不需要的字段)或 Under-fetching(需要请求多个端点才能凑齐数据)。
几乎没有:精确获取所需数据,不多也不少。
类型系统
弱类型:依赖 OpenAPI/Swagger 等文档来约定数据类型。
强类型:通过 Schema Definition Language (SDL) 定义,API 自带文档,类型安全。
版本管理
常见:通过 URL 路径(如/v2/api)或请求头进行版本控制,维护成本高。
无版本概念:通过向 Schema 中添加新字段、并标记旧字段为deprecated来实现平滑演进。
错误处理
依赖 HTTP 状态码(如404,500)来表示请求的成功或失败。
即使部分字段查询失败,通常也返回 HTTP 200。具体的错误信息放在响应体的errors字段中。
2.前端为什么/何时选择GraphQL?
选择GraphQL并发要完全取代REST,而是在特定场景下,它能为前端开发带来巨大的便利和性能优势。
1.当UI复杂,数据来源多样时
场景:一个现代前端页面(例如仪表盘)可能需要同时展示用户信息、他的最新文章列表、以及收到的通知。
REST痛点:这可能需要前端并行或串行地发起3个不同的API请求(/api/user,/api/posts,/api/notifications),即Under-fetching。前端需要自己管理这多个请求的加载和错误状态,非常繁琐。
GraphQL优势:前端可以用一个查询,一次性获取到所有需要的数据,极大地简化了状态管理和数据聚合的逻辑。
2.当网络带宽受限,需要极致性能时(尤其是移动端)
场景:一个文章列表页,在手机上可能只需要显示标题和作者,而在PC端则需要显示标题、作者、摘要和发布日期。
REST痛点:后端通常只会提供一个返回所有字段的“大而全”的接口。在移动端,摘要和日期等字段就成了不必要的流量浪费,即Over-fetching。
GraphQL优势:移动端和PC端可以根据自己的UI需求,编写不同的查询语句,从同一个API获取不多不少、刚刚好的数据,有效节省了用户的流量和电量。
3.当需要和前后端团队解锁,加速迭代时
场景:前端需要在一个组件上新增显示一个字段,这个字段后端已经有了,只是没在接口里返回。
REST痛点:前端需要向后端提需求,等待后端修改接口、测试、发布后,前端才能使用。
GraphQL优势:只要这个字段存在与GraphQL的Schema中,前端工程师就可以直接修改查询语句来获取它,无需后端做任何改动。这赋予了前端更大的灵活性和自主性,大大提升了开发效率。
4.当需要服务于多个不同形态的客户端时
场景:你的产品同时有Web应用、iOS应用、Android应用和一个智能手表应用。
REST痛点:每个客户端的数据需求都千差万别,可能需要后端维护多套不同的API,或者一个极其复杂的带有很多查询参数的API。
GraphQL优势:所有客户端可以共享同一个GraphQL API,并根据自身的UI需求,量身定制自己的数据查询。
总结:如果你的应用只是简单的CRUD(增删改查),或者后端接口非常稳定简单,REST依然是一个轻量且优秀的选择。但如果你的应用UI复杂、客户端多样、追求高性能且需要快速迭代,那么GraphQL带来的开发体验和性能提升将是革命性的。
十三、WebSocket、SSE、轮询的选型与重连/心跳设计。
一句话概括:
这三者是解决客户端与服务器实时通信问题的不同方案:轮询是客户端反复“拉”数据的原始方式;SSE是服务器向客户端单向“推”数据的标准模式;而WebSocket则是提供了真正意义上的双向实时“对话”通信。
详细解析:
1.技术对比与选型
特性
短轮询 (Short Polling)
SSE (Server-Sent Events)
WebSocket
通信方向
客户端 → 服务器 (请求/响应)
单向:服务器 → 客户端
双向:客户端 ↔ 服务器
实现原理
客户端定时发起 HTTP 请求
客户端发起一次 HTTP 请求,服务器保持连接,持续推送数据
通过 HTTP/1.1 Upgrade 请求,建立一个独立的 TCP 连接
协议
HTTP/HTTPS
HTTP/HTTPS
WS/WSS (自定义协议)
开销
极大,每次请求都有完整的 HTTP 头
较小,只需一次 HTTP 连接头开销
极小,握手后数据帧很小
延迟
高,取决于轮询间隔
低
极低,接近 TCP 延迟
原生支持
所有浏览器
除 IE/Edge (Legacy) 外所有现代浏览器
所有现代浏览器
选型指南
何时选择轮询(Polling)?
场景:当实时性要求不高,数据更新频率很低(如没几分钟更新一次),或者需要兼容极其古老的浏览器时。
例子:一个后台管理系统的订单状态通知,轮询检查新订单状态。
注意:现代应用中应尽量避免使用轮询,尤其是短轮询,因为它效率低下且浪费资源。长轮询(Long Polling)是一个稍好的变体,它在没有数据时会挂起连接,但本质上仍是请求-响应模式。
何时选择SSE(Server-Sent Events)?
场景:当你的业务场景是典型的“从服务器到客户端”的单向数据流时。这是它的最佳应用领域。
例子:新闻推送、股票行情更新、体育比赛实时比分、系统状态通知(如CI/CD构建进度)。
优势:
1.实现简单:基于标准HTTP,后端实现相对容易,前端有原生的EventSourceAPI。
2.自带断线重连:EventSourceAPI内部实现了自动重连机制,非常省心。
3.事件流:可以发送带有事件类型的消息,方便前端分类处理。
何时选择WebSocket?
场景:当你需要真正意义上的双向、低延迟通信时。
例子:
1.即时通讯(IM):在线聊天室、私信。
2.协同编辑:Google Docs、Miro等允许多人同时编辑一个文档。
3.在线多人游戏:实时同步玩家的位置和状态。
4.需要客户端频繁向服务器发送数据的物联网应用。
2.重连与心跳设计(主要针对WebSocket)
对于需要保持长连接的WebSocket(以及需要自定义重连逻辑的SSE),健壮性设计至关重要。
心跳机制(Heartbeat)
目的:
1.防止连接因空闲被断开:很多网络设备(如NAT网关、防火墙)会自动关闭长时间没有数据传输的TCP连接。心跳包可以模拟“流量”,保持连接活跃。
2.快速检测“假死”连接:有时网络中断,但客户端和服务器的TCP层并未立即感知到。通过心跳可以更快地判断地方是否在线。
实现:
1.客户端启动一个定时器(如每30秒),向服务器发送一个预定义的心跳信息(如{“type“:”ping”})。
2.服务器收到ping后,立即回复一个pong消息。
3.客户端如果在“发送ping+一个超时时间(如5秒)”后仍未收到pong,就可以判断连接已断开,然后触发重连逻辑。
断线重连(Reconnection)
目的:在连接意外断开(网络波动、服务器重启)后,能够自动恢复连接,对用户尽可能透明。
实现:
1.监听WebSocket实例的onclose和onerror事件。
2.在事件回顾中,执行重连。
3.核心:避免立即、无限次地重连。这会形成“死亡循环”,对客户端和服务器造成巨大压力。
4.采用“指数退避+随机抖动”策略:
1.指数退避(Exponential Backoff):设置一个初始重连延迟(如1s),如果失败,下一次延迟翻倍(2s,4s,8s...),并设置一个最大延迟上限(如60s)。
2.随机抖动(Jitter):在计算出的延迟上增加一个小的随机值,防止所有掉线的客户端在同一时刻“风暴般”地重连服务器。
5.加锁:设置一个重连锁,防止在一次重连正在进行时,又因为其他错误触发了新的重连。
十四、讲一下资源加载优化:按需、分包、预获取与关键路阻断排查。
一句话概括:
资源加载优化的核心是加速首次有效渲染。我们通过排查并缩短关键渲染路径(CRP)来打好基础,利用代码分包和按需加载来减小初始载荷,再通过预获取技术智能加载未来资源,最终实现极致的加载性能和用户体验。
详细解析:
1.关键渲染路径(Critical Rendering Path)阻断排查
是什么?
关键渲染路径是指浏览器从接收HTML、CSS、JavaScript到将其渲染成像素展示在屏幕上所经历的一系列步骤。默认情况下,CSS和同步的JavaScript都是渲染阻塞(Render-Blocking)资源,浏览器必须等待它们下载、解析和执行完毕后,才能绘制页面。
如何排查?
使用浏览器开发者工具的Performance和Network面板。
Network面板:观察请求瀑布流,寻找那些耗时很长、且阻塞了后续资源下载或页面渲染(DOMContentLoaded事件)的JS/CSS文件。
Performance面板:录制加载过程,查看Main主线程是否有长时间的脚本执行(黄色块)或样式计算(紫色块)任务,这些都是阻塞的信号。
如何优化?
JavaScript阻塞:
将前。
使用defer属性:脚本会并行下载,并在HTML解析完毕后、DOMContentLoaded事件前按顺序执行。(推荐)
使用async属性:脚本会并行下载,并在下载完成后立即执行,可能会阻塞HTML解析。适用于无依赖的独立脚本(如统计脚本)。
CSS阻塞:
内联关键CSS:将渲染首屏内容所必需的CSS(即关键CSS)直接内联到的标签中,让首屏能无阻塞地快速渲染。
异步加载非关键CSS:使用等技术来异步加载剩余的CSS。
2.代码分包(Code Splitting)
是什么?
将一个巨大的JavaScript单体文件(bundle)拆分成多个更小的、按需加载的块(chunk)。这是现代前端框架和构建工具(Webpack/Vite)的核心功能。
策略:
按路由分包:最常用和有效的策略。为每个页面/路由创建一个独立的JS文件,用户访问某个页面时,才加载对应的代码。
按组件分包:对于一些不常使用但体积较大的组件(如复杂的弹窗、图表库),可以将其拆分,仅在需要渲染时才通过动态import()加载。
公共库分包:将多个页面都用到的公共库(如React,Lodash)打包成一个单独的vendor文件,以便利用浏览器缓存。
3.按需加载(Lazy Loading)
是什么?
代码分包是“准备工作”,按需加载是“执行动作”。它是一种延迟加载非视口内或非关键资源的技术。
应用:
图片和iframe:使用IntersectionObserverAPI,当元素进入视口时才加载。现代浏览器已原生支持。
JS模块/组件:结合代码分包,使用动态import()语法在特定交互(如点击按钮)或条件下加载代码。
4.预获取(Preloading & Prefetching)
是什么?
这是一种“智能”加载技术,它基于我们对用户行为的预测,提前加载用户可能很快就会需要的资源。
技术对比:
:
用途:用于加载当前页面肯定会用到、但浏览器发现较晚的关键资源。
例子:深层CSS中定义的LCP图片或字体文件。
行为:告诉浏览器以高优先级下载该资源,但不执行它。当浏览器真正需要时,资源已在缓存中,可立即使用。
:
用途:用于加载用户在未来导航中可能会用到的资源。
例子:预加载用户鼠标悬停的链接所指向的页面的JS/CSS。
行为:告诉浏览器在空闲时以低优先级下载该资源,并放入缓存。
总结:这四种策略相辅相成。首先通过排查关键渲染路径来解决最紧迫的阻塞问题;然后通过分包和按需加载大幅削减初始负载体积;最后,利用预获取优化后续的用户交互和导航体验,共同打造一个流畅、快速的Web应用。
十五、讲一下资源加载优化:按需、分包、预获取与关键路阻断排查。
一句话概括:
async和defer都能让
详细解析:
为了更好地理解,我们首先需要知道浏览器遇到普通
默认行为(无async/defer):
1.浏览器开始解析HTML。
2.遇到
3.开始下载脚本。
4.下载完成后,立即执行脚本。
5.脚本执行完毕后,才恢复HTML解析。
结论:下载和执行都会阻塞页面渲染,用户会感到明显的卡顿。
现在我们来对比defer和async的不同之处。
1.defer-“延迟执行”
加载:脚本的下载与HTML解析并行进行(不阻塞)。
执行:脚本会延迟到整个HTML文档解析完毕后,但在DOMContentLoaded事件触发之前执行。
执行顺序:如果有多个defer脚本,它们将严格按照在HTML中出现的顺序依次执行。
时序图:
HTML 解析: |=======================| (解析完成)
JS 下载: |------下载中------|
JS 执行: |==执行==|
DOMContentLoaded 事件: |---触发---|
使用场景:
这是首选和最常用的方案。
适用于任何需要操作DOM的脚本,因为它保证执行时DOM已经完整。
适用于有依赖关系的脚本,因为它保证了执行顺序。例如,先加载库文件,再加载业务逻辑文件。
2.async-“异步执行”
加载:脚本的下载与HTML解析并行进行(不阻塞)。
执行:脚本在下载完成后立即执行。此时,它会暂停HTML解析。
执行顺序:如果有多个async脚本,它们的执行顺序完全不确定,取决于哪个脚本先下载完成。
时序图:
HTML 解析: |========| (暂停) |===========|
JS 下载: |----下载中----|
JS 执行: |==执行==| (下载完立即执行)
使用场景:
适用于那些完全独立、无依赖的第三方脚本。
例如:网站分析脚本(Google Analytics)、广告脚本、监控脚本等。这些脚本不操作DOM,也不依赖其他脚本,早执行晚执行都无所谓。
十六、大规模列表的性能优化:虚拟列表与增量渲染。
一句话总结:
虚拟列表通过“仅渲染可视区域”极限减少DOM数量,解决内存与渲染压力;增量渲染通过“分时分批渲染”避免长时间阻塞主线程,优先保证页面可交互性。两者常结合使用以应对超大规模列表场景。
核心方案对比
特性
虚拟列表 (Virtual List)
增量渲染 (Incremental Rendering)
核心原理
仅创建和渲染可视区域及其附近的列表项,通过绝对定位模拟完整滚动条。
将整个列表的渲染任务拆分成多个小块,在浏览器空闲时段分批渲染。
解决痛点
DOM 节点过多导致的内存占用高、渲染性能差、滚动卡顿。
一次性渲染耗时过长导致的主线程长期阻塞,页面“卡死”。
关键技术
滚动监听、位置计算、transform定位、动态高度处理(难点)。
requestAnimationFrame,setTimeout或任务分片(Time Slicing)。
适用场景
精确滚动的长列表(如表格、聊天记录、联系人列表)。
初始加载体验优先的超长列表(如日志文件、一次性渲染数万条数据)。
用户体验
滚动流畅,体验如同原生列表。
逐步加载,避免页面长时间无响应,但快速滚动可能看到空白。
选型与实践建议:
1.如何选择?
1.追求极致滚动性能->首选虚拟列表。
2.处理海量数据(如10W+)初始加载->可用增量渲染作为补充或临时方案。
3.终极方案:两者结合。虚拟列表架构,初始化时用增量渲染分批计算和填充占位高度,避免首次渲染卡顿。
2.现成方案推荐:
1.无需重复造轮子,优先使用成熟库:
1.Vue生态:vue-virtual-scroller
2.React生态:react-window,react-virtualized
3.通用方案:各大UI组件库(如Ant Design,Element Plus)的虚拟滚动表格/列表组件。
总结:虚拟列表是空间优化的王者,增量渲染是时间调度的策略,理解其不同维度上的优化本质,是解决大规模列表性能问题的关键。
十七、讲一下文件上传:分片/断点续传、秒传与并发控制。
一句话概括:
现代大文件上传体系通过文件分片将大文件拆解,基于此实现断点续传以应对网络中断;通过文件哈希进行预校验,实现秒传以优化重复上传;并通过并发控制管理分片上传,以平衡上传速度与系统稳定性。
详细解析:
1.分片上传(Chunking)
是什么?
将一个大文件在前端通过File.slice()方法,切割成多个较小的数据块(chunk),然后逐个或分批上传。
为什么需要?
避免请求超时:HTTP请求有超时限制,上传一个巨大的文件(如几个GB)的单个请求,很容易因网络波动或服务器限制而超时失败。
提高稳定性:单个小分片上传失败,只需重传这个分片,而不是整个文件。
是断点续传的基础:只有将文件分片,才能记录哪些部分已成功,哪些需要续传。
实现流程:
1.前端获取File对象。
2.定义一个切片大小chunkSize(例如2MB)。
3.使用for循环和file.slice(start,end)方法,将文件切割成一个分片数组。
4.为每个分片附加索引、文件标识等信息,准备上传。
2.断点续传(Resumable Upload)
是什么?
在上传过程中,如果网络中断、浏览器关闭或用户暂停,下次恢复上传时,可以从上一次中断的地方继续,而无需从头开始。
实现流程(握手->上传->合并):
1.生成文件唯一标识(File ID):在上传开始前,前端需要为文件生成一个唯一的ID。最佳实践是计算整个文件的内容哈希(如MD5或SHA-256),因为内容相同的不同名文件,哈希是一样的。这个计算过程可能耗时,应使用Web Worker在后台线程进行,避免阻塞UI。
2.预检/握手(Pre-upload Handshake)
1.前端向服务器发送一个“预检”请求,携带这个文件哈希。
2.服务器收到后,检查该哈希对应的文件上传状态:
1.从未上传过:告知前端从第0个分片开始传。
2.部分上传过:返回一个列表,告诉前端哪些分片已经存在了。
3.已上传完毕:直接触发“秒传”逻辑。
3.分片上传:前端根据预检结果,只上传服务器没有的分片。
4.合并请求(Merge Request):所有分片都上传成功后,前端再发送一个“合并”请求,通知服务器该文件的所有分片都已就绪。
5.服务器合并:服务器根据文件哈希,找到所有分片文件,按正确的顺序将它们合并成一个完整的文件,并清理掉临时分片。
3.秒传(Instant Upload)
是什么?
当用户上传一个服务器上已经存在的文件时,系统无需再次接收文件内容,直接在几秒钟内提示用户上传成功。
实现原理:
秒传的实现完全依赖于断点续传的“预检”步骤。
当服务器在预检阶段,通过文件哈希发现这个文件已经完整存在于存储中时,它会直接返回一个“上传成功”的响应。
后端服务此时只需在用户的个人文件列表中,创建一个指向这个已存在文件的引用或记录即可。整个过程没有任何文件实体数据的传输,因此速度极快。
4.并发控制(Concurrency Control)
是什么?
在同时上传多个分片时,控制同时进行的网络请求数量。
为什么需要?
浏览器限制:浏览器对同域名下的并发TCP连接数有限制(通常是6-8个)。一次性发起上百个分片的上传请求,大部分都会被阻塞在队列中。
提高稳定性:过多的并发请求会抢占带宽,可能导致整体上传速度变慢,甚至引起某些请求失败。
服务器压力:减轻服务器瞬时的并发处理压力。
实现:
本质上是实现一个异步任务的并发池。
1.创建任务队列:将所有待上传的分片封装成一个返回Promise的异步函数数组。
2.设置并发数limit:例如:limit=4。
3.执行并发池:
1.同时从任务队列中取出limit个任务开始执行。
2.使用Promise.race()或类似机制,每当有一个任务完成时,就从队列中取下一个新任务来补充,始终保持有limit个任务在“进行中”。
3.直到所有任务都执行完毕。
4.错误处理:当某个分片上传失败时,可以将其重新放回队列末尾进行重试,并记录重试次数。
十八、讲一下移动端适配:viewport、rem/em、设备像素比与响应式方案。
一句话概括:
移动端适配的核心是让Web页面在不同尺寸和像素密度的设备上都能获得清晰、一致的视觉体验。这通常通过设置理想视口(viewport)开始,利用rem方案将CSS尺寸与根字体大小挂钩实现等比缩放,深刻理解设备像素比(DPR)对高清屏的影响。并结合响应式设计的媒体查询,共同打造出一套灵活、健壮的跨终端UI方案。
详细解析:
1.视口(Viewport)
是什么?
视口是浏览器中用于呈现网页的区域。在移动端,存在两个视口:
布局视口(Layout Viewport):浏览器为了兼容为桌面设计的网站,默认会创建一个很宽的虚拟视口(如980px),然后将页面缩小以适应手机屏幕。这就是为什么桌面网页在手机上看起来字很小的原因。
视觉视口(Visual Viewport):用户在屏幕上实际看到的区域。
核心操作:理想视口(Ideal Viewport)
我们通过在HTML的中添加一个标签,来告诉浏览器放弃默认的布局视口,使用设备的真实宽度作为布局视口。
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
width=device-width:将布局视口的宽度设置为设备的屏幕宽度。
initial-scale=1.0:设置初始缩放比例为1,即不缩放。
这是所有移动端适配方案的基石。
2.rem/em-相对长度单位
em:相对于父元素的font-size。如果父元素的字体大小是16px,那么1.5em就等于2.4px。em的问题在于,由于层层继承,其最终的像素值计算可能变得复杂和不可预测。
rem(Root EM):相对于根元素()的font-size。这是一个全局性的相对单位。
rem适配方案原理:
1.设置基准:选择一个设计稿宽度,例如750px。
2.动态计算根字体大小:通过JavaScript监听屏幕宽度的变化,根据一个公式来动态设置元素的font-size。一个常见的做法是将屏幕宽度分成10份,即document.documentElement.style.fontSize = window.innerWidth / 10 + 'px';。
3.单位转换:在写CSS时,所有需要适配的尺寸(如width,height,margin,font-size)都用rem单位。其值可以通过设计稿像素值/(设计稿宽度/10)来计算。例如,在750px设计稿上一个150px宽的元素,其width就是150/(750/10)=2rem。
优势:通过改变一个根font-size,就可以让整个页面的所有元素实现等比例缩放,完美还原设计稿的视觉比例。这是目前国内移动端适配的主流方案。
3.设备像素比(Device Pixel Ratio,DPR)
是什么?
DPR=物理像素/CSS像素(设备独立像素)。它描述了在一个CSS像素的长度上,塞进了多少个物理像素点。
iPhone 4(DPR=2):1个CSS像素由2x2=4个物理像素点来渲染。
iPhone X(DPR=3):1个CSS像素由3x3=9个物理像素点来渲染。
带来的问题:
图片模糊:在DPR>1的高清屏(Retina屏)上,如果用一个100x100像素的图片去填充一个100x100CSS像素的区域,实际上是用100x100个图片像素点去覆盖200x200(DPR=2)或300x300(DPR=3)个物理像素点,图片自然会被拉伸而变得模糊。
1px边框问题:在高清屏上,border:1px solid black;实际上会占据2个或3个物理像素的宽度,看起来比设计稿要粗。
解决方案:
高清图适配:
提高2x图、3x图,然后通过媒体查询或srcset属性来让浏览器根据DPR选择合适的图片。
@media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 2dppx) {
.logo { background-image: url(logo@2x.png); }
}
1px边框实现:
使用伪元素+transform:scaleY(0.5)的方式。创建一个2px高的伪元素,然后将其在Y轴方向上缩放到50%,在DPR=2的屏幕上看起来就是1物理像素宽。
4.响应式设计(Responsive Web Design)
是什么?
rem方案侧重于等比缩放,而响应式设计则侧重于在不同屏幕尺寸区间内,改变页面的布局和结果。
核心技术:媒体查询(Media Queries)
/* 默认样式,用于小屏幕 */
.container { width: 100%; }
/* 当屏幕宽度大于等于 768px (平板) 时 */
@media (min-width: 768px) {
.container { display: flex; }
.sidebar { width: 30%; }
.main-content { width: 70%; }
}
/* 当屏幕宽度大于等于 1200px (桌面) 时 */
@media (min-width: 1200px) {
.container { max-width: 1140px; margin: 0 auto; }
}
与rem方案的关系:
两者并不互斥,而是互补的。在一个复杂的项目中,通常会结合使用:
主体内容和组件使用rem方案,保证在移动端各种尺寸下的视觉比例一致。
整体页面布局使用响应式设计,在从手机到平板再到桌面的大尺寸跨域时,通过媒体查询调整布局结构(如从单栏变为多栏),以提供更优化的浏览体验。
十九、讲一下代码分割与 Tree Shaking:原理、命名空间污染与副作用标注。
一句话总结:
代码分割通过动态导入(import())实现按需加载,优化首屏速度;Tree Shaking利用ES Module的静态分析移除未使用代码,优化打包体积;而正确的副作用标注(sideEffects:false)和避免命名空间污染是保证了Tree Shaking高效工作的关键前提。
核心原理精讲:
概念
核心原理
实现方式与关键点
代码分割
将代码拆分成多个块,按需加载。
import()语法:Webpack 等打包器遇到此语法会自动拆分出新 chunk 文件,运行时异步加载。
Tree Shaking
基于 ES Module 的静态结构,移除未被使用的导出(死代码)。
使用 ESM 语法:import/export是基础。
避免命名空间污染:全局变量会使分析失效,打包器因无法确定引用关系而不敢删除代码。
副作用标注
声明模块是否具有**“副作用”**(如修改全局变量、注入样式),指导打包器能否安全摇树。
package.json中设置:
"sideEffects": false(声明整个包无副作用)
"sideEffects": ["*.css"](声明某些文件有副作用)
代码中使用/*#__PURE__*/:标记函数调用无副作用。
总结与实践:
目标:代码分割优化加载性能,Tree Shaking 优化打包体积。
最佳实践:
1.使用import()实现路由和组件懒加载。
2.使用ES Module语法编写代码和库。
3.开发npm库时,务必在package.json中正确配置sideEffects属性。
4.避免使用未声明无副作用的第三方库,它们会增加你的打包体积。
二十、构建工具对比与选型:Vite/Webpack/Rollup 的适配场景。
一句话概括:
这三者是主流的前端构建工具,定位各有侧重:Webpack是一个功能全面、生态成熟的“全能型”模块打包器,适用于复杂的大型项目;Rollup专注于打包纯净、高效的JavaScript库;而Vite则利用现代浏览器特性,通过创新的开发服务器和基于Rollup的生产构建,为应用开发提供了极致的开发体验和优异的打包性能。
详细解析:
1.Webpack:功能全面的“瑞士军刀”
核心理念:“一切皆模块”。Webpack通过各种Loader,可以将任何类型的文件(JavaScript,CSS,图片,字体等)都视为模块进行处理和依赖管理;再通过强大的Plugin体系,在构建流程的各个节点进行自定义操作(如打包优化、资源管理、环境变量注入等)。
优点:
生态极其成熟:拥有海量的Loader和Plugin,几乎可以解决任何工程化需求和边界情况。社区庞大,解决方案丰富。
功能强大全面:支持代码分割、Tree Shaking、热模块替换(HMR)、懒加载等几乎所有现代构建功能。
高度可配置:提供了极高的灵活性,可以深度定制构建流程,以适应复杂和特殊的大型项目。
缺点:
配置复杂:上手门槛高,webpack.config.js的配置可能非常冗长和复杂。
开发体验极慢:在冷启动和热更新时,需要对所有模块进行打包和转换,在大型项目中可能会感到明显的延迟。
适配场景:
大型、复杂的单页应用(SPA):特别是那些有复杂构建需求、需要深度定制、依赖大量非JavaScript资源(如CSS预处理器、图片优化)的项目。
需要极致稳定性和兼容性的企业级项目:Webpack经受了最广泛的生产环境考验。
2.Rollup:精致的JS库打包器
核心理念:专注于JavaScript,Rollup的设计目标是打包出体积更小、性能更好的JavaScript库,它利用ES Modules(ESM)的静态特性,生成非常干净、扁平化的代码。
优点:
Tree Shaking效果极佳:由于其设计初衷就是处理ESM,Rollup的Tree Shaking能力非常出色,能生成不含任何冗余代码的纯净包。
输出格式多样:支持输出多种模块格式(ESM,CommonJS,UMD,AMD,IIFE),非常适合发布到npm的库。
配置相对简单:专注于JS打包,其配置项比Webpack少,更易于理解。
缺点:
并非JS资源处理较弱:默认不支持处理CSS、图片等资源,需要依赖插件,且生态不如Webpack丰富。
开发服务器功能薄弱:其rollup-plugin-serve等插件提供的开发服务器功能远不如Webpack和Vite强大,HMR支持也不完善。
适配场景:
构建JavaScript库/框架:这是Rollup的核心优势领域。几乎所有主流的前端库(如React,Vue,Three.js)都使用Rollup进行打包。
构建纯粹的、对体积要求极致的应用。
3.Vite:下一代前端构建工具
核心理念:利用现代浏览器。Vite在开发阶段彻底改变了构建范式。
开发阶段(Dev Server):
No-bundle(无打包):Vite启动一个原生ESM开发服务器。它不会像Webpack那样在启动时打包整个项目,而是直接将源码(如vue,.jsx文件)按需提供给浏览器。
浏览器原生ESM:浏览器通过
极致的速度:这意味着冷启动速度极快(几乎是秒开),并且HMR(热更新)速度与项目规模无关,因为每次更新只需重新编译被修改的那个文件即可。
生产阶段(Production Build):
使用Rollup:为了获得最佳的加载性能,Vite在生产构建时,会调用Rollup对代码进行打包、Tree Shaking和优化。这结合了Rollup在打包库方面的优势。
优点:
无与伦比的开发体验:闪电般的冷启动和热更新速度。
开箱即用:内置了对TypeScript,JSX,CSS预处理器等的支持,配置极其简洁。
基于Rollup的优化产物:生产构建的性能有保障。
缺点:
生态相对年轻:虽然发展迅速,但插件生态和对某些特殊场景的处理能力,与Webpack相比仍有差距。
需要现代浏览器支持:开发服务器依赖原生ESM,无法兼容旧版浏览器(但生产构建产物可以配置兼容性)。
适配场景:
所有新的Web应用开发:对于绝大多数新的Vue和React项目,Vite已经成为首选的构建工具。
追求极致开发效率的团队。
可以不考虑旧版浏览器的项目。
二十一、讲一下单页应用与多页应用、SSR/CSR/SSG 的权衡与迁移。
一句话概括:
Web应用架构的选择,是在开发体验、用户体验、性能和SEO之间的权衡。MPA是传统模式,稳定简单;SPA提供更流程的“应用感”。在渲染模式上,CSR开发简单但SEO和首屏性能差;SSR解决了这些问题但服务器压力大;而SSG则为内容固定的网站提供了极致的性能和SEO效果。现代框架(如Next.js,Nuxt)通过同构/通用渲染,使得在这些模式间的迁移和混合使用成为可能。
详细解析:
1.SPA(Single-Page Application)vs MPA(Multi-Page Application)
MPA(多页应用)
工作方式:传统的网站模式。每次页面跳转(点击链接),浏览器都会向服务器发送一个全新的请求,服务器返回一个完整的HTML文件,浏览器重新加载整个页面。
优点:
首屏加载块:每个页面只加载自身所需的资源。
SEO友好:每个页面都有独立的URL和完整的HTML内容,非常利于搜索引擎爬取。
架构简单,易于理解和维护。
缺点:
页面切换体验差:每次跳转都会有“白屏”等待时间,缺乏流畅的“应用感”。
状态管理复杂:夸页面共享数据困难。
SPA(单页应用)
工作方式:整个应用只有一个主HTML文件。首次加载时,会下载所有必要的HTML,CSS,JS。后续的页面导航,实际上是通过JavaScript动态地、局部地更新页面内容,并使用History API来管理浏览器URL,而不重新请求整个页面。
优点:
用户体验极佳:页面切换流畅、快速、没有白屏,提供了接近原生应用的体验。
组件化:非常适合现代前端框架(React,Vue,Angular)的组件化开发模式。
前后动分离:职责清晰,后端专注于提供API。
缺点:
首屏加载慢:需要一次性加载较大的JS文件,导致首次渲染时间(FCP/LCP)较长。
SEO难度大:初始返回的HTML文件通常是一个空的
2.渲染模式的权衡:CSR,SSR,SSG
这些渲染模式主要是在SPA的背景下,为了解决其首屏慢和SEO差的问题而演进出来的。
CSR(Client-Side Rendering)-客户端渲染
过程:浏览器下载一个近乎空白的HTML和一个巨大的JS bundle->浏览器执行JS->JS请求API获取数据->JS根据数据渲染出页面内容。
优点:开发简单,服务器压力小(只需提供静态文件和API)。
缺点:首屏白屏时间长、SEO极差。这是“纯粹的”SPA的默认渲染模式。
SSR(Server-Side Rendering)-服务端渲染
过程:用户请求页面->Node.js服务器接收请求->服务器请求API获取数据->在服务端将组件渲染成完整的HTML字符串->将HTML和少量用于“激活”(Hydration)的JS一切返回给浏览器->浏览器直接显示HTML内容,然后JS执行并接管页面交互。
优点:
首屏加载块:用户能立刻看到有内容的页面。
SEO完美:返回给爬虫的是完整的HTML。
缺点:
服务器压力大:每个请求都需要在服务器上实时渲染。
架构更复杂:需要维护一个Node.js服务,且需要处理好服务端和客户端的环境差异(所谓的“同构”开发)。
SSG(Static Site Generation)-静态站点生成
过程:在构建时(build time),预先获取所有需要的数据,为每一个页面都生成一个对应的、完整的HTML文件->将这些HTML文件和静态资源部署到CDN。
优点:
性能极致:用户直接从最佳的CDN节点获取静态HTML,加载速度无与伦比。
SEO完美。
服务器成本极低,甚至无需服务器(只需静态托管服务)。
缺点:
内容更新不及时:每次数据变更,都需要重新构建和部署整个网站。
不适用于高度动态、个性化的内容(如用户个人中心)。
3.迁移与现代方案
现代前端框架,如Next.js(for React)和Nuxt.js(for Vue),极大地简化了这些模式的选择和迁移。
同构/通用渲染(Isomorphic/Universal Rendering):
这些框架的核心理念是提供一套代码,既可以在服务端运行(SSR),也可以在客户端运行(CSR)。开发者只需按照框架的约定写组件即可。
混合渲染(Hybrid Rendering):
Next.js和Nuxt允许你在同一个应用中,为不同的页面选择不同的渲染策略。
例如:
博客文章、产品介绍页:内容不常变,使用SSG以获得最佳性能。
用户个人中心、仪表盘:内容高度个性化和动态,使用CSR。
新闻首页、电商列表页:需要SEO且内容频繁更新,使用SSR。
增量静态再生(ISR-Incremental Static Regeneration):Next.js还提供了一种SSG的变体,运行静态页面在一定时间间隔后(或通过webhook触发)在后台自动重新生成,解决了SSG更新不及时的问题。
迁移路径:
从一个纯CSR的SPA,可以平滑地迁移到Next.js/Nuxt。通常步骤是:
1.将项目结构调整为框架要求的目录结构。
2.将路由从客户端路由库(如react-router-dom)迁移到框架内置的基于文件系统的路由。
3.改造数据获取逻辑,使用框架提供的特定函数(如getServerSidePropsfor SSR,getStaticPropsfor SSG)来在构建时或服务端获取数据。
通过这种方式,团队可以根据业务需求,灵活地为每个页面选择最优的渲染策略,实现性能、SEO和开发效率的最佳平衡。
二十二、讲一下单页应用与多页应用、SSR/CSR/SSG 的权衡与迁移。
一句话总结:
时间循环是JavaScript管理异步任务的核心极致,它遵循“宏任务->清空所有微任务->渲染(如有需要)->下一轮宏任务”的循环顺序执行,从而保证了代码的有序性和渲染的高效性。
核心极致讲解:
事件循环(Event Loop)的运作过程可以清晰地通过下图展示,它揭示了JavaScript如何协调执行任务、处理异步回调并更新页面:
1.任务类型与执行时机
任务类型
常见例子
执行时机
宏任务 (MacroTask)
setTimeout,setInterval,setImmediate(Node), I/O 操作, UI 渲染, 主线程脚本
在每次事件循环中执行一个宏任务。
微任务 (MicroTask)
Promise.then/catch/finally, await后的代码,MutationObserver,queueMicrotask
在当前宏任务执行完毕后,立即清空整个微任务队列。
2.关键特性与渲染时机
微任务优先:这是最重要的规则,一个宏任务完成后,必须连续执行完成所有微任务,才会进行渲染或执行下一个宏任务。这就是为什么Promise.then总是比setTimeout(fn,0)先执行。
渲染时机(UI Render):浏览器会在一次事件循环中多次检查是否需要渲染(通常每秒60次,即16.7ms/帧)。渲染本身发生在微任务队列被清空之后、下一个宏任务执行之前。这意味着:
如果在当前循环的微任务中进行了大量计算,会阻塞渲染,导致页面卡顿。
Vue的nextTick就是将DOM更新回调收入微队列,从而在本次循环的渲染前拿到最新的DOM。
3.经典面试题分析
console.log('1. Start Script'); // 【宏任务1】同步代码
setTimeout(() => {
console.log('6. setTimeout'); // 【宏任务2】回调
}, 0);
Promise.resolve()
.then(() => {
console.log('4. Promise 1'); // 【微任务1】
})
.then(() => {
console.log('5. Promise 2'); // 【微任务2】
});
console.log('2. End Script'); // 【宏任务1】同步代码
// 点击按钮
button.addEventListener('click', () => {
console.log('8. UI Click Task'); // 【宏任务3】UI交互回调
});
// 输出顺序: 1 -> 2 -> 4 -> 5 -> 6 -> (点击后输出8)
执行顺序解析:
1.【宏任务1】:执行主脚本
1.执行所有同步代码,输出1.Start Script和2.End Script。
2.遇到setTimeout,将其回调函数分发到宏任务队列。
3.遇到Promise.then,将其回调函数分发到微任务队列。
2.【清空微任务队列】
1.当前宏任务执行完毕,开始依次执行微任务队列中的所有任务。
2.输出4.Promise 1,该微任务又产生了新的微任务(下一个then)。
3.继续执行新产生的微任务,输出5.Promise 2。
3.【可能渲染】:微任务队列清空后,浏览器可能进行UI渲染。
4.【下一轮宏任务】:从宏任务队列中取出setTimeout的回调并执行,输出6.setTimeout。
5.如果此时用户点击了按钮,会生成一个新的宏任务,在后续的事件循环中执行,输出8.UI Click Task。
二十三、DOM 操作的性能风险知道嘛?讲一下批量更新策略。
一句话概括:
直接、频繁地操作DOM会引发昂贵的重拍(Reflow)和重绘(Repaint),从而导致页面卡顿。为了优化性能,我们应遵循读写分离的原则,通过批量更新策略(如使用DocumentFragment或一次性字符串拼接)将多次操作合并为一次,并利用现代框架的Virtual DOM机制,将性能风险降到最低。
详细解析:
1.性能风险:重排(Reflow)与重绘(Repaint)
浏览器渲染页面的过程是:解析HTML生成DOM树->解析CSS生成CSSOM树->结合两者生成渲染树(Render Tree)->布局(Layout/Reflow)计算每个节点的位置和大小->绘制(Paint/Repaint)将节点绘制成像素。
重排(Reflow):
定义:当DOM元素的几何属性(如width,height,margin,padding,border)或结构(添加/删除节点)发生变化,导致浏览器需要重新计算元素在页面上的位置和尺寸时,就会发生重排。
代价:极其昂贵,一次重排会影响其所有子节点以及部分祖先节点,导致需要重新布局整个页面或页面的大部分区域。
重绘(Repaint):
定义:当DOM元素的外观属性(如color,background-color,visibility)发生变化,但不影响其几何布局时,浏览器只需重新绘制该元素的外观。
代价:比重排开销小,因为它跳过了布局计算的步骤。
关系:重排必然导致重绘,但重绘不一定导致重排。
2.触发性能问题的坏习惯
最糟糕的实践是在循环中交错进行DOM的读写操作。
// 极差的性能反例
function updateStyles(elements) {
for (let i = 0; i < elements.length; i++) {
// 1. 写操作
elements[i].style.width = elements[i].offsetWidth + 10 + 'px';
// 2. 读操作 (offsetWidth)
}
}
问题分析:
offsetWidth是一个“读”操作,为了给你一个精确的值,浏览器必须强制刷新渲染队列,立即执行一次重排,以计算出最新的布局。
因此,在循环的每一次迭代中,都会发生:写->强制重排->读->写->强制重排...。如果elements数量很大,页面会因此产生剧烈的卡顿。
3.批量更新策略
核心思路是将多次DOM修改,合并成一次性地提交给浏览器。
策略一:读写分离
这是最基本的优化原则。将所有“读”操作集中在一起,所有“写”操作集中在一起。
// 优化后的代码
function updateStyles(elements) {
const widths = [];
// 1. 集中读取
for (let i = 0; i < elements.length; i++) {
widths[i] = elements[i].offsetWidth;
}
// 2. 集中写入
for (let i = 0; i < elements.length; i++) {
elements[i].style.width = widths[i] + 10 + 'px';
}
}
效果:第一次循环读取完所有offsetWidth,第二次循环修改所有样式。浏览器通常会优化这些连续的写操作,最终可能只进行一次重排。
策略二:使用DocumentFragment
是什么:DocumentFragment是一个“文档片段”,可以看作一个存在于内存中的、轻量级的DOM容器。对它的所有操作都不会触发重排和重绘。
适用场景:需要向DOM中插入大量新节点时。
const fragment = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
const li = document.createElement('li');
li.textContent = `Item ${i}`;
fragment.appendChild(li); // 在内存中操作,不触发重排
}
// 最后,将整个 fragment 一次性插入 DOM
document.getElementById('my-list').appendChild(fragment); // 只触发一次重排
策略三:一次性字符串拼接
是什么:通过拼接HTML字符串,然后一次性地使用innerHTML将其写入DOM。
适用场景:需要动态生成并插入一大段复杂的HTML结构时。
let html = '';
for (let i = 0; i < 1000; i++) {
html += `<li>Item ${i}</li>`;
}
document.getElementById('my-list').innerHTML = html; // 只触发一次重排
注意:innerHTML的性能非常好,但要注意防范XSS攻击。
策略四:CSSclass切换
如果需要修改一个元素的多个样式,不要逐条用JS修改element.style,而是预先定义一个CSS类,然后通过JS一次性地切换element.className或element.classList.add()。这会将多次样式变更合并为一次。
4.现代框架的解决方案:Virtual DOM
现代框架的解决方案:Virtual DOM
现代框架如React和Vue通过引入Virtual DOM(虚拟DOM)极大地简化了这个问题。
工作原理:
1.当状态变更时,框架会在内存中重新构建一颗新的Virtual DOM树。
2.通过Diffing算法,对比新旧两颗Virtual DOM树的差异,找出最小化的变更集。
3.最后,框架将这些变更一次性地、以最优的方式应用到真实的DOM上。
开发者收益:开发者只需要关系状态的改变,而无需手动进行任何DOM操作优化。框架的Virtual DOM机制已经自动地、高效地为我们处理了批量更新。
二十四、了解CSS 性能与优先级嘛?讲一下:选择器、重排重绘、合成层与硬件加速。
一句话总结:
CSS优先级由选择器特异性决定,而性能优化的核心在于避免触发昂贵的重排(布局变化)与重绘(样式变化),最高效的手段是利用transform和opacity等属性将元素提升为合成层,以启用GPU硬件加速,跳过布局和绘制流程。
核心要点解析:
概念
描述与作用
关键实践
优先级 (Specificity)
解决样式冲突的规则。ID > Class > 元素。!important权重最高,但应避免使用。
使用足够具体的选择器,但避免过度嵌套,以保持良好性能与可维护性。
重排 (Reflow)
当布局或几何属性(如宽、高、位置)改变时,浏览器重新计算所有元素位置的过程。开销巨大。
批量修改样式、避免频繁读写offsetHeight等布局属性。
重绘 (Repaint)
当元素的外观属性(如颜色、背景色)改变但不影响布局时,浏览器重新绘制像素的过程。开销较大。
减少不必要的样式变更。重排必然导致重绘。
合成层 (Composite Layer) / 硬件加速
浏览器将元素提升为独立图层,其transform和opacity的变化由 GPU 直接处理,跳过重排和重绘,性能极佳。
对需要动画或频繁变化的元素使用 transform: translateZ(0) 或 will-change: transform 开启硬件加速。
性能优化策略总结:
1.减少重排:
1.集中改变样式:使用classList一次性修改多个样式,而非多次操作style。
2.脱离文档流:对复杂动画使用position:absolute/fixed,减少影响范围。
3.批量DOM操作:使用DocumentFragment进行多次DOM插入。
2. 减少重绘:
1.利用合成层,优先使用transform和opacity来实现动画和变化。
3.高效利用合成层:
1.GPU加速:对动画元素使用transform:translate3d(0,0,0)或will-change:transform将其提升至单独图层,由GPU渲染。
2.避免层爆炸:切勿滥用硬件加速,过多的合成层会消耗大量内存(层爆炸),反而降低性能。
最终建议:在开发中,优先使用transform和opacity来实现动画,这是性能最优的路径。务必在Chrome DevTools的Performance和Layers面板中验证性能瓶颈和层创建情况。
二十五、说一下资源体积优化,包括图片/字体/第三方依赖的治理。
一句话总结:
资源体积优化需系统化治理:图片采用WebP/AVIF格式、响应式与懒加载;字体进行子集化并使用font-display:swap;第三方依赖通过分析、Tree Shaking、CDN与按需引入等手段做“减法”。
核心优化策略
资源类型
优化手段
具体做法与收益
图片
现代格式
使用 WebP/AVIF 替代 PNG/JPG,体积减少 25-50%。
响应式与懒加载
使用srcset提供合适尺寸的图片,loading="lazy"延迟加载视口外图片。
CDN 处理
使用云服务通过 URL 参数动态裁剪、转换格式,节省存储与流量。
字体
子集化
提取实际使用的字符生成字体子集,中文字体文件可从 MB 降至 KB 级。
优化加载
使用font-display: swap避免渲染阻塞,优先使用 WOFF2 格式。
第三方依赖
依赖分析
使用webpack-bundle-analyzer分析构建产物,定位体积大的依赖。
Tree Shaking
确保使用 ESM 格式的库,并配置sideEffects: false移除未使用代码。
CDN & External
通过
按需引入
使用 babel 插件(如babel-plugin-import)实现组件库按需导入。
实施建议
1.分析先行:使用分析工具(如webpack-bundle-analyzer)量化问题,精准优化。
2.自动化:将图片压缩、字体子集化等流程集成到构建工具(Webpack/Vite)中。
3.持续监控:在CI/CD流程中加入包体积监控,防止体积反弹。
通过以上针对性措施,可显著提升应用加载速度与用户体验。
二十六、讲一下前端路由(React Router 等)的原理。
一句话概括:
前端路由通过拦截导航事件,把URL与组件映射起来,在不刷新页面的前提下用Hash或History API改变地址并按路由表渲染对应视图。
详细解析:
两种模式:Hash(#后变化,监听hashchange)与History(pushState/replaceState改地址,监听popstate,服务器需兜底到index.html)。
核心流程:定义路由表->创建匹配器(支持动态参数/通配符/嵌套)->拦截/与调用navigate()时执行导航->根据location匹配出路由->按层级渲染对应组件(可做代码分割/懒加载)。
导航守卫:进入/离开前执行校验、鉴权、数据预取与重定向。
辅助能力:404/重定向、查询参数解析、状态与滚动位置恢复、基路径配置。
SSR/同构:服务端先匹配并预取数据,返回HTML;客户端hydrate后继续用同一套路由逻辑接管导航。
二十七、微前端:qiankun/iframe 的隔离机制、路由与资源共享。
一句话概括:
iframe靠浏览器进程级隔离最彻底但集成感差;qiankun以single-spa为内核配合沙箱(Proxy+样式隔离)在同页实现“软隔离”,提供主/子应用路由编排与按需资源复用。
详细解释:
隔离机制:
iframe:天然DOM/CSS/JS/全局对象隔离,跨域还隔Cookie/Storage;父子通信靠postMessage,样式与事件冒泡不互相影响。
qiankun:装载子应用时注入沙箱(Proxy劫持window、快照沙箱回滚副作用)、CSS Scope/Shadow DOM/动态样式隔离,卸载还原全局状态。
路由编排:
主应用掌控顶层路由,匹配到某前缀时激活相应子应用;子应用内部继续用自身路由(hash或history),并与主路由做前缀/基路径对齐。
资源共享:
公共依赖可通过external/共享CDN避免重复加载;运行时以import-map/系统JS管理版本。
iframe模式共享困难(需父页注入或跨域策略),qiankun可在主应用注入全局服务或通信总线(如events、store)。
取舍:
安全与隔离优先->iframe;体验与集成优先->qiankun(需治理全局副作用与依赖版本)。
二十八、讲一下前端的依赖治理:锁版本、按需引入、去重与安全扫描。
一句话概括:
依赖治理=“可重复构建+最小可用+版本统一+安全合格”,用锁版本保证确定性,用按需与Tree-Shaking控体积,用去重与统一版本稳运行,再以安全/许可证扫描兜底。
详细解释:
锁版本:提交lockfile(npm/yarn/pnpm),禁用^/~(或设save-prefix="),CI校验“锁文件未变更”。
统一与去重:monorepo用workspace/hoist;用overrides/resolutions统一底层库版本,npm dedupe降重复。
按需引入:babel-plugin-import/unplugin-auto-import、ESM动态import;标注sideEffects,启用了Tree-Shaking与代码分割,体积阈值告警。
镜像与缓存:私有registry/代理,锁定源与校验哈希。
安全与合规:npm audit/snyk/Dependabot;生成SBOM,许可证白名单与阻断策略,漏洞高危强制升级。
二十九、讲一下资源并行加载与连接复用(Keep-Alive/HTTP2/3)。
一句话概括:
提升首屏/吞吐的关键是“少建连接、并行传输、按优先级送资源”:HTTP/1.1用Keep-Alive复用连接,HTTP/2用单连接多路复用,HTTP/3基于QUIC彻底消除队头阻塞,并配合预加载与优先级调度实现真正并行。
详细解释:
并行加载:合理拆分与按需分包,提前建链与拉取;Priority Hints/HTTP/2优先级树决定谁先传。
连接复用:
HTTP/1.1 Keep-Alive复用TCP,仍受“每域并发连接数”限制,易队头阻塞;避免域名分片滥用。
HTTP/2 单TCP多路复用+HPACK压头,串改并;慎用Server Push(已不推荐)。
HTTP/3 基于QUIC(UDP)+TLS1.3,顶流复用,无TCP队头阻塞,建链更快。
实践:开启H2/H3、TLS会话复用/0-RTT、连接池复用;静态资源CDN同域复用;控制并行请求数与优先级,监控TTFB/LCP/吞吐与失败率。
三十、讲一下Service Worker:离线缓存、预缓存与更新策略。
一句话概括:
Service Worker通过拦截请求并读写Cache Storage,实现首屏预缓存与离线可用;更新靠版本化与激活接管,线上按资源类型选择合适的缓存策略(如缓存优先/网络优先/陈旧换新)。
详细解释:
预缓存:install事件中提前cache.addAll(静态清单),保证离线首屏可用;注意带指纹文件与跨域可缓存性(CORS)。
运行时缓存:fetch里按类型走策略:
cache-first(图标/字体/版本化静态资源,命中快);
network-first(HTML/JSON,保证新鲜度,失败回退缓存/离线页);
stale-while-revalidate(CSS/JS,先返回缓存并后台更新)。
更新策略:给缓存命名加版本,activate时删除旧版本;可用self.skipWaiting()加速新SW激活,clients.claim()立即接管页面;提示用户“有新版本可用”并提供刷新按钮。
回退与路由:离线时对导航请求返回离线页或上次成功的shell;细分API/静态/导航三类路由。
监控与配额:控制缓存大小与逐出策略;注意私有缓存与CDN缓存并存的失陪;记录命中率与失败率。
三十一、讲一下复杂表格/图表的渲染性能与交互优化。
一句话概括:
核心是“少渲染,批处理、分层绘制、懒更新”,表格用虚拟滚动/列裁剪与批量DOM更新,图表用数据降采样/Canvas(WebGL)与分片计算,交互上做节流与增量高亮,整体用Worker与缓存兜住卡顿。
详细解释:
表格:
虚拟列表与列裁剪,只渲染可视窗口;行列分区复用DOM,分页与服务端排序/筛选。
批量插入/更新(Fragment/innerHTML/批量setState),避免频繁回流;懒加载展开行、冻结列用绝对定位/分层容器。
计算与导出丢给Web Worker;大数据导出流式/分块。
图表:
大数据选Canvas/WebGL;下采样(LTTB)、聚合(bin/heatmap)、抽稀标签;只更新变动系列。
分层绘制(坐标/网格/数据/高亮分层),交互高亮单独层,动画降帧与节流。
通用:
事件节流/防抖(滚动/缩放/拖拽);离屏/缓存(OffscreenCanvas、渲染缓存)。
指标与监控:首帧时间、滚动掉帧、交互延迟;按阈值降级(隐藏阴影、简化标注)。
三十二、前端权限体系:菜单、路由与按钮级控制的设计。
一句话概括:
用后端主导的RBAC/ABAC做“真鉴权”,前端只负责“能见能点”的展示与拦截;菜单=路由表+权限过滤,路由守卫校验通行,按钮用指令/高阶组件做到可见/可点控制,权限变更可动态刷新。
详细解释:
登录拿到token+roles/perms(可含租户/组织维度),本地持久化并设置过期/续签。
路由表在meta中声明requireRoles/perms;根据用户权限生成侧边菜单与面包屑,动态addRoutes,未匹配落404,拒绝落403.
按钮/区块用v-perm/withPermission包装:隐藏/禁用两种策略;权限变更经全局总线刷新视图。
后端必须做接口与数据级鉴权(前端仅防误操作);敏感操作二次确认与审计。
权限缓存与失效联动,多端登出;埋点记录被拒绝与异常,便于排错与审计。
三十三、低网速与弱网环境的体验保障与降级。
一句话概括:
先识别网络状态,再“轻资源+断点续+离线兜底”,用渐进加载与可恢复交互把核心路径抓住,非关键能力按优先级降级或延迟启用。
详细解释:
识别网络:navigator.connection/RTT、超时重试统计,弱网模式开关。
资源:按需与分包,优先首屏;图片多规格自适应/WEBP,懒加载与占位骨架。
请求:超时/退避重试、并发上限、断点续传/分块上传,失败提示可重试。
缓存:Service Worker预缓存+state-while-revalidate;本地草稿/乐观更新。
渐进呈现:流式/分段渲染,关键数据先到先渲,非关键延迟。
降级:关闭动画/高分图/重排操作;提供纯文本/简版页。
监控:弱网比、超时率、重试成功率、LCP/INP,达阈值自动切弱网模式。
三十四、讲一下UI 动画,包括:CSS/JS 动画的性能差异与时序控制。
一句话概括:
能用CSS做的就用CSS(硬件加速、浏览器优化好),需要复杂时序/交互/物流效果时用JS(requestAnimationFrame/动画库),两者都要尽量只改transform/opacity并控制刷新节奏。
详细解释:
性能差异:
CSS动画由浏览器合成线程驱动,transform/opacity可走合成层,避免回流;transform/animation可复用优化。
JS动画灵活,可做时间线、物理模拟、事件响应,但若直接操作style需用rAF并避免布局抖动。
时序控制:
CSS:cubic-bezier、steps、延迟与迭代;用animationed/transitionend监听阶段。
JS:rAF驱动、GSAP/Web Animations API;时间线/序列化/、并行/串行动画管理。
实践:
只改transform/opacity;必要时will-change但要适度;避免逐帧读写布局(用读写分离)。
降帧与节流:长动画或弱机型降级;可用WAAPI统一时序,结合CSS做性能关键段。
三十五、SSO/第三方登录的流程与风险控制。
一句话概括:
SSO/第三方登录通过标准协议(SAML/OIDC/OAuth2)把认证外包给身份提供方,前端拿授权码或令牌换取会话,但必须在重定向、令牌存放与回放攻击上做严格防护与最小权限。
详细解释:
典型流程(OIDC Code Flow):客户端找到IdP->用户认证并同意->带code重定向回回调地址->后端用code+client_secret换token->建立本地会话并设置短期cookie。
风险与控制:
CSRF/回放:state/nonce校验、防重复使用,回调只接收一次;回调域名白名单。
令牌安全:access/refresh token只存服务端或httpOnly cookie;前端仅存临时非敏信息。
权限最小化:按scope申请最小权限;短期令牌+刷新;登出联动。
链路安全:全程HTTPS、TLS1.2+;防开发重定向。
账户关联:首次登录的账号绑定流程与风险提示;异常低登录告警与MFA。
可观测性:登录成功率、失败原因分布、回放/CSRF拦截率、平均登录时延。
三十六、讲一下静态资源 CDN 策略与缓存失效。
一句话概括:
CDN的核心是“长缓存+文件名指纹”,把资源推到边缘并尽量命中;变更走新URL,不强制回收旧缓存,结合回源/预热与多活域名保障可用性与吞吐。
详细解释:
发布与缓存:
构建产物带content hash(app.abc123.js),HTTP Cache-Control:max-age=31536000,immutable;HTML不缓存或短缓存。
多域名与HTTP/2/3权衡:一般同域复用连接更优,必要时按类型拆域。
失效与回收:
优先“指纹换URL而非强刷缓存”;确需下线走Purge API/版本目录切换。
灰度:新老版本并存,HTML控制入口;预热热点资源与关键地区。
回源与可用:
源站多活、健康检查;CDN超时/失效的回源与重试;流量调度与Geo路由。
跨域与安全:CORS、Subreasource Integrity、TLS。
监控:
命中率、回源率、RTT、错误率、地区分布;异常自动降级与回滚。
三十七、设计一个“搜索建议”组件:触发策略、防抖、聚合与兜底。
概括:
一个健壮的“搜索建议”组件,其核心是在用户输入时,异步获取并展示一个相关的建议列表。为实现优秀的性能和体验,必须采用输入防抖(Debounce)策略来避免高频请求;通过请求中止(AbortController)机制来处理过期的请求;设计聚合多个数据源的能力以丰富建议内;并提供缓存与兜底策略,以应对接口异常或无结果的情况,最终为用户提供快速、准确、可靠的搜索引导。
详细设计拆解:
1.核心功能与状态管理
首先,我们需要定义组件的基础结构和所需的状态。
UI结构:
一个作为用户的输入框。
一个下拉列表
- 或
- 。
核心状态(State):
inputValue(string):当前输入框的值,与用户输入同步。
suggestions(array):搜索建议列表数据。
isLoading(boolean):是否正在加载建议(用于显示loading指示器)。
isOpen(boolean):下拉列表是否可见。
activeIndex(number):当前通过键盘高亮的建议项索引。
error(string | null):是否发生错误及其信息。
2.触发策略与性能优化
这是组件设计的第一个关键点。我们不希望用户的每次按键都触发一次API请求。
触发时机:监听输入框的onChange事件来更新inputValue。
性能策略:防抖(Debounce)
原理:当用户连续输入时,我们不立即发起请求。而是等待用户停止输入一小段时间(例如300ms)后,才将最后一次的inputValue用于发起API请求。
实现:
1.使用useEffectHook来监听inputValue的变化。
2.在useEffect内部,设置一个setTimeout来延迟执行API请求。
3.useEffect的清理函数(cleanup function)中,必须调用clearTimeout来清除上一个定时器。
useEffect(() => { if (!inputValue) { setSuggestions([]); return; } const handler = setTimeout(() => { fetchSuggestions(inputValue); }, 300); // 300ms 防抖延迟 return () => { clearTimeout(handler); }; }, [inputValue]);性能策略:请求中止(Request Cancellation)
场景:用户输入“react”,请求已发出;紧接着用户又快速输入“native”,第二个请求也发出了。如果第二个请求(“native”)的结果先于第一个请求(“react”)返回,就会导致建议列表先显示“native”的结果,然后又被“过期的”、“react”的结果覆盖,造成UI闪烁和数据错乱。
实现:使用AbortControllerAPI。
1.在发起fetch请求前,创建一个AbortController实例。
2.将controller.signal传递给fetch的options对象。
3.在useEffect的清理函数中,或在下一次请求发起前,调用controller.abort()。这会立即中止上一次还未完成的fetch请求。
3.聚合与数据源(Aggregation & Data Sources)
一个强大的搜索建议,其数据来源可能是多样的。
设计:
将fetchSuggestions函数设计成一个聚合器。
它可以并行地向多个数据源发起请求,例如:
api/suggestions?q=...(来自后端的实时建议)
api/search-history?q=...(来自用户的个人搜索历史)
一个本地的、静态的关键词列表(Fuzzy Search)。
使用Promise.allSettled()等待所有请求完成,然后将不同来源的结果聚合、去重、排序后,再更新到suggestions状态中。可以在UI上对不同来源的建议进行分组展示(例如,“搜索历史”、“相关建议”)。
4.兜底与容错(Fallback & Error Handling)
当API请求失败或没有返回结果时,组件应该提供友好的反馈,而不是一片空白。
客户端缓存:
可以使用一个简单的内存缓存(如一个Map对象),或者sessionStorage,来缓存用户最近的几次搜索及其结果。
在发起API请求前,先检查缓存中是否有对应的结果。如果有,可以直接使用缓存数据,实现“瞬间”响应。
无结果时的兜底(No-Result Fallback):
当API返回空数组时,不应该直接关闭下拉列表。
应该在下拉列表中显示一条友好的提示,例如“未找到相关建议”,或者提供一个“搜索全部‘xxx’”的选项,允许用户直接执行全文搜索。
错误处理(Error Handling):
在fetch的.catch()块中捕获错误(特别是请求被abort时会抛出AbortError,需要忽略)。
将错误信息更新到error状态,并在UI上显示一个错误提示,如”建议服务加载失败,请稍后重试“。
5.键盘导航与可访问性(a11y)
键盘导航:
监听输入框的onKeyDown事件。
ArrowDown/ArrowUp:遍历suggestions列表,更新activeIndex,并为高亮项添加active类。
Enter:选中activeIndex对应的建议项,将其值填入输入框,并触发搜索。
Escape:关闭建议列表。
ARIA属性:
为整个组件容器添加role="combobox"。
为输入框添加role="textbox",aria-autocomplete="list",aria-controls="suggestions-list"。
为建议列表
- 添加id="suggestions-list",role="listbox"。
- 添加role="option",并根据activeIndex动态设置aria-selected="true"。
通过将以上这些策略有机地结合在一起,我们就可以构建出一个功能完备、性能优异、体验流畅且高度可靠的搜索建议组件。
三十五、设计一个“文件上传”端到端方案:选型、并发、秒传与失败恢复。
概括:
一个现代化的前端文件上传方案,应根据文件大小和业务场景进行技术选型。对于小文件,采用简单的multipart/form-data表单直传;对于大文件,则必须采用”分片上传+断点续传“的策略。该策略通过前端文件分片和哈希计算(Web Worker),与后端预检/握手接口配合,实现”秒传“和失败恢复能力;并利用并发控制池管理分片上传,最终由后端异步合并分片,完成整个上传流程,确保了在各种网络环境下上传的高效、稳定与可靠。
详细设计拆解:
1.技术选型:场景驱动
场景一:小文件上传(几十MB以下)
如:用户头像、表单附件、普通图片。
方案:multipart/form-data宣传
前端:
1.使用让用户选择文件。
2.创建一个FormData对象。
3.调用formData.append('file',fileObject)将文件对象添加到表单数据中。
4.使用axios或fetch,将FormData对象作为请求体,以POST方法直接上传到后端指定接口。
后端:
1.使用任何标准Web框架(如Express的multer中间件)都能轻松地解决multipart/form-data格式,获取文件内容并保存。
优点:实现及其简单、标准、开箱即用。
缺点:不支持断点续传,整个文件必须一次性上传成功,不适合大文件和弱网环境。
场景二:大文件上传(几百MB到GB级别)
如:高清视频、大型软件安装包、数据集文件。
方案:分片上传(Chunking Upload)+断点续传(Resumable Upload)
这是一个更复杂但更健壮的系统,涉及前后端的密切配合。下面将详细展开。
2.大文件上传方案:端到端流程。
Phase 1:前端准备阶段
1.文件选择:用户选择一个大文件。
2.生成文件唯一标识(File ID):
1.这是整个方案的“身份证”。最佳实践是计算文件的内容哈希(如MD5或更快的SparkMD5)。
2.性能优化:计算哈希是一个CPU密集型操作,必须在Web Worker中进行,以避免冻结UI主线程。
3.Web Worker会读取文件内容(可以按分片增量计算),最终将计算出的fileHash返回给主线程。
3.文件分片(Slicing):
1.在主线程中,使用File.prototype.slice()方法,将大文件切割成多个固定大小的数据块(chunk),例如,每片2MB。
2.将这些分片Blob对象,连同它们的索引(index)和文件哈希(fileHash),一起存储在一个任务列表中。
Phase 2:上传前“握手”(Handshake)
1.发生预检请求(/verify):
1.在开始上传任何分片之前,前端向后端发生一个“预检”请求。
2.请求参数:{fileHash, fileName, totalChunks}。
2.后端处理预检:
1.后端收到请求后,根据fileHash检查该文件的状态:
1.Case A:秒传(Instant Upload):
1.在服务器的文件库中,发现已存在具有相同的fileHash的、已合并的完整文件。
2.直接返回{ shouldUpload: false, message: 'Upload success' }。前端收到后,直接提示用户上传成功。流程结束。
2.Case B:断点续传(Resumable Upload):
1.在服务器的临时分片目录中,发现了部分已上传的、属于该fileHash的分片。
2.返回{ shouldUpload: true, uploadedChunks:['1', '3', '5'] },告诉前端哪些分片已经存在了。
3.Case C:全新上传(New Upload):
1.服务器上没有任何关于此fileHash的信息。
2.返回 { shouldUpload: true, uploadedChunks: [] }。
Phase 3:并发上传与失败恢复:
1.过滤已上传分片:前端根据预检接口返回的uploadedChunks列表,从自己的任务列表中,过滤掉那些不需要上传的分片。
2.创建并发控制池:
1.为了避免一次性发起成百上千请求,前端需要实现一个异步任务并发池。
2.设置一个并发数,例如concurrency = 4。
3.同时从任务队列中取出4个分片任务进行上传。每当一个上传成功,就从队列中再取一个新任务,始终保持4个并发。
3.上传单个分片:
1.每个分片的上传请求,也使用FormData格式。
2.请求参数:{ chunk: chunkBlob, index: chunkIndex, fileHash }。
3.后端接收到分片后,在临时目录中,以fileHash-index的格式保存。
4.失败恢复(Failure Recovery):
1.请求重试:在并发池的单个任务中,为每个分片的上传请求包裹一层重试逻辑。如果上传失败(网络错误或服务器 5xx 错误),则自动重试2-3次。
2.暂停/续传:如果用户手动暂停,或关闭了服务器。下次用户选择同一个文件时,流程会从Phase 1重新开始。但由于fileHash不变,在Phase 2的预检中,服务器会正确地返回已上传的分片列表,从而自然地实现了断点续传。
Phase 4:合并与清理
1.发送合并请求(/merge):
1.当前端的分片任务队列全部完成后,向后端发送一个“合并”请求。
2.请求参数:{ fileHash, fileName, chunkSize }。
2.后端异步合并:
1.后端收到合并请求后,不应立即在当前请求中执行合并,因为合并一个大文件可能耗时很长。
2.最佳实践:
1.立即返回一个“合并请求已收到,正在处理中”的响应给前端。
2.将这个合并任务推入一个后台的消息队列(如RabbitMQ,Kafka)。
3.由一个专门的后台工作进场(Worker)消费队列中的任务,在后台慢慢地、安全地进行文件合并。
1.合并逻辑:根据fileHash和totalChunks,读取所有临时分片,按索引顺序写入到一个最终的文件中。
2.清理:合并成功后,删除所有临时分片文件。
3.状态通知:合并完成后,可以通过WebSocket或其他方式,通知前端文件已处理完毕。
通过这个端到端的闭环设计,我们可以构建一个能够从容应对各种文件大小和网络状况的、工业级的上传系统。
为每个建议项
- 添加role="option",并根据activeIndex动态设置aria-selected="true"。
列表中的每个建议项