BOM

location

location.href: https://www.baidu.com/search?class=browser&id=2#comments

  • .orgin => ‘https://www.baidu.com
  • .protocol => ‘https:’
  • .host => ‘www.baidu.com’
  • .port => ‘’.
  • .pathname => ‘/search/‘
  • .search => ‘?class=browser&id=2’
  • .hash => ‘#comments’
  • .assign(‘’) // 跳转到指定 path => 替换 pathname
  • .replace(‘’) // 同上,同时替换浏览历史
  • .reload()
  • .toString() // 产出当前地址字符串

URI & URL: uniform resource identifier / locator

history

路由相关

浏览器系统信息集合

1
navigator.userAgent; // 获取当前用户的环境信息

screen

表征显示区域

  1. window 视窗判断

    • window.innerHeight
    • window.innerWidth
    • document.documentElement.clientHeight
    • document.documentElement.clientWidth
    • document.body.clientWidth
    • document.body.clientWidth
  2. 网页视图的 size -> offsetHeight = clientHeight + 滚动条 + 边框

    • document.documentElement.offsetHeight
    • document.documentElement.offsetWidth
    • document.body.offsetHeight
    • document.body.offsetWidth
  3. 动态定位:

    • scrollLeft / scrollTop - 距离常规左 / 上滚动距离
    • offsetLeft / offsetTop - 距离常规左 / 上距离
  4. el.getBoundingClientRect()

    • el.getBoundingClientRect().top
    • el.getBoundingClientRect().left
    • el.getBoundingClientRect().bottom
    • el.getBoundingClientRect().right - 兼容性: IE 是会多出来 2 像素

Event 事件模型

  1. addEventListener(event, function, useCapture): 默认冒泡 false

  2. 阻止默认事件:

    • stopPropgation(): 阻止传递行为,无法阻止默认事件
    • preventDefault(): 阻止默认事件 - a
    • stopImmediatePropagation(): 阻止相同节点绑定多个同类事件
  3. 兼容性: attachEvent vs addEventListener

    • 传参: attachEvent 对于事件名需要加上’on’
    • 执行顺序: attachEvent - 后绑定先执行,addEventListener - 先绑定先执行
    • 解绑: detachEvent vs removeEventListener
    • 阻断: event.cancelBubble = true vs event.stopPropgation()
    • 默认事件拦截: event.returnValue = false vs event.preventDefault()
  4. 性能优化: 事件代理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
class bindEvent {
constructor(element) {
this.element = element;
}
// 绑定
addEventListener = (type, handler) => {
if (this.element.addEventListener) {
this.element.addEventListener(type, handler, false);
} else if (this.element.attachEvent) {
this.element.attachEvent("on" + type, () => {
handler.call(element);
});
} else {
this.element["on" + type] = handler;
}
};
// 解绑
removeEventListener = (type, handler) => {
if (this.element.removeEventListener) {
this.element.removeEventListener(type, handler, false);
} else if (this.element.detachEvent) {
this.element.detachEvent("on" + type, () => {
handler.call(element);
});
} else {
this.element["on" + type] = null;
}
};
// 阻断
static stopPropgation(e) {
if (e.stopPropagation) {
e.stopPropagation();
} else {
e.cancelBubble = true;
}
}
// 默认拦截
static preventDefault(e) {
if (e.preventDefault) {
e.preventDefault();
} else {
e.returnValue = false;
}
}
}

网络层

ajax fetch

fetch

  • 默认不带 cookie
  • 错误不会 reject
  • 不支持超时设置
  • 需要借用 AbortController 中止 fetch

fetch 中断请求

场景:输入框输入触发多次请求,需要将之前发送的请求中断掉,保留最后一个

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let controller;
input.oninput = async () =>{
controller && controller.abort();
controller = new AbortController();
try {
const list = await fetch(
"http://localhost:9527/api/search?key=' + input.value,
{
signal: controller.signal,
}
).then((resp) =>resp . json());
createSuggest(list);
} catch {
console. log ( 'aborted ' );
}
};

封装 fetch 超时

请求超时则中断 fetch

1
2
3
4
5
6
7
8
9
10
11
12
13
function createFetch(timeout){
return (resource, options)=>{
let controller = new AbortController();
options = options || {};
options.signal = controller.signal;

setTimeout((()=>{
controller.abort():
}, timeout);
return fetch(resource,options))
}
}
createFetch(300)('url')

ajax demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
let xhr = new XMLHttpRequest();
xhr.open("GET", "http://domain/service");

// request state change event
xhr.onreadystatechange = function () {
// request completed?
if (xhr.readyState !== 4) return;

if (xhr.status === 200) {
// request successful - show response
console.log(xhr.responseText);
} else {
// request error
console.log("HTTP error", xhr.status, xhr.statusText);
}
};

// xhr.timeout = 3000; // 3 seconds
// xhr.ontimeout = () => console.log('timeout', xhr.responseURL);

// progress事件可以报告长时间运行的文件上传
// xhr.upload.onprogress = p => {
// console.log(Math.round((p.loaded / p.total) * 100) + '%');
// }

// start request
xhr.send();

fetch("http://domain/service", {
method: "GET",
})
.then((response) => response.json())
.then((json) => console.log(json))
.catch((error) => console.error("error:", error));

// 默认不带cookie

fetch("http://domain/service", {
method: "GET",
credentials: "same-origin",
});

// 错误不会reject
// HTTP错误 (例如404 Page Not Found 或 500 Internal Server Error) 不会导致Fetch返回的Promise标记为reject;.catch()也不会被执行。
// 想要精确的判断 fetch是否成功,需要包含 promise resolved 的情况,此时再判断 response.ok是不是为 true

fetch("http://domain/service", {
method: "GET",
})
.then((response) => {
if (response.ok) {
return response.json();
}
throw new Error("Network response was not ok.");
})
.then((json) => console.log(json))
.catch((error) => console.error("error:", error));

// 不支持直接设置超时, 可以用promise
function fetchTimeout(url, init, timeout = 3000) {
return new Promise((resolve, reject) => {
fetch(url, init).then(resolve).catch(reject);
setTimeout(reject, timeout);
});
}

