(二)浏览器 面试题详解(2024)

252 阅读29分钟

按照网上习惯性的文章撰写路线,第二篇应该是 CSS,也确实,各种各样的标签,也是由 CSS 在背后默默地做属性支持。但是对于 HTML 的很多问题,跟浏览器的关系太大了,让我不禁想去好好看看 HTML 跟浏览器之间的那些事。另外,可能一提到前端,绝大多数人都会脱口而出“JavaScript”,不过我觉得 CSS 才是前端技术里最精彩绝伦的技术,所以后面我会对 CSS 做一个特殊处理,先让我先把浏览器相关面试题做一份梳理吧。​

先简单认识一下什么是浏览器......

1、浏览器的主要组成部分

  • 用户界面:除了浏览器主窗口显示的请求的页面外,其他显示的各个部分都属于用户界面。
  • 浏览器引擎:在用户界面和呈现引擎之间传送指令。
  • 呈现引擎:负责显示请求的内容。如果请求的内容是 HTML,它就负责解析 HTML 和 CSS 内容,并将解析后的内容显示在屏幕上。
  • 网络:用于网络调用(如 HTTP 请求)。其接口与平台⽆关,并为所有平台提供底层实现。
  • 用户界⾯后端:用于绘制基本的窗口小部件,比如组合框和窗口。其公开了与平台⽆关的通用接口,而在底层使用操作系统的⽤户界面方法。
  • JavaScript 解释器:用于解析和执行 JavaScript 代码。
  • 数据存储:这是持久层。浏览器需要在硬盘上保存各种数据(如 Cookie)。HTML5  定义了“网络数据库”,这是⼀个完整(轻便)的浏览器内数据库。Chrome 浏览器的每个标签⻚都是⼀个独立的进程。

2、浏览器内核

什么是浏览器内核

浏览器内核作为浏览器最核心的部分Rendering Engine,也就是渲染引擎,它决定了浏览器如何解析网页语法并渲染网页内容。

常见的浏览器内核

浏览器/RunTime内核(渲染引擎)JavaScript 引擎
ChromeBlink(28~) Webkit(Chrome 27)V8
FireFoxGeckoSpiderMonkey
SafariWebkitJavaScriptCore
EdgeEdgeHTMLChakra(for JavaScript)
IETridentChakra(for JScript)
PhantomJSWebkit JavaScriptCoreNode.js - V8

上面其实做个简单的了解就可以了,接下来是关于浏览器我觉得最精彩的一个问题,其中又包含了多个经典的面试题。在以前的文章我有做过总结,这里我再次梳理一下。

3、浏览器中输入一个 url 到显示页面经历的过程 🔥

这个过程其实就是一次完整的 http(1.0) 请求过程:

  1. URL 解析:浏览器对输入的 URL 进行解析,提取出主机名。
  2. DNS 查询‌: DNS 查询主机名,得到对应的 IP 地址。
  3. 建立 TCP 连接:根据这个 IP ,发起 TCP 连接( TCP 的三次握手)
  4. 发送 HTTP 请求:建立 TCP 连接后,发起 HTTP 请求
  5. 服务器返回 HTTP 响应:服务器响应数据发送回浏览器,包括状态码、HTML、CSS、JS
  6. 浏览器解析并渲染页面:浏览器接收响应后,解析响应内容,渲染页面。

整个过程清晰可见,让我们开始逐个击破吧 ~~~

URL 是啥?

URL(Uniform Resource Locator),统一资源定位符,用于定位互联网上资源,俗称网址。

看一下它的定义规则:

scheme://host.domain:port/path/filename

各部分解释如下:

  • scheme(协议):定义因特网服务的类型。常见的协议有httphttpsftpfile,其中最常见的类型是 http,而https则是进行加密的网络传输。
  • host(主机):定义域主机(http 的默认主机是 www)
  • domain(主机名):定义因特网域名,比如 w3school.com.cn
  • port(端口):定义主机上的端口号(http 的默认端口号是 80)
  • path(路径):定义服务器上的路径(如 /index.html,如果省略,则文档必须位于网站的根目录中)。
  • filename(资源名):定义文档/资源的名称

