feng xiaohan
setTimeout

this 问题

  • setTimeout 的第一个参数是回调函数,无论传入的是什么样的函数,在调用它的时候都是没有 this 指向的,所以默认都指向 window 对象(无论是否是严格模式);
  • setTimeout 也不能直接使用 call 等方法;
    setTimeout.call(myArray, myArray.myMethod, 2.0 * 1000); // 出错
    

要解决这个问题可以使用:

设置包装函数

包装函数可以是具名函数,也可以是箭头函数,它们唯一的区别就是在严格模式下函数中的 this 指向问题,具名函数中的 this 是 window,而箭头函数是 undefined。

setTimeout(function () {
  console.log(this);
  myArray.myMethod();
}, 1.0 * 1000);

使用 bind

setTimeout(myArray.myMethod.bind(myArray), 1.0 * 1000);

延时比指定值更长

  • 嵌套延时:HTML 标准中规定,一旦 setTimeout 嵌套了 5 次,浏览器将强制执行 4ms 的最小超时;
  • 非活动标签延时:为了优化后台标签的加载损耗(以及降低耗电量),浏览器会在非活动标签中强制执行一个最小的超时延迟;
  • 超时延迟:浏览器(页面,操作系统)忙于其他任务,导致时间执行比预期晚;

    毕竟 setTimeout 是一个异步的宏任务,是要加入任务队列里的,哪怕设置延时为 0 也是要比同步任务晚执行的。

  • 加载页面时延时:当前标签页正在加载时,Firefox 将推迟触发 setTimeout() 计时器。直到主线程被认为是空闲的(类似于 window.requestIdleCallback()),或者直到加载事件触发完毕,才开始触发。
  • webExtension:在浏览器扩展中 setTimeout 不会可靠的工作;
  • 最大延时:浏览器内部以 32 位带符号整数存储延时,如果设置的时延超出了大约 24.8 天时就会溢出,导致定时器会被立刻执行;

requestIdleCallback

插入一个函数,这个函数将在浏览器空闲时期被调用。

textContent 和别的区别

textContent 和别的区别

两者都是用于获取元中的内容的。

innerText

  • textContent 是 Node 对象提供的;而 innerText 是 HTMLElement 对象提供的。
  • textContent 会获取该元素里所有的内容,包括受 css 样式影响(隐藏)的元素内容和 script 标签里的内容;innerText 则不会。
    因此使用 textContent 可以防止 XSS 攻击。

    在 IE 小于等于 11 的版本,对 innerText 的行为有修改。

  • 由于 innerText 受 CSS 样式的影响,它会触发回流去确保是最新的计算样式。

innerHTML

  • innerHTML 是 Element 对象的属性;
  • innerHTML 返回完整的 DOM 元素;
Fetch

也是用于客户端和服务端数据交互的技术。相对于 xhr,它的更加简洁、易于使用。并且基于 Promise,可以使用链式调用。
但是它不支持超时控制(如果需要则要手动设置 setTimeout),终止请求很麻烦(需要基于一个构造函数 AbortController 完成)

Promise

Fetch 使用

fetch 可以设置两个参数,第一个参数是必填的,也就是访问的 url 地址;第二个参数是一个对象,可以配置一些请求的信息,比如请求方法、请求头、请求体等。

设置好之后 fetch 其实返回的是一个 Promise,在.then()中接收的默认参数其实是一个 response 对象,并不是一个拿到值的结果。

在这个 response 对象中有各种响应的信息,并且在它的原型对象上有一些响应体解析方法,我们需要根据后端返回的格式来调用不同的响应体解析方法来拿到这个结果:

  • text():将响应体解析为纯文本字符串返回;
  • json():将响应体解析为 JSON 格式并返回一个 JS 对象;
  • blob():将响应体解析为二进制数据并返回一个 Blob 对象;
  • arrayBuffer():将响应体解析为二进制数据并返回一个 ArrayBuffer 对象;
  • formData():将响应体解析为 FormData 对象;

然后在第二个.then()中拿到最终的结果。

AJAX

异步的 JS 和 XML,是一种用于网页进行异步交换数据的技术,它可以在不刷新整个页面的情况下向服务器异步发起请求获取数据,从而实现动态更新页面(异步更新页面)。它减少了响应实现和带宽的消耗,提高了用户的交互体验。但是它不利于 SEO。(因为 SEO 在进行爬虫抓取的时候没有办法去抓取 AJAX 里的内容以及发送的 url),如果要做 SEO 可以使用服务端渲染技术(SSR)

没有 AJAX 时需要手动刷新页面。

SEO

搜索引擎优化。