// 中止fetch
const controller = new AbortController();

fetch("http://domain/service", {
method: "GET",
signal: controller.signal,
})
.then((response) => response.json())
.then((json) => console.log(json))
.catch((error) => console.error("Error:", error));

controller.abort();

request header

  • method:
  • path:
  • scheme:
  • accept:
  • accept-encoding:
  • cache-control:
  • cookie:
  • origin:
  • referer:
  • user-agent:

问题: 为什么常见的 cdn 域名和业务域名不一样?例如:www.baidu.com / cdn.baidu-a.com

  1. 安全问题:域名一样会导致 request header 里带上业务域名的 cookie
  2. cdn 常作为拉取静态资源,不需要 cookie 信息
  3. 并发请求数:资源请求和数据请求分开

response header

  • access-control-allow-credentials:
  • access-control-allow-origin:
  • content-encoding:
  • content-type:
  • date:
  • set-cookie:服务端返回的登录 cooki
  • set-cookie:
  • status:

常用状态码

  • 200 get 成功
  • 201 post 成功
  • 301 永久重定向
  • 302 临时重定向
  • 304 协商缓存 服务器文件未修改
  • 400 客户端请求有语法错误,不能被服务器识别
  • 403 服务器受到请求,但是拒绝提供服务,可能是跨域
  • 404 请求的资源不存在
  • 405 请求的 method 不允许
  • 500 服务器发生不可预期的错误

ajax 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
interface IOptions {
url: string;
type?: string;
data: any;
timeout?: number;
}

function formatUrl(json) {
let dataArr = [];
json.t = Math.random();
for (let key in json) {
dataArr.push(`${key}=${encodeURIComponent(json[key])}`)
}
return dataArr.join('&');
}

export function ajax(options: IOptions) {
return new Promise((resolve, reject) => {
if (!options.url) return;

options.type = options.type || 'GET';
options.data = options.data || {};
options.timeout = options.timeout || 10000;

let dataToUrlstr = formatUrl(options.data);
let timer;

// 1.创建
let xhr;
if ((window as any).XMLHttpRequest) {
xhr = new XMLHttpRequest();
} else {
xhr = new ActiveXObject('Microsoft.XMLHTTP');
}

if (options.type.toUpperCase() === 'GET') {
// 2.连接
xhr.open('get', `${options.url}?${dataToUrlstr}`, true);
// 3.发送
xhr.send();
} else if (options.type.toUpperCase() === 'POST') {
// 2.连接
xhr.open('post', options.url, true);
xhr.setRequestHeader('ContentType', 'application/x-www-form-urlencoded');
// 3.发送
xhr.send(options.data);
}

// 4.接收
xhr.onreadystatechange = () => {
if (xhr.readyState === 4) {
clearTimeout(timer);
if (xhr.status >= 200 && xhr.status < 300 || xhr.status === 304) {
resolve(xhr.responseText);
} else {
reject(xhr.status);
}
}
}

if (options.timeout) {
timer = setTimeout(() => {
xhr.abort();
reject('超时');
}, options.timeout)
}

// xhr.timeout = options.timeout;
// xhr.ontimeout = () => {
// reject('超时');
// }
});
}

浏览器渲染原理

页面加载流程

  1. 用户输入 url 并回车:浏览器进程会根据用户输入的信息判断是搜索还是网址,如果是搜索内容,就将搜索内容+默认搜索引擎合成新的 URL;如果用户输入的内容符合 URL 规则,浏览器进程就会根据 URL 协议,在这段内容上加上协议合成合法的 URL。
    • 在此之前可执行 beforeunload 事件
  2. 浏览器导航栏显示 loading 状态,但是页面还是呈现之前的页面不变,因为新页面的响应数据还没有获得。
  3. 浏览器进程构建请求行信息,通过进程间通信 (IPC) 把 url 请求发送给网络进程。
  4. 网络进程接收到 url 请求后检查本地缓存是否缓存了该请求资源,如果有则将该资源返回给浏览器进程。
    • 会根据强制缓存规则查找本地是否缓存了当前 URL 是否存在强缓存。如果有缓存资源,那么直接返回资源给浏览器进程
    • 如果在缓存中没有查找到资源,那么再查找是否存在协商缓存信息,如果有把协商缓存信息写入请求头中,否则直接进入网络请求流程。
  5. 如果没有,网络进程向 web 服务器发起 http 请求 (网络请求) ,请求流程如下:
    • 进行 DNS 解析,获取服务器 ip 地址 (先查找 DNS 缓存 (浏览器 -> 操作系统缓存 -> 路由器缓存 -> ISP 缓存) ,再发起 DNS 网络请求)
    • 利用 ip 地址和服务器建立 tcp 连接 (TCP 三次握手建立的连接并不是真实的物理连接,而是虚连接,连接的本质就是在客户端与服务端开辟本次连接所需要的资源 (内存、进程等) )
    • 完成构建请求信息 (请求行、请求头等信息,并把和该域名相关的 Cookie 等数据附加到请求头中) 并发送请求 (调用 Socket 利用 TCP 通过三次握手连接建立后,之前准备好的 HTTP 请求报文被送入发送队列,接下来就交给了 TCP 完成后续过程)
    • 服务器响应后,网络进程接收响应头和响应信息,并解析响应内容
  6. 网络进程解析响应流程:
    • 检查状态码,如果是 301/302,则需要重定向,从 Location 自动中读取地址,重新进行第三步,如果是 200,则继续处理请求
    • 检查响应类型 Content-Type,如果是字节流类型,则将该请求提交给下载管理器,该导航流程结束,不再进行后续的渲染,如果是 html 等资源则将其转发给浏览器进程
  7. 浏览器进程接收到网络进程的响应头数据之后,检查当前 url 是否和之前打开的渲染进程根域名是否相同 (同一个站点) ,如果相同,则复用原来的进程,如果不同,则开启新的渲染进程 (process-per-site-instance 策略) 。
  8. 渲染进程准备好后,浏览器进程发送 CommitNavigation 消息到渲染进程,发送 CommitNavigation 时会携带响应头、等基本信息。渲染进程接收到消息和网络进程建立传输数据的“管道”。
    • 当浏览器进程接收到网络进程的响应头数据之后,便向渲染进程发起“提交文档”的消息;
    • 渲染进程接收到“提交文档”的消息后,会和网络进程建立传输数据的“管道”;
    • 管道建立完成后,网络进程接收到数据后就往这个管道里面放,而渲染进程则从管道的另外一端不断地读取数据,并同时将读取的数据传给 HTML 解析器,解析器动态接收字节流,并将其解析为 DOM。
    • 等文档数据传输完成之后,渲染进程会返回“确认提交”的消息给浏览器进程;
    • 浏览器进程在收到“确认提交”的消息后,会更新浏览器界面状态,包括了安全状态、地址栏的 URL、前进后退的历史状态,并更新 Web 页面。
  9. 渲染进程接收完数据后,向浏览器进程发送“确认提交”。
  10. 浏览器进程接收到确认消息后更新浏览器界面状态:安全、地址栏 url、前进后退的历史状态、更新 web 页面。