DNS 怎么找到域名的?

我们先看几个小概念

  • DNS:一个网络服务器
  • DNS 协议:提供通过域名查找 IP 地址,或逆向从 IP 地址反查域名的服务
  • DNS 域名解析:即在 DNS 上记录一条信息记录(域名对应的 IP 地址)

那浏览器如何通过域名去查询 URL 对应的 IP 呢?

过程(面试):浏览器自身域名缓存区找 =》操作系统的域名缓存区找 =》本地 hosts 文件找 =》DNS 服务器找

为什么 HTTP 协议要基于 TCP 来实现? TCP 是一个端到端的可靠的面相连接的协议, HTTP 基于传输层 TCP 协议,不用担心数据传输的各种问题(当发生错误时,会重传)

拿到页面资源后,浏览器是如何渲染页面的呢? 🔥

  1. 构建 DOM 树:浏览器解析 HTML 文档,将标签转换成 DOM 节点构建 DOM 树,并获取外部资源(CSS 文件、JS文件、图片等)
  2. 构建 CSSOM 树:同时解析 CSS 文件,构建 CSSOM 树,描述了各个 DOM 节点的样式
  3. 生成 渲染树:将 DOM 树和 CSSOM 树结合在一起,生成渲染树(只包含可见元素)
  4. 布局(重排):浏览器计算渲染树中每个节点的几何形状和位置,执行布局(首次重排)
  5. 绘制:遍历渲染树,使用 UI 层绘制每个节点 ,呈现界面

既然涉及到了布局和渲染,那怎么能少了重绘和重排(也即回流)呢 ~~~

4、对重排和重绘的了解

概念

  • 重排:渲染树中的元素布局或几何属性(如尺寸、隐藏)等改变,需要重新构建
  • 重绘:元素外观(如背景颜色)改变引起的浏览器行为,使元素外观重新绘制

注意点

  • 每个页面至少发生一次重排,即页面第一次加载的时候
  • 重排必定引发重绘,重绘不一定引发重排

补充

触发重排的条件:任何页面布局或几何属性的改变

  • 页面渲染初始化(无法避免)
  • 添加或删除可见的DOM元素
  • 元素位置的改变,或使用动画
  • 元素尺寸的改变(大小,外边距,边框)
  • 浏览器窗口尺寸的变化(resize事件)
  • 填充内容的改变(文本或图片大小改变,引起计算值宽高改变)
  • 读取某些元素属性(offsetLeft/Top/Height/Width, clientLeft/Top/Height/Width,scrollLeft/Top/Height/Width等)

如何优化?

重排重绘会造成耗时、浏览器卡顿,那么如何做优化呢?(尽量减少DOM操作)

  • 浏览器优化:浏览器会把引起重排、重绘的操作放入一个队列,等队列中的操作到了一定数量或者时间间隔,就 flush 这个队列进行一个批处理。这样就可以让多次重排重绘变成一次。
  • 代码优化:减少对渲染树的操作,可以合并多次 DOM 和样式的修改,并减少对样式的请求。

一些代码优化操作举例:

  • 修改元素样式的时候,直接修改样式名className(尽量一次修改元素样式,不要一会改一点。也就是把新样式放在另一个样式名中)
  • 某些元素先设置成display: none,然后进行页面布局操作,再设置display: block(这样只会引发两次重绘回流)
  • 使用cloneNoderepalceChild技术(引发一次重绘回流)
  • 将需要多次重排的元素,position属性设为absolutedfixed(元素脱离文档流,变化不会影响其他元素)
  • 当需要创建多个节点的时候,使用DocumentFragment创建完后一次性的加入(如循环创建一个li,让循环结束后所有的li都创建完了(fragment中)再一次性加入(文档))

顺利完成了一次资源的请求,我们浏览器拿到了服务器发送过来的资源,为了节省某些资源上的性能,当然少不了存储了,那么请列出你所知道的浏览器的一些存储办法以及它们的区别吧 ~

5、浏览器本地存储的方式与区别

这一块以前做过一张还不错的表,这里附上,并做了一些改进。

本地存储区别表