AJAX 实现

  • 创建一个 XMLHttpRequest 的实例对象;
  • 使用 open 方法定义请求的方式、url 地址、是否异步(默认异步);
  • 如果是 post 请求需要设置请求头 setRequestHeader;
  • 如果需要向后端传递信息,可以使用 send 方法传递;

    get 请求一般不用 xhr.send(null),有也是作为 query 在 url 地址中传递;

  • 通过在 onreadystatechange 函数中监听 readyState 的值,来接收后端返回的数据,这个函数在整个 AJAX 初始到连接到结束都会执行;

    0: 未初始化 —— XMLHttpRequest 对象已经创建,但还没有调用 open 方法;
    1:已开启 —— open 方法被调用,send 方法没被调用;
    2:已发送 —— send 方法已调用,请求已经发给服务器了;
    3:正在接收 —— 服务器正在处理请求并返回数据;
    4:完成 —— 服务器已经完成了数据传输;
    status 200: 服务器返回状态码。

  • 或者直接使用 addEventListener 去监听 load 事件,该事件只会在 readyState=4 的时候执行。
  • 也可以使用 addEventListener 去监听一个 progress,来获取一个进度;
  • 也可以设置超时时间 timeout,然后使用 addEventListener 去监听一个 timeout;
  • 也可也中断请求;

HTTP 状态码

  • 1xx:信息性状态码。比如 102 服务器已经收到并正在处理该请求;
  • 2xx:请求成功响应。
  • 3xx:重定向消息。304 服务端告诉客户端数据没有变换,可以使用缓存。
  • 4xx:客户端响应错误。400 参数错误,401 可能是 token 掉了,403 没有权限。
  • 5xx:服务端错误。

JSON

JSON 是一种数据格式,常用于前后端数据传输。它体积小,传输快,易于用户阅读和编写,也可也被各种编程语言轻松解析。

SSE

server sent events。是一种用于实现服务器主动向客户端单向推送数据的技术,也被称为事件流。它是基于 HTTP 协议的,实现在客户端和服务端之间建立一条持久化连接(长连接),并通过这条连接实现服务器向客户端的实时数据推送。单工通讯

客户端向服务端发起请求后,服务端就与客户端建立连接,服务端一有数据就向客户端发送,不需要客户端再发起请求。

HTTP 协议特性

是否有长连接。

协议对比-AJAX(HTTP)/ SSE / WebSocket

AJAX

客户端向服务端发送一个 HTTP 请求后,服务端响应一次。一来一回一来一回。

SSE

客户端向服务端发起请求,传递 SSE 数据,此时服务端可以不立即返回消息,隔一阵之后在返回消息,并且不用客户端再次发起请求,就可以一直给客户端返回消息,实现单工通讯。

WebSocket

客户端向服务端发起请求后,客户端和服务端都可以向对方发送消息,全双工通讯。

适用场景

chatGPT 返回的数据、实时数据大屏(后端实时向前端推送数据,不一定非要使用 WebSocket)。

使用

前端

需要使用 HTML5 提供的 EventSource API。

  • 构造一个 EventSource 的实例,该构造函数需要传入一个用于建立连接的 url,还可以传入一个可选的配置对象。
    • 配置对象可以配置发送请求头信息,与服务器失去连接之后重新连接的时间间隔等。
  • 然后可以使用实例对象的一些方法来监听或者操纵整个 SSE 连接:
    • close():关闭与服务端的连接,停止接收服务端发送的数据;
    • onopen():和服务器建立好连接之后触发的函数(第一次建立连接);
    • onerror():建立连接失败触发的函数;
    • onmessage():接收到服务端数据时触发的函数,默认参数为后端返回的数据;

      注意:后端可以设置返回多个事件的名称(res.write(‘event: ‘)),默认为 message。前端需要监听这个事件名称,才能接收到对应数据。

后端

需要在返回响应里设置请求头信息 Content-Type 为 text/event-stream

跨域解决

由于浏览器的同源策略,浏览器会拒绝跨域请求。

jsonp(前后端协作)

利用 script 标签的 src 不受同源策略的限制,(通过动态创建 script 发送)可以跨域请求数据。但是只能发送 get 请求。

后端返回的是一个函数,该函数是在前端定义的,后端把要返回的值注入到函数的参数里面。

代理服务器(前端)

webpack、vite 等。

注意:该方法只在开发环境生效,如果需要生产环境需要修改 nginx 配置。

设置请求头-跨域资源共享 cors(后端)

后端设置请求头 Access-Control-Allow-Origin。

nginx 代理

适合于上线时使用,前后端不需要有什么修改。