浏览器是如何渲染页面的

当浏览器的网络线程收到 HTML 文档后,会产生一个渲染任务,并将其传递给渲染主线程的消息队列。

在事件循环机制的作用下,渲染主线程取出消息队列中的渲染任务,开启渲染流程。

整个渲染流程分为多个阶段:HTML 解析、样式机算、布局、分层、绘制、分块、光栅化、画。

每个阶段都有明确的输入输出,上一个阶段的输出会成为下一个阶段的输入。

这样,整个渲染流程就形成了一套组织严密的生产流水线。

HTML 解析(DomParser())

  1. 解析过程中遇到 CSS 解析 cSS,遇到 JS 执行 JS。为了提高解析效率,浏览器在开始解析前,会启动一个预解析的线程,率先下载 HTML 中的外部 cSS 文件和外部的 JS 文件。
  2. 如果主线程解析到 link 位置,此时外部的 CSS 文件还没有下载解析好,主线程不会等待,继续解析后续的 HTML。这是因为下载和解析 CSS 的工作是在预解析线程中进行的。这就是 css 不会阻塞 HTML 解析的根本原因。
  3. 如果主线程解析到 script 位置,会停止解析 HTML,转而等待 JS 文件下载好,并将全局代码解析执行完成后,才能继续解析 HTML。这是因为 JS 代码的执行过程可能会修改当前的 DOM 树,所以 DOM 树的生成必须暂停。这就是 JS 会阻塞 HTML—解析的根本原因。
  4. 第一步完成后,会得到_DOM 树和 CSSOM 树,浏览器的默认样式、内部样式、外部样式、行内样式均会包含在 CSSOM 树中。

样式计算

  1. 主线程会遍历得到的 DOM 树,依次为树中的每个节点计算出它最终的样式 styleSheets,称之为 Computed Style。CSS 来源通常有三种:
    • 通过 link 引用的外部 CSS 文件
    • <style> 标记内的 CSS
    • 元素的 style 属性内嵌的 CSS
  2. 在这一过程中,很多预设值会变成绝对值,比如 red 会变成 rgb(255,0,0);相对单位会变成绝对单位,比如 em 会变成 px
  3. 这一步完成后,会得到一棵带有样式的 DOM 树

布局

  1. 布局阶段会依次遍历 DOM 树的每一个节点,计算每个节点的几何信息。例如节点的宽高、相对包含块的位置。
  2. 大部分时候,DOM 树和布局树并非一一对应。比如 display:none 的节点没有几何信息,因此不会生成到布局树;又比如使用了伪元素选择器,虽然 DOM 树中不存在这些伪元素节点,但它们拥有几何信息,所以会生成到布局树中。还有匿名行盒、匿名块盒等等都会导致 DOM 树和布局树无法一一对应。 (内容必须放在行盒中;行盒和块盒不能相邻)

分层

  1. 主线程会使用一套复杂的策略对整个布局树中进行分层。
  2. 分层的好处在于,将来某一个层改变后,仅会对该层进行后续处理,从而提升效率。
  3. 滚动条、堆叠上下文、transform、opacity 等样式都会或多或少的影响分层结果,也可以通过 will-change 属性更大程度的影响分层结果。一般的分层依据
    1. 拥有层叠上下文属性的元素会被提升为单独的一层,z-index 等。扩展:z-index 失效的情况:
      • 父元素 position 为 relative 时,子元素的 z-index 失效。解决:父元素 position 改为 absolute 或 static;
      • 元素没有设置 position 属性为非 static 属性。解决:设置该元素的 position 属性为 relative,absolute 或是 fixed 中的一种;
      • 元素在设置 z-index 的同时还设置了 float 浮动。解决:float 去除,改为 display:inline-block;
    2. 需要剪裁 (clip) 的地方也会被创建为图层

绘制

  1. 主线程会为每个层单独产生绘制指令集,用于描述这一层的内容该如何画出来。
  2. 完成绘制后,主线程将每个图层的绘制信息提交给合成线程,剩余工作将由合成线程 (渲染进程中) 完成。

分块

  1. 合成线程首先对每个图层进行分块,将其划分为更多的小区域。
  2. 它会从线程池中拿取多个线程来完成分块工作。

光栅化

  1. 合成线程会将块信息交给 GPU 进程,以极高的速度完成光栅化。
  2. GPU 进程会开启多个线程来完成光栅化,并且优先处理靠近视口区域的块。
  3. 光栅化的结果,就是一块一块的位图。

画 (合成和显示)

  1. 合成线程拿到每个层、每个块的位图后,生成一个个「指引 (quad) 」信息。
  2. 指引会标识出每个位图应该画到屏幕的哪个位置,以及会考虑到旋转、缩放等变形。
  3. 变形发生在合成线程,与渲染主线程无关,这就是 transform 效率高的本质原因。
  4. 合成线程会把 quad 提交给 GPU 进程,由 GPU 进程产生系统调用,提交给 GPU 硬件,完成最终的屏幕成像。