WebStorage
cookieIndexedDBsessionstoragelocalstorage
数据生命周期一般由服务器生成,可设置过期时间 (expires),与窗口或浏览器是否关闭无关。若在浏览器设置, 默认浏览器关闭后失效无过期时间,数据持久保存仅在当前会话有效,关闭窗口或浏览器窗口后清除无过期时间,除非主动删除
存放数据≤ 4kb,数量最多 20 条,存储字符串较大,通常超过 5M5M,存储字符串5M
与服务端通信保存在浏览器端。每次都会携带在 HTTP 请求头中,参与服务器通信保存在客户端(本地存储),不参与服务器通信
安全性安全性较低(cookie 诈骗 cookie 截取)
易用性一般接口需要自己封装接口可以直接使用
作用域同源窗口共享同源窗口共享,支持更复杂的数据结构同源窗口共享,每个标签页或窗口都有独立的 sessionStorage同源窗口共享
使用场景主要用于用户身份验证、会话管理 1、判断用户是否登录过网站,方便下次登录实现自动登录或记住密码 2、上次登录的时间等信息 3、上次查看的页面 4、浏览计数需要存储大量结构化数据的应用,如离线应用、复杂数据查询等敏感账号一次性登录、表单(只需要在用户浏览一组页面期间保存而关闭浏览器后就可以丢弃的数据 )常用于长期登录(判断用户是否登录),适合长期保存在本地的数据。用户设置、偏好、缓存数据、购物车信息、HTML5 游戏产生的一些本地数据
优点具有极高的扩展性和可用性1、存储空间大 2、节省网络流量 3、可在本地直接获取,不需要与服务器交互 4、获取速度快 5、安全性较高 6、更多丰富易用的 API 接口 7、支持事件通知机制,可以将 数据更新的通知发送给监听者 8、操作方便:setItem、 getItem、removeItem、 clear、key、length 9、临时存储
缺点1、大小受限 2、用户可以禁用 cookie,使功能受限 3、安全性较低 4、有些状态不能保存在客户端 5、同源请求时会被携带(不论是否需要),数据过多影响性能 6、cookie 数据有路径(path)的概念, 可以限制 cookie 只属于某个路径下

简化表

特性CookiesLocalStorageSessionStorageIndexedDB
存储大小~4KB~5MB~5MB大量(GB级别)
生命周期可设置持久存储会话存储持久存储
数据发送会随请求发送不会发送不会发送不会发送
数据类型只能存储字符串只支持字符串只支持字符串支持复杂数据结构
使用场景认证、跟踪用户设置、缓存临时数据离线应用、复杂查询

优缺点分析

Cookies

优点

  • 跨请求:Cookies 会随每个 HTTP 请求发送到服务器,适合用于用户身份验证和会话管理。
  • 灵活的过期时间:可以设置过期时间,支持持久性存储。

缺点

  • 存储限制:每个 cookie 的大小通常限制在 4KB 左右,存储空间有限。
  • 性能影响:每次请求都会发送 cookie,可能影响性能。
  • 安全性:容易受到 CSRF(跨站请求伪造)攻击。

LocalStorage

优点

  • 存储容量大:通常支持 5MB 或更多的存储空间,适合存储大量数据。
  • 简单易用:API 简单,使用方便,数据可以持久保存。
  • 不随请求发送:数据仅存储在客户端,不会影响服务器请求。

缺点

  • 安全性:数据以明文存储,容易被 JavaScript 访问,存在 XSS(跨站脚本)攻击风险。
  • 同步问题:LocalStorage 是同步操作,在大型数据量操作时可能影响性能。

SessionStorage

优点

  • 存储容量大:同样支持 5MB 或更多的存储空间。
  • 会话独立性:每个标签页或窗口有独立的存储,适合临时数据存储。
  • 不随请求发送:仅存储在客户端,不会影响服务器请求。

缺点

  • 生命周期短:数据在窗口或标签页关闭后会被清除,适合短期存储而非持久存储。
  • 安全性:同样容易受到 XSS 攻击。

IndexedDB

优点

  • 存储容量大:支持存储大量结构化数据,通常是 GB 级别。
  • 异步操作:支持异步 API,适合于复杂数据存储和批量操作,提高性能。
  • 支持复杂数据:可以存储对象、数组等复杂数据结构,适合离线应用。