HTTP 和 HTTPS

HTTP

HTTP 的缺点

  • 使用明文传输(不加密),内容可能会被盗用;
  • 不会验证通信方的身份,可能会被劫持或伪装;
  • 无法验证报文的完整性,报文可能会被篡改;

HTTPS

对信息进行加密,有信息完整性校验和通信方身份校验。

HTTPS = HTTP + TLS/SSL

TLS 和 SSL

TLS(Transport Layer Security)和 SSL(Secure Sockets Layer)是用于保护网络通信安全的协议。他们提供了加密和认证机制,用于确保数据传输的机密性和完整性。

TLS 是将对称加密、非对称加密和散列函数融合起来,安全性很高。

SSL 是最早的加密协议,后来被 TLS 取代。TLS 是 SSL 的升级版本,提供更强大的安全性和更先进的加密算法。目前主流的浏览器和服务器都支持 TLS 协议。

目前广泛使用的是 TLS 1.2 和 TLS 1.3.

** 是在应用层的下面,传输层的上面去做的加密。**

加密方式

对称加密

加密和解密的密钥是相同的。

常见的有 DES、AES。
发送方(加密方):AES 算法 + 密钥 + 明文(传输信息)= CDSABIFHSC==(生成乱码)
接收方(解密方):AES 算法 + 密钥 + CDSABIFHSC==(生成乱码)= 明文(传输信息

非对称加密

加密和解密使用不同的密钥,使用接收方的公钥进行加密,接收方用自己的私钥进行解密。

常见的有 RSA、DSA。
发送方(加密方):RSA + 公钥 + 明文(传输信息)= CDSABIFHSC==(生成乱码)
接收方(解密方):RSA + 私钥 + CDSABIFHSC==(生成乱码)= 明文(传输信息

例子——将网站变为使用 HTTPS 的服务

该方法在开发和测试环境中使用,生产环境上还是使用第三方机构(阿里云、腾讯云等)颁发的证书文件。

  • 安装 openSSL 生成私钥;

    Mac 自带,Windows 需要去官网下载。

需要用到以下文件来完成证书相关操作:

  1. 私钥文件:用于对加密数据进行解密(如 private-key.pem);
  2. 证书签名请求文件:用于向 CA 申请 SSL/TSL 证书签名(如 certificate.csr);
  3. SSL/TSL 证书文件:用于对客户端发送的请求进行验证,确保通信的安全和可靠(如 certificate.pem);
  • 创建私钥
openssl genpkey -algorithm RSA -out private-key.pem -aes256
  • 生成 pem 签名文件
openssl req -new -key private-key.pem -out certificate.csr

“-key private-key.pem”:表示使用指定的私钥文件”private-key.pem”来加密证书签名请求中的密钥对。

  • 生成数字证书

Node

import https from "https";
import fs from "node:fs";

//http 端口号 80 https端口号 443
https
  .createserver(
    {
      key: fs.readFilesync("private-key.pem"),
      cert: fs.readFilesync("certificate.pem"),
      //密码短语
      passphrase: "123456",
    },
    (req, res) => {
      res.writeHead(200);
      res.end("success");
    }
  )
  .listen(443, () => {
    console.log("https server is running at https://localhost:433");
  });

nginx

在 nginx 的安装目录的 conf/nginx.conf 下配置。

JWT

json web token。实现身份验证或授权,也就是鉴权。(常用于用户登录后需要存储用户信息)

有点类似 cookie 和 session。

原理

在服务端生成一个加密的令牌(token),将用户的信息封装到这个令牌中;前端每一次发送请求时去携带这个 token,服务端就可以将这个 token 进行解码,读取用户的信息,这样就达到了一个身份验证和鉴权。

组成

JWT 由 . 分割为三个部分:

  • 头部(Header):一般由令牌的类型和使用的签名算法组成(一般不用去管),是一个 json 对象。
  • 负载(Payload):包含需要传输的信息(用户身份、权限、令牌发布者等),也是一个 json 对象,使用 Base64 进行编码。
  • 验证签名(Verify Signature):通过指定算法对 Header 和 Payload 生成数字签名。

实现

后端(express)

  • 安装所需依赖
pnpm i express cors jsonwebtoken

cors:用于解决跨域。

import express from "express";
import jwt from "jsonwebtoken";
import cors from "cors";

let Key = "SVCSD"; // 加验私钥,一般藏在环境变量里
const app = express();
app.use(express.json()); // 中间件支持json
app.use(express.urlencoded({ extended: false }));
app.use(cors()); // 中间件解决跨域

let user = {
  name: "admin",
  password: "123456",
  id: 1,
};

// 1.登录返回前端token用于授权
app.post("/api/login", (req, res) => {
  if (req.body.name == user.name && req.body.password == user.password) {
    res.json({
      message: "登录成功",
      token: jwt.sign({ id: user.id }, Key, { expiresIn: "1h" }), // payload,私钥,token配置对象(如 token 过期时间)
    });
  } else {
    res.status(403).json({ message: "用户名或密码错误" });
  }
});

// 2.列表接口,只有授权状态才能访问,不然就会403
app.post("/api/list", (req, res) => {
  let token = req.headers.authorization; // 前端 token 存放在请求头的 authorization 里,规范
  jwt.verify(token, Key, (err, decode) => {
    // 使用 jwt 去验证 token 是否正确
    if (err) {
      // token 没有权限按规范返回403
      res.status(403).json({ message: "无权限" });
    } else {
      res.json({
        list: [
          { id: 1, title: "标题1" },
          { id: 2, title: "标题2" },
        ],
      });
    }
  });
});

app.listen(3000, () => {
  console.log("server started");
});

前端(fetch)

btn.onclick = () => {
  fetch("http://localhost:3000/api/login", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      name: name.value,
      password: password.value,
    }),
  })
    .then((res) => res.json())
    .then((res) => {
      cnosole.log(res);
      localStorage.setItem("token", res.token);
    });
};
fetch("http://localhost:3000/api/list", {
  headers: {
    Authorization: localStorage.getItem("token"),
    // "Authorization": `Bearer ${localStorage.getItem("token")}`
  },
})
  .then((res) => res.json())
  .then((res) => {
    cnosole.log(res);
  });