reflow

  1. reflow 的本质就是重新计算 layout 树。
  2. 当进行了会影响布局树的操作后,需要重新计算布局树,会引发 layout。
  3. 为了避免连续的多次操作导致布局树反复计算,浏览器会合并这些操作,当 JS 代码全部完成后再进行统一计算。所以,改动属性造成的 reflow 是异步完成的。
  4. 也同样因为如此,当 JS 获取布局属性时,就可能造成无法获取到最新的布局信息。
  5. 浏览器在反复权衡下,最终决定在获取属性立即 reflow。

针对 reflow 优化项

  1. 使用 transform 和 opacity 代替 top、left 和 width 等属性来进行动画效果的实现。因为 transform 和 opacity 不会引起回流。
  2. 尽量使用绝对定位 (position: absolute) 来移动元素,而不是修改元素的布局属性。因为绝对定位会脱离文档流,不会引起其他元素的重新布局。
  3. 避免使用 table 布局,因为 table 的每个单元格的内容变化都会引起回流。可以使用 CSS 的 display: table 和 display: table-cell 来实现类似的效果。
  4. 避免在循环中多次修改 DOM 元素的样式,可以先把需要修改的样式保存在变量中,然后一次性地更新 DOM 元素的样式。
  5. 避免频繁地读取布局属性 (如 offsetWidth、offsetHeight 等) ,可以把这些属性的值缓存起来使用。
  6. 使用虚拟 DOM 技术,例如 React 和 Vue.js 等框架,在组件更新时只更新变化的部分,而不是整个 DOM 树。
  7. 使用 CSS 的 will-change 属性来提前告诉浏览器某个元素即将被修改,并且浏览器可以对该元素进行一些优化。
  8. 避免频繁地修改 DOM 树的结构,可以采用一些优化策略,例如使用文档片段 (DocumentFragment) 进行批量插入和删除操作,或者使用字符串拼接的方式生成 HTML 代码。
  9. 使用 debounce 或 throttle 来降低频繁调用回流的次数,例如使用 lodash 库中的 debounce 和 throttle 方法。
  10. 使用 requestAnimationFrame 替代 setInterval 可以提升浏览器的性能。
  11. 尽量减少页面中元素的数量和复杂度,可以对不需要展示的元素进行隐藏或延迟加载,减少回流的发生。

repaint

  1. repaint 的本质就是重新根据分层信息计算了绘制指令。
  2. 当改动了可见样式后,就需要重新计算,会引发 repaint。
  3. 由于元素的布局信息也属于可见样式,所以 reflow 一定会引起 repaint。

transform 效率高原理

  1. 因为 transform 既不会影响布局也不会影响绘制指令,它影响的只是渲染流程的最后一个「draw」阶段
  2. 由于 draw 阶段在合成线程中,所以 transform 的变化几乎不会影响渲染主线程。反之,渲染主线程无论如何忙碌,也不会影响 transform 的变化。

浏览器缓存方式

http 缓存

强缓存

(memory cache 和 disk cache): 浏览器本地根据服务器设置的过期时间来判断是否使用缓存,未过期则从本地缓存里拿资源,已过期则重新请求服务器获取最新资源。

浏览器第一次请求远程服务器的某个资源时,如果服务器希望浏览器得到该资源后一段时间内不要再发送请求过来,直接从浏览器里的缓存里取,则服务器可以通过在响应头里设置 Cache-Control: max-age=31536000,max-age 代表缓存时间,单位为秒,这里的数据换算过来就是一年,意味着在一年内浏览器不会再向服务器发送请求。

  1. 不会向服务器发送请求,直接从缓存中读取资源。
  2. 在 chrome 控制台的 Network 选项中可以看到该请求返回 200 的状态码,并且 Size 显示 from disk cache 或 from memory cache。
  3. Expires:缓存过期时间,用来指定资源到期的时间,是服务器端的具体的时间点。
  4. Cache-Control:请求头、响应头都可以设置 (ctrl + f5 请求头中会携带 no-cache)
    • client 和 server 都不设置 Cache-Control:很简单,就不存在缓存。通通走网络请求
    • client 设置 Cache-Control,但是 server 没有设置 Cache-Control:无效,即无缓存。不过有个例外,『Cache-Control: max-age=0,则会向 server 请求,以检查一次是否有资源文件修改来决定是否仍然用本地已有的缓存。如果 server 查询有资源修改,则返回 状态码 200 及修改过的新资源;否则无任何修改,则返回 状态码 304 表示仍可使用缓存,即常说的 304 表示缓存重定向』
    • client 没有设置 Cache-Control,而 server 设置了 Cache-Control:有缓存,具体依照 server-response 里的 Cache-Control 具体设置值来走缓存。如:max-age=30,则下次 client 请求时,在 30s 内就直接从缓存中取,而不会从 server 请求;否则,过期就从网络请求。周而复始……
    • client 和 server 都设置 Cache-Control:有缓存,主要以 server 为准。例如,client-request 的 Cache-Control: max-age=60,server-response 的 Cache-Control: max-age=30,则最终的缓存有效期只会在 30s 内,此外即为过期则就会从网络请求了。即,以 server 的 max-age 过期时长为准。

协商缓存

浏览器本地每次都向服务器发起请求,由服务器来告诉浏览器是从缓存里拿资源还是返回最新资源给浏览器使用。

浏览器初次请求资源,服务器返回资源,同时生成一个 Etag 值携带在响应头里返回给浏览器,当浏览器再次请求资源时会在请求头里携带 If-None-Match,值是之前服务器返回的 Etag 的值,服务器收到之后拿该值与资源文件最新的 Etag 值做对比。

  1. Last-Modified(response header) => If-Modified-Since(request header)
  2. Etag(response header) => If-None-Match(request header)

实际使用策略

  1. 对与频繁变动的资源:使用 Cache-Control: no-cache,使浏览器每次都请求服务器,然后配合 ETag 或者 Last-Modified 来验证资源是否有效。这样的做法虽然不能节省请求数量,但是能显著减少响应数据大小。
  2. 对于不常变化的资源:通常在处理这类资源时,给它们的 Cache-Control 配置一个很大的 max-age=31536000 (一年),这样浏览器之后请求相同的 URL 会命中强制缓存。而为了解决更新的问题,就需要在文件名(或者路径)中添加 hash, 版本号等动态字符,之后更改动态字符,从而达到更改引用 URL 的目的,让之前的强制缓存失效 (其实并未立即失效,只是不再使用了而已)。

