花椒和邻居's Blog 花椒和邻居's Blog
首页
前端
开源
  • 技术文档
  • GitHub技巧
  • Nodejs
  • 博客搭建
  • Java程序员
收藏
关于
随笔
GitHub (opens new window)

花椒和邻居

工作了三年半的前端实习生
首页
前端
开源
  • 技术文档
  • GitHub技巧
  • Nodejs
  • 博客搭建
  • Java程序员
收藏
关于
随笔
GitHub (opens new window)
  • 前端开发笔记

    • 重识 HTML,掌握页面基本结构和加载过程
    • CSS:页面布局的基本规则和方式
    • JavaScript 如何实现继承?
    • JavaScript 引擎如何执行 JavaScript 代码?
    • 单线程的 JavaScript 如何管理任务?
    • 一个网络请求是怎么进行的?
      • HTTP 协议和前端开发有什么关系?
      • 深入剖析浏览器中页面的渲染过程
      • 改善编程思维:从事件驱动到数据驱动
      • 掌握前端框架模板引擎的实现原理
      • 为什么小程序特立独行
      • 单页应用与前端路由库设计原理
      • 代码构建与 Webpack 必备技能
      • 提升编程体验:组件化与模块化设计
      • AngularReactVue 三大前端框架的设计特色
      • 如何设计合适的状态管理方案
      • 如何搭建前端监控体系为业务排忧解难
      • 如何进行性能分析的自动化实现
      • 前端性能优化与解决方案
      • 如何进行技术方案调研与设计
      • 如何设计一个前端项目
      • 通过前端工程化提升团队开发效率
      • 大型前端项目的痛点和优化方案
      • 如何通过前期准备和后期复盘让项目稳定上线
    • JavaScript

    • 3D地图相关

    • 前端
    • 前端开发笔记
    花椒和邻居
    2022-04-14
    目录

    一个网络请求是怎么进行的?

    从这一讲开始,我会介绍浏览器相关的内容,比如浏览器中的网络请求过程、HTTP 协议在浏览器中的应用、浏览器中页面的渲染过程,等等。

    我们知道,浏览器的主要功能是展示网页资源,包括向服务器发起请求、从服务器获取相关资源,并将网页显示在浏览器窗口中。

    当我们去面试的时候,常常会被问到一个问题:在浏览器里面输入 url,按下回车键,会发生什么?

    这个问题涉及浏览器中的运行机制和页面加载流程,并且这些内容也都穿插在我们日常开发中,包括前后端联调、对网页进行性能优化等。

    今天我会先跟你聊一聊浏览器中网络请求是怎么进行的,这样你对整个网页渲染会有个更好的认识。

    页面的请求过程

    当我们打开某个网站的页面,浏览器就会发起网络请求获取该页面的资源,我们也可以从控制台看到以下的请求信息:

    图片4_waifu2x_1x_2n_png_waifu2x_2x_2n_png.png图片4_waifu2x_1x_2n_png_waifu2x_2x_2n_png.png

    在 Network 面板里,我们能看到所有浏览器发起的网络请求,包括页面、图片、CSS 文件、XHR 请求等,还能看到请求的状态(200 成功、404 找不到、缓存、重定向等等)、耗时、请求头和内容、返回头和内容等。

    图中第一个就是网站页面的请求,返回<html>页面。

    接下来,浏览器会加载页面,同时页面中涉及的外部资源也会根据需要,在特定的时机触发请求下载,包括我们看到的 PNG 图片、JavaScript 文件(这里没有 CSS 样式,是因为样式被直出在<html>页面内容里了)。

    回到前面的问题,实际上当我们在浏览器输入网页地址,按下回车键后,浏览器的处理过程如下:

    1. DNS 域名解析(此处涉及 DNS 的寻址过程),找到网页的存放服务器;

    2. 浏览器与服务器建立 TCP 连接;

    3. 浏览器发起 HTTP 请求;

    4. 服务器响应 HTTP 请求,返回该页面的 HTML 内容;

    5. 浏览器解析 HTML 代码,并请求 HTML 代码中的资源(如 JavaScript、CSS、图片等,此处可能涉及 HTTP 缓存);

    6. 浏览器对页面进行渲染呈现给用户(此处涉及浏览器的渲染原理)。

    HTTP 缓存和浏览器渲染原理会分别在第 7 讲和第 8 讲中讲述,今天我们主要围绕 HTTP 请求相关展开。

    首先我们来看 DNS 解析过程。

    DNS 解析

    DNS 的全称是 Domain Name System,又称域名系统,它负责把www.qq.com这样的域名地址翻译成一个 IP(比如14.18.180.206),而客户端与服务器建立 TCP 连接需要通过 IP 通信。

    让客户端和服务器连接并不是靠域名进行,在网络中每个终端之间实现连接和通信是通过一个唯一的 IP 地址来完成。在建立 TCP 连接前,我们需要找到建立连接的服务器,DNS 的解析过程可以让用户通过域名找到存放文件的服务器。

    DNS 解析过程会进行递归查询,分别依次尝试从以下途径,按顺序地获取该域名对应的 IP 地址。

    • 浏览器缓存

    • 系统缓存(用户操作系统 Hosts 文件 DNS 缓存)

    • 路由器缓存

    • 互联网服务提供商 DNS 缓存(联通、移动、电信等互联网服务提供商的 DNS 缓存服务器)

    • 根域名服务器

    • 顶级域名服务器

    • 主域名服务器

    DNS 解析过程会根据上述步骤进行递归查询,如果当前步骤没查到,则自动跳转到到下一步骤通过下一个 DNS 服务器进行查找。如果最终依然没找到,浏览器便会将页面响应为打开失败。

    除此之外,我们在前后端联调过程中也常常需要配置 HOST,这个过程便是修改了浏览器缓存或是系统缓存。通过将特定域名指向我们自身的服务器 IP 地址,便可以实现通过域名访问本地环境、测试环境、预发布环境的服务器资源。

    那为什么需要配置域名 HOST,而不直接使用 IP 地址进行访问呢?这是因为浏览器的同源策略会导致跨域问题。

    同源策略要求,只有当请求的协议、域名和端口都相同的情况下,我们才可以访问当前页面的 Cookie/LocalStorage/IndexDB、获取和操作 DOM 节点,以及发送 Ajax 请求。通过同源策略的限制,可以避免恶意的攻击者盗取用户信息,从而可以保证用户信息的安全。

    对于非同源的请求,我们常常称为跨域请求,需要进行跨域处理。常见的跨域解决方案有这几种。

    • 使用document.domain + iframe:只有在主域相同的时候才能使用该方法。

    • 动态创建 script(JSONP):通过指定回调函数以及函数的传参数据,让页面执行相应的脚本内容。

    • 使用location.hash + iframe:利用location.hash来进行传值。

    • 使用window.name + iframe:原理是window.name值在不同的页面(甚至不同域名)加载后依旧存在。

    • 使用window.postMessage()实现跨域通信。

    • 使用跨域资源共享 CORS(Cross-origin resource sharing)。

    • 使用 Websockets。

    其中,CORS 作为现在的主流解决方案,它允许浏览器向跨源服务器,发出 XMLHttpRequest 请求,从而克服了 Ajax 只能同源使用的限制。实现 CORS 通信的关键是服务器,只要服务端实现了 CORS 接口,就可以进行跨源通信。

    DNS 解析完成后,浏览器获得了服务端的 IP 地址,接下来便可以向服务端发起 HTTP 请求。目前大多数 HTTP 请求都建立在 TCP 连接上,因此客户端和服务端会先建立起 TCP 连接。

    TCP 连接的建立

    TCP 连接的建立过程比较偏通信底层,在前端日常开发过程中不容易接触到。但有时候我们需要优化应用的加载耗时、请求耗时或是定位一些偏底层的问题(请求异常、HTTP 连接无法建立等),都会或多或少依赖这些偏底层的知识。

    另外,从面试的角度看,我们需要掌握 TCP/UDP 的区别、TCP 的三次握手和四次挥手内容。

    • TCP 协议提供可靠传输服务,UDP 协议则可以更快地进行通信;

    • 三次握手:指 TCP 连接的建立过程,该过程中客户端和服务端总共需要发送三个包,从而确认连接存在。

    • 四次挥手:指 TCP 连接的断开过程,该过程中需要客户端和服务端总共发送四个包以,从而确认连接关闭。

    当客户端和服务端建立起 TCP 连接之后,HTTP 服务器会监听客户端发起的请求,此时客户端会发起 HTTP 请求。

    HTTP 请求与 TCP 协议

    由客户端发起的 HTTP 请求,服务器收到后会进行回复,回复内容通常包括 HTTP 状态、响应消息等,更具体的会在下一讲 HTTP 协议中进行介绍。

    前面说过,目前大多数 HTTP 请求都是基于 TCP 协议。TCP 协议的目的是提供可靠的数据传输,它用来确保可靠传输的途径主要包括两个:

    1. 乱序重建:通过对数据包编号来对其排序,从而使得另一端接收数据时,可以重新根据编号还原顺序。

    2. 丢包重试:可通过发送方是否得到响应,来检测出丢失的数据并重传这些数据。

    通过以上方式,TCP 在传输过程中不会丢失或破坏任何数据,这也是即使出现网络故障也不会损坏文件下载的原因。

    因此,目前大多数 HTTP 连接基于 TCP 协议。不过,在 HTTP/3 中底层支撑是 QUIC 协议,该协议使用的是 UDP 协议。因为 UDP 协议丢弃了 TCP 协议中所有的错误检查内容,因此可以更快地进行通信,更常用于直播和在线游戏的应用。

    也就是说,HTTP/3 基于 UDP 协议实现了数据的快速传输,同时通过 QUIC 协议保证了数据的可靠传输,最终实现了又快又可靠的通信。

    除了以上的内容,其实我们还可以去了解关于 TCP/IP 协议的分层模型、IP 寻址过程,以及 IP 协议又是如何将数据包准确无误地传递这些内容,也需要关注 HTTP/2、HTTP/3、HTTPS 这些协议的设计变更了什么、又解决了什么。

    或许这些内容对于大多数前端开发来说,都很少会直接接触。但它就像乘法口诀在高考数学题中的角色,基本上所有题目中都会使用到,但我们很少会认为自己是因为掌握了乘法口诀才能顺利解答题目。

    同样的,我们对网络请求的认知也常常忽略了底层 TCP/IP 知识,基本上围绕着“前端发起了请求,后台就能收到”“请求没有按预期结果返回,要么是请求包内容有误,要么后台服务异常”这样的理解去进行处理。

    但如果某一天,我们的应用整体请求耗时突然变长,这个过程中前端和后台都没有时间上能关联的发布单,我们到底应该如何进行定位呢?如果我们对一个网络请求的完整流程不够了解,又怎么定位到底是哪个步骤出现问题了呢?甚至我们都不会想到,将 HTTP 切换到 HTTPS 也可能会影响到请求耗时。

    下面,我们就来看一下 HTTP 请求在前端开发过程中是如何进行编程实现的,这就不得不提到 Ajax 请求了。

    Ajax 请求

    Ajax 请求这个词会频繁出现在我们的工作对话内容中,但它并不是 JavaScript 的规范,而是 Jesse James Garrett 提出的新术语:Asynchronous JavaScript and XML,意思是用 JavaScript 执行异步网络请求。

    网络请求的发展

    对于浏览器来说,网络请求是用来从服务端获取需要的信息,然后解析协议和内容,来进行页面渲染或者是信息获取的过程。

    在很久以前,我们的网络请求除了静态资源(HTML/CSS/JavaScript 等)文件的获取,主要用于表单的提交。我们在完成表单内容的填写之后,点击提交按钮,接下来表单开始提交,浏览器就会刷新页面,然后在新页面里告诉你操作是成功了还是失败了。

    除了页面跳转刷新会影响用户体验,在表单提交过程中,使用同步请求会阻塞进程。此时用户无法继续操作页面,导致页面呈现假死状态,使得用户体验变得糟糕。

    为了避免这种情况,我们开始使用异步请求 + 回调的方式,来进行请求处理,这就是 Ajax。

    随着时间发展,Ajax 的应用越来越广,如今使用 Ajax 已经是前端开发的基本操作。但 Ajax 是一种解决方案,在前端中的具体实现依赖使用XMLHttpRequest相关 API。页面开始支持局部更新、动态加载,甚至还有懒加载、首屏加载等等,都是以XMLHttpRequest为前提。

    XMLHttpRequest

    XMLHttpRequest让发送一个 HTTP 请求变得非常容易,我们只需要简单的创建一个请求对象实例,并对它进行操作:

    var request = new XMLHttpRequest(); // 新建XMLHttpRequest对象
    

    request.onreadystatechange = function () {  // 状态发生变化时,函数被回调  if (request.readyState == 4) {    // 成功完成    // 判断响应结果:    if (request.status == 200) {      // 成功,通过responseText拿到响应的文本      console.log(request.responseText);   } else {      // 失败,根据响应码判断失败原因:      console.log(request.status);   } } };

    // 发送请求 // open的参数: // 一:请求方法,包括get/post等 // 二:请求地址 // 三:表示是否异步请求,若为false则是同步请求,会阻塞进程 request.open("GET", "/api/categories", true); request.send();

    上面是处理一个 HTTP 请求的方法。我们通常会将它封装成一个通用的方法,方便调用。上面例子中我们根据返回的request.status 是否为200来判断是否成功,但实际上200-400(不包括400)的范围,都可以算是成功的,因为其中还包括使用缓存、重定向等情况。

    我们将其封装起来,同时使用 ES6 的Promise的方式,我们可以将其变成一个通过Peomise进行异步回调的请求函数:

    function Ajax({ method, url, params, contentType }) {
     const xhr = new XMLHttpRequest();
     const formData = new FormData();
     Object.keys(params).forEach((key) => {
       formData.append(key, params[key]);
    });
     return new Promise((resolve, reject) => {
       try {
         xhr.open(method, url, false);
         xhr.setRequestHeader("Content-Type", contentType);
         xhr.onreadystatechange = function () {
           if (xhr.readyState === 4) {
             if (xhr.status >= 200 && xhr.status < 400) {
               // 这里我们使用200-400来判断
               resolve(xhr.responseText);
             } else {
               // 返回请求信息
               reject(xhr);
             }
           }
         };
         xhr.send(formData);
       } catch (err) {
         reject(err);
       }
    });
    }
    

    通过这样简单的封装,我们就可以以 Promise 的方式来发起 Ajax 请求。

    但在具体的项目使用过程中,我们通常还需要考虑更多的问题,比如防抖节流、失败重试、缓存能力、浏览器兼容性、参数处理等。

    这就是 HTTP 请求的编程实现。

    小结

    对前端开发来说,网络请求是开发过程中最基础却又常常容易被忽略的部分。很多人总认为网络请求不过是“向后台发请求,后台进行响应”这样简单的逻辑,而忽略了它在用户体验中的重要性。

    实际上,在前端性能优化中,网络请求的优化往往占据了很大一部分,包括首屏直出、分包加载、数据分片拉取、使用缓存、预加载等,都是通过合理地减少网络请求内容、减少网络请求的等待耗时等方式,达到很不错的优化效果。

    那么,学完本讲页面的请求过程之后,你认为可以提升页面加载速度的优化方式都有哪些呢?欢迎在留言区分享你的经验。


    # 精选评论

    # **8923:

    看了这篇文章,了解一个网址url输入的过程,还有http请求的编码优化,前端性能优化可以从这几方面着手:首屏直出、分包加载,使用缓存,预加载等方式减少网络请求内容,这样提升网页响应的速度

    # 856:

    document.domain + iframe 这里的iframe是什么?

    #     讲师回复:

    # **安:

    配置host解决的是,根据网页域名来调取其他域名或者ip的接口,相当于做了一个中间者,但是不能解决跨域失败的问题吧?

    #     讲师回复:

        本地调试时,比如使用 npm run dev 起服务,本地地址常常为 localhost 或者是 127.0.0.1,这些地址如果直接发起请求是会跨域且可能被拒绝的,配置 host 很多时候是为了将支持跨域的域名映射到本地,从而可以正常地访问和进行本地调试

    # **贤:

    proxy 代理也可以解决跨域问题吧 devServer: { proxy: { '/API': { // 定义代理的名称 changeOrigin: true, // 是否启动代理 target: 'http://xijipan.dev.grdoc.org', // 代理的域名     pathRewrite: {'^/API','/'} // 如果你的真实的api路径中没有/API这一个路径,把这句加上,如果本来就有/API这一路径的话,这句一定要去掉,要不然会导致域名找不到的 } } },

    去GitHub编辑 (opens new window)
    上次更新: 2022/11/09, 03:51:49
    单线程的 JavaScript 如何管理任务?
    HTTP 协议和前端开发有什么关系?

    ← 单线程的 JavaScript 如何管理任务? HTTP 协议和前端开发有什么关系?→

    最近更新
    01
    为什么小程序特立独行
    04-23
    02
    单页应用与前端路由库设计原理
    04-23
    03
    代码构建与 Webpack 必备技能
    04-23
    更多文章>
    Theme by Vdoing | Copyright © 2022-2023 花椒和邻居 | MIT License
    • 跟随系统
    • 浅色模式
    • 深色模式
    • 阅读模式