理解 JavaScript Fetch API – wiki基地


深入理解 JavaScript Fetch API:现代网络请求的利器

在现代 Web 开发中,与服务器进行数据交互是核心功能之一。无论是获取数据、提交表单,还是上传文件,客户端都需要向服务器发起 HTTP 请求。长期以来,JavaScript 中进行异步 HTTP 请求的标准方法是 XMLHttpRequest (XHR) 对象。然而,随着 Web 技术的不断发展和对更简洁、更强大异步编程模型的需求,Fetch API 应运而生,并迅速成为进行网络请求的主流方式。

Fetch API 提供了一种更现代、更灵活的方式来发起网络请求。它基于 Promise,这使得处理异步响应变得更加容易,并提供了更强大的功能,例如流式处理响应、以及对请求和响应的更多底层控制。本文将深入探讨 JavaScript Fetch API,从基础用法到高级特性,帮助你全面掌握这一重要的 Web API。

一、告别 XHR:Fetch API 的诞生背景与优势

在 Fetch API 出现之前,开发者主要依赖 XMLHttpRequest (XHR) 进行 AJAX 请求。虽然 XHR 功能强大且兼容性好,但它的 API 设计存在一些痛点:

  1. 基于事件回调: XHR 的异步操作依赖大量的事件监听(如 onreadystatechange, onload, onerror 等),这使得代码容易陷入“回调地狱”,尤其是当需要处理多个相互依赖的请求时。
  2. API 冗余和不够直观: 设置请求头、请求体、处理不同类型的响应数据(JSON、文本、 Blob 等)需要调用多个不同的方法和属性,不够简洁。
  3. 对 Promise 支持不足: 虽然可以通过包装 XHR 来实现 Promise 化,但这并非原生支持,增加了额外的复杂性。
  4. 流式处理能力有限: 对于处理大型响应数据,XHR 不易实现流式处理。

Fetch API 的设计目标正是为了解决这些问题,提供一个更简洁、更强大、基于 Promise 的网络请求接口。

Fetch API 的核心优势包括:

  • 基于 Promise: Fetch 返回的是一个 Promise,这使得异步操作的链式调用和错误处理变得非常优雅,与 async/await 语法结合使用更是如虎添翼,代码可读性大大提高。
  • 更清晰的 API: Fetch 的设计更符合 HTTP 协议的逻辑,通过 RequestResponse 对象来抽象请求和响应,属性和方法更直观。
  • 分离头部和主体: Fetch 在接收响应时,首先解析头部信息,然后才开始读取主体。Promise 在接收到头部后就会解决 (resolve),后续再异步读取主体内容(如 JSON、文本等),这与 XHR 的 onreadystatechange 状态变化有所不同。
  • 支持更多高级特性: 原生支持流式处理(通过 Response.body)、服务工作线程 (Service Workers) 的请求拦截、以及对 CORS 的更精细控制等。
  • 不发送跨域 cookie (默认): 在进行跨域请求时,默认不发送 cookie 和其他用户凭据,这有助于提高安全性,但也需要开发者明确配置 credentials 选项来发送。

鉴于这些优势,Fetch API 已经成为现代 Web 开发进行网络请求的首选。

二、Fetch API 的基本用法

Fetch API 的最基本用法非常简单,只需要调用 fetch() 函数,传入要请求的 URL 即可:

“`javascript
fetch(‘https://api.example.com/data’)
.then(response => {
// response 对象代表了整个响应(包括头部信息,但不包括响应主体)
console.log(‘Response received:’, response);

// 检查响应状态码是否表示成功(200-299)
if (!response.ok) {
  // 如果不是成功的状态码,抛出错误,Promise 将被拒绝 (reject)
  throw new Error(`HTTP error! status: ${response.status}`);
}

// 解析响应主体为 JSON。这是一个异步操作,返回一个新的 Promise
return response.json();

})
.then(data => {
// 成功解析 JSON 数据后,在这里处理数据
console.log(‘Data received:’, data);
})
.catch(error => {
// 处理任何在 fetch 或解析过程中发生的错误(包括网络错误和上面抛出的 HTTP 错误)
console.error(‘There was a problem with the fetch operation:’, error);
});
“`