websql

较新的版本 chrome 支持,有以下特点

  • Web Sql 数据库 API 实际上不是 HTML5 规范的组成部分
  • 在 HTML5 之前就已经存在了,是单独的规范
  • 它是将数据以数据库的形式存储在客户端,根据需求去读取
  • 跟 Storage 的区别是: Storage 和 Cookie 都是以键值对的形式存在的
  • Web Sql 更方便于检索,允许 sql 语句查询
  • 让浏览器实现小型数据库存储功能
  • 这个数据库是集成在浏览器里面的,目前主流浏览器基本都已支持

核心方法:

  1. openDatabase: 使用现有数据库或创建新数据库创建数据库对象
  2. transaction: 根据情况控制事务提交或回滚
  3. executeSql: 执行真实的 SQL 查询
1
2
3
4
5
// openDatabase 五个参数:数据库名、版本号、描述、数据库大小、创建回调
var db = openDatabase(" mydatabase ", "1.0", "Test DB", 2 * 1024 * 1024);
db.transaction(function (tx) {
tx.executeSql("CREATE TABLE IF NOT EXISTS t1 (id unique, log)");
});

indexDB

浏览器可能对 indexDB 有 50M 大小的限制,一般用户保存大量用户数据并要求数据之间有搜索需要的场景

  1. 异步 API:Web Workers 内部和外部都可以使用。调用完后会立即返回,而不会阻塞调用线程。要异步访问数据库,要调用 window 对象 indexedDB 属性的 open() 方法。该方法返回一个 IDBRequest 对象 (IDBOpenDBRequest);异步操作通过在 IDBRequest 对象上触发事件来和调用程序进行通信
  2. 同步 API:浏览器暂不支持,需要和 Web Workers 一起使用
  • 指一般网站为了辨别用户身份、进行 session 跟踪而储存在用户本地终端上的数据 (通常经过加密) 。 cookie 一般通过 http 请求中在头部一起发送到服务器端。一条 cookie 记录主要由键、值、域、过期时间、大小组成,一般用户保存用户的认证信息

  • 不同域名之间的 cookie 信息是独立的,如果需要设置共享可以在服务器端设置 cookie 的 path 和 domain 来实现共享。浏览器端也可以通过 document.cookie 来获取 cookie,并通过 js 浏览器端也可以方便地读取/设置 cookie 的值

localstorage

1
2
3
4
localStorage.setItem(key, value); //设置记录
localStorage.getItem(key); //获取记录
localStorage.removeItem(key); //删除该域名下单条记录
localStorage.clear();

sessionstorage

application cache

将大部分图片资源、js、css 等静态资源放在 manifest 文件配置中。当页面打开时通过 manifest 文件来读取本地文件或是请求服务器文件

优势:

  • 离线浏览 – 用户可在离线时浏览完整网站 (window.ApplicationCache 接口和 window.applicationCache 对象)
  • 速度 – 缓存资源为本地资源,因此加载速度较快
  • 服务器负载更少 – 浏览器只会从发生了更改的服务器下载资源

cacheStorage

在 ServiceWorker 的规范中定义,可以保存每个 serverWorker 申明的 cache 对象

1
2
3
4
5
6
// 均返回 promise 对象
cacheStorage.open();
cacheStorage.match();
cacheStorage.has();
cacheStorage.delete();
cacheStorage.keys();

flash

基本不用,主要基于 flash 有读写浏览器端本地目录的功能,同时也可以向 js 提供调用的 api,则页面可以通过 js 调用 flash 去读写特定的磁盘目录,达到本地数据缓存的目的

浏览器缓存位置

分为四种:如果都没有命中,就发起请求来获取资源

用户行为如何触发缓存:

  • 打开网页,地址栏输入地址:查找 disk cache 中是否有匹配。如有则使用;如没有则发送网络请求
  • 普通刷新 (F5):因为 TAB 并没有关闭,因此 memory cache 是可用的,会被优先使用(如果匹配的话),其次才是 disk cache
  • 强制刷新 (Ctrl + F5):浏览器不使用缓存,因此发送的请求头部均带有 Cache-control:no-cache(为了兼容,还带了 Pragma: no-cache),服务器直接返回 200 和最新内容

memory cache

内存中的缓存:一般有脚本、字体、图片等

  • 优点:读取速度快
  • 缺点:一旦关闭 Tab 页面,内存中的缓存也就被释放了

disk cache

硬盘中的缓存:一般非脚本,比如 css 等

  • 优点:容量大
  • 缺点:读取速度慢

Service Worker

服务器与客户端之间的代理服务器,伴随着 PWA (渐进式 web 应用程序 Progressive Web App) 出现,主要作用是拦截请求,修改响应,从而控制页面加载

  • Service Worker 是运行在浏览器背后的独立线程,可以用来实现缓存功能,比如缓存静态资源等
  • 只能被使用在 https 或者本地的 localhost 环境下
  • Service Worker 的缓存与浏览器其他内建的缓存机制不同,它可以让我们自由控制缓存哪些文件、如何匹配缓存、如何读取缓存,并且缓存是持续性的

主要流程

  • 在 ServiceWorker 的启动过程中,若有任何环节出错,则 ServiceWorker 会被直接废弃,直到下次刷新页面,将重新启动

Push Cache

  • Push Cache (推送缓存) 是 HTTP/2 中的内容,当以上三种缓存都没有命中时,它才会被使用
  • 它只在会话 (Session) 中存在,一旦会话结束就被释放,并且缓存时间也很短暂

跨域解决方案

同源策略限制以下几种行为:

  • Cookie、LocalStorage 和 IndexDB 无法读取
  • DOM 和 JS 对象无法获得
  • AJAX 请求不能发送

JSONP 跨域

利用 <script> 标签没有跨域限制,通过 <script> 标签 src 属性,发送带有 callback 参数的 GET 请求,服务端将接口返回数据拼凑到 callback 函数中,返回给浏览器,浏览器解析执行,从而拿到 callback 函数返回的数据

