网络请求
Fetch
fetch() 是现代 JavaScript 中用于发起网络请求的通用方法,它基于 Promise,支持异步操作。虽然“AJAX”(Asynchronous JavaScript And XML)是这类请求的传统术语,但如今我们通常处理的是 JSON 或二进制数据,而非 XML。
使用方法
基本语法:
let promise = fetch(url, [options])
url:要请求的资源地址。options:可选配置对象(如method、headers、body等)。若未提供
options,默认发送 GET 请求,仅下载 URL 内容。
获取响应分为两个阶段:
响应头阶段(Headers received)
服务器返回响应头后,
fetch创建一个Response对象。此时可通过
response.status(HTTP 状态码)或response.ok(布尔值,200–299 为true)判断请求是否成功。注意:即使 HTTP 状态为 404 或 500,
fetch也不会 reject Promise;只有在网络错误(如 DNS 失败、断网)时才会 reject。响应体阶段(Body reading)
响应体通过
response.body提供,它是一个ReadableStream,允许分块多次读取,但整个body数据只能读取一次。提供多种便捷方法解析响应体:
| 方法 | 返回类型 | 说明 |
|——|——–|——|
| response.text() | string | 以纯文本形式读取响应 |
| response.json() | object | 将响应解析为 JSON 对象 |
| response.formData() | FormData | 解析为表单数据对象 |
| response.blob() | Blob | 以二进制大对象(带 MIME 类型)返回 |
| response.arrayBuffer() | ArrayBuffer | 返回底层二进制数据 |
// 使用 async/await
let url = 'https://api.github.com/repos/javascript-tutorial/en.azusatea.top/commits';
let response = await fetch(url);
if (response.ok) {
let commits = await response.json();
console.info(commits[0].author.login);
} else {
console.info("HTTP Error: " + response.status);
}
```javascript
// 使用 Promise 链式调用
fetch('https://api.github.com/repos/javascript-tutorial/en.azusatea.top/commits')
.then(response => response.json())
.then(commits => console.info(commits[0].author.login));
Response Headers
response.headers 是一个类 Map 对象(具有类似 Map 的方法,但不是真正的 Map):
let response = await fetch('https://api.github.com/repos/javascript-tutorial/en.azusatea.top/commits');
// 获取单个 header
console.info(response.headers.get('Content-Type')); // application/json; charset=utf-8
// 遍历所有 headers
for (let [key, value] of response.headers) {
console.info(`${key} = ${value}`);
}
Request Headers
可通过 options.headers 设置请求头,常用于身份验证等场景:
let response = await fetch(protectedUrl, {
headers: {
Authentication: 'secret'
}
});
浏览器禁止手动设置的请求头
以下请求头由浏览器自动管理,JavaScript 无法覆盖或设置:
| 请求头 | 说明 |
|——-|——|
| Accept-Charset | 客户端支持的字符集 |
| Accept-Encoding | 支持的内容编码(如 gzip) |
| Connection / Keep-Alive | 连接控制 |
| Content-Length | 请求体字节长度 |
| Cookie / Cookie2 | Cookie 数据(Cookie2 已废弃) |
| Date | 消息生成时间 |
| Host | 目标主机(HTTP/1.1 必需) |
| Origin | 请求来源(用于 CORS) |
| Referer | 来源页面 URL(注意拼写) |
| Transfer-Encoding | 传输编码方式(如 chunked) |
| Upgrade | 协议升级请求(如 WebSocket) |
| Via | 代理路径追踪 |
| Proxy-*(如 Proxy-Authorization) | 仅用于客户端与代理通信 |
| Sec-*(如 Sec-Fetch-Site) | 浏览器自动添加的安全元数据 |
关键点:
- 所有 CORS 相关头(
Origin,Access-Control-*)均由浏览器自动处理。Sec-*和Proxy-*是前缀类别,代表一类安全或代理专用头。
其他 HTTP 方法(POST、PUT、DELETE 等)
通过 options.method 和 options.body 发送非 GET 请求:
// 发送 JSON 数据
let user = { name: 'John', surname: 'Smith' };
let response = await fetch('/api/user', {
method: 'POST',
headers: {
'Content-Type': 'application/json;charset=utf-8'
},
body: JSON.stringify(user)
});
let result = await response.json();
console.info(result.message);
```javascript
// 直接发送 Blob(如图片)
let response = await fetch('/upload', {
method: 'POST',
body: imageBlob // 自动设置 Content-Type 为 blob.type
});
支持的 body 类型包括:
- 字符串(如 JSON)
FormDataBlob/File/ArrayBufferURLSearchParams
FormData
FormData 用于构建表单数据,支持字段和文件混合上传。当作为 fetch 的 body 时,自动设置 Content-Type: multipart/form-data。
构造方法
let formData = new FormData([formElement]);
示例:从 HTML 表单提交
<form id="formElem">
<input type="text" name="name" value="John">
<input type="text" name="surname" value="Smith">
<input type="submit">
</form>
<script>
formElem.onsubmit = async (e) => {
e.preventDefault();
let response = await fetch('/api/submit', {
method: 'POST',
body: new FormData(formElem) // 自动收集所有字段
});
let result = await response.json();
console.info(result.message);
};
</script>
示例:包含文件上传
<form id="formElem">
<input type="text" name="firstName" value="John">
Picture: <input type="file" name="picture" accept="image/*">
<input type="submit">
</form>
<script>
formElem.onsubmit = async (e) => {
e.preventDefault();
let response = await fetch('/api/upload', {
method: 'POST',
body: new FormData(formElem)
});
let result = await response.json();
console.info(result.message);
};
</script>
FormData 方法一览
| 方法 | 说明 |
|——|——|
| append(name, value) | 添加字段(允许多值) |
| append(name, blob, fileName) | 添加文件字段(模拟 <input type="file">) |
| set(name, value) | 设置字段(覆盖同名字段) |
| set(name, blob, fileName) | 设置文件字段(覆盖) |
| get(name) | 获取第一个同名字段值 |
| has(name) | 检查是否存在该字段 |
| delete(name) | 删除所有同名字段 |
关键区别:
append()→ 允许多个同名字段(适合多文件上传)set()→ 确保唯一值(适合更新)
Fetch 下载进度
fetch 支持下载进度监控(通过 response.body 流),但不支持上传进度(需使用 XMLHttpRequest)。
基础流读取
const reader = response.body.getReader();
while (true) {
const { done, value } = await reader.read(); // value: Uint8Array
if (done) break;
console.log(`Received ${value.length} bytes`);
}
完整进度示例
let response = await fetch('https://api.github.com/repos/.../commits?per_page=100');
const reader = response.body.getReader();
const contentLength = +response.headers.get('Content-Length'); // 总大小
let receivedLength = 0;
let chunks = [];
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
receivedLength += value.length;
console.log(`Received ${receivedLength} of ${contentLength} bytes`);
}
// 合并 chunks 为 Uint8Array
let allChunks = new Uint8Array(receivedLength);
let position = 0;
for (let chunk of chunks) {
allChunks.set(chunk, position);
position += chunk.length;
}
// 解码为字符串并解析 JSON
let result = new TextDecoder().decode(allChunks);
let commits = JSON.parse(result);
console.info(commits[0].author.login);
注意:若
Content-Length未知(如流式响应),应限制chunks内存使用,避免溢出。
Fetch 终止:AbortController
使用 AbortController 可中止 fetch 或其他异步操作。
基本用法
let controller = new AbortController();
let signal = controller.signal;
// 监听中止事件
signal.addEventListener('abort', () => console.log("Aborted!"));
// 触发中止
controller.abort();
console.log(signal.aborted); // true
与 Fetch 结合
let controller = new AbortController();
setTimeout(() => controller.abort(), 1000); // 1 秒后中止
try {
let response = await fetch('/long-request', {
signal: controller.signal
});
} catch (err) {
if (err.name === 'AbortError') {
console.log("Fetch aborted!");
} else {
throw err;
}
}
批量中止
一个 AbortController 可同时中止多个任务:
let urls = ['url1', 'url2', 'url3'];
let controller = new AbortController();
// 自定义异步任务
let ourJob = new Promise((_, reject) => {
controller.signal.addEventListener('abort', reject);
});
// 多个 fetch
let fetchJobs = urls.map(url => fetch(url, { signal: controller.signal }));
// 等待全部完成
await Promise.all([...fetchJobs, ourJob]);
// 调用 controller.abort() 将中止所有任务
Fetch 跨源请求(CORS)
源(Origin) = 协议 + 域名 + 端口。跨源请求受 CORS(Cross-Origin Resource Sharing) 策略限制。
默认行为
try {
await fetch('http://example.com'); // 跨域
} catch (err) {
console.log(err); // TypeError: NetworkError...
}
CORS 是安全机制,防止恶意脚本窃取其他网站数据。
旧式跨源技巧(已过时)
表单提交到
<iframe>
可发送数据,但无法读取响应(因同源策略)。JSONP(JSON with Padding)
利用<script>标签跨域加载 JS 代码:
// 1. 定义全局回调
function gotWeather({ temperature, humidity }) {
console.log(`Temp: ${temperature}`);
}
// 2. 动态插入 script
let script = document.createElement('script');
script.src = 'http://weather.com/api?callback=gotWeather';
document.body.append(script);
// 3. 服务器返回:gotWeather({ temperature: 25, humidity: 78 });
缺点:仅支持 GET,存在 XSS 风险,现代开发应使用 CORS。
CORS 分类
1. 简单请求(Simple Requests)
满足以下条件即为简单请求:
- 方法:GET、POST、HEAD
- Headers:仅限
AcceptAccept-LanguageContent-LanguageContent-Type为以下之一:application/x-www-form-urlencodedmultipart/form-datatext/plain
流程:
- 浏览器自动添加
Origin: https://your-site.com请求头。 - 服务器检查
Origin,若允许,则响应中包含:Access-Control-Allow-Origin: https://your-site.com # 或 Access-Control-Allow-Origin: * - 浏览器收到后,允许 JavaScript 访问响应。
若无
Access-Control-Allow-Origin,浏览器拦截响应并报错。
2. 非简单请求(需预检)
不满足上述条件的请求(如 PUT、自定义 Header、JSON body)会触发 预检(Preflight)请求。
流程:
浏览器先发送
OPTIONS请求:OPTIONS /api/data Origin: https://your-site.com Access-Control-Request-Method: PUT Access-Control-Request-Headers: API-Key,Content-Type服务器响应预检:
HTTP/1.1 200 OK Access-Control-Allow-Origin: https://your-site.com Access-Control-Allow-Methods: PUT,DELETE Access-Control-Allow-Headers: API-Key,Content-Type Access-Control-Max-Age: 86400 # 缓存权限 24 小时预检通过后,浏览器发送实际请求。
预检对开发者透明,JavaScript 仅看到最终结果。
暴露额外响应头
默认仅能读取以下响应头:
Cache-ControlContent-LanguageContent-TypeExpiresLast-ModifiedPragma
若需访问其他头(如 API-Key),服务器必须设置:
Access-Control-Expose-Headers: API-Key, X-Custom-Header
凭据(Credentials)
跨源请求默认不携带 Cookie 或 HTTP 认证信息。
要发送凭据,需设置:
fetch('http://another.com', {
credentials: 'include' // 或 'same-origin'
});
服务器必须响应:
Access-Control-Allow-Origin: https://your-site.com # 不能为 *
Access-Control-Allow-Credentials: true
重要:使用凭据时,
Access-Control-Allow-Origin不能为*,必须指定确切源。
最佳实践:
- 优先使用
fetch+AbortController管理请求生命周期。 - 跨域问题由后端配置 CORS 解决,前端确保正确设置
credentials和 headers。 - 大文件下载使用流式读取监控进度;上传进度需回退到
XMLHttpRequest。
Fetch API
完整 options 对象列表:
let promise = fetch(url, {
method: "GET", // POST,PUT,DELETE,等。
headers: {
// 内容类型 header 值通常是自动设置的
// 取决于 request body
"Content-Type": "text/plain;charset=UTF-8"
},
body: undefined, // string,FormData,Blob,BufferSource,或 URLSearchParams
referrer: "about:client", // 或 "" 以不发送 Referer header,
// 或者是当前源的 url
referrerPolicy: "no-referrer-when-downgrade", // no-referrer,origin,same-origin...
mode: "cors", // same-origin,no-cors
credentials: "same-origin", // omit,include
cache: "default", // no-store,reload,no-cache,force-cache,或 only-if-cached
redirect: "follow", // manual,error
integrity: "", // 一个 hash,像 "sha256-abcdef1234567890"
keepalive: false, // true
signal: undefined, // AbortController 来中止请求
window: window // null
});
referrer,referrerPolicy
referrer:指定Referer请求头的值。可设为:"about:client"(默认):由浏览器决定;"":不发送Referer;- 同源的 URL:发送该 URL 作为来源(跨源 URL 会被忽略)。
比如:实际上是在
https://azusatea.top/path-one/页面发送的请求,但可以手动设置为https://azusatea.top/path-two/的同源路径 referrerPolicy:控制在何种情况下发送Referer以及发送多少信息。常用值包括:"no-referrer":从不发送;"origin":仅发送协议 + 域名 + 端口;"same-origin":同源时发送完整 URL,跨源时不发送;"unsafe-url":始终发送完整 URL(即使从 HTTPS 到 HTTP);"no-referrer-when-downgrade"(默认):HTTPS → HTTP 时不发送,其他情况发送完整 URL。
mode
"cors"(默认):允许标准跨源请求,响应需包含 CORS 头才能被读取。"same-origin":只允许同源请求,任何跨源请求都会被拒绝。"no-cors":允许发送“安全”的跨源请求(如 GET、HEAD、POST 且使用简单头),但 JavaScript 无法读取响应内容(返回 opaque 响应),常用于加载第三方资源(如图片、脚本)。
credentials
"same-origin"(默认):仅在同源请求中携带 Cookie 和 HTTP 认证信息。"include":始终携带凭据(包括跨域),但服务器必须返回Access-Control-Allow-Credentials: true且不能使用通配符Access-Control-Allow-Origin: *。"omit":从不发送凭据(Cookie、HTTP 认证等)。
cache
控制 HTTP 缓存行为:
"default":使用标准 HTTP 缓存策略(可能返回缓存或重新验证)。"no-store":完全绕过缓存,强制网络请求。"reload":跳过缓存发起新请求,并用响应更新缓存。"no-cache":先检查缓存是否有效(发条件请求,如带If-None-Match)。"force-cache":即使缓存过期也使用它。"only-if-cached":仅当请求可从缓存满足时才返回结果(否则失败),仅适用于same-origin。
redirect
"follow"(默认):自动跟随重定向(最多 20 次)。"error":遇到 3xx 重定向状态码时抛出错误。"manual":不自动跳转,返回一个类型为"opaqueredirect"的特殊响应对象(无法读取其内容或状态)。
integrity
用于子资源完整性(SRI)校验。提供一个以算法开头的 base64 编码哈希值(如 "sha256-Bpf...)。若响应内容的哈希与之不匹配,请求将失败。常用于确保 CDN 资源未被篡改。
keepalive
false(默认):页面卸载时请求可能被取消。true:允许请求在页面关闭后继续在后台完成(如发送分析数据)。限制:总请求体大小 ≤ 64KB,且无法处理响应(因为页面文档已经被卸载)。- 需要收集有关访问的大量统计信息,则应该将其定期以数据包的形式发送出去,这样就不会留下太多数据给最后的 onunload 请求。
- 此限制是被应用于当前所有 keepalive 请求的总和的。换句话说,可以并行执行多个 keepalive 请求,但它们的 body 长度之和不得超过 64KB。
signal
传入 AbortController.signal 实例,用于中止正在进行的 fetch 请求。调用 abort() 后,Promise 将 reject 一个 AbortError。
window
保留字段,必须为 window 或 null,无实际功能(历史兼容用途)。
URL 对象
此对象提供了一系列创建和解析url的便捷接口,即使使用字符串就可以用作请求,但使用URL对象可以更加便捷。
对象创建
语法:
new URL(url, [base])
url:设置了base则仅需要传入路径,否则需要完整路径base:可选的base URL,设置了此参数,且参数url只有路径,则会根据这个base生成URL
let url1 = new URL('https://azusatea.top/profile/admin');
let url2 = new URL('/profile/admin', 'https://azusatea.top');
console.info(url1); // https://azusatea.top/profile/admin
console.info(url2); // https://azusatea.top/profile/admin
// 或者基于已有的URL对象进行创建
let newUrl1 = new URL('tester/1', url1);
console.info(newUrl1); // https://azusatea.top/profile/tester/1
let newUrl2 = new URL('/tester/1', url1);
console.info(newUrl2); // https://azusatea.top/tester/1
URL对象属性:
let url = new URL('https://azusatea.top:8080/profile/admin?version=v1&name=CC#hash');
console.info(url.href); // https://azusatea.top:8080/profile/admin?version=v1&name=CC#hash
console.info(url.origin); // https://azusatea.top:8080
console.info(url.protocol); // https:
console.info(url.host); // azusatea.top:8080
console.info(url.hostname); // azusatea.top
console.info(url.port); // 8080
console.info(url.pathname); // /profile/admin
console.info(url.search); //?version=v1&name=CC
console.info(url.hash); // #hash
在网络传输(fetch、XHR等)中,允许直接传入URL对象,URL 对象可以替代字符串传递给任何方法,因为大多数方法都会执行字符串转换,会将 URL 对象转换为具有完整 URL 的字符串。
查询参数 SearchParams
简单的查询参数可以直接添加在 URL 字符串中:?version=v1&name=CC,但如果包含空格、非拉丁字母等,则需要使用 url.searchParams(URLSearchParams 对象)进行编码处理:
append(name, value):按照 name 添加参数
let url = new URL('https://google.com/search');
const params = url.searchParams;
// 或者直接使用
const params = new URLSearchParams();
// 对于空格等会被编码 URL会变为 ?q=test+me%21
params.append('query', 'test me!');
params.append('user', 'John');
params.append('user', 'Pete'); // 允许多个同名参数
delete(name):按照 name 移除所有匹配的参数
params.delete('user'); // 删除所有 user 参数
get(name):按照 name 获取第一个对应的参数值,若不存在则返回 null
const firstUser = params.get('user'); // "John"
getAll(name):获取所有具有相同 name 的参数值,返回字符串数组
const allUsers = params.getAll('user'); // ["John", "Pete"]
has(name):检查是否存在指定 name 的参数,返回布尔值
if (params.has('version')) { /* ... */ }
set(name, value):设置或替换指定 name 的参数(删除所有旧值,只保留新值)
params.set('user', 'Alice'); // 现在 user 只有 "Alice"
sort():按参数名对所有参数进行字典序排序(原地排序,无返回值),很少使用
params.sort(); // 例如将 user 放在 version 前面
可迭代性:URLSearchParams 是可迭代对象,支持 for...of、扩展运算符等,行为类似 Map
for (const [key, value] of params) {
console.log(key, value);
}
// 或
const entries = [...params]; // 转为 [key, value] 数组
手动编码函数(对于不使用URL对象的情况,又需要对字符串URL进行编码):
但不建议使用,这些方法基于RFC2396规范,对于IPv6的编解码方式不同,而URL与URLSearchParams基于RFC3986 最新规范,很好的支持IPv6
// IPv6 地址的合法 url let url = 'http://[2607:f8b0:4005:802::1007]/'; console.info(encodeURI(url)); // http://%5B2607:f8b0:4005:802::1007%5D/ console.info(new URL(url)); // http://[2607:f8b0:4005:802::1007]/
encodeURI—— 编码整个 URL。decodeURI—— 解码为编码前的状态。encodeURIComponent—— 编码 URL 组件,例如搜索参数,或者 hash,或者 pathname。decodeURIComponent—— 解码为编码前的状态。// 在 url 路径中使用西里尔字符 let url1 = encodeURI('http://site.com/привет'); console.info(url1); // http://site.com/%D0%BF%D1%80%D0%B8%D0%B2%D0%B5%D1%82
let music = encodeURIComponent('Rock&Roll');
let url2 = https://google.com/search?q=${music};
console.info(url2); // https://google.com/search?q=Rock%26Roll
## XMLHttpRequest
内建的浏览器对象,不仅是 XML,可以操纵任何数据,但现代做法是更多地使用 `fetch`,或者在监控上传进度的场景(`fetch` 做不到上传跟踪)。
### 基础使用
1. **创建**
// 不需要传入任何构造参数
let xhr = new XMLHttpRequest();
2. **初始化**
xhr.open(method, URL, [async, user, password])
let url = new URL('https://google.com/search');
url.searchParams.set('q', 'test me!');
// 参数 'q' 被编码
xhr.open('GET', url); // https://google.com/search?q=test+me%21
// 第三个参数设为 false,则启用同步请求,此时 XHR 很多高级功能将无法使用,很少使用
// xhr.open('GET', url, false);
> 虽然方法叫做 `open`,但此时并没有发送请求,仅配置请求。
- `method`:HTTP 方法。通常是 `"GET"` 或 `"POST"`
- `URL`:要请求的 URL,通常是一个字符串,也可以是 `URL` 对象(建议使用 `URL` 对象,**确保字符正确编码**)
- `async`:显式地设置为 `false`,那么请求将会以同步的方式处理
- `user`,`password`:HTTP 基本身份验证(如果需要的话)的登录名和密码
3. **发送请求**:GET 请求则无参调用即可,POST 等其他请求传入对应的 body 即可
xhr.send([body])
4. **监听 xhr 事件以获取响应**
- `load`:当请求完成(即使 HTTP 状态为 400 或 500 等),并且响应已完全下载
- `error`:当无法发出请求,例如网络中断或者无效的 URL
- `progress`:在下载响应期间定期触发,报告已经下载了多少
xhr.onload = function() {
console.info(`Loaded: ${xhr.status} ${xhr.response}`);
};
xhr.onerror = function() { // 仅在根本无法发出请求时触发
console.info(`Network Error`);
};
xhr.onprogress = function(event) { // 定期触发
// event.loaded —— 已经下载了多少字节
// event.lengthComputable = true,当服务器发送了 Content-Length header 时
// event.total —— 总字节数(如果 lengthComputable 为 true)
console.info(`Received ${event.loaded} of ${event.total}`);
}
5. **获取服务端响应数据**
- `status`:HTTP 状态码(200、404、403 等),对于非 HTTP 错误则为 0
- `statusText`:HTTP 状态字符串,200 对应 OK,404 对应 Not Found,403 为 Forbidden
- `response`:响应体 body(就脚本可能是 `responseText`)
- `timeout`(可选):超时时间,指定时间内请求没有成功执行,请求就会被取消,并且触发 `timeout` 事件
### 响应类型
使用 `xhr.responseType` 属性来设置响应格式:
- `""`(默认)—— 响应格式为字符串,
- `"text"` —— 响应格式为字符串,
- `"arraybuffer"` —— 响应格式为 ArrayBuffer(对于二进制数据,请参见 ArrayBuffer,二进制数组),
- `"blob"` —— 响应格式为 Blob(对于二进制数据,请参见 Blob),
- `"document"` —— 响应格式为 XML document(可以使用 XPath 和其他 XML 方法)或 HTML document(基于接收数据的 MIME 类型)
- `"json"` —— 响应格式为 JSON(自动解析)
// xhr.response 的响应即为对应的数据类型对象
xhr.responseType = 'json';
### readyState
XHR 具有状态机,会随着它的处理进度变化而变化。可以通过 `xhr.readyState` 来了解当前状态
UNSENT = 0; // 初始状态
OPENED = 1; // open 被调用
HEADERS_RECEIVED = 2; // 接收到 response header
LOADING = 3; // 响应正在被加载(接收到一个数据包)
DONE = 4; // 请求完成
XMLHttpRequest 对象以 `0 → 1 → 2 → 3 → … → 3 → 4` 的顺序在它们之间转变。每当通过网络接收到一个数据包,就会重复一次状态 3,可以通过 `readystatechange` 事件来跟踪(现在很少使用此监听事件)
xhr.onreadystatechange = function() {
if (xhr.readyState == 3) {
// 加载中
}
if (xhr.readyState == 4) {
// 请求完成
}
};
### 中止请求
触发 `abort` 事件,且 `xhr.status` 变为 0
xhr.abort(); // 终止请求
### HTTP header
1. **设置请求头**
XHR 允许设置自定义请求头(浏览器专门管理的请求头除外),而且一旦设置,无法撤销
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.setRequestHeader('X-Auth', '123');
xhr.setRequestHeader('X-Auth', '456');
// header 将是:
// X-Auth: 123, 456
2. **获取响应头**(`Set-Cookie` 和 `Set-Cookie2` 除外)
xhr.getResponseHeader('Content-Type')
3. **获取所有响应头**:返回除 `Set-Cookie` 和 `Set-Cookie2` 外的所有 response header。
xhr.getAllResponseHeaders()
// header 以单行形式返回
// Cache-Control: max-age=31536000
// Content-Length: 4260
// Content-Type: image/png
// Date: Sat, 08 Sep 2012 16:53:16 GMT
header 之间的换行符始终为 `"\r\n"`,具有 `": "` 的标准格式,因此可以处理成字典对象格式
let headers = xhr
.getAllResponseHeaders()
.split('\r\n')
.reduce((result, current) => {
let [name, value] = current.split(': ');
result[name] = value;
return result;
}, {});
// headers['Content-Type'] = 'image/png'
### 上传进度跟踪
专门用于跟踪上传事件:`xhr.upload`
- `loadstart` —— 上传开始。
- `progress` —— 上传期间定期触发。
- `abort` —— 上传中止。
- `error` —— 非 HTTP 错误。
- `load` —— 上传成功完成。
- `timeout` —— 上传超时(如果设置了 `timeout` 属性)。
- `loadend` —— 上传完成,无论成功还是 error。
xhr.upload.onprogress = function(event) {
console.info(Uploaded ${event.loaded} of ${event.total} bytes);
};
xhr.upload.onload = function() {
console.info(Upload finished successfully.);
};
xhr.upload.onerror = function() {
console.info(Error during the upload: ${xhr.status});
};
### 跨源请求
像 `fetch` 一样,默认情况下不会将 cookie 和 HTTP 授权发送到其他域,需要设置 `withCredentials` 属性
let xhr = new XMLHttpRequest();
xhr.withCredentials = true;
xhr.open('POST', 'http://anywhere.com/request');
## 可恢复的文件上传
虽然`xhr.upload.onprogress` 来跟踪上传进度,但只在数据 **被发送** 时触发,但是服务器是否接收到了?浏览器并不知道,这个方法仅用于显示一个进度条,为了知道当前服务器已接受的字节数,需要额外发送一个请求。
实现思路:
1. 创建文件的唯一ID,用来标记要上传的文件,用来和服务端确认要恢复的文件是哪一个
```javascript
let fileId = file.name + '-' + file.size + '-' + file.lastModified;
```
2. 和服务器进行通信,获取当前文件已有的字节数(此处使用自定义header对文件进行跟踪,如果服务器上尚不存在该文件,则服务器响应应为 0)
> 服务器应该检查其记录,如果有一个上传的该文件,并且当前已上传的文件大小恰好是 `X-Start-Byte`,那么就将数据附加到该文件
```javascript
let response = await fetch('status', {
headers: {
'X-File-Id': fileId
}
});
// 服务器已有的字节数
let startByte = +await response.text();
```
3. 使用`Blob`对象和`slice`切片对文件进行处理
```javascript
xhr.open("POST", "upload", true);
// 文件 id,以便服务器知道我们要恢复的是哪个文件
xhr.setRequestHeader('X-File-Id', fileId);
// 发送要从哪个字节开始恢复,因此服务器知道当前是正在恢复文件
xhr.setRequestHeader('X-Start-Byte', startByte);
xhr.upload.onprogress = (e) => {
console.log(`Uploaded ${startByte + e.loaded} of ${startByte + e.total}`);
};
// 文件可以是来自 input.files[0],或者另一个源
xhr.send(file.slice(startByte));
```
## 长轮询
与服务器保持长久连接最简单方式不依赖特定协议,容易实现。
### 常规轮询
也就是定期轮询,定期向服务器发出请求,例如,每 10 秒一次,作为响应,服务器首先通知自己,客户端处于在线状态,然后 —— 发送目前为止的消息包。
这可行,但是也有些缺点:
- 消息传递的延迟最多为 10 秒(两个请求之间)。
- 即使没有消息,服务器也会每隔 10 秒被请求轰炸一次,即使用户切换到其他地方或者处于休眠状态,也是如此。就性能而言,这是一个很大的负担。
### 长轮询
其流程为:
1. 请求发送到服务器。
2. 服务器在有消息之前不会关闭连接。
3. 当消息出现时 —— 服务器将对其请求作出响应。
4. 浏览器立即发出一个新的请求。
如果连接丢失,可能是因为网络错误,浏览器会立即发送一个新请求。
客户端示例代码:
async function subscribe() {
let response = await fetch(“/subscribe”);
if (response.status == 502) {
// 状态 502 是连接超时错误,
// 连接挂起时间过长时可能会发生,
// 远程服务器或代理会关闭它
// 让我们重新连接
await subscribe();
} else if (response.status != 200) {
// 一个 error —— 让我们显示它
showMessage(response.statusText);
// 一秒后重新连接
await new Promise(resolve => setTimeout(resolve, 1000));
await subscribe();
} else {
// 获取并显示消息
let message = await response.text();
showMessage(message);
// 再次调用 subscribe() 以获取下一条消息
await subscribe();
}
}
subscribe();
## WebSocket
这是一种在浏览器和服务器之间建立持久连接来交换数据的方法。数据可以作为“数据包”在两个方向上传递,而无需中断连接也无需额外的 HTTP 请求。适用于网络游戏,实时交易系统等系统。
### 建立连接
使用 `ws://`(非加密)或 `wss://`(加密,推荐)协议创建连接:
let socket = new WebSocket(“wss://example.com/chat”);
- **始终优先使用 `wss://`**:它基于 TLS 加密,能绕过不兼容的代理,并防止中间人窃听。
- 连接建立后会触发 `open` 事件;若失败则触发 `error`。
### 握手过程(HTTP 升级)
WebSocket 连接始于一个特殊的 HTTP 请求,包含以下关键头:
- `Upgrade: websocket` 和 `Connection: Upgrade`:请求协议升级。
- `Sec-WebSocket-Key`:浏览器生成的随机 Base64 字符串。
- `Sec-WebSocket-Version: 13`:协议版本。
- `Origin`:客户端页面源,用于服务器做跨源校验。
- 可选:`Sec-WebSocket-Protocol`(子协议)、`Sec-WebSocket-Extensions`(如压缩)。
服务器若支持,则返回 `101 Switching Protocols` 状态码及 `Sec-WebSocket-Accept`(由 Key 计算得出)。此后通信完全脱离 HTTP,使用 WebSocket 帧协议。
> ⚠️ 无法用 `fetch` 或 `XMLHttpRequest` 模拟此握手,因为 JavaScript 不能设置上述特殊头。
### 子协议与扩展
可在构造时指定子协议(第二参数):
new WebSocket(url, [“json”, “chat-protocol”]);
服务器从列表中选择一个并在响应中确认。例如:
// 客户端
const socket = new WebSocket(“wss://example.com”, “chat-protocol”);
// 服务端(Node.js + ws 库)
wss.on('connection', (ws, req) => {
console.log('Agreed subprotocol:', ws.protocol); // “chat-protocol”
});
### 事件处理
WebSocket 对象有四个核心事件:
- `open` —— 连接已建立,
- `message` —— 接收到数据,
- `error` —— WebSocket 错误,
- `close` —— 连接已关闭。
const socket = new WebSocket(“wss://example.com”);
socket.onopen = (event) => {
console.log(“[open] Connection established”);
socket.send(“Hello Server!“);
};
socket.onmessage = (event) => {
console.log([message] Received: ${event.data});
};
socket.onclose = (event) => {
if (event.wasClean) {
console.log(`[close] Clean close, code=${event.code}, reason=${event.reason}`);
} else {
console.log("[close] Connection died");
}
};
socket.onerror = (error) => {
console.error(“[error]“, error.message);
};
### 数据传输
- **发送**:`socket.send(data)` 支持字符串、`Blob`、`ArrayBuffer` 等。
- **接收**:
- 文本数据始终为字符串。
- 二进制数据格式由 `socket.binaryType` 控制(默认 `"blob"`,可设为 `"arraybuffer"`)。
// 发送文本
socket.send(“Hello”);
// 发送二进制数据
const buffer = new Uint8Array([1, 2, 3]);
socket.send(buffer);
// 接收二进制数据为 ArrayBuffer
socket.binaryType = “arraybuffer”;
socket.onmessage = (event) => {
if (event.data instanceof ArrayBuffer) {
const view = new Uint8Array(event.data);
console.log("Binary data:", view);
}
};
### 连接状态
通过 `socket.readyState` 获取当前状态:
- `0` — `CONNECTING`
- `1` — `OPEN`
- `2` — `CLOSING`
- `3` — `CLOSED`
function getConnectionState(socket) {
switch (socket.readyState) {
case 0: return "Connecting...";
case 1: return "Open";
case 2: return "Closing...";
case 3: return "Closed";
}
}
### 关闭连接
主动关闭时可提供关闭码和原因:
socket.close(1000, “User logged out”);
常见关闭码:
- `1000`:正常关闭(默认)。
- `1001`:终端离开(如页面关闭)。
- `1009`:消息过大。
- `1011`:服务器内部错误。
- `1006`:连接异常断开(无关闭帧,不可手动设置)。
### 流量控制
对于用户的网速很慢,可以反复地调用 socket.send(data)。但是数据将会缓冲(储存)在内存中,并且只能在网速允许的情况下尽快将数据发送出去。
`socket.bufferedAmount` 返回当前缓冲区中待发送的字节数。可用于限速:
// 每 100ms 检查一次 socket
// 仅当所有现有的数据都已被发送出去时,再发送更多数据
setInterval(() => {
if (socket.readyState === 1 && socket.bufferedAmount === 0) {
socket.send(getNextMessage());
}
}, 100);
## Server-Sent Events (SSE)
Server-Sent Events 是一种基于标准 HTTP 的单向持久连接机制,允许服务器向浏览器**持续推送文本数据**。它使用内建的 `EventSource` 接口,适用于实时通知、价格更新、日志流等场景。
与 WebSocket 相比:
- **单向通信**:仅服务器 → 客户端(WebSocket 是双向)。
- **仅支持文本**:不能传输二进制数据(WebSocket 支持)。
- **基于普通 HTTP**:无需新协议,兼容性好,可被代理和 CDN 友好处理。
- **自动重连 + 消息恢复**:内置断线重连和 `Last-Event-ID` 机制。
### 基本用法
创建连接只需提供 URL:
const eventSource = new EventSource(“/events”);
服务器必须返回:
- 状态码 `200`
- Header: `Content-Type: text/event-stream`
- 消息格式为特殊文本流
#### 消息格式(服务器端)
data: Hello\n
data: {“user”:“Alice”,“msg”:“Hi!“}\n
data: Line one
data: Line two\n
\n
- 每条消息以 `data:` 开头,后接内容(冒号后空格可选)。
- 多行消息:多个 `data:` 连续出现,最终合并为一行(换行符需显式编码为 `\n`)。
- 消息之间用 `\n\n` 分隔。
- 实际开发中通常发送 JSON 字符串。
#### 客户端接收
eventSource.onmessage = (event) => {
console.log(“Received:“, event.data); // 字符串
};
// 或使用 addEventListener
eventSource.addEventListener(“message”, (event) => {
const data = JSON.parse(event.data);
console.log(data);
});
### 跨源请求
支持 CORS,用法类似 `fetch`:
// 基础跨源
const source = new EventSource(“https://api.example.com/stream”);
// 携带凭证(cookies)
const source = new EventSource(“https://api.example.com/stream”, {
withCredentials: true
});
服务器需返回:
Access-Control-Allow-Origin: https://your-site.com
Access-Control-Allow-Credentials: true // 如果使用 withCredentials
### 自动重连机制
- 连接断开后,浏览器**自动重试**。
- 默认重试间隔:约 3 秒。
- 服务器可通过 `retry:` 指令自定义延迟(单位:毫秒):
retry: 10000
data: Next message after 10s if disconnected\n
- 若服务器返回 **HTTP 204**,浏览器将**停止重连**。
- 调用 `eventSource.close()` 可手动终止连接(之后不会重连):
eventSource.close(); // readyState 变为 CLOSED
> ⚠️ 一旦关闭,无法“重启”同一个 `EventSource`,需新建实例。
### 消息 ID 与断点续传
为支持断线后精准恢复,每条消息可附带唯一 ID:
id: 1001
data: First message\n
id: 1002
data: Second message\n
- 浏览器自动记录最后收到的 ID 到 `eventSource.lastEventId`。
- 重连时,自动在请求头中携带:
Last-Event-ID: 1002
- 服务器据此从下一条消息继续推送。
> 💡 最佳实践:将 `id:` 放在 `data:` **之后**,确保消息完整接收后再更新 `lastEventId`。
### 连接状态
通过 `readyState` 获取当前状态:
console.log(eventSource.readyState);
// 0 — CONNECTING(初始或重连中)
// 1 — OPEN(已连接)
// 2 — CLOSED(已关闭)
### 自定义事件类型
服务器可指定事件名:
event: userJoin
data: Alice\n
event: priceUpdate
data: {“symbol”:“BTC”,“price”:60000}\n
客户端需用 `addEventListener` 监听:
eventSource.addEventListener(“userJoin”, (event) => {
console.log(${event.data} joined);
});
eventSource.addEventListener(“priceUpdate”, (event) => {
const update = JSON.parse(event.data);
updateUI(update);
});
### 错误处理
监听 `error` 事件捕获连接问题:
eventSource.onerror = (event) => {
console.error(“SSE connection error”);
// 注意:短暂断开也会触发 error,但浏览器会自动重连
};
- 若响应状态码非 `200/204/301/307` 或 `Content-Type` 不是 `text/event-stream`,则**不会重连**。
### 完整示例:实时通知流
**客户端:**
const es = new EventSource(“/notifications”);
es.onopen = () => console.log(“SSE connected”);
es.onmessage = (e) => showNotification(e.data);
es.addEventListener(“alert”, (e) => {
showAlert(JSON.parse(e.data));
});
es.onerror = (e) => {
if (es.readyState === EventSource.CLOSED) {
console.log("SSE permanently closed");
}
};
**服务器(Node.js 伪代码):**
app.get(“/notifications”, (req, res) => {
res.writeHead(200, {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
"Connection": "keep-alive"
});
let id = 0;
const send = (event, data) => {
id++;
if (event) res.write(`event: ${event}\n`);
res.write(`id: ${id}\ndata: ${JSON.stringify(data)}\n\n`);
};
send(null, “Welcome!“);
send(“alert”, { title: “New message”, body: “You have a reply” });
// 模拟断开(浏览器会自动重连)
setTimeout(() => res.end(), 10000);
});