注意:Bearer 字段不是一定要加的,它是一个规范,只要看见 token 前面加了这个东西,就说明它使用了 OAuth 2.0 规范(jwt 也使用了该规范)。加了后端也需要去处理这个 Bearer

TCP

tcp/ip

TCP 三次握手

  • seq:sequence number,序列号(通过各种算法 isn 随机生成的);
  • ack:acknowledgement number,确认号(ack = seq + 1);
  • ACK:acknowledgement,确认序列号有效;
  • SYN:synchronous,发起新连接;
  • FIN:finish,完成;

第一次握手:
客户端向服务端发起建立新连接(SYN),并且发送序列号(seq);

序列号用于确保发送消息的顺序不乱。

第二次握手:
服务端接收到客户端的序列号(seq),判断该序列号的确认号是否正确(ack),如果正确就标记一个 ACK,然后向客户端发起建立新连接(SYN),并且带上服务端自己的序列号(seq);

第三次握手:
客户端接收到之后会判断服务端的 seq 是否正确,如果正确的话也会标记一个 ACK

客户端的 seq = 第一次握手客户端的 seq + 1
ack = 服务端 seq + 1

TCP 四次挥手

四次挥手可以由客户端或者服务端发起。此处以客户端主动发起为例子。

第一次挥手:
客户端向服务端发送连接完成状态(FIN),并添加上客户端的 seq;此时客户端从 established 状态变为 fin_wait_1 等待状态;

第二次挥手:
服务端收到客户端的信息后需要去验证 seq,判断 ack 是否正确,如果正确就标记一个 ACK 返回给客户端;
此时客户端会进入 fin_wait_2 阶段,在这个阶段内,如果有未完成或未处理的任务,会将所有任务处理完成,处理完之后服务端才会发起第三次挥手,最终来结束状态;

第三次挥手:
服务端发起 FIN + ACK(ack)+ 服务端 seq;
此时客户端进入超时等待状态 time_wait。

该状态的存在是为了确保第四次挥手的 ACK 标记不丢失,确保连接能正常断开,保证 TCP 的可靠性。如果 ACK 标记丢失了,服务端会再次发起断开连接请求(FIN),传递 ACK(再执行一次第三次挥手)。

第四次挥手:
客户端超时等待完成后回向服务端发起 ACK (ack)+ seq,并彻底关闭连接。

浏览器中输入 url 到底发生了什么

首先解释 url 是什么。

URL

访问协议 + 域名(服务器名称地址)+ 请求文件(资源)路径名

有了域名就可以进行一个 DNS 查询

DNS(域名系统,domain name system)

域名系统产生:服务器一般都有一个 IP,而这个 IP 地址很难记住(168.127.4.xxxx)。如果使用域名就很好记了(bing.com)。
DNS 主要是将 IP 和域名进行一个映射,通过域名找到对应的 IP 的服务器,然后去找到正确的资源。

所以用户再输入 url 后,会去查找 dns。