缺点:只能发送 get 请求

  1. 原生 JS 实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    <script>
    var script = document.createElement('script');
    script.type = 'text/javascript';

    // 传参一个回调函数名给后端,方便后端返回时执行这个在前端定义的回调函数
    script.src = 'http://www.domain2.com:8080/login?user=admin&callback=handleCallback';
    document.head.appendChild(script);

    // 回调执行函数
    function handleCallback(res) {
    alert(JSON.stringify(res));
    }
    </script>

    // 服务端返回如下
    handleCallback({"success": true, "user": "admin"})
  2. jquery Ajax 实现

    1
    2
    3
    4
    5
    6
    7
    $.ajax({
    url: "http://www.domain2.com:8080/login",
    type: "get",
    dataType: "jsonp", // 请求方式为jsonp
    jsonpCallback: "handleCallback", // 自定义回调函数名
    data: {},
    });
  3. Vue axios 实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    this.$http = axios;
    this.$http
    .jsonp("http://www.domain2.com:8080/login", {
    params: {},
    jsonp: "handleCallback",
    })
    .then((res) => {
    console.log(res);
    });

跨域资源共享 (CORS)

允许浏览器向跨源服务器,发出 XMLHttpRequest 请求,克服了 AJAX 只能同源使用的限制。ie 不低于 ie10

简单请求

  1. 同时满足以下两个条件

    • 使用以下方法之一:head、get、post
    • 请求的 header 是:Accept、Accept-Language、Content-Language、Content-Type (只限于三个值:application/x-www-form-urlencoded、multipart/form-data、text/plain)

    对于简单请求,直接发出 CORS 请求,即在头信息中增加一个 Origin 字段,说明本次请求来自哪个源 (协议 + 域名 + 端口) ,服务器根据这个值,决定是否同意这次请求

    1
    2
    3
    4
    5
    6
    GET /cors HTTP/1.1
    Origin: http://api.bob.com
    Host: api.alice.com
    Accept-Language: en-US
    Connection: keep-alive
    User-Agent: Mozilla/5.0...
  2. CORS 请求设置的响应头字段:

    • Access-Control-Allow-Origin (必选) :要么是请求时 Origin 字段的值,要么是一个 \*,表示接受任意域名的请求
    • Access-Control-Allow-Credentials (可选) :表示是否允许发送 Cookie,默认无该字段,表示不允许
    • Access-Control-Expose-Headers (可选) :CORS 请求时,XMLHttpRequest 对象的 getResponseHeader() 方法只能拿到 6 个基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。如果想拿到其他字段,就必须在 Access-Control-Expose-Headers 里面指定。上面的例子指定,getResponseHeader(‘FooBar’)可以返回 FooBar 字段的值。

非简单请求

  1. put、delete 或者 Content-Type 字段:application/json

    非简单请求的 CORS 请求,会在正式通信之前,增加一次 HTTP 查询请求,称为”预检”请求 (preflight)

  2. 预检请求:options 请求

    1
    2
    3
    4
    5
    6
    7
    8
    OPTIONS /cors HTTP/1.1
    Origin: http://api.bob.com // 表示请求来自哪个源
    Access-Control-Request-Method: PUT
    Access-Control-Request-Headers: X-Custom-Header
    Host: api.alice.com
    Accept-Language: en-US
    Connection: keep-alive
    User-Agent: Mozilla/5.0..
    • Access-Control-Request-Method (必选) :用来列出浏览器的 CORS 请求会用到哪些 HTTP 方法,上例是 PUT
    • Access-Control-Request-Headers (可选) :逗号分隔的字符串,指定浏览器 CORS 请求会额外发送的头信息字段,上例是 X-Custom-Header
  3. 预检请求的回应:服务器收到”预检”请求以后,检查了 Origin、Access-Control-Request-Method 和 Access-Control-Request-Headers 字段以后,确认允许跨源请求,就可以做出回应。回应中有以下字段

    • Access-Control-Allow-Methods (必选) :逗号分隔的一个字符串,表明服务器支持的所有跨域请求的方法
    • Access-Control-Allow-Headers:如果浏览器请求包括 Access-Control-Request-Headers 字段,则 Access-Control-Allow-Headers 字段是必需的。它也是一个逗号分隔的字符串,表明服务器支持的所有头信息字段
    • Access-Control-Allow-Credentials (可选) :与简单请求相同含义
    • Access-Control-Max-Age (可选) :指定本次预检请求的有效期,单位为秒

CORS 跨域示例

  • 前端设置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
var xhr = new XMLHttpRequest(); // IE8/9需用window.XDomainRequest兼容

// 前端设置是否带cookie
xhr.withCredentials = true;

xhr.open("post", "http://www.domain2.com:8080/login", true);
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
xhr.send("user=admin");

xhr.onreadystatechange = function () {
if (xhr.readyState == 4 && xhr.status == 200) {
alert(xhr.responseText);
}
};
  • 后端设置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
var http = require("http");
var server = http.createServer();
var qs = require("querystring");

server.on("request", function (req, res) {
var postData = "";

// 数据块接收中
req.addListener("data", function (chunk) {
postData += chunk;
});

// 数据接收完毕
req.addListener("end", function () {
postData = qs.parse(postData);

// 跨域后台设置
res.writeHead(200, {
"Access-Control-Allow-Credentials": "true", // 后端允许发送Cookie
"Access-Control-Allow-Origin": "http://www.domain1.com", // 允许访问的域 (协议+域名+端口)
/*
* 此处设置的cookie还是domain2的而非domain1,因为后端也不能跨域写cookie(nginx反向代理可以实现),
* 但只要domain2中写入一次cookie认证,后面的跨域接口都能从domain2中获取cookie,从而实现所有的接口都能跨域访问
*/
"Set-Cookie": "l=a123456;Path=/;Domain=www.domain2.com;HttpOnly", // HttpOnly的作用是让js无法读取cookie
});

res.write(JSON.stringify(postData));
res.end();
});
});

server.listen("8080");
console.log("Server is running at port 8080...");

ningx 代理跨域

