1. 跨域
什么是跨域?
跨域是因浏览器的同源策略引发的限制:当网页的协议/域名/端口任意一项与请求资源不一致时,浏览器会拦截跨域请求的响应(注意:请求能发出,响应被浏览器拦截)。
同源策略限制内容
- 存储型数据:Cookie、LocalStorage 等
- DOM 访问:不同源页面的 DOM 无法操作
- AJAX 响应:请求可发送,但结果被浏览器拦截
例外标签:<img>、<link>、<script> 可跨域加载资源。
主流跨域解决方案
- CORS(推荐)
原理:服务端在响应头添加 Access-Control-Allow-* 字段(如 Access-Control-Allow-Origin: *)。
特点:
- 简单请求(GET/POST/HEAD)自动完成
- 非简单请求(如PUT/DELETE)需先发
OPTIONS预检请求 - 带 Cookie 需设
withCredentials: true及服务端Access-Control-Allow-Credentials: true
- 代理服务器
原理:前端请求同源代理,代理转发至目标服务器(避开浏览器限制)。
实现:
- 开发环境:Webpack/Vite 配置
proxy - 生产环境:Nginx 反向代理
- JSONP(历史方案)
- 原理:利用
<script>标签无跨域限制的特性,通过回调函数获取数据。 - 局限:仅支持 GET 请求,存在 XSS 风险,逐渐被淘汰。
其他方案(简述)
- WebSocket:协议本身支持跨域
- postMessage:多窗口间数据传输
示例回答
“跨域是因浏览器同源策略导致不同源请求的响应被拦截。主流方案有三种:一是 CORS,后端配置响应头允许跨域;二是用代理服务器中转请求;三是 JSONP,通过 script 标签回调获取数据(仅限 GET)。首选 CORS,因其安全灵活且支持所有 HTTP 方法。”
2. 浏览器缓存机制
浏览器缓存分为强制缓存和协商缓存,浏览器加载一个页面的简单流程如下:
- 浏览器先根据这个资源的http头信息来判断是否命中强缓存。如果命中则直接加在缓存中的资源,并不会将请求发送到服务器。(强缓存)
- 如果未命中强缓存,则浏览器会将资源加载请求发送到服务器。服务器来判断浏览器本地缓存是否失效。若可以使用,则服务器并不会返回资源信息,浏览器继续从缓存加载资源。(协商缓存)
- 如果未命中协商缓存或者判断浏览器本地缓存失效了,则服务器会将完整的资源返回给浏览器,浏览器加载新资源,并更新缓存。(新的请求)
强缓存
强缓存:不发送请求,直接使用本地缓存
强缓存实现方式:
Expires(HTTP/1.0)
-
绝对时间,如:
Expires: Wed, 21 Oct 2025 07:28:00 GMT -
缺点:依赖客户端时间,可能不准确
Cache-Control(HTTP/1.1)
-
相对时间,如:
Cache-Control: max-age=3600 -
常用指令:
public:允许代理缓存、private:仅浏览器缓存、no-cache:强制协商缓存、no-store:禁止缓存、max-age:缓存有效期(秒)
特点:
- 命中时返回200状态码,显示
(from cache) Cache-Control优先级高于Expires
协商缓存
协商缓存:需要向服务器验证缓存是否有效
触发条件:强缓存失效时触发
实现方式:
Last-Modified/If-Modified-Since
- 首次响应:
Last-Modified: [时间] - 后续请求:
If-Modified-Since: [上次Last-Modified值] - 缺点:精度只到秒级,文件内容不变但修改时间变化时会失效
ETag/If-None-Match
- 首次响应:
ETag: [文件指纹] - 后续请求:
If-None-Match: [上次ETag值] - 优点:精确判断文件内容变化
特点:
- 命中时返回304状态码
- 服务器优先验证ETag,再验证Last-Modified
缓存流程
-
检查强缓存(Cache-Control/Expires)
- 有效:使用缓存
- 无效:进入协商缓存
-
发送请求验证协商缓存(ETag/Last-Modified)
- 有效:返回304,使用缓存
- 无效:返回200和新资源
常见面试问题
Q:为什么要有ETag?
A:解决Last-Modified的局限性:
- 秒级精度不足
- 文件内容未变但修改时间变化
- 服务器时间不准确
Q:Cache-Control的no-cache和no-store区别?
- no-cache:可以缓存,但使用前必须验证
- no-store:完全禁止缓存
Q:如何彻底禁用缓存?
设置:Cache-Control: no-store
面试回答示例
"强缓存通过
Cache-Control/Expires直接读本地资源(状态码 200),协商缓存由服务器验证返回 304 或新数据。彻底禁止缓存有三种方式:一是在请求头设置Cache-Control: no-store;二是在 URL 后加时间戳等随机参数;三是服务端响应头指定no-store或立即过期策略,其中no-store是最彻底的解决方案。"
3. 打包工具作用
模块化支持:
现代前端开发通常采用模块化的方式,将代码拆分成多个独立的模块。每个模块负责特定的功能,这样可以提高代码的可维护性和复用性。然而,浏览器并不直接支持模块化开发,因此需要打包工具将这些模块合并成一个或多个浏览器可以识别的静态文件。
性能优化:
- 「代码压缩」:去除代码中的空格、注释等不必要的字符,减小文件大小。
- 「代码分割」:将代码分割成多个小文件,按需加载,减少初始加载时间。
- 「Tree Shaking」:移除未使用的代码,进一步减小文件大小。
- 「缓存优化」:通过哈希值等方式,使浏览器能够缓存静态资源,加快后续加载速度。
资源统一处理:
前端项目不仅包含 JavaScript 文件,还可能包含 CSS、图片、字体等多种类型的文件。打包工具可以处理这些不同类型的文件,并将它们合并成一个或多个浏览器可以识别的静态文件。
开发效率提升:
- 「热更新」:在代码发生变化时,自动重新编译并刷新浏览器,减少开发者的等待时间。
- 「代码检查」:在编译过程中自动检查代码中的错误和潜在问题,帮助开发者及时发现和修复问题。
- 「自动化测试」:在编译过程中自动运行测试用例,确保代码质量。
兼容性解决:
不同的浏览器对 JavaScript 和 CSS 的支持程度不同。打包工具可以通过转译(transpiling)等方式,将现代 JavaScript 和 CSS 转换为兼容性更好的版本,确保在各种浏览器中都能正常运行。
生态整合:
现代前端开发通常依赖于各种第三方库和框架。打包工具可以自动下载和管理这些依赖,并将它们合并到最终的打包文件中,简化了项目的依赖管理。
面试一句话总结:
“打包工具实现模块化构建、性能优化(压缩/拆包/摇树)、资源编译(CSS/JS/图片)、开发提效(HMR),并解决浏览器兼容问题,是现代前端工程的基建支柱。”
4. Webpack与Vite区别
1. 构建速度
Webpack:冷启动需打包所有模块(尤其大型项目慢),热更新重建部分模块。
Vite:冷启动快10-100倍(ESM原生加载 + Go语言esbuild预构建),热更新仅重载修改模块(毫秒级)。
2. 开发模式
Webpack:打包代码成Bundle → 浏览器加载Bundle(修改后需局部重建)。
Vite:原生ESM加载 → 浏览器直接请求源码(按需编译,不打包)。
3. 生产处理
Webpack:深度打包优化(Tree-shaking、代码分割等)。
Vite:用Rollup打包(与开发行为解耦,保留ESM优势)。
4. 插件生态
Webpack:插件生态成熟(覆盖复杂场景)。 Vite:兼容Rollup插件(生态较新,但满足主流需求)。
面试一句话回答:
“Vite基于浏览器ESM和esbuild实现秒级冷启动+实时热更新,开发阶段无需打包;Webpack通过打包Bundle实现兼容性(生态更成熟)。生产环境Vite用Rollup打包,Webpack自带深度优化。
Webpack 的构建流程是怎样的?
- 初始化:读取配置文件,初始化 Compiler 对象。
- 编译:从入口文件开始,递归解析依赖,生成依赖图。
- 构建:根据依赖图,调用 loader 处理模块,生成 chunk。
- 输出:将 chunk 写入文件系统,生成最终的 bundle 文件。
回答思路:
- Webpack 的构建流程可以分为初始化、编译、构建和输出四个阶段。
- 通过 loader 处理不同类型的文件,通过 plugin 扩展功能。
Vite 的构建流程是怎样的?
- 开发环境:Vite 启动一个开发服务器,利用浏览器的原生 ES 模块支持,按需加载模块。Vite 通过拦截浏览器请求,将模块转换为浏览器可识别的格式。
- 生产环境:Vite 使用 Rollup 进行打包,生成优化的静态资源。
回答思路:
- Vite 在开发环境下利用 ES 模块的特性,避免了打包的过程,启动速度更快。
- 生产环境下,Vite 使用 Rollup 进行打包,生成优化的静态资源。
Webpack 的优化策略有哪些?
- 代码分割(Code Splitting) :通过
SplitChunksPlugin将代码拆分成多个 chunk,实现按需加载。 - Tree Shaking:通过 ES 模块的静态分析,移除未使用的代码。
- 缓存:使用
cache-loader或hard-source-webpack-plugin缓存构建结果,加快构建速度。 - 懒加载(Lazy Loading) :通过动态导入(
import())实现懒加载,减少初始加载时间。 - 压缩代码:使用
TerserPlugin压缩 JavaScript 代码,使用CssMinimizerPlugin压缩 CSS 代码。
Vite 的优化策略有哪些?
- 按需加载:利用 ES 模块的特性,按需加载模块,减少初始加载时间。
- 预构建:Vite 会预构建依赖(
node_modules中的模块),减少开发环境下的请求数量。 - 缓存:Vite 会缓存预构建的依赖,加快后续启动速度。
- 生产环境优化:Vite 使用 Rollup 进行打包,支持 Tree Shaking 和代码压缩。
webpack 优点
首屏加载、懒加载
-
由于 dev 启动过程中已经完成整个打包操作,直接将构建好的首屏内容发送给浏览器,不存在性能问题;
-
也是由于进行了打包操作,所以的依赖在 dev 构建过程中都得以处理,懒加载也不存在问题
webpack 缺点
复杂应用的情况下:本地启动时间会比较长、热更新的反应速度比较慢
- 由于本地开发环境,webpack 也会先进行打包,然后再在服务器运行项目,所以如果项目庞大就会出现启动慢的情况
- 一些打包器的开发服务器将构建内容存入内存,这样它们只需要在文件更改时使模块图的一部分失活,但它也仍需要整个重新构建并重载页面。
- 这样代价很高,并且重新加载页面会消除应用的当前状态,所以打包器支持了动态模块热替换(HMR):允许一个模块 “热替换” 它自己,而不会影响页面其余部分。这大大改进了开发体验。
- 然而即使采用了 HMR 模式,其热更新速度也会随着应用规模的增长而显著下降。
Vite 优点
Vite 启动项目快、热更新快
-
Vite 的原理是借助了浏览器对 ESM 规范的支持,Vite 无需进行 bundle 操作,Vite 项目、源文件之间的依赖关系通过浏览器对 ESM 规范的支持来解析;
-
所以在启动过程中,只需要进行一些初始化的操作,其余全部交由浏览器处理,所以项目启动非常之快; 即使是启动大型项目,也不会出现卡顿的现象。
-
本地开发环境,在监听到文件变化以后,直接通过 ws 连接,通知浏览器去重新加载变化的文件; 在 Vite 中 HMR 是在原生 ESM 上执行的,编辑一个文件时,Vite 只需要精确地编辑模块本身,使得 HMR 始终能保持快速更新;
-
源码缓存:Vite 同时利用 HTTP 头来加速整个页面的重新加载,源码模块的请求会进行协商缓存;依赖模块缓存:解析后的依赖请求则会进行强缓存,因此一旦被缓存它们将不需要再次请求。
Vite 缺点
首屏加载慢、懒加载慢
- 没有对文件进行 bundle 操作,会导致大量的 http 请求
- dev 服务运行期间会对源文件做转换操作,需要时间
- 尽管预构建很快,但是也会阻塞首屏的加载
- Vite 需要把 webpack dev 启动完成的工作,移接到了 dev 响应浏览器的过程中,时间加长
- 但是由于缓存的存在,当第一次加载完成之后,再次 reload 的时候性能会有所提升
- 和首屏加载一样,动态加载的文件需要对源文件进行转换操作
- 可能会有大量的 http 请求,懒加载的性能同样会受到影响
5. 什么是CDN,它有什么作用?
CDN(内容分发网络) 是一种分布式网络架构,由分布在不同地理位置的边缘节点服务器组成,旨在通过缓存内容并将其分发到离用户最近的节点,提升内容传输速度、减少延迟并优化用户体验。CDN广泛应用于网站加速、视频流媒体、文件下载等场景。
CDN的工作原理
CDN通过将源站的内容缓存到各地的边缘节点,当用户发起请求时,系统会根据用户的地理位置、网络运营商等因素,将请求引导到最近的缓存节点。如果缓存节点已有所需内容,则直接返回;否则会从源站获取内容并缓存到节点,供后续用户访问。
以下是CDN的主要流程:
- 用户请求内容时,DNS解析会将请求指向最优的CDN节点。
- 如果节点已有缓存内容,直接返回给用户。
- 若无缓存内容,节点会向源站请求并缓存,随后返回给用户。
核心作用:
- 提升访问速度:通过就近访问缓存节点,减少物理距离和网络延迟,显著提升页面加载速度。
- 降低带宽成本:缓存技术减少源站的直接请求次数,从而降低带宽消耗。
- 提高可用性:分布式架构能有效应对高流量和硬件故障,确保服务稳定。
- 增强安全性:通过DDoS防护、TLS/SSL加密等技术,提升网站的安全性。
- 减轻源站压力:分担源站负载,避免因流量激增导致的宕机。
技术原理:
- 智能调度:DNS解析定位最优节点
- 边缘缓存:节点存储资源副本
- 内容预热:提前缓存热门资源
面试一句话回答:
“CDN通过全球边缘节点缓存静态资源,让用户就近访问,大幅提升加载速度并保护源服务器,同时提供高可用性和安全防护。”
6. CommonJS与ESM区别
1. 语法差异
CommonJS:require() 导入 | module.exports 导出
ES模块:import 导入 | export 导出
2. 加载机制
CommonJS同步加载:阻塞执行,适用于服务端(Node.js)
ES模块异步加载:非阻塞,浏览器原生支持(编译时静态解析依赖)
3. 执行时机
CommonJS:运行时加载(动态导入)
ES模块:编译时解析(静态导入,支持 import() 动态语法)
4. 导出本质
CommonJS:导出值的拷贝(修改导出值不影响原模块)
ES模块:导出值的引用(实时绑定,修改导出值同步影响所有导入)
5. 作用域
CommonJS:模块级作用域(不污染全局)
ES模块:文件级作用域(严格模式默认启用)
6. 使用场景
CommonJS:Node.js 环境主流方案
ES模块:浏览器原生支持,现代前端构建标配(支持 Tree-shaking)
面试一句话回答:
“CommonJS 同步加载、导出值拷贝,用于 Node.js;ES 模块异步加载、导出实时绑定,是浏览器原生标准。ES 模块支持静态分析实现 Tree-shaking,而 CommonJS 依赖动态解析。”
7.当在浏览器中输入 Google.com 并且按下回车之后发生了什么?
(1)解析URL: 首先会对 URL 进行解析,分析所需要使用的传输协议和请求的资源的路径。如果输入的 URL 中的协议或者主机名不合法,将会把地址栏中输入的内容传递给搜索引擎。如果没有问题,浏览器会检查 URL 中是否出现了非法字符,如果存在非法字符,则对非法字符进行转义后再进行下一过程。
(2)缓存判断: 浏览器会判断所请求的资源是否在缓存里,如果请求的资源在缓存里并且没有失效,那么就直接使用,否则向服务器发起新的请求。
(3)DNS解析: 下一步首先需要获取的是输入的 URL 中的域名的 IP 地址,首先会判断本地是否有该域名的 IP 地址的缓存,如果有则使用,如果没有则向本地 DNS 服务器发起请求。本地 DNS 服务器也会先检查是否存在缓存,如果没有就会先向根域名服务器发起请求,获得负责的顶级域名服务器的地址后,再向顶级域名服务器请求,然后获得负责的权威域名服务器的地址后,再向权威域名服务器发起请求,最终获得域名的 IP 地址后,本地 DNS 服务器再将这个 IP 地址返回给请求的用户。用户向本地 DNS 服务器发起请求属于递归请求,本地 DNS 服务器向各级域名服务器发起请求属于迭代请求。
(4)获取MAC地址: 当浏览器得到 IP 地址后,数据传输还需要知道目的主机 MAC 地址,因为应用层下发数据给传输层,TCP 协议会指定源端口号和目的端口号,然后下发给网络层。网络层会将本机地址作为源地址,获取的 IP 地址作为目的地址。然后将下发给数据链路层,数据链路层的发送需要加入通信双方的 MAC 地址,本机的 MAC 地址作为源 MAC 地址,目的 MAC 地址需要分情况处理。通过将 IP 地址与本机的子网掩码相与,可以判断是否与请求主机在同一个子网里,如果在同一个子网里,可以使用 APR 协议获取到目的主机的 MAC 地址,如果不在一个子网里,那么请求应该转发给网关,由它代为转发,此时同样可以通过 ARP 协议来获取网关的 MAC 地址,此时目的主机的 MAC 地址应该为网关的地址。
(5)TCP三次握手: 下面是 TCP 建立连接的三次握手的过程,首先客户端向服务器发送一个 SYN 连接请求报文段和一个随机序号,服务端接收到请求后向服务器端发送一个 SYN ACK报文段,确认连接请求,并且也向客户端发送一个随机序号。客户端接收服务器的确认应答后,进入连接建立的状态,同时向服务器也发送一个ACK 确认报文段,服务器端接收到确认后,也进入连接建立状态,此时双方的连接就建立起来了。
(6)HTTPS握手: 如果使用的是 HTTPS 协议,在通信前还存在 TLS 的一个四次握手的过程。首先由客户端向服务器端发送使用的协议的版本号、一个随机数和可以使用的加密方法。服务器端收到后,确认加密的方法,也向客户端发送一个随机数和自己的数字证书。客户端收到后,首先检查数字证书是否有效,如果有效,则再生成一个随机数,并使用证书中的公钥对随机数加密,然后发送给服务器端,并且还会提供一个前面所有内容的 hash 值供服务器端检验。服务器端接收后,使用自己的私钥对数据解密,同时向客户端发送一个前面所有内容的 hash 值供客户端检验。这个时候双方都有了三个随机数,按照之前所约定的加密方法,使用这三个随机数生成一把秘钥,以后双方通信前,就使用这个秘钥对数据进行加密后再传输。
(7)返回数据: 当页面请求发送到服务器端后,服务器端会返回一个 html 文件作为响应,浏览器接收到响应后,开始对 html 文件进行解析,开始页面的渲染过程。
(8)页面渲染: 浏览器首先会根据 html 文件构建 DOM 树,根据解析到的 css 文件构建 CSSOM 树,如果遇到 script 标签,则判端是否含有 defer 或者 async 属性,要不然 script 的加载和执行会造成页面的渲染的阻塞。当 DOM 树和 CSSOM 树建立好后,根据它们来构建渲染树。渲染树构建好后,会根据渲染树来进行布局。布局完成后,最后使用浏览器的 UI 接口对页面进行绘制。这个时候整个页面就显示出来了。
(9)TCP四次挥手: 最后一步是 TCP 断开连接的四次挥手过程。若客户端认为数据发送完成,则它需要向服务端发送连接释放请求。服务端收到连接释放请求后,会告诉应用层要释放 TCP 链接。然后会发送 ACK 包,并进入 CLOSE_WAIT 状态,此时表明客户端到服务端的连接已经释放,不再接收客户端发的数据了。但是因为 TCP 连接是双向的,所以服务端仍旧可以发送数据给客户端。服务端如果此时还有没发完的数据会继续发送,完毕后会向客户端发送连接释放请求,然后服务端便进入 LAST-ACK 状态。客户端收到释放请求后,向服务端发送确认应答,此时客户端进入 TIME-WAIT 状态。该状态会持续 2MSL(最大段生存期,指报文段在网络中生存的时间,超时会被抛弃) 时间,若该时间段内没有服务端的重发请求的话,就进入 CLOSED 状态。当服务端收到确认应答后,也便进入 CLOSED 状态。
8.白屏检测
如何设计出一种,在准确性、通用型、易用性等方面均表现良好的检测方案呢?
方案一:检测根节点是否渲染
原理很简单,在当前主流 SPA 框架下,DOM 一般挂载在一个根节点之下(比如 <div id="app"></div> ),发生白屏后通常是根节点下所有 DOM 被卸载,该方法通过检测根节点下是否挂载 DOM,若无则证明白屏
这是简单明了且有效的方案,但缺点也很明显:其一切建立在 白屏 === 根节点下 DOM 被卸载 成立的前提下,缺点是通用性较差,对于有骨架屏的情况束手无策
方案二:Mutation Observer 监听 DOM 变化
通过此 API 监听页面 DOM 变化,并告诉我们每次变化的 DOM 是被增加还是删除
但这个方案有几个缺陷
1)白屏不一定是 DOM 被卸载,也有可能是压根没渲染,且正常情况也有可能大量 DOM 被卸载
2)遇到有骨架屏的项目,若页面从始至终就没变化,一直显示骨架屏,这种情况 Mutation Observer 也束手无策
方案三:页面截图检测
整体流程:对页面进行截图,将截图与一张纯白的图片做对比,判断两者是否足够相似
但这个方案有几个缺陷:
1、方案较为复杂,性能不高;一方面需要借助 canvas 实现前端截屏,同时需要借助复杂的算法对图片进行对比
2、通用性较差,对于有骨架屏的项目,对比的样张要由纯白的图片替换成骨架屏的截图
方案四:采样对比
该方法是对页面取关键点,进行采样对比,在准确性、易用性等方面均表现良好,也是最终采用的方案
对于有骨架屏的项目,通过对比前后获取的 dom 元素是否一致,来判断页面是否变化