缺点

  • API 复杂:相对于其他存储方式,IndexedDB API 较复杂,需要更多学习和实现成本。
  • 浏览器支持差异:不同浏览器对 IndexedDB 的实现和性能可能有所不同。

总结

选择合适的存储方式应根据具体的应用需求、数据类型和安全性要求来决定。对于短期和小量数据,Cookies 和 SessionStorage 是合适的选择;对于长期和大量数据,LocalStorage 和 IndexedDB 更为适合。

对浏览器的存储方式有了清晰的了解以后,我们应该再思考,存下的数据如何使用会性能更好?存下的数据我们是否需要做适时的更新?浏览器底层有什么特别的机制?

6、浏览器缓存策略

面试的时候,少不了关于性能优化的问题,基于上一个问题,不难想到浏览器端应该做的一个优化手段 ==> 减少 HTTP请求。为此我们可以做HTTP缓存控制,也就是浏览器缓存策略。

关于浏览器缓存的初步回答

  1. 浏览器(HTTP)缓存能够帮助服务器提高并发性能,很多资源不需要重复请求,可直接从浏览器中拿缓存(通过 HTTP 获取的资源)
  2. 浏览器缓存分类:强缓存、协商缓存
  3. 强缓存通过Expires  和Cache-control控制,协商缓存通过Last-modifyEtag控制

浏览器缓存策略(机制)

缓存策略主要发生在三个对象之间:浏览器、浏览器缓存、服务器

image-20220414112109733

  • 浏览器每次发起请求,都会先在浏览器缓存中查找该请求的结果以及缓存标识

  • 浏览器每次拿到返回的请求结果都会将该结果和缓存标识存入浏览器缓存中

我们根据请求结果和缓存标识来判断是否需要向服务器重新发起HTTP请求,而这个过程我们使用的缓存策略也不相同。

缓存策略都是通过设置HTTP Header来实现的,基于此我们把浏览器缓存分为强缓存和协商缓存。这两个缓存其实就是浏览器做缓存时的不同处理过程。

强缓存

HTTP Header 实现:  和  Cache-Control

  • Expires(http1.0):缓存过期时间(绝对时间),用来指定资源到期的时间,是服务器端的具体的时间点(在响应 http 请求时告诉浏览器在过期时间前浏览器可以直接从浏览器缓存取数据,而无需再次请求)。受限于本地时间,如果修改了本地时间,可能会造成缓存失效。
  • Cache-Control(http1.1):是一个相对时间,代表资源的有效期。(优先)

现在基本上都会同时设置ExpiresCache-Control

强缓存判断是否缓存的依据来自于是否超出某个时间或者某个时间段,而不关心服务器端文件是否已经更新,这可能会导致加载文件不是服务器端最新的内容,那我们如何获知服务器端内容是否已经发生了更新呢?此时我们需要用到协商缓存。

协商缓存

强缓存未命中,浏览器携带缓存标识向服务器发起请求,由服务器根据缓存标识决定是否使用缓存的过程(判断该标识对应的资源是否更新)。

看看协商缓存的结果情况:

  • 协商缓存生效,返回 304 和 Not Modified,告诉浏览器使用本地缓存
  • 协商缓存失效,返回 200 和 请求结果(新资源),并存入缓存

强缓存未命中,是什么时候才未命中呢?协商缓存,协商的是什么呢?

其实就是:浏览器问服务器,我缓存的资源有没有更新啊?

  • 没有更新:浏览器可以用缓存(304)
  • 更新了:浏览器不能用缓存,服务器发新的给浏览器(200)