本质与 CORS 原理相同

  1. nginx 配置解决 iconfont 跨域

    1
    2
    3
    4
    5
    6
    // 浏览器跨域访问js、css、img等常规静态资源被同源策略许可,
    // 但iconfont字体文件(eot|otf|ttf|woff|svg)例外,此时可在nginx的静态资源服务器中加入以下配置

    location / {
    add_header Access-Control-Allow-Origin *;
    }
  2. nginx 反向代理接口跨域

    通过 Nginx 配置一个代理服务器域名与 domain1 相同,端口不同,做跳板机,反向代理访问 domain2 接口,并且可以顺便修改 cookie 中 domain 信息,方便当前域 cookie 写入,实现跨域访问

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    #proxy服务器
    server {
    listen 81;
    server_name www.domain1.com;

    location / {
    proxy_pass http://www.domain2.com:8080; #反向代理
    proxy_cookie_domain www.domain2.com www.domain1.com; #修改cookie里域名
    index index.html index.htm;

    # 当用webpack-dev-server等中间件代理接口访问nignx时,此时无浏览器参与,故没有同源限制,下面的跨域配置可不启用
    add_header Access-Control-Allow-Origin http://www.domain1.com; #当前端只跨域不带cookie时,可为*
    add_header Access-Control-Allow-Credentials true;
    }
    }

ningx 中间件代理跨域

  1. 非 vue 框架的跨域:使用 node + express + http-proxy-middleware

    • 前端代码
    1
    2
    3
    4
    5
    6
    7
    8
    var xhr = new XMLHttpRequest();

    // 前端开关:浏览器是否读写cookie
    xhr.withCredentials = true;

    // 访问http-proxy-middleware代理服务器
    xhr.open("get", "http://www.domain1.com:3000/login?user=admin", true);
    xhr.send();
    • 中间件服务器代码
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    var express = require("express");
    var proxy = require("http-proxy-middleware");
    var app = express();

    app.use(
    "/",
    proxy({
    // 代理跨域目标接口
    target: "http://www.domain2.com:8080",
    changeOrigin: true,

    // 修改响应头信息,实现跨域并允许带cookie
    onProxyRes: function (proxyRes, req, res) {
    res.header("Access-Control-Allow-Origin", "http://www.domain1.com");
    res.header("Access-Control-Allow-Credentials", "true");
    },

    // 修改响应信息中的cookie域名
    cookieDomainRewrite: "www.domain1.com", // 可以为false,表示不修改
    })
    );

    app.listen(3000);
    console.log("Proxy server is listen at port 3000...");
  2. vue 框架的跨域:使用 node + vue + webpack + webpack-dev-server

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    module.exports = {
    entry: {},
    module: {},
    ...
    devServer: {
    historyApiFallback: true,
    proxy: [{
    context: '/login',
    target: 'http://www.domain2.com:8080', // 代理跨域目标接口
    changeOrigin: true,
    secure: false, // 当代理某些https服务报错时用
    cookieDomainRewrite: 'www.domain1.com' // 可以为false,表示不修改
    }],
    noInfo: true
    }
    }

document.domain + iframe 跨域

此方案仅限主域相同,子域不同的跨域应用场景。实现原理:两个页面都通过 js 强制设置 document.domain 为基础主域,就实现了同域

  • 父窗口
1
2
3
4
5
<iframe id="iframe" src="http://child.domain.com/b.html"></iframe>
<script>
document.domain = 'domain.com';
var user = 'admin';
</script>
  • 子窗口
1
2
3
4
<script>
document.domain = 'domain.com'; // 获取父窗口中变量 console.log('get js data
from parent ---' + window.parent.user);
</script>

location.hash + iframe 跨域

实现原理: a 欲与 b 跨域相互通信,通过中间页 c 来实现。 三个页面,不同域之间利用 iframe 的 location.hash 传值,相同域之间直接 js 访问来通信。

具体实现:A 域:a.html -> B 域:b.html -> A 域:c.html,a 与 b 不同域只能通过 hash 值单向通信,b 与 c 也不同域也只能单向通信,但 c 与 a 同域,所以 c 可通过 parent.parent 访问 a 页面所有对象

  • a.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<iframe id="iframe" src="http://www.domain2.com/b.html" style="display:none;"></iframe>
<script>
var iframe = document.getElementById('iframe');

// 向b.html传hash值
setTimeout(function() {
iframe.src = iframe.src + '#user=admin';
}, 1000);

// 开放给同域c.html的回调方法
function onCallback(res) {
alert('data from c.html ---> ' + res);
}
</script>
  • b.html
1
2
3
4
5
6
7
8
9
<iframe id="iframe" src="http://www.domain1.com/c.html" style="display:none;"></iframe>
<script>
var iframe = document.getElementById('iframe');

// 监听a.html传来的hash值,再传给c.html
window.onhashchange = function () {
iframe.src = iframe.src + location.hash;
};
</script>
  • c.html
1
2
3
4
5
6
7
<script>
// 监听b.html传来的hash值
window.onhashchange = function () {
// 再通过操作同域a.html的js回调,将结果传回
window.parent.parent.onCallback('hello: ' + location.hash.replace('#user=', ''));
};
</script>

window.name + iframe 跨域

window.name 属性的独特之处:name 值在不同的页面 (甚至不同域名) 加载后依旧存在,并且可以支持非常长的 name 值 (2MB)

  • a.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
var proxy = function (url, callback) {
var state = 0;
var iframe = document.createElement("iframe");

// 加载跨域页面
iframe.src = url;

// onload事件会触发2次,第1次加载跨域页,并留存数据于window.name
iframe.onload = function () {
if (state === 1) {
// 第2次onload(同域proxy页)成功后,读取同域window.name中数据
callback(iframe.contentWindow.name);
destoryFrame();
} else if (state === 0) {
// 第1次onload(跨域页)成功后,切换到同域代理页面
iframe.contentWindow.location = "http://www.domain1.com/proxy.html";
state = 1;
}
};

document.body.appendChild(iframe);

// 获取数据以后销毁这个iframe,释放内存;这也保证了安全 (不被其他域frame js访问)
function destoryFrame() {
iframe.contentWindow.document.write("");
iframe.contentWindow.close();
document.body.removeChild(iframe);
}
};

