写在前面的话:最近复习过程中积累的笔记,本文内容主要涉及浏览器渲染、跨域、事件机制及常见的 DOM/BOM/URL/Fetch API。内容来源五花八门,如有不妥或错误欢迎指出。
跨域问题
同源指的是协议、域名、端口都一致。同源策略是指浏览器为了安全,会限制不同源网页之间的通信,例如会限制 fetch 请求、WebSocket 连接、部分 HTML 标签资源如字体、Web Worker 脚本的加载、Cookie、LocalStorage 和 IndexDB 的读取。对于 XMLHttpRequest 请求,在跨域时,浏览器可以发出请求,服务器也会响应数据,但是浏览器会禁止 JS 读取服务器响应的数据。
跨域就是使用一些方法来避开同源策略的限制,使不同域之间可以通信,常用的方法包括:
-
CORS 跨域资源共享:后端设置 HTTP 响应头,将
Access-Control-Allow-Origin配置为允许请求的网站地址。CORS 区分了简单请求和复杂请求。简单请求指的是请求头为
GET、POST、HEAD,且请求头只包含Accept、Accept-Language、Content-Language、Content-Type(值只能是application/x-www-form-urlencoded、multipart/form-data或text/plain,即表单和纯文本)和Origin。对于复杂请求,如
PUT、DELETE或者Content-Type字段的类型是application/json,浏览器在正式通信之前会发送一次OPTIONS预检请求,预检请求的目的就是确认安全,避免服务器响应不允许的请求导致数据被篡改和删除(请求头为 GET、HEAD 的简单请求,服务器即使响应了数据也无法被浏览器读取,部分请求头为 POST 的简单响应是 HTML 支持的表单信息,很难造成攻击)。预检请求的请求头关键字段如下:OPTIONS /resource HTTP/1.1 Origin: https://a.com // 表明请求来自哪个源 Access-Control-Request-Method: POST // 表明请求方法(必选) Access-Control-Request-Headers: x-custom-token, content-type // 表明请求头字段(可选)服务器收到预检请求以后,检查了
Origin、Access-Control-Request-Method、Access-Control-Request-Headers字段以后,确认允许跨域请求,浏览器才会发出正式请求。HTTP/1.1 204 No Content Access-Control-Allow-Origin: https://a.com Access-Control-Allow-Methods: POST, GET, OPTIONS Access-Control-Allow-Headers: Content-Type Access-Control-Max-Age: 86400 -
PROXY代理:由同源的服务器代为转发请求,绕过浏览器的同源限制,但只在开发时有效。
-
JSONP:利用
<script>标签不受同源策略限制的特点,将请求作为脚本引入,但只能发起GET请求。 -
WebSocket 协议不受同源策略的限制,所以可以通过 WebSocket 进行跨域通信。但是浏览器在发起 WebSocket 握手请求时依然会进行类似于预检请求的检查,将 Origin 告知给服务端,服务器可以检查 Origin 头并决定是否建立连接,如果服务器不检查,就会允许建立连接。在建立 WebSocket 连接之后,通信不受跨域限制。
跨标签页通信
跨域标签页通信也属于跨域问题,常用的方法有 postMessage 和 SharedWorker 。
-
iframe相关跨域场景。(1)如果只是想要嵌入并展示页面,但无数据通信,此时只需要使用
iframe标签并设置src属性。(2)如果当主域相同,子域不同,可以在两个页面中设置
document.domain为基础主域,就实现了同域。父窗口:
http://www.domain.com/a.html<iframe id="iframe" src="http://child.domain.com/b.html"></iframe> <script> document.domain = 'domain.com'; var user = 'admin'; </script>子窗口:
child.domain.com/a.html<script> document.domain = 'domain.com'; // 获取父窗口中变量 console.log('get js data from parent ---> ' + window.parent.user); </script>(3)如果父子页面需要跨域通信,现代浏览器提供了
window.postMessage(data,origin)。postMessage是为跨域通信而设计的,它的原理是为两个具体页面提供通信管道:<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'); }; // 监听message,接受domain2返回数据 window.addEventListener('message', function(e) { alert('data from domain2 ---> ' + e.data); }, false); </script><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> -
SharedWorker实现多标签通信。在页面中,我们可以使用new关键字并且根据同源的SharedWorker脚本来初始化一个Shared Worker,使用同一个脚本创建的SharedWorker会复用同一个实例。操作主要基于port属性,如start激活端口,开始发送消息;close断开连接;postMessage发送消息。const worker = new SharedWorker('./worker.js'); // 必须同源 worker.port.onmessage = (e) => { if (e.data.event === 'connected') { worker.port.postMessage({ event: 'init', payload: { username: 'king' } }) } }虽然
SharedWorker要求创建的实例与根据的脚本必须是同源的,但是在SharedWorker脚本内部,我们可以通过设置允许访问的 Origin 白名单并检查connect事件的event.origin来实现受控的跨域通信。// shared-worker.js 内部的代码 const allowedOrigins = ['https://a.com', 'https://trusted-b.com']; self.onconnect = (event) => { const requestOrigin = event.origin; // 检查请求的来源是否在白名单内 if (!allowedOrigins.includes(requestOrigin)) { // 不在白名单,拒绝连接(可以选择关闭端口或不做任何处理) console.log(`Rejected connection from: ${requestOrigin}`); const port = event.ports[0]; port.close(); // 显式关闭连接 return; } // 在白名单内,允许连接并设置消息监听 const port = event.ports[0]; port.onmessage = (e) => { console.log('Message from trusted origin:', e.data); }; port.postMessage('Hello from SharedWorker!'); };
Worker API
Web Worker 用于在浏览器中创建独立于主线程的后台线程,适用于数量密集型场景,如大数据计算、解析文件、图像处理、加密解密等,但不能直接访问 window 因此也不能操作 DOM。
Service Worker 本质上充当浏览器与服务器之间的代理层,它能够拦截网络请求,当网络离线时,请求可以从缓存中获取资源,适用于实现离线缓存、消息通知等。常配合 Cache API、Push API、Background Sync API 使用。 Cache API 帮助其缓存请求和相应数据。
Shared Worker 令多个标签页可以共享同一个线程。
浏览器缓存机制
浏览器每次发起请求,都会先在浏览器缓存中查找该请求的结果以及缓存标识,根据结果来决定是否使用,这是强制缓存。如果是第一次发起请求,那么该请求的缓存结果和标识不存在,浏览器会向服务器发起请求并将结果和标识存入缓存。(这里的缓存是磁盘缓存)
浏览器在缓存中查找请求结果与标识,并且该结果未失效,那么强制缓存生效,浏览器将使用缓存中的结果。如果该结果失效,浏览器将进行协商缓存。协商缓存就是强制缓存失效后,浏览器携带缓存标识向服务器发起请求,由服务器根据缓存标识决定是否使用缓存的过程。如果协商缓存生效,返回 304(Not Modified) ;如果协商缓存失效,返回 200和请求结果。浏览器将该结果和缓存标识存入缓存中。
强制缓存中,HTTP/1.0 请求头控制缓存的字段是 Expires ,其原理是对比客户端与服务端返回的时间,可能因时差等因素存在误差。HTTP/1.1请求头中控制缓存的字段是 Cache-Control,默认值通常为 public(响应可以被用户浏览器、代理服务器、CDN等缓存),如果存在身份验证则为 private(响应只能被用户浏览器缓存), Cache-Control 还有值 no-cache (可以缓存,并会强制向服务器验证缓存是否过期,可以与 public/private 一起使用),no-store (不可缓存),max-age=600(缓存内容将在一段时间之后失效,如600秒后)。
协商缓存基于两种头部:第一种基于时间实现,请求头字段为 If-Modified-Since,响应头字段为 Last-Modified;第二种基于唯一标识符,优先度高,请求头字段为 If-None-Match,响应头字段为 Etag,是响应资源唯一标识。
进程、线程、协程
- 进程是操作系统进行资源分配的基本单位。 当我们启动一个应用程序时,计算机会创建一个进程来执行任务代码并为其分配内存空间,该应用程序的状态都保存在该内存里,若程序关闭则内容会被回收。进程提供资源隔离,不同的进程间进行数据传递需要通过进程间通信管道 IPC。许多应用程序会使用多个进程,来防止一个进程崩溃导致整个程序崩溃。
- 线程是操作系统能够进行运算调度的最小单位。 一个进程上可以有多个线程,多线程共享内存,需用锁机制避免资源竞争。
- 协程基于线程,比线程更轻量,适合高并发 I/O 密集型任务(如爬虫、聊天系统),协程的实际应用有 Python 的
asyncio或 Golang 的goroutine。
模块化
在模块化出现之前,网页加载所需要的 JS 文件通常写在 <script> 里,如果直接使用 <script src="..."> 那么 JS 文件会同步加载并执行,阻塞 HTML 继续向下解析。使用 async 和 defer 属性可以异步加载, async 会令 JS 文件在加载完成后立即执行,并且不保证执行顺序,defer 会令 JS 文件在HTML解析完成之后严格按照在文档中的顺序执行。在过去,defer 通常比 async 用得更广泛,因为它能保证多个脚本的执行顺序,更符合大多数应用脚本的依赖需求,同时又不阻塞渲染。
现代前端更多地使用模块化来引入依赖。模块化很好地解决了一些问题,比如不再有依赖和变量的命名冲突,依赖顺序会在模块化中统一管理,易于复用。主流的模块化规范有 CommonJS、ESModule、AMD、CMD等。
CJS 和 ESM 的区别:
- CJS 更早出现,是 Node 社区标准,主要用于 Node.js(服务端),新增了 require 函数和 module.exports 对象。ESM 是官方标准,新增了
import / export语法。 - CJS 是在运行时加载和分析依赖,因此会难以进行 tree-shaking。ESM 是编译时静态分析依赖关系,这使得依赖关系在代码运行前就确定了,便于 tree-shaking。
CJS 的核心是 require() 函数,这个函数不是一个关键字,而是一个可以在代码中任何地方被调用的普通函数。当 Node.js 逐行执行代码时,遇到 require() 例如 const moduleA = require('./moduleA'); 这一行时,将会暂停当前模块的执行,去同步地读取并执行 moduleA.js 文件的所有代码,获取 moduleA.js 中通过 module.exports 导出的对象并将导出的对象赋值给变量 moduleA,之后继续执行当前模块 require() 语句之后的代码。
渲染机制
浏览器可以分为用户界面、浏览器引擎、渲染引擎。
最初的浏览器是单线程的,这有很多问题,如一个页面卡死造成整体崩溃、JS 可以访问任意资源。
现代浏览器结构是多进程的,根据功能有浏览器进程(控制除标签页外的用户界面,包括地址栏、书签、进退按钮,以及和其他进程协调工作)、缓存进程、网络进程、插件进程、GPU进程(用于整个浏览器页面的渲染)、渲染器进程(控制标签页内的界面)等。
默认情况下,无论访问不同站点还是同一站点,每个标签页都有自己的进程,这样可以做到资源隔离,更加安全。但 Chrome 浏览器可以更改进程模型,包括默认情况一共有四种,其他还有同一站点使用同一进程、一个标签页里的所有站点使用同一进程、浏览器引擎和渲染引擎共用一个进程。
输入 URL 之后,浏览器进程先通过 URL 格式检测等确定这是一个需要通过网络加载的 URL,然后会通知网络进程。网络线程获取到数据后,通过 SafeBrowsing 来判断是否是恶意站点,如果安全,网络线程会告知浏览器线程,浏览器线程会创建一个渲染器进程,通过 IPC 管道将数据传输给渲染器进程,渲染器进程的任务是将这些 HTML、CSS、JS、Image 等数据渲染成用户可交互的页面。
渲染器线程解析 HTML 构建 DOM 树,过程中往往会引入 CSS、图片、JS 等资源。 JS 可能影响 DOM 树生成,因此碰到 script 标签就会停止解析 HTML 而去加载和解析 JS。CSS、图片不影响 DOM 树生成,因此不会阻塞 HTML 解析。 渲染器线程解析 CSS 构建 CSSOM 树,与 DOM 树的构建是互不干扰的,没有严格先后顺序,但需要等到 DOM 树和 CSSOM 树都解析完成之后才能合并为 Layout tree(渲染树或布局树),Layout tree 与最后网页上的节点是一一对应的。主线程遍历 Layout tree 创建绘制记录表(paint record)记录绘制顺序,这样就有了 Layer tree(图层树)。主线程将数据传输给合成器线程,合成器线程绘制(paint)图层,并发送给栅格线程将其栅格化(rastering)并切分为图块,再将数据传输给浏览器线程,浏览器线程再把数据传输给 GPU 线程,最终渲染在网页上。
改变一个元素的几何属性,如位置、宽高、显示与否,主线程会重新计算样式、布局,重新生成 Layout tree,更新绘制记录表并重新栅格化,即重排。如果没有改变元素的位置与宽高,而只改变了颜色、透明度等,那么布局和 Layout tree 仍然保持不变,主线程只会重新计算样式,更新绘制记录表并重新栅格化,即重绘。也就是说,重排必然导致重绘,重绘不一定会重排。
关于隐藏元素的几种方法,opacity:0 和 visibility:hidden 只导致重绘,元素虽然不显示但是仍存在于 Layout tree。使用 display:none 会导致重排和重绘。
重排、重绘、JS 都运行在主线程上,如果 JS 耗时过长,就会造成页面卡顿,优化方法例如 requestAnimationFrame 可以帮助拆分 JS 线程,防止页面卡顿,这也是 React Fiber 的原理。另外是使用 transform 进行样式变更,因为它影响的是图层,不会导致重新布局和绘制,而是运行在合成器线程和栅格线程(不阻塞主线程)。
事件机制
浏览器的 DOM 事件流( event flow )存在三个阶段:事件捕获阶段、处于目标阶段、事件冒泡阶段。事件捕获是当某元素触发某一类型事件的时候,浏览器会从根节点开始逐级向该元素遍历,寻找同类型事件。事件冒泡与之相反,当某元素触发某一类型事件,那么从该元素起逐级向外层的元素检测是否存在与本身同样的事件。
一次事件执行,会先进行事件捕获,再到目标本身,最后再进行事件冒泡,不过这并不意味着一次事件会在捕获到冒泡的过程中触发两次,因为事件冒泡和事件捕获本身并不会主动触发事件,需要我们决定事件在哪个阶段执行。大多数事件会在冒泡阶段执行。
element.addEventListener(event, function, useCapture);
addEventListener 第三个参数默认值是false,表示在事件冒泡阶段调用事件处理函数;如果参数为true,则表示在事件捕获阶段调用处理函数。
事件代理就是利用事件冒泡(或事件捕获)机制,来避免大量事件注册。例如,需要监听列表中的每个列表项上的点击事件,可以直接监听列表,而非监听每个列表项,这样每个列表项上的点击事件被触发时就会被上层元素捕获。
通过 stopPropagation() 可以阻止事件冒泡。 通过 preventDefault() 可以阻止默认行为。
浏览器内置的事件类型主要包括以下几种,也可以使用 Event 构造函数或者 CustomEvent 构造函数自定义事件。
// 常用鼠标事件
element.addEventListener('click', handler); // 点击
element.addEventListener('dblclick', handler); // 双击
element.addEventListener('mousedown', handler); // 鼠标按下
element.addEventListener('mouseup', handler); // 鼠标释放
element.addEventListener('mousemove', handler); // 鼠标移动
element.addEventListener('mouseenter', handler); // 鼠标进入
element.addEventListener('mouseleave', handler); // 鼠标离开
// 键盘事件
element.addEventListener('keydown', handler); // 按键按下
element.addEventListener('keyup', handler); // 按键释放
// 表单相关事件
form.addEventListener('submit', handler); // 表单提交
input.addEventListener('change', handler); // 值改变
input.addEventListener('input', handler); // 输入时
input.addEventListener('focus', handler); // 获得焦点
input.addEventListener('blur', handler); // 失去焦点
// 窗口和文档事件
window.addEventListener('load', handler); // 页面加载完成
window.addEventListener('resize', handler); // 窗口大小改变
window.addEventListener('scroll', handler); // 滚动
document.addEventListener('DOMContentLoaded', handler); // DOM加载完成
// 移动端触摸事件
element.addEventListener('touchstart', handler); // 触摸开始
element.addEventListener('touchmove', handler); // 触摸移动
element.addEventListener('touchend', handler); // 触摸结束
DOM API
文档对象模型(DOM API)是浏览器提供的一套允许 JS 操作 HTML 页面的 API。
如何获取元素位置和宽高?
// DOM 元素尺寸属性,触发重排,是 layout tree 的数据
const width1 = element.offsetWidth; // content + padding + border 常用于获取元素在页面上实际占用的盒子宽高。
const width2 = element.clientWidth; // content + padding 常用于获取可见区域的宽高
const width3 = element.scrollWidth; // content + padding + 滚动距离 常用于获取滚动内容的完整大小
// element.scrollWidth > element.clientWidth 常用于判断单行文本溢出
// element.scrollWidth > parseInt(getComputedStyle(element).lineHeight) 常用于自适应换行时的判断
// CSS计算的宽度值,触发重排,返回带px单位字符串
const width4 = window.getComputedStyle(element).width;
// 不触发重排,返回内联样式style中的width属性,很少用
const width5 = element.style.width;
// 不触发重排,返回DOMRect对象,包含:x / y / top / right / bottom / left / width / height
const rect = element.getBoundingClientRect();
// 相对父元素的偏移量
const offset = element.offsetTop;
选择器API
const element = document.getElementById('myId'); // 返回匹配元素
const elements = document.getElementsByClassName('myClass'); // 返回 HTMLCollection
const divs = document.getElementsByTagName('div'); // 返回 HTMLCollection
const inputs = document.getElementsByName('username'); // 返回 NodeList
const firstItem = document.querySelector('.item'); // 返回第一个匹配元素
const allItems = document.querySelectorAll('.item'); // 返回 NodeList
类名操作 classList API
element.classList.add('active', 'highlight');
element.classList.remove('active', 'hidden');
element.classList.toggle('active');
const isActive = element.classList.contains('active');
element.classList.replace('old-class', 'new-class');
数据集API(data-*)
// HTML: <div data-user-id="123" data-user-role="admin">
const userId = element.dataset.userId; // "123" 获取属性的值
element.dataset.loading = 'true'; // 修改属性
BOM API
浏览器对象模型(BOM API)允许 JS 操作浏览器,如窗口、地址栏、历史记录、计时器等,主要围绕 window 对象(window 包括 document、navigator、location、history等,即 BOM 对象包括 DOM 对象)。浏览器打开一个页面就会创造一套 BOM 对象。
window 对象常用 API(窗口信息)
// 窗口尺寸和滚动
const width = window.innerWidth; // 视口宽度
const height = window.innerHeight; // 视口高度
const scrollY = window.scrollY; // 垂直滚动位置
const scrollX = window.scrollX; // 水平滚动位置
// scrollY/scrollX 是标准属性,pageYOffset/pageXOffset 是旧浏览器支持的属性
window.scrollY === window.pageYOffset; // true
window.scrollX === window.pageXOffset; // true
// 窗口操作
window.open('https://example.com', '_blank'); // 打开新窗口
window.close(); // 关闭当前窗口
window.resizeTo(800, 600); // 调整窗口大小
window.moveTo(100, 100); // 移动窗口
navigator 对象常用 API(浏览器信息)
// 浏览器信息
console.log(navigator.userAgent); // 用户代理字符串
console.log(navigator.language); // 浏览器语言
console.log(navigator.languages); // 用户偏好语言数组
// 功能检测
console.log(navigator.onLine); // 是否在线
console.log(navigator.cookieEnabled); // 是否启用cookie
console.log('geolocation' in navigator); // 是否支持地理定位
// 硬件信息
console.log(navigator.hardwareConcurrency); // CPU核心数
console.log(navigator.deviceMemory); // 设备内存(GB)
// 现代API检测
console.log('serviceWorker' in navigator); // 是否支持Service Worker
console.log('clipboard' in navigator); // 是否支持剪贴板API
location 对象常用 API(URL管理)
// 获取URL信息
console.log(location.href); // 完整URL
console.log(location.origin); // 协议+主机+端口
console.log(location.protocol); // 协议 (http:, https:)
console.log(location.host); // 主机+端口
console.log(location.hostname); // 主机名
console.log(location.port); // 端口
console.log(location.pathname); // 路径
console.log(location.search); // 查询参数
console.log(location.hash); // 哈希值
// 导航操作
location.assign('https://example.com'); // 跳转到新页面
location.replace('https://example.com'); // 替换当前页面(无历史记录)
location.reload(); // 重新加载页面
location.reload(true); // 强制从服务器重新加载
// URL操作
location.search = '?page=2&sort=name'; // 修改查询参数
location.hash = '#section1'; // 修改哈希值
history 对象常用 API(浏览历史)
// 导航历史
history.back(); // 后退一页
history.forward(); // 前进一页
history.go(-2); // 后退两页
history.go(1); // 前进一页
// 添加历史记录(现代SPA应用常用)
history.pushState({ page: 1 }, 'Title', '/page1'); // 添加新历史记录
history.replaceState({ page: 2 }, 'Title', '/page2'); // 替换当前历史记录
// 获取状态
console.log(history.state); // 当前历史记录状态
console.log(history.length); // 历史记录数量
// 监听历史变化
window.addEventListener('popstate', (event) => {
console.log('历史记录变化', event.state);
});
screen 对象常用 API(屏幕信息)
// 屏幕尺寸
console.log(screen.width); // 屏幕宽度
console.log(screen.height); // 屏幕高度
console.log(screen.availWidth); // 可用宽度(排除任务栏等)
console.log(screen.availHeight); // 可用高度
// 色彩和像素
console.log(screen.colorDepth); // 颜色深度
console.log(screen.pixelDepth); // 像素深度
// 方向信息
console.log(screen.orientation?.type); // 屏幕方向
console.log(screen.orientation?.angle); // 旋转角度
// 监听屏幕方向变化
screen.orientation.addEventListener('change', () => {
console.log('屏幕方向改变');
});
埋点上报
埋点上报统计用户页面停留时长,一般会在每次切换到后台时上报、关闭或刷新页面时再一次上报。可以用 Navigator.sendBeacon() 配合 Page Visibility API 统计用户页面停留时长。
let startTime = Date.now(); // 当前开始计时的时间
let totalTime = 0; // 总停留时间(毫秒)
// visibilitychange 用于监听前后台切换、标签页切换,依赖页面可见性
document.addEventListener("visibilitychange", () => {
if (document.hidden) {
totalTime += Date.now() - startTime;
console.log("页面切到后台,累计时间:", totalTime);
} else {
startTime = Date.now();
console.log("页面重新激活,继续计时");
}
});
// pagehide 用于监听页面关闭、刷新,依赖 bfcache ,sendBeacon 保证页面卸载时数据上报的可靠性
window.addEventListener("pagehide", () => {
if (!document.hidden) {
totalTime += Date.now() - startTime;
}
navigator.sendBeacon("/api/track", JSON.stringify({
duration: totalTime,
timestamp: Date.now()
}));
});
URL API
URL API 可用于解析、修改和生成 URL。
const url = new URL("https://example.com:8080/path?name=John&age=30#section1");
// 解析 URL
console.log(url.href); // "https://example.com:8080/path?name=John&age=30#section1"
console.log(url.protocol); // "https:"
console.log(url.hostname); // "example.com"
console.log(url.port); // "8080"
console.log(url.pathname); // "/path"
console.log(url.search); // "?name=John&age=30"
console.log(url.hash); // "#section1"
console.log(url.searchParams); // URLSearchParams { 'name' => 'John', 'age' => '30' }
// 修改 URL
url.pathname = "/new-path";
url.search = "?category=books";
url.hash = "#top";
URLSearchParams 用于解析、修改和生成 URL 查询参数。
const params1 = new URLSearchParams("?name=John&age=30");
const params2 = new URLSearchParams(window.href.search);
console.log(params.get("name")); // "John"
console.log(params.get("age")); // "30"
params.delete("age"); // 删除参数
params.append("age", "25"); // 增加参数
params.set("name", "Bob"); // 修改参数
params.sort(); // 按键名排序
console.log(params.toString()); // "name=Bob&age=25"
如何将 params 对象转换为查询字符串 query ?
const params = { username: 'xxx', password: '123' };
const query = new URLSearchParams(params).toString();
console.log(query); // "username=xxx&password=123"
URL 中的 ? /
都可能在浏览器 URL 中出现,? 是查询参数,# 是片段标识符。
? 后的内容常作为查询参数,会作为 HTTP 请求的一部分发送到服务端(可通过 location.search 获取)。
# 位于 URL 末尾,仅在前端生效(可通过 location.hash 获取)。常用于:1. 定位到页面内的某个元素(如 <div id="section1">);2. 单页应用中用于无刷新导航(如 https://example.com/#/about)。
Fetch API
XHR 与 Fetch API 都是浏览器内置的发起请求的技术,Fetch API 在 2015年出现,基本取代了 XHR。
Axios 是基于 XHR 或 Fetch API 的封装库。
Ajax 是一种设计模式,核心是异步,即在不刷新页面的前提下获取数据并更新页面。
Promise 是 JS 内置的对象,XHR 不支持 Promise,Fetch API 支持 Promise,目前大多数的异步请求都基于 Promise,async/await 是操作 Promise 的语法糖。
// 发送 GET 请求
fetch("https://api.example.com/data")
.then(response => response.json()) // response对象:返回包含完整HTTP响应的 Promise 对象。常用于检查状态,如response.ok;返回解析后的对象response.json();返回解析后的字符串response.text()。response只能读取一次。
.then(data => console.log("服务器返回:", data)) // data对象:输出解析后的业务数据,可以读取多次。
.catch(error => console.error("请求失败", error));
// 发送 POST 请求
fetch("https://api.example.com/post", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ username: "Alice", age: 25 }),
})
.then(response => response.json())
.then(data => console.log("服务器返回:", data))
.catch(error => console.error("请求失败:", error));
// PUT、DELETE 请求同理
Stream API
Fetch stream api 提供双向通信,可以手动更灵活地处理数据,常用于大文件上传等。
fetch('https://www.url.com')
.then((response) => {
const reader = response.body.getReader();
return new ReadableStream({
start(controller) {
return pump();
function pump() {
return reader.read().then(({done, value}) =>{
if (done) {
controller.close();
return;
}
controller.enqueue(value);
return pump();
})
}
}
})
})
.then((stream) => new Response(stream))
.then((response) => response.blob())
.then((blob) => URL.createObjectURL(blob))
.then((url) => console.log((image.src = url)))
.catch((err) => console.error(err));
SSE(Server-Sent Events)也支持流式数据传输,支持服务端向客户端的单向通信,提供长连接和重连机制。
const eventSource = new EventSource('/api/events');
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
};
eventSource.onerror = (error) => {
console.error('SSE连接错误:', error);
};
eventSource.close();
GET / POST / PUT
-
GET 方法用于获取资源,通常用于查询数据。可以被缓存(如果服务器允许),幂等(多次执行操作,结果都相同),请求参数通常放在 URL 中。
-
POST 方法用于新增资源,常用于用户注册、文件上传等。不缓存,在请求体中传输数据,非幂等(每次请求都会创建相同的资源,导致服务器数据发生变化)。
POST 请求什么时候用 Form Data,什么时候用 Request Payload?
Form Data 的值都是字符串,适用于文件上传(multipart/form-data,兼容性好,⽆需额外处理⼆进制数据),或者老系统提交表单(使⽤ application/x-www-form-urlencoded)
Request Payload( 设置
'Content-Type': 'application/json'请求头 )适用于非文件上传场景,可以清晰地表示各类数据结构,现代开发的数据传输大多采用这种方式。 -
PUT 方法用于更新或替换资源(PATCH 方法更适合局部更新)。不缓存,在请求体中传输数据,幂等。
gz压缩 / br压缩
Gzip 作为通⽤压缩⽅案兼容性最好,适合动态内容快速压缩;⽽ Brotli 通过更复杂的算法实现更⾼压缩率,特别适合静态资源优化。在项⽬中可以⽤ Nginx 同时⽀持两者,对静态⽂件预⽣成 .br 和 .gz 版本,根据请求头的 Accept-Encoding ⾃动返回最优格式。低版本浏览器可能不支持 br压缩,可以优先采⽤br,并使⽤gz进⾏兜底。
Web Storage API
主要用于在本地存储数据,提供了两种存储方式 localStorage 和 sessionStorage ,使用方法:
// 存储数据
localStorage.setItem("username", "Alice");
sessionStorage.setItem("sessionID", "abc123");
// 读取数据
console.log(localStorage.getItem("username")); // "Alice"
console.log(sessionStorage.getItem("sessionID")); // "abc123"
// 删除指定键
localStorage.removeItem("username");
sessionStorage.removeItem("sessionID");
// 清空所有存储数据
localStorage.clear();
sessionStorage.clear();
Cookie 存储不能超过 4k;sessionStorage 和 localStorage 可以达到 5M;IndexedDB > 1GB。
Cookie 在设置的过期时间之前一直有效,在每次发送请求时自动传递到服务器,常用于存储 sessionId 等。
LocalStorage 数据永久存储,用于存储用户设置、缓存数据等长期数据(如果存满了再执行 setItem 会抛 QuotaExceededError,此时建议使用 LRU 缓存或者存进 IndexedDB )。
SessionStorage 数据在当前浏览器窗口关闭后自动删除,用于存储临时数据,仅在用户当前会话中使用,常用于存储表单等页面状态。
IndexedDB 可视为浏览器里的非关系型数据库,支持索引、事务等。索引(Index)用于加快数据库的查询速度,例如主键是一种索引。事务(Transaction)是一个不可分割的工作逻辑单元,这一系列操作要么都成功,要么都失败,来保证数据的一致性。
document.cookie = "username=Jone";
const date = new Date();
date.setTime(date.getTime() + (7 * 24 * 60 * 1000)); // 7天后过期
document.cookie = `username=Jone;
expires=${date.toUTCString()};
max-age=${30 * 24 * 60 * 60};
path=/;
domain=.example.com;
secureCookie=value;
Secure;
SameSite=Lax;
HttpOnly
`;
// expires表示过期时间,使用GMT/UTC格式,max-age表示存活秒数,优先级高于expires
// Secure表示仅通过 HTTPS 传输,SameSite控制跨站请求时是否发送Cookie,Strict/Lax/None
浏览器开发工具常用命令
fn + f12 打开浏览器开发工具
command + option + J 打开控制台
command + shift + P 打开命令
$_ // 返回上一句语句的执行结果
$0 // 返回上一个选择的节点
$1 // 返回上上一个选择的节点,$2表示上上上一个,诸如此类
console.error(1)
console.warn(1)
console.clear()
// 按组输出
console.group('test group')
console.log(1)
console.log(2)
console.groupEnd('test group')
// 输出执行时间
console.time()
fn // 执行的函数
console.timeEnd()
// 将数组以表格形式呈现
console.table([{id:1, name:'Jack'}, {id:2, name:'Alice'}])