HTTP Header 实现:Last-Modified  和  ETag (帮助浏览器跟服务器进行协商)

  • Last-Modified:浏览器第一次请求一个资源的时候,服务器返回的header中会加上Last-ModifyLast-modify是一个时间标识该资源的最后修改时间 。当浏览器再次请求该资源时,发送的请求头中会包含 If-Modify-Since,该值为缓存之前返回的Last-Modify。服务器收到If-Modify-Since后,根据资源的最后修改时间判断是否命中缓存。如果命中缓存,则返回http304,并且不会返回资源内容,并且不会返回Last-Modify。由于对比的服务端时间,所以客户端与服务端时间差距不会导致问题。但是有时候通过最后修改时间来判断资源是否修改还是不太准确(Last-Modified只能以秒计时)。于是出现了ETag/If-None-Match,根据资源内容是否修改来决定缓存策略。
  • Etag(http1.1):服务器响应请求时,返回当前资源文件的一个唯一标识(由服务器生成),Etag/If-None-Match返回的是一个校验码。ETag可以保证每一个资源是唯一的,只要资源有变化,Etag就会重新生成。服务器根据浏览器上发送的If-None-Match值来判断是否命中缓存。

HTTP1.1Etag的出现主要是为了解决几个Last-Modified比较难解决的问题:

  • Last-Modified标注的最后修改只能精确到秒,如果某些文件在 1 秒钟以内,被修改多次的话,它将不能准确标注文件的修改时间

  • 如果某些文件会被定期生成或者改完又改回来,内容并没有任何变化,但 Last-Modified 却改变了,导致文件没法使用缓存

  • 有可能存在服务器没有准确获取文件修改时间,或者与代理服务器时间不一致等情形 可能面试官会问你,Last-ModifiedETag哪个更好呢?

  • 精度上:Last-Modified单位是秒,ETag单位是每次,ETag精确度更优

  • 性能上:Last-Modified只需要记录时间,而Etag需要服务器通过算法来计算出一个hash值。故 Last-Modified性能更好

  • 优先级:服务器校验优先考虑Etag

关于优先级的补充Etag是服务器自动生成或者由开发者生成的对应资源在服务器端的唯一标识符,能够更加准确的控制缓存。Last-ModifiedETag是可以一起使用的,服务器会优先验证ETag,一致的情况下,才会继续比对Last-Modified,最后才决定是否返回304

缓存访问

这个过程我们针对强缓存和协商缓存进行讨论(借用大佬的图片)

强缓存优先于协商缓存进行,若强缓存(Expires 和 Cache-Control)生效则直接使用缓存,若不生效则进行协商缓存(Last-Modified / If-Modified-Since 和 Etag / If-None-Match),协商缓存由服务器决定是否使用缓存,若协商缓存失效,那么代表该请求的缓存失效,返回 200,重新返回资源和缓存标识,再存入浏览器缓存中;生效则返回 304,继续使用缓存。

image-20220414112343658

唔 ~ 终于理完了,太不容易了。面试的时候能说到这里已经很牛了哇,但是调皮的面试官可能还会问一个比较细的问题。 emmmm......

如果什么缓存策略都没设置,那么浏览器会怎么处理?

对于这种情况,浏览器会采用一个启发式的算法,通常会取响应头中的 Date 减去 Last-Modified 值的 10% 作为缓存时间。

其他

实际应用场景

  • 频繁变动的资源
  • 不常变化的资源
  • 用户行为对浏览器缓存的影响
新开窗口有效有效
用户操作Expires/Cache-ControlLast-Modified/Etag
地址栏回车有效有效
页面链接跳转有效有效
前进、后退有效有效
F5 刷新无效有效
Ctrl+F5 刷新无效无效

说到浏览器的策略,除了缓存策略,我们不得不提的就是浏览器同源策略了 ~~

7、浏览器同源策略

同源

源:

  • 协议
  • 域名
  • 端口

同源即协议、域名和端口都相同。

什么是同源策略

浏览器的同源策略是一种安全功能,同源策略限制了从同一个源加载的文档或脚本如何与来自另一个源的资源进行交互。这是一个用于隔离潜在恶意文件的安全机制(不同源之间,不能进行交互)

如 google.com 下的 js 脚本采用 ajax 读取 baidu.com 里面的文件数据是会报错的。

既然说到 ajax ,脑海里不禁涌现出一个又爱又恨的面试题了哈哈哈哈,敲了这么多文字,动手来点代码疏通下筋骨吧。

来,给我手写一个原生 ajax ~~~ (嘘,简单的就行啦!)