// 请求跨域b页面数据
proxy("http://www.domain2.com/b.html", function (data) {
alert(data);
});
1
<script>window.name = 'This is domain2 data!';</script>

postMessage 跨域

postMessage 是 HTML5 XMLHttpRequest Level 2 中的 API,且是为数不多可以跨域操作的 window 属性之一,它可用于解决以下方面的问题:

  • 页面和其打开的新窗口的数据传递
  • 多窗口之间消息传递
  • 页面与嵌套的 iframe 消息传递
  • 上面三个场景的跨域数据传递

用法:postMessage(data,origin)方法接受两个参数:

  • data: html5 规范支持任意基本类型或可复制的对象,但部分浏览器只支持字符串,所以传参时最好用 JSON.stringify()序列化
  • origin: 协议+主机+端口号,也可以设置为 “*“,表示可以传递给任意窗口,如果要指定和当前窗口同源的话设置为 “/“
  • a.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<iframe id="iframe" src="http://www.domain2.com/b.html" style="display:none;"></iframe>
<script>
var iframe = document.getElementById('iframe');
iframe.onload = function() {
var data = {
name: 'aym'
};
// 向domain2传送跨域数据
iframe.contentWindow.postMessage(JSON.stringify(data), 'http://www.domain2.com');
};

// 接受domain2返回数据
window.addEventListener('message', function(e) {
alert('data from domain2 ---> ' + e.data);
}, false);
</script>
  • b.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<script>
// 接收domain1的数据
window.addEventListener('message', function(e) {
alert('data from domain1 ---> ' + e.data);

var data = JSON.parse(e.data);
if (data) {
data.number = 16;

// 处理后再发回domain1
window.parent.postMessage(JSON.stringify(data), 'http://www.domain1.com');
}
}, false);
</script>

WebSocket 协议跨域

WebSocket protocol 是 HTML5 一种新的协议。它实现了浏览器与服务器全双工通信,同时允许跨域通讯,是 server push 技术的一种很好的实现。借助 Socket.io 封装的库可方便使用

  • 前端代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<div>user input:<input type="text"></div>
<script src="https://cdn.bootcss.com/socket.io/2.2.0/socket.io.js"></script>
<script>
var socket = io('http://www.domain2.com:8080');

// 连接成功处理
socket.on('connect', function() {
// 监听服务端消息
socket.on('message', function(msg) {
console.log('data from server: ---> ' + msg);
});

// 监听服务端关闭
socket.on('disconnect', function() {
console.log('Server socket has closed.');
});
});

document.getElementsByTagName('input')[0].onblur = function() {
socket.send(this.value);
};
</script>
  • node 代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
var http = require("http");
var socket = require("socket.io");

// 启http服务
var server = http.createServer(function (req, res) {
res.writeHead(200, {
"Content-type": "text/html",
});
res.end();
});

server.listen("8080");
console.log("Server is running at port 8080...");

// 监听socket连接
socket.listen(server).on("connection", function (client) {
// 接收信息
client.on("message", function (msg) {
client.send("hello:" + msg);
console.log("data from client: ---> " + msg);
});

// 断开处理
client.on("disconnect", function () {
console.log("Client socket has closed.");
});
});

浏览器跨页签通信

LocalStorage/SessionStorage

storage 事件仅在 不同标签页间的 LocalStorage 数据变化时才会触发,同一标签页内的 LocalStorage 变化不会触发该事件

  1. 写入数据:使用 localStorage.setItem(key, value) 方法将数据存储到 LocalStorage 中
  2. 监听数据变化:通过监听 LocalStorage 的 storage 事件来检测数据的变化,并进行相应的处理:
1
2
3
4
5
window.addEventListener("storage", (e) => {
if (e.key === STORAGE_KEY) {
creatMessageElement(e.newValue);
}
});

BroadcastChannel

发送者将消息广播到所有订阅该频道的标签页,该频道在同源下的所有浏览器上下文共用,一个名称只对应一个频道。

SharedWorker

SharedWorker 是一种在多个标签页之间共享的后台线程。

Web Workers 分为两种:

专用线程 Dedicated worker:一个专用 worker 仅能被生成它的脚本所使用,也就是只能在当前窗口当前页面使用。

共享线程 Shared worker:一个共享 worker 可以被多个脚本使用,即使这些脚本正在被不同的 window、iframe 或者 worker 访问,所以 SharedWorker 允许不同标签页之间共享一个后台线程,从而实现数据和消息的共享。

window.open + window.opener

当我们使用 window.open 打开页面时,将返回一个被打开页面 window 的引用。被打开的页面可以通过 window.opener 获取到打开它的页面的引用,通过这种方式我们就将这些页面建立起联系。

WebSocket

浏览器和服务器之间建立持久连接的协议,可以实现双向通信。通过 WebSocket,我们可以在不同的标签页之间进行实时的数据传输和通信。

在跨标签通信方面,我们可以在每个标签页中都创建一个 WebSocket 连接,并通过 WebSocket 发送和接收消息。当一个标签页发送消息时,其他标签页可以通过监听 WebSocket 事件来接收消息,并做出相应的处理。

Service Worker

Service Worker 是一种独立于网页的脚本,可以在后台运行,提供离线缓存和消息传递等功能。标签页可以通过 Service Worker 进行通信,发送消息和接收消息。

Window.postMessage()

通过调用 postMessage() 方法并指定目标窗口的 origin,可以将消息发送到其他标签页,并通过监听 message 事件来接收消息。

Cookies

当一个标签页更新数据时,将数据写入到 Cookies 中,其他标签页可以通过监听 Cookies 变化事件或定时读取 Cookies 来获取最新的数据。

使用 Cookies 进行通信是一种简单的方法,但它主要用于在客户端和服务器之间传递数据,而不是直接实现跨标签页通信。Cookies 会自动在客户端和服务器之间进行传递,因此可以在不同的标签页之间共享数据。

IndexedDB

IndexedDB 是浏览器提供的一个客户端数据库,可以在不同的标签页之间存储和读取数据。一个标签页可以将数据写入 IndexedDB,其他标签页可以监听 IndexedDB 的变化事件或定时从 IndexedDB 中读取数据来实现数据的共享和状态的同步。