这个例子展示了 Fetch API 的两个关键点:

  1. fetch() 函数返回一个 Promise,该 Promise 在接收到响应头部时被解决。
  2. 解决后的 Promise 的值是一个 Response 对象。这个 Response 对象包含响应的状态码、头部等信息。要获取响应的主体内容(如 JSON、文本),需要调用 Response 对象上的异步方法(如 json(), text(), blob() 等),这些方法也会返回 Promise。

因此,Fetch 请求通常是一个两步过程

  1. 调用 fetch(url, options) 发起请求,获取代表响应头部的 Promise。
  2. 在第一个 Promise 解决后,调用 response.json(), response.text() 等方法解析响应主体,获取代表响应主体内容的 Promise。

三、使用 async/await 简化 Fetch 代码

Fetch API 天然支持 Promise,这使得它与 async/await 语法结合使用时异常强大,可以将异步代码写得像同步代码一样直观。上面的例子使用 async/await 可以重写如下:

“`javascript
async function fetchData() {
try {
const response = await fetch(‘https://api.example.com/data’);

if (!response.ok) {
  throw new Error(`HTTP error! status: ${response.status}`);
}

const data = await response.json();
console.log('Data received:', data);

} catch (error) {
console.error(‘There was a problem with the fetch operation:’, error);
}
}

fetchData();
“`

使用 async/await 后,代码流程更加清晰,更接近我们编写同步代码的习惯,极大地提高了可读性和可维护性。在实际开发中,强烈推荐使用 async/await 来处理 Fetch 请求。

四、配置请求:options 对象详解

fetch() 函数的第二个参数是一个可选的 options 对象,用于配置请求的各种属性,如 HTTP 方法、请求头、请求体、缓存策略等。

javascript
fetch(url, options);

options 对象可以包含以下常用属性:

  1. method: HTTP 请求方法,如 'GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'。默认值是 'GET'
    javascript
    method: 'POST'

  2. headers: 请求头。可以是一个 Headers 对象,也可以是一个普通的对象字面量。键值对表示请求头名称和对应的值。设置 Content-Type 对于发送请求体(尤其是 POST 或 PUT 请求)非常重要。
    javascript
    headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer your_token_here'
    }
    // 或者使用 Headers 对象
    // const myHeaders = new Headers();
    // myHeaders.append('Content-Type', 'application/json');
    // headers: myHeaders

  3. body: 请求体。用于 'POST', 'PUT', 'PATCH' 等方法发送数据。可以是 string, Blob, BufferSource, FormData, URLSearchParams, ReadableStream 等类型。

    • 发送 JSON 数据时,需要先使用 JSON.stringify() 转换,并设置 Content-Typeapplication/json
    • 发送表单数据时,可以使用 FormData 对象,此时通常无需手动设置 Content-Type,浏览器会自动设置一个带有 boundarymultipart/form-data
      “`javascript
      // 发送 JSON 数据
      body: JSON.stringify({ username: ‘example’, password: ‘password’ })

    // 发送表单数据
    // const formData = new FormData();
    // formData.append(‘username’, ‘example’);
    // formData.append(‘password’, ‘password’);
    // body: formData
    “`

  4. mode: 请求模式,用于控制跨域请求的行为。

    • 'cors' (默认): 允许跨域请求,遵循 CORS 协议。服务器需要返回相应的 CORS 头(如 Access-Control-Allow-Origin)。
    • 'no-cors': 尝试向其他源发起请求,但不会发送一些敏感头部(如用户凭据),并且响应对象是“不透明”的 (Opaque),无法访问其状态、头部、主体等信息,主要用于发送跟踪 ping 或向其他域的服务发送简单请求而不关心响应内容。只能使用 'GET', 'HEAD', 'POST' 方法,且头部受到严格限制。
    • 'same-origin': 只允许同源请求,如果请求不同源,会直接失败。
    • 'navigate': 用于导航请求(如用户点击链接或提交表单),只能用于顶级导航。
  5. credentials: 凭据策略,用于控制是否发送 cookie、HTTP Basic Auth 等用户凭据。

    • 'omit' (默认): 不发送任何凭据。
    • 'same-origin': 只在同源请求时发送凭据。
    • 'include': 始终发送凭据,即使是跨域请求。跨域发送凭据需要服务器返回 Access-Control-Allow-Credentials: true 头部。
  6. cache: 缓存策略。控制请求如何与 HTTP 缓存交互。

    • 'default' (默认): 遵循浏览器和服务器的缓存协议。
    • 'no-store': 不读取缓存,不写入缓存。
    • 'reload': 跳过缓存,强制从网络获取资源,但会更新缓存。
    • 'no-cache': 不读取缓存,但会向服务器发送条件式请求 (If-None-Match, If-Modified-Since),如果资源未改变则返回 304,否则从网络获取并更新缓存。
    • 'force-cache': 强制使用缓存,即使缓存过期,除非没有缓存。
    • 'only-if-cached': 只在缓存中有匹配项时使用缓存,否则失败。只能用于 'same-origin' 模式。
  7. referrer: 设置请求的 Referer 头部。可以是 'no-referrer', 'client', 或一个 URL 字符串。

  8. referrerPolicy: 控制 Referer 头部信息的策略。例如 'no-referrer', 'same-origin', 'strict-origin', 'unsafe-url' 等。
  9. integrity: 用于子资源完整性 (Subresource Integrity, SRI) 检查,验证获取的资源是否被篡改。通常用于 <script><link> 标签。
  10. keepalive: 一个布尔值,当页面关闭或卸载时,允许浏览器继续发送请求。主要用于发送分析数据或日志,保证请求在页面卸载前发送成功。通常与 navigator.sendBeacon() 配合使用,Fetch API 支持此选项提供了另一种选择。
  11. signal: 用于中止 (abort) 请求。与 AbortController 结合使用。

五、处理响应:Response 对象详解

fetch() 返回的 Promise 解决后,我们得到一个 Response 对象。这个对象包含了关于响应的各种信息,并提供方法来访问响应的主体。

Response 对象的重要属性:

  • ok: 一个布尔值,表示响应的状态码是否在 200-299 范围内(成功)。这是检查 HTTP 状态是否成功的常用方法。
  • status: 响应的 HTTP 状态码(如 200, 404, 500)。
  • statusText: 响应状态码对应的文本信息(如 “OK”, “Not Found”, “Internal Server Error”)。
  • headers: 一个 Headers 对象,包含了响应的所有头部信息。可以通过 response.headers.get('Content-Type') 等方法访问。
  • url: 响应的最终 URL,考虑到重定向。
  • type: 响应的类型,如 'basic', 'cors', 'opaque', 'error'.
  • redirected: 一个布尔值,表示响应是否经过了重定向。
  • body: 一个 ReadableStream 对象,表示响应的主体。这允许流式读取数据,但通常我们使用更便捷的方法来获取主体内容。
  • bodyUsed: 一个布尔值,表示响应的主体是否已经被读取过。响应主体只能被读取一次。

Response 对象的重要方法(用于解析响应主体,这些方法都返回 Promise):

  • json(): 将响应主体解析为 JSON 对象。
  • text(): 将响应主体解析为纯文本字符串。
  • blob(): 将响应主体解析为 Blob 对象(用于处理二进制数据,如图片)。
  • arrayBuffer(): 将响应主体解析为 ArrayBuffer(另一种处理二进制数据的方式)。
  • formData(): 将响应主体解析为 FormData 对象(用于处理 multipart/form-dataapplication/x-www-form-urlencoded 格式的响应)。

注意: 这些方法只能调用一次。如果多次调用,会抛出错误。

六、错误处理

Fetch API 的错误处理分为两种类型:

  1. 网络错误: DNS 查询失败、连接被拒绝、网络中断等导致请求未能成功发出的错误。这些错误会使得 fetch() 返回的 Promise 被拒绝 (reject)。使用 .catch()try...catch 来捕获。
  2. HTTP 错误: 请求成功发出并收到了响应,但响应的状态码表示错误(如 404 Not Found, 500 Internal Server Error)。Fetch API 不会因为这些状态码而拒绝 Promise。Promise 仍然会被解决 (resolve),你需要在 .then()await 之后检查 response.okresponse.status 来判断请求是否在逻辑上成功,并手动抛出错误以便被 .catch() 捕获。

一个健壮的 Fetch 错误处理应该同时考虑这两种情况:

“`javascript
async function fetchWithErrorHandling(url, options) {
try {
const response = await fetch(url, options);

// 检查 HTTP 状态码是否成功
if (!response.ok) {
  // 获取错误信息(如果服务器提供了)
  // 注意:这里的 response.json() 也可能失败,需要额外的try...catch
  let errorMsg = `HTTP error! status: ${response.status}`;
  try {
    const errorBody = await response.json();
    if (errorBody && errorBody.message) {
        errorMsg = `HTTP error! status: ${response.status}, message: ${errorBody.message}`;
    } else if (response.statusText) {
         errorMsg = `HTTP error! status: ${response.status}, text: ${response.statusText}`;
    }
  } catch (parseError) {
      // 无法解析错误响应体,使用默认错误信息
      console.warn('Could not parse error response body:', parseError);
  }

  // 抛出自定义错误,包含状态码等信息
  const httpError = new Error(errorMsg);
  httpError.status = response.status;
  httpError.response = response; // 可选:保留原始响应对象
  throw httpError;
}

// 成功时解析并返回数据
const data = await response.json();
return data;

} catch (error) {
// 捕获网络错误或上面抛出的 HTTP 错误
console.error(‘Fetch operation failed:’, error);
// 可以根据 error 的类型或属性进行更精细的处理
if (error.message.startsWith(‘HTTP error!’)) {
console.log(‘Handled an HTTP error.’);
} else {
console.log(‘Handled a network error.’);
}
// 可以选择重新抛出错误或者返回一个特定的错误状态
throw error; // 向上层调用者传递错误
}
}

// 调用这个封装函数
fetchWithErrorHandling(‘https://api.example.com/nonexistent’) // 模拟 404 错误
.catch(err => {
console.log(‘Caught error from wrapper:’, err);
if (err.status === 404) {
console.log(‘Specifically handled 404.’);
}
});

fetchWithErrorHandling(‘https://invalid-url-that-does-not-exist.com’) // 模拟网络错误
.catch(err => {
console.log(‘Caught error from wrapper:’, err);
// 网络错误没有 status 属性
if (!err.status) {
console.log(‘Specifically handled network error.’);
}
});
“`
通过这种封装,我们可以统一处理两种类型的错误,并提供更详细的错误信息。

七、中止 Fetch 请求:AbortController

在某些场景下,我们可能需要在请求完成之前取消它,例如用户在搜索框快速输入时取消前一个未完成的搜索请求,或者组件卸载时取消未完成的请求以避免内存泄漏。Fetch API 结合 AbortController 提供了中止请求的能力。

AbortController 是一个简单的 API,它包含一个 signal 属性和一个 abort() 方法。我们将 signal 传递给 Fetch 请求的 options 对象的 signal 属性。当调用 controller.abort() 方法时,与该信号关联的 Fetch 请求就会被中止。中止的 Fetch Promise 会被拒绝 (reject),并抛出一个 AbortError

“`javascript
const controller = new AbortController();
const signal = controller.signal;

const fetchTimeout = setTimeout(() => {
controller.abort(); // 10秒后中止请求
}, 10000);

fetch(‘https://api.example.com/large-data’, { signal: signal })
.then(response => {
clearTimeout(fetchTimeout); // 请求成功,清除超时计时器
if (!response.ok) {
throw new Error(HTTP error! status: ${response.status});
}
return response.json();
})
.then(data => {
console.log(‘Data received:’, data);
})
.catch(error => {
clearTimeout(fetchTimeout); // 无论成功失败,都清除计时器
if (error.name === ‘AbortError’) {
console.log(‘Fetch request was aborted.’);
} else {
console.error(‘Fetch error:’, error);
}
});

// 可以在其他地方根据用户操作等调用 controller.abort()
// 例如:用户点击了取消按钮
// document.getElementById(‘cancelButton’).addEventListener(‘click’, () => {
// controller.abort();
// });
使用 `async/await` 的写法:javascript
async function fetchWithAbort() {
const controller = new AbortController();
const signal = controller.signal;

const fetchTimeout = setTimeout(() => {
controller.abort();
}, 10000);

try {
const response = await fetch(‘https://api.example.com/large-data’, { signal: signal });
clearTimeout(fetchTimeout);

if (!response.ok) {
  throw new Error(`HTTP error! status: ${response.status}`);
}

const data = await response.json();
console.log('Data received:', data);

} catch (error) {
clearTimeout(fetchTimeout);
if (error.name === ‘AbortError’) {
console.log(‘Fetch request was aborted.’);
} else {
console.error(‘Fetch error:’, error);
}
}
}

fetchWithAbort();
``AbortController` 是中止 Fetch 请求的标准方式,非常实用。

八、Fetch API 的局限性与高级话题

虽然 Fetch API 强大且灵活,但它也有一些局限性,或者说某些功能不如 XHR 直观:

  1. 没有内置的请求超时控制: 需要结合 AbortControllersetTimeout 手动实现超时逻辑(如上面的例子所示)。
  2. 没有内置的请求进度监听: 对于文件上传或下载,Fetch API 原生不易监听进度。虽然可以通过读取 response.bodyReadableStream 并结合 TransformStream 来实现,但这比 XHR 的 onprogress 事件要复杂得多。
  3. 不发送跨域 cookie 默认: 如前所述,需要显式设置 credentials: 'include'
  4. 不会对非 2xx 状态码抛出错误: 需要手动检查 response.ok

高级话题简述:

  • Request 对象: 可以先创建一个 Request 对象,再将其作为参数传递给 fetch()。这在需要复制或修改请求时很有用。
    “`javascript
    const myRequest = new Request(‘https://api.example.com/data’, {
    method: ‘POST’,
    headers: { ‘Content-Type’: ‘application/json’ },
    body: JSON.stringify({ name: ‘test’ })
    });

    fetch(myRequest)
    .then(…)
    * **Headers 对象:** 提供了方便的方法来操作请求头和响应头。javascript
    const myHeaders = new Headers();
    myHeaders.append(‘Content-Type’, ‘application/json’);
    myHeaders.set(‘X-Custom-Header’, ‘fetch-demo’);
    myHeaders.has(‘Content-Type’); // true
    myHeaders.delete(‘X-Custom-Header’);

    fetch(url, { headers: myHeaders });
    ``
    * **流式处理 (
    ReadableStream):** 对于非常大的响应,可以直接访问response.body获取一个ReadableStream`,逐块读取数据而无需等待整个响应下载完成。这在处理大型文件或持续更新的数据流时非常有用。
    * 服务工作线程 (Service Workers) 中的应用: Fetch API 是 Service Workers 的核心,用于拦截和处理网络请求,实现离线访问、缓存策略、请求修改等功能。

九、Fetch vs XMLHttpRequest 总结比较

特性 Fetch API XMLHttpRequest (XHR)
异步模式 基于 Promise 基于事件回调 (onreadystatechange, onload等)
API 设计 简洁,更符合 HTTP 概念 (Request/Response) 冗余,方法和属性分散
语法 清晰,尤其配合 async/await 相对繁琐,易形成回调嵌套
错误处理 网络错误 Promise rejected;HTTP 错误需手动检查 response.ok 并抛出 网络错误/部分 HTTP 错误触发 onerror;其他状态需检查 status
响应主体 通过异步方法 (json(), text()等) 读取一次 通过 responseText, responseXML 等属性读取,可多次读取
流式处理 原生支持 (response.bodyReadableStream) 不原生支持,实现复杂
凭据 (Cookie) 默认不发送跨域凭据,需显式设置 credentials 默认发送同源和部分跨域凭据(取决于浏览器策略)
超时控制 无内置选项,需结合 AbortController/setTimeout 有内置 timeout 属性
上传/下载进度 原生不易监听,需处理流 有内置 onprogress 事件
中止请求 使用 AbortController 使用 abort() 方法
现代特性 支持 Service Workers, CORS 精细控制 支持较少
兼容性 现代浏览器广泛支持,IE 不支持 几乎所有浏览器都支持(包括 IE)

十、实践建议与代码示例

在实际开发中,为了提高代码的可重用性和健壮性,通常会对 Fetch API 进行封装,构建自己的网络请求工具函数或类。

一个简单的 Fetch 封装函数示例(包含基本错误处理和 JSON 数据处理):

“`javascript
const API_BASE_URL = ‘https://api.example.com’; // 你的 API 基础 URL

async function request(endpoint, options = {}) {
const url = ${API_BASE_URL}${endpoint};
const { method = ‘GET’, headers, body, …restOptions } = options;

const config = {
method,
headers: {
// 默认头部,可以被 options.headers 覆盖
‘Content-Type’: ‘application/json’,
…headers,
},
// 对于 GET 或 HEAD 方法,不应包含 body
body: method === ‘GET’ || method === ‘HEAD’ ? undefined : JSON.stringify(body),
…restOptions, // 允许传递其他 Fetch 选项,如 mode, credentials, signal 等
};

// 移除 body 为 undefined 的情况,fetch 会自动处理
if (config.body === undefined) {
delete config.body;
// 如果没有 body 且 Content-Type 默认设置了 application/json,需要移除
if (config.headers[‘Content-Type’] === ‘application/json’ && (method === ‘GET’ || method === ‘HEAD’)) {
delete config.headers[‘Content-Type’];
// 如果headers对象为空,可以考虑移除 headers 属性
if (Object.keys(config.headers).length === 0) {
delete config.headers;
}
}
}

try {
const response = await fetch(url, config);

if (!response.ok) {
  let errorData = null;
  try {
     // 尝试读取错误响应体,即使是错误状态码,服务器也可能返回 JSON 格式的错误信息
     errorData = await response.json();
  } catch (parseError) {
      // 如果无法解析 JSON,忽略此错误
      console.warn(`Could not parse error response body from ${url}:`, parseError);
  }

  const error = new Error(`HTTP error! status: ${response.status}`);
  error.status = response.status;
  error.statusText = response.statusText;
  error.response = response; // 保存原始响应对象
  error.data = errorData; // 保存解析到的错误数据
  throw error;
}

// 成功时尝试解析 JSON,如果响应体为空或非 JSON,这里可能会失败
// 根据实际 API 响应决定是否必须解析 JSON
try {
    const data = await response.json();
    return data; // 返回解析后的数据
} catch (parseError) {
     // 如果响应成功但不是 JSON (例如 204 No Content),则返回原始响应对象或 null
     console.warn(`Could not parse JSON response from ${url}:`, parseError);
     // 根据 API 约定,如果成功但无体,可以返回 response 或 null
     return response; // 例如,对于 DELETE 请求,可能只需确认状态码
}

} catch (error) {
// 捕获网络错误或上面抛出的 HTTP 错误
console.error(Request failed for ${url}:, error);
throw error; // 重新抛出以便调用者处理
}
}

// 使用示例:GET 请求
request(‘/users/123’)
.then(user => console.log(‘User data:’, user))
.catch(err => console.error(‘Failed to fetch user:’, err));

// 使用示例:POST 请求
request(‘/posts’, {
method: ‘POST’,
body: { title: ‘New Post’, content: ‘…’ },
headers: { ‘Authorization’: ‘Bearer token’ }
})
.then(newPost => console.log(‘Created post:’, newPost))
.catch(err => console.error(‘Failed to create post:’, err));

// 使用示例:带中止信号的 GET 请求
const controller = new AbortController();
const signal = controller.signal;
request(‘/data?large=true’, { signal: signal })
.then(data => console.log(‘Large data received:’, data))
.catch(err => {
if (err.name === ‘AbortError’) {
console.log(‘Large data fetch aborted.’);
} else {
console.error(‘Failed to fetch large data:’, err);
}
});

// 假设在某个时刻需要中止请求
// controller.abort();
“`
这个封装函数提供了一个更友好的接口,默认处理 JSON,并统一了错误处理。你可以根据项目需求进一步扩展,例如添加拦截器功能、处理文件上传下载进度等。

十一、总结

Fetch API 是 JavaScript 中进行网络请求的现代标准。它基于 Promise,提供了简洁、灵活的 API 设计,并支持许多高级特性。尽管在某些方面(如超时和进度监听)不如 XMLHttpRequest 直观,但其 Promise 特性、与 async/await 的良好集成以及对新 Web 标准的支持,使其成为绝大多数前端网络请求场景的首选。

掌握 Fetch API 是现代 Web 开发者的必备技能。通过深入理解其基本用法、options 对象配置、Response 对象处理、错误处理机制以及 AbortController 等高级特性,你可以更高效、更健壮地进行网络通信,构建功能强大的 Web 应用。

现在,是时候在你的项目中使用 Fetch API 替换掉老旧的 XHR 代码,享受 Promise 带来的便利和代码的清晰性了!


发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注

滚动至顶部