var xhr = new XMLHttpRequest(); // 创建 Ajax 对象
xhr.open("get", "https://blog.csdn.net/huohuoit"); // 告诉 Ajax 请求地址以及请求方式
xhr.send(); // 发送请求数据
xhr.onreadystatechange = function () {
  // 获取服务器端给与客户端的响应数据
  if (xhr.readyState == 4 && xhr.status == 200) {
    console.log(xhr.responseText);
  }
};

// 啊 ~~~ 报错啦!~~~

限制问题

浏览器中的大部分内容都是受同源策略限制的,如:

  • Cookie、LocalStorage、IndexedDB 等存储性内容
  • DOM 节点
  • AJAX 请求发送后,结果被浏览器拦截了
  • 一些第三方插件如 FLASH

但是有一些资源时不受同源策略限制的:

  • 页面中的链接,重定向以及表单提交
  • scriptimgiframelinkvideo等标签

在浏览器中,scriptimgiframelinkvideo等标签都可以跨域加载资源,而不受同源策略的限制,通过src属性加载的资源,浏览器都会发起一个GET请求,但是浏览器限制了JavaScript的权限,使用js不能读、写加载的内容。

你可以通过这几个标签来跨域加载资源,但是,发起的GET请求返回的数据,通过js获取不到。

注意:通过script标签获取js文件里的全局属性,方法等,可以通过js读取到。是因为这些都是挂载在window对象上的。

上面我们提到了跨域,相信你经常会在面试题上看到这个问题,那就顺着上文开始跟我一起拿下它吧。

8、跨域

什么是跨域

什么是跨域呢?我们上面提到了同源,同源中有“三个同”,其中一个就是“域”,但是这里的域不是这个域哈,这里的域是指“源”,也即域名地址。

上面我们讨论到,浏览器同源策略下,会引起不同源之间不能进行交互的问题。那么跨域其实就是解决不同源之间请求发送数据、通信等交互问题的解决方法。

我们知道,发生跨域的时候,是会发送请求的,但是可能拿不到跨域的资源,那么这个请求是否真的发出去了呢?

事实是,跨域请求能发出去,服务端也能收到请求并正常返回结果,只是结果被浏览器拦截了。

在上一个同源策略问题中,我们提到了一些是否受同源策略限制的情况。可以看到,通过表单提交的方式是可以发起跨域请求的,而AJAX却不可以。

归根结底,跨域是为了阻止用户读取到另一个域名下的内容。表单提交并不会获取新的内容,所以可以发起跨域请求;而AJAX是可以获取到响应内容的,浏览器会判定为不安全,所以会拦截响应的内容。

note:这个问题说明跨域并不能完全阻止CSRF,因为请求已经发送出去了。

跨域的实现

跨域的实现一般都是后端作处理,针对面试,我们前端只需要了解一些典型的方案即可。

jsonp跨域(最经典的跨域方案)

哦豁?jsonjsonp?两者其实没啥关系,只是jsonp请求后得到的是json数据格式

我们可以更详细一点:

  • jsonpJSON With Padding(填充式json或参数式json)的简写
  • j 组成(两部分):回调函数数据。回调函数是用来处理服务器端返回的数据,回调函数的名字一般是在请求中指定的。而数据就是我们需要获取的数据,也就是服务器端的数据。

jsonp实现跨域的请求原理:

动态创建script标签,然后利用scriptsrc属性不受同源策略约束来跨域获取数据。

你可能会跟我一样疑惑,为什么是  script 标签?

在上一个问题的最后,我们说到 scriptimgiframelinkvideo等标签可以跨域加载资源,但是为啥只有 script标签可以请求到数据呢。

script 在请求得到数据后,遇到js代码,就会解析执行(js文件里写的代码肯定要被执行的)

jsonp的优点:

  • 实现简单
  • 兼容性⾮常好

jsonp的缺点:*

  • 只⽀持get请求(因为 script 标签只能get
  • 有安全性问题,容易遭受XSS攻击(注入恶意代码,篡改页面内容,可以采用字符串过滤来规避此问题)
  • 需要服务端配合jsonp进⾏⼀定程度的改造

jsonp 的实现(了解即可):

function JSONP({ url, params, callbackKey, callback }) {
  // 在参数⾥制定 callback 的名字
  params = params || {};
  params[callbackKey] = "jsonpCallback"; // 预留 callback
  window.jsonpCallback = callback; // 拼接参数字符串
  const paramKeys = Object.keys(params);
  const paramString = paramKeys.map((key) => `${key}=${params[key]}`).join("&");
  // 插⼊ DOM 元素
  const script = document.createElement("script");
  script.setAttribute("src", `${url}?${paramString}`);
  document.body.appendChild(script);
}
JSONP({
  url: "https://blog.csdn.net/huohuoit",
  params: {
    key: "test",
  },
  callbackKey: "_cb",
  callback(result) {
    console.log(result.data);
  },
});

千万要记住:JSONP要借助服务端的支持哦!

CORS 跨域(最流行的跨域方案)

CORS(Cross-Origin Resource Sharing,跨源资源共享)定义了在访问跨源资源时,浏览器与服务器应该如何沟通。

机制原理:它使⽤额外的HTTP头来告诉浏览器 让运⾏在⼀ 个origin (domain)上的Web应⽤被准许访问来⾃不同源服务器上的指定的资源。当⼀个资源从与该资源本身所在的服务器不同的域、协议或端⼝请求⼀个资源时,资源会发起⼀个跨域HTTP请求。

CORS需要浏览器和后端同时支持

浏览器会自动进行CORS通信,而实现CORS通信的关键是后端。只要后端实现了CORS,就实现了跨域。

这一块可以看看阮一峰老师的:跨域资源共享CORS详解

如果你用nodeexpress,可以在后端做如下配置

//CORS middleware
var allowCrossDomain = function (req, res, next) {
  res.header("Access-Control-Allow-Origin", "http://example.com");
  res.header("Access-Control-Allow-Methods", "GET,PUT,POST,DELETE");
  res.header("Access-Control-Allow-Headers", "Content-Type");
  next();
}; //...
app.configure(function () {
  app.use(express.bodyParser());
  app.use(express.cookieParser());
  app.use(express.session({ secret: "huohuoit" }));
  app.use(express.methodOverride());
  app.use(allowCrossDomain);
  app.use(app.router);
  app.use(express.static(__dirname + "/public"));
});

Nginx 跨域(最简单方便的跨域方案)

实现原理:

  • 同源策略是浏览器需要遵循的标准,而如果是服务器向服务器请求就无需遵循同源策略
  • 所有客户端的请求都必须先经过nginx的处理,nginx作为代理服务器再讲请求转发给node或者java服务,这样就规避了同源策略。

代理服务器,需要做以下几个步骤:

  • 接受客户端请求 。
  • 将请求转发给服务器。
  • 拿到服务器响应数据。
  • 将响应转发给客户端。

实现思路:搭建一个中转Nginx服务器,用于转发请求,作反向代理实现跨域,只需要修改Nginx的配置即可解决跨域问题,支持所有浏览器,支持session,不需要修改任何代码,并且不会影响服务器性能。

这一块网上有很多优秀的教程,大家可以自行搜索学习哈!!!

其他跨域方案

  • HTML5 XMLHttpRequest有⼀个APIpostMessage()⽅法允许来⾃不同源的脚本采⽤异步⽅式进⾏有限的通信,可以实现跨⽂本档、多窗⼝、跨域消息传递。
  • WebSocket:是⼀种双向通信协议,在建⽴连接之后,WebSocketserverclient都能主动向对⽅发送或接收数据,连接建⽴好了之后clientserver之间的双向通信就与HTTP⽆关了,因此可以跨域。
  • window.name + iframewindow.name属性值在不同的⻚⾯(甚⾄不同域名)加载后依旧存在,并且可以⽀持⾮常⻓的name值,我们可以利⽤这个特点进⾏跨域。
  • location.hash + iframea.html欲与c.html跨域相互通信,通过中间⻚b.html来实现。 三个⻚⾯,不同域之间利⽤iframelocation.hash传值,相同域之间直接js访问来通信。
  • document.domain + iframe: 该⽅式只能⽤于⼆级域名相同的情况下,⽐如a.test.comb.test.com适⽤于该⽅ 式,我们只需要给⻚⾯添加document.domain ='test.com'表示⼆级域名都相同就可以实现跨域,两个⻚⾯都通过js强制设置document.domain为基础主域,就实现了同域。

WEB 服务器

这里我主要关心服务器的作用:一台服务器可以作为源服务器,也可以作为中转服务器,甚至可以在一台服务器上搭建多个不同域名的网站。

虚拟主机

HTTP/1.1规范允许一台HTTP服务器搭建多个Web站点。利用虚拟主机的功能,可以在一台物理服务器(一个IP 地址)上虚拟出多个主机,每个主机映射一个独立的域名。因此,当用户访问域名http://huohuoit.com时,DNS域名系统会将其解析成IP地址,根据IP找到物理服务器,然后再通过请求首部的 HOST 字段(HTTP/1.1 要求必带)确认对应的虚拟主机。

代理服务器

代理服务器就是客户端和服务端之间的“中间商”,即HTTP请求通过代理服务器转发给服务器,再将服务器的响应返回给客户端的行为。代理服务器可以用来作为缓存服务器,也可以用来隐藏用户身份(正向代理)或者服务器身份(反向代理)增加安全性。

  • 正向代理:是客户端为了从源服务器中取得内容,由客户端向代理服务器发出请求,并指定目标访问服务器,然后,代理服务器向源服务器转交请求,并将获得的内容返回给客户端。隐藏了真实请求的客户端,即服务端不知道正式请求客户是谁。(科学上网)

  • 反向代理:是由客户端向代理服务器发出请求,代理服务器收到请求后判断请求方向,然后再将请求结果反回给客户端。隐藏了源服务器的信息,用户不需要知道是具体哪一台服务器提供的服务,只要知道反向代理服务器是谁。(实现负载均衡,Nginx)

  • 反向代理解决跨域问题:例如在使用 vue-cli 这种脚手架工具进行开发时,经常会遇到跨域的问题,因为项目自身启动本地服务是需要占用一个端口(如 http://localhost:8080)的,所以必然会产生跨域的问题(因为本地服务端口和服务端接口地址不同源)。出现跨域是因为浏览器有同源策略的限制,但服务器是没有的同源策略的限制的。当我们本地服务(http://localhost:8080)要请求目标服务器(http://huohuoit.com)的资源的时候,我们不直接请求http://huohuoit.com,而是请求本地服务自身http://localhost:8080 (这时是同源请求,不存在跨域),本地代理服务再将接口转发给http:huohuoit.com(注意这时候是两个服务器直接的通信了,所以更不存在跨域),本地服务获取到目标服务器的响应数据之后通过再代理伪装成本地服务请求的返回值返回给客户端。

我们梳理一下反向代理解决跨域问题的流程

本地服务在浏览器向本地服务发起请求 --> 本地代理转发 --> 目标服务器 --> 响应数据后通过代理伪装成本地服务器请求的返回值 --> 浏览器接受到目标服务器的数据

如果你有兴趣的话,这里再给出一个vue-cli反向代理配置

//vue.config.js

devServer: {
  port: 8080, // 配置端口
  open: true, // 项目启动自动开启浏览器
  compress: true, // 开启压缩
  overlay: { // 设置让浏览器 overlay 同时显示警告和错误
  warnings: true,
  errors: true
  },
  // 设置请求反向代理
  proxy: {
    '/api': { // 要代理的接口的匹配字符
    target: process.env.BASE_URL, // 接口域名
    secure: false,
    changeOrigin: true
    }
  }
},

需要注意如果要用反向代理,则在axios配置的时候,请求baseURL必须设置为字符串/,否则proxy会匹配不到/api导致代理失败。

缓存服务器

缓存服务器指的是将需要频繁访问的网络内容存放在离用户较近、访问速度更快的服务器中,以提高内容访问速度的一种技术。缓存服务器是浏览器和源服务器之间的中间服务器,浏览器先向这个中间服务器发起HTTP请求,经过处理后(比如权限验证,缓存匹配等),再将请求转发到源服务器。