DNS 查找顺序

  1. 首先去浏览器的 dns 缓存 中查找;
  2. 如果没有命中就去操作系统中 dns 缓存中找;
  3. 如果没有命中,去本地 hosts 文件查找;

    hosts 文件:定义了一个映射关系,也就是一个 IP 地址和一个与之有映射关系的主机名。

  4. 如果没有命中,操作系统会向域名服务器发送请求(递归查找);
  • 首先查找本地 dns 服务器;
  • 如果没有找到,会向根域名服务器查找(.);
  • 如果没有找到,会向顶级域名服务器查找(com.);
  • 如果没有找到,会向权威域名服务器查找(baidu.com.);但是如果配置了 CDN,dns 会将最终的域名解释权通过获取对应的 cname 域名交给 cdn 专用的 dns 服务器。

通过 dns 查询到了对应的 IP 地址之后,就可以向它发送网络请求了。
发送请求前会通过传输层 TCP/IP 进行三次握手,保证数据传输的可靠,然后就可以发送 HTTP 请求了。

HTTP 的 options 预检请求

浏览器在发送一个跨域的 HTTP 的 post 请求时,通常会发送一个 options 的请求:这个 OPTIONS 请求被称为预检请求(pre-flightrequest),用于确定接收请求的服务端能否进行正常通讯(浏览器有可能已经缓存了这个预检请求导致不发送);预检请求发送条件:

  • 请求头 Content-Type 设置 application/json;
  • 用户自定义了请求头;

浏览器缓存

浏览器的缓存分为强缓存和协商缓存;

强缓存

让浏览器强制缓存服务端提供的资源。一般是用于静态资源的缓存(css 等)。

该缓存可以通过后端配置 ExpiresCache-Control max-age=10

这样浏览器在第二次请求这个资源时就不需要经过服务端了,直接从缓存中读取。
存储位置有硬盘缓存和内存缓存。浏览器多次读取缓存时可能会直接从内存缓存中读取,第一次都是从硬盘缓存中读取。

协商缓存

浏览器跟后端协商进行缓存。

通过对比 last-modified 和 if-modified-since(GMT 时间),etag 和 if-none-match(任意形式)来判断该资源是否被改动过。

如果发现该资源没有变动,服务端则会返回 304,表示资源没有改变,并且响应体为空。浏览器拿到后,就知道原本可能过期的缓存其实还可以继续使用。如果资源改变了,就会返回 200,且响应体带上最新资源。

浏览器缓存完成后,就进行 TCP 的四次挥手了断开连接了。此时浏览器已经拿到了对应的 html 资源准备开始渲染。

浏览器渲染页面

  • 浏览器将 html 里的标签解析成一个 dom 树(抽象语法树);

  • 浏览器的渲染引擎将 CSS 样式表转化为浏览器可以理解的 styleSheets,计算出 DOM 节点的样式,该过程会处理样式的继承和层叠,属性值标准化,被称为 cssom;

    css 样式的来源主要有三种:link 外部引入的 css、style 标签内的 css、内联 css。

  • 浏览器渲染时肯定会触发回流和重绘。

回流

当渲染树中部分或全部元素的尺寸、结构、或某些属性发生改变时,浏览器重新渲染部分或全部 document 的过程称为回流。

能够引起回流的操作:

  • 页面首次渲染
  • 浏览器窗口大小发生变换
  • 元素尺寸或位置发生改变
  • 元素内容发生改变
  • 元素字体大小变换
  • 添加或删除可见的 dom 元素
  • 激活 css 伪类(:hover)
  • 查询某些属性或调用某些方法

    clientwidth、clientHeight 、clientTop、clientLeftoffsetwidth 、offsetHeight 、offsetTop 等

重绘

当页面中元素样式的改变并不影响它在文档流中的位置时,浏览器会将新样式赋予给元素并重新绘制它,这个过程称为重绘。

然后浏览器通过 v8 引擎(c++编写)解析 js。

浏览器解析 js

涉及编译原理一些内容。

通过解析器将我们输入的代码变成抽象语法树,然后再转换为中间代码(字节码),再由解释器将字节码解析成机器码。

中间代码的存在是为了兼容不同的操作系统,让我们的代码具有跨平台执行的功能。
v8 使用的是 jit 解析器,即即时编译,边解析边执行;(中间有一个监视器去寻找后续需要解析的代码)
还有 AOT 解析器,直接编译成二进制文件,苹果里用的很多。

通过解释器解释后的机器码(0101)就到了 CPU 中。开始进入计算机组成原理部分,输入设备-> 存储器 -> 输出设备