Fetch API 的内部状态机:请求、响应、体(Body)读取的异步流程

Fetch API 自其诞生以来,已经成为现代 Web 开发中进行网络请求的主流方式。它以其简洁的 Promise 接口和对 HTTP 管道的清晰建模,取代了传统的 XMLHttpRequest,为开发者带来了更优雅、更强大的异步请求体验。然而,Fetch API 表面上的简洁背后,隐藏着一个复杂的内部状态机,它精确地协调着从请求发起、网络传输、响应接收到最终数据体(Body)读取的每一个异步步骤。

理解 Fetch API 的内部状态机,对于编写健壮、高效且具备良好错误处理机制的网络应用至关重要。我们将深入探讨这一机制,从请求的构建到响应体的消费,揭示 Fetch API 如何在各个阶段管理其异步状态。

Fetch API 的核心构成与异步基石

Fetch API 的核心由几个关键接口组成:

  1. fetch() 方法: 全局函数,用于发起网络请求。它返回一个 Promise,该 Promise 在网络响应的头部被接收时解析为一个 Response 对象。
  2. Request 接口: 表示一个请求资源的对象。你可以手动创建它来更精细地配置请求,例如设置方法、URL、头部、模式、缓存策略和请求体。
  3. Response 接口: 表示对请求的响应的对象。它包含了响应的状态码、状态文本、头部信息,以及一个 Body 混入(Mixin),用于处理响应体。
  4. Headers 接口: 允许你查询、添加或删除 HTTP 头部。
  5. Body 混入 (Mixin): RequestResponse 对象都实现了 Body 混入,它提供了一系列异步方法(如 json(), text(), blob(), arrayBuffer(), formData())来消费请求或响应的体数据。

Fetch API 的异步特性完全基于 JavaScript 的 Promise 机制。每次通过 fetch() 发起请求,都会得到一个 Promise。这个 Promise 将经历 pending(待定)、fulfilled(已兑现)或 rejected(已拒绝)三种状态。理解这些状态何时以及如何转换,是掌握 Fetch API 内部机制的关键。

Fetch API 内部状态机:高层概览

Fetch API 的网络请求生命周期可以被抽象为一个多阶段的状态机。它不像一个简单的函数调用那样立即返回结果,而是在后台通过一系列的异步操作来完成。

从高层来看,这个过程大致遵循以下步骤:

  1. 请求初始化: 客户端构建 Request 对象(或由 fetch() 内部构建)。
  2. 请求调度与发送: 浏览器将请求发送到网络层。
  3. 网络传输: 请求在网络中传输,到达服务器。
  4. 服务器处理与响应: 服务器处理请求并生成响应。
  5. 响应头部接收: 浏览器接收到响应的头部信息。
  6. 响应体读取: 浏览器开始接收并读取响应体数据。
  7. 数据解析与交付: 响应体数据被解析成应用程序可用的格式。

Fetch API 的 Promise 机制将这些阶段划分为两个主要的异步操作:

  • 第一个 Promise: fetch() 方法本身返回的 Promise。它在收到响应头部时解析,提供一个 Response 对象。
  • 第二个 Promise: Response 对象上体读取方法(如 json(), text() 等)返回的 Promise。它在整个响应体被完全读取并解析后解析,提供最终的数据。

这种两阶段的 Promise 结构,正是 Fetch API 精妙之处,它允许开发者在知道请求是否成功(通过响应状态码)的同时,异步地处理潜在的大型响应体。

阶段一:请求的构建与调度

Fetch API 的旅程始于一个请求。这个请求可以是简单的 URL 字符串,也可以是一个经过详细配置的 Request 对象。

1.1 Request 对象的创建与配置

你可以通过 fetch() 函数直接传入 URL 和一个可选的 options 对象来发起请求:

// 最简单的 GET 请求
fetch('https://api.example.com/data')
  .then(response => response.json())
  .then(data => console.log(data))
  .catch(error => console.error('Error:', error));

或者,为了更精细地控制请求,你可以显式地创建一个 Request 对象:

const requestUrl = 'https://api.example.com/users';
const requestOptions = {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_AUTH_TOKEN'
  },
  body: JSON.stringify({
    name: 'Alice',
    email: '[email protected]'
  }),
  mode: 'cors', // 跨域请求模式
  cache: 'no-cache', // 不使用缓存
  credentials: 'omit', // 不发送 cookies
  signal: new AbortController().signal // 用于取消请求的信号
};

const myRequest = new Request(requestUrl, requestOptions);

fetch(myRequest)
  .then(response => {
    // ... 处理响应
  })
  .catch(error => {
    // ... 处理错误
  });

Request 对象的属性定义了请求的所有方面:

  • url: 请求的目标 URL。
  • method: HTTP 方法(GET, POST, PUT, DELETE 等)。
  • headers: 一个 Headers 对象,包含所有请求头。
  • body: 请求体,可以是 USVString (UTF-8 字符串), Blob, BufferSource, FormData, URLSearchParams, ReadableStream
  • mode: 请求模式(cors, no-cors, same-origin, navigate),影响 CORS 策略。
  • cache: 缓存模式(default, no-store, reload, no-cache, force-cache, only-if-cached)。
  • credentials: 凭证模式(omit, same-origin, include),控制是否发送 cookies。
  • redirect: 重定向模式(follow, error, manual)。
  • referrer: referrer 值。
  • referrerPolicy: referrer 策略。
  • integrity: 子资源完整性。
  • keepalive: 允许请求在页面卸载时继续发送。
  • signal: AbortSignal 对象,用于取消请求。

1.2 fetch() 调用与 Promise 状态初始化

当你调用 fetch(myRequest)fetch(url, options) 时,Fetch API 的内部状态机开始工作。

  1. Promise 返回: fetch() 函数会立即返回一个 Promise 对象。此时,这个 Promise 的状态是 pending
  2. 请求调度: 浏览器会将这个请求交给其底层的网络栈处理。这包括:
    • DNS 解析: 将域名解析为 IP 地址。
    • TCP 连接建立: 与服务器建立 TCP 连接(三次握手)。
    • TLS 握手: 如果是 HTTPS 请求,进行 TLS 握手以建立加密通道。
    • 发送请求行和头部: 构造 HTTP 请求行(方法、路径、协议版本)和所有请求头部,并通过网络发送。
    • 发送请求体: 如果请求包含体(如 POST、PUT 请求),请求体数据会开始通过网络流式传输到服务器。

在此阶段,如果发生任何网络级别的错误(例如,DNS 解析失败、无法建立 TCP 连接、CORS 预检请求失败等),fetch() 返回的 Promise 将会立即进入 rejected 状态,并带有一个 TypeError(或更具体的错误,如 NetworkError)。

状态转换:

当前状态 事件/动作 下一状态 描述
初始 fetch() 被调用 Request Pending 返回一个 pending 状态的 Promise。
Request Pending 网络错误发生 Rejected (网络错误) 例如 DNS 失败、连接超时、CORS 预检失败。
fetch('https://nonexistent-domain-12345.com/data')
  .then(response => console.log('This will not be logged'))
  .catch(error => {
    console.error('Fetch request failed:', error.name, error.message);
    // 示例输出: "Fetch request failed: TypeError Failed to fetch"
  });

阶段二:网络交互与响应头部接收

一旦请求成功调度,它就在网络中传输。服务器接收到请求,进行处理,并开始发送响应。

2.1 服务器处理与响应发送

服务器在接收到完整的请求(包括请求体)后,执行其业务逻辑,例如查询数据库、进行计算等,然后构建一个 HTTP 响应。这个响应包含:

  • 状态行: HTTP 协议版本、状态码(如 200 OK, 404 Not Found, 500 Internal Server Error)和状态文本。
  • 响应头部: 各种元数据,如 Content-TypeContent-LengthCache-ControlSet-Cookie 等。
  • 响应体: 实际的数据负载(例如 JSON 字符串、HTML 文档、图片二进制数据)。

2.2 响应头部的接收与 Promise 解析

Fetch API 的一个关键设计点是:fetch() 返回的 Promise 会在浏览器接收到响应的 HTTP 头部时立即解析。这意味着在整个响应体数据到达之前,你就可以访问 Response 对象。

当 Promise 解析时,它会提供一个 Response 对象,该对象包含:

  • status: HTTP 状态码(例如 200, 404, 500)。
  • statusText: 状态文本(例如 "OK", "Not Found", "Internal Server Error")。
  • ok: 一个布尔值,表示响应是否成功(true 表示状态码在 200-299 范围内)。这是判断 HTTP 级别成功与否的便捷方式。
  • headers: 一个 Headers 对象,可以用于查询响应头。
  • url: 响应的最终 URL(可能因重定向而改变)。
  • type: 响应类型(例如 basic, cors, opaque)。
  • body: 一个 ReadableStream 对象,代表响应体的数据流。此时,响应体可能还未完全下载。

重要提示: 即使 HTTP 状态码表示错误(例如 404 Not Found 或 500 Internal Server Error),fetch() 返回的 Promise 仍然会解析(即进入 fulfilled 状态),而不是拒绝。你需要手动检查 response.okresponse.status 来判断请求的业务逻辑是否成功。

状态转换:

当前状态 事件/动作 下一状态 描述
Request Pending 服务器发送响应头部 Response Headers Ready fetch() Promise 解析,提供一个 Response 对象。
Response 对象包含状态码、头部等信息,但响应体尚未完全下载。
fetch('https://api.example.com/status/404') // 假设这是一个返回 404 的端点
  .then(response => {
    console.log('Response Headers Ready!');
    console.log('Status:', response.status);       // 404
    console.log('Status Text:', response.statusText); // Not Found
    console.log('OK:', response.ok);             // false
    console.log('Content-Type:', response.headers.get('Content-Type'));

    if (!response.ok) {
      // 即使是 404,Promise 也已解析,但我们仍需处理 HTTP 错误
      throw new Error(`HTTP error! Status: ${response.status}`);
    }
    return response.json(); // 继续处理响应体
  })
  .then(data => {
    console.log('Data:', data);
  })
  .catch(error => {
    console.error('Caught error:', error.message); // "Caught error: HTTP error! Status: 404"
  });

fetch('https://api.example.com/data') // 假设这是一个返回 200 OK 的端点
  .then(response => {
    console.log('Response Headers Ready for 200 OK!');
    console.log('Status:', response.status);       // 200
    console.log('OK:', response.ok);             // true
    return response.json(); // 继续处理响应体
  })
  .then(data => console.log('Data:', data))
  .catch(error => console.error('Error:', error));

阶段三:响应体的读取与数据处理

一旦 fetch() Promise 解析并提供了 Response 对象,下一步就是读取其体数据。Response 对象实现了 Body 混入,提供了一系列方便的方法来处理不同类型的响应体。

3.1 Body 混入及其方法

Body 混入提供了以下异步方法,每个方法都返回一个 Promise

  • response.json(): 将响应体解析为 JSON 对象。如果响应体不是有效的 JSON,返回的 Promise 将被拒绝。
  • response.text(): 将响应体解析为纯文本字符串。
  • response.blob(): 将响应体解析为 Blob 对象。适用于处理二进制数据,如图片、文件。
  • response.arrayBuffer(): 将响应体解析为 ArrayBuffer 对象。适用于更底层的二进制数据操作。
  • response.formData(): 将响应体解析为 FormData 对象。适用于处理表单数据。

这些方法有一个关键特性:它们只能被调用一次。一旦你调用了其中一个方法,响应体就被“消费”了。再次调用会导致错误。这是因为响应体是一个数据流,一旦读取,数据就流过了。

状态转换:

当前状态 事件/动作 下一状态 描述
Response Headers Ready 调用 response.json()/text() Body Reading Pending 返回一个新的 Promise,等待整个响应体被读取和解析。
Body Reading Pending 数据流开始传输 Body Reading In Progress 浏览器从网络中接收响应体数据块。
Body Reading In Progress 整个响应体接收完成并解析 Fulfilled (数据可用) 体读取 Promise 解析,提供解析后的数据(JSON, 文本等)。
解析错误发生 Rejected (解析错误) 例如,调用 json() 但响应体不是有效的 JSON。
网络断开/超时(在下载期间) Rejected (网络错误) 在下载响应体数据时发生网络中断。

3.2 异步体读取的详细流程

当你调用 response.json() 或其他体读取方法时,会发生以下情况:

  1. 返回新的 Promise: 该方法立即返回一个新的 Promise,其状态为 pending
  2. 启动体下载: 浏览器开始从网络连接中下载剩余的响应体数据。这个过程是异步进行的,可能需要一些时间,特别是对于大型文件。
  3. 数据缓冲与解析: 下载的数据被缓冲起来。一旦所有数据都被接收,并且根据所选的方法(json(), text() 等)进行了解析,体读取的 Promise 就会进入 fulfilled 状态,并将其解析后的数据作为值返回。
  4. 解析错误: 如果在解析过程中发生错误(例如,response.json() 尝试解析一个非 JSON 格式的响应),体读取的 Promise 将会进入 rejected 状态,并带有一个相应的错误。
fetch('https://api.example.com/users/1')
  .then(response => {
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
    // 此时 response.body 是一个 ReadableStream,数据还在下载中
    return response.json(); // 返回一个新的 Promise,开始读取和解析 JSON
  })
  .then(userData => {
    // 只有当整个 JSON 体被下载并成功解析后,这个 then 块才会被执行
    console.log('User Data:', userData);
    console.log('User Name:', userData.name);
  })
  .catch(error => {
    console.error('Error fetching or parsing user data:', error);
  });

3.3 处理不同类型的响应体

JSON 数据:

// POST 请求发送 JSON 并接收 JSON 响应
fetch('https://api.example.com/products', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({ name: 'New Product', price: 99.99 })
})
  .then(response => {
    if (!response.ok) {
      // 检查 HTTP 状态码,抛出错误以进入 catch 块
      return response.json().then(err => { throw new Error(err.message || 'Server error'); });
    }
    return response.json(); // 解析响应体为 JSON
  })
  .then(product => {
    console.log('Created product:', product);
  })
  .catch(error => {
    console.error('Failed to create product:', error.message);
  });

文本数据:

fetch('https://api.example.com/readme.txt')
  .then(response => {
    if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
    return response.text(); // 解析响应体为纯文本
  })
  .then(textData => {
    console.log('README content:', textData);
  })
  .catch(error => console.error('Error fetching text:', error));

二进制数据 (Blob):

// 假设 'https://api.example.com/image.jpg' 返回一张图片
fetch('https://api.example.com/image.jpg')
  .then(response => {
    if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
    return response.blob(); // 解析响应体为 Blob
  })
  .then(imageBlob => {
    const imageUrl = URL.createObjectURL(imageBlob);
    const imgElement = document.createElement('img');
    imgElement.src = imageUrl;
    document.body.appendChild(imgElement);
    console.log('Image loaded as Blob:', imageBlob);
    // 记得在不再需要时释放 URL 对象
    // URL.revokeObjectURL(imageUrl);
  })
  .catch(error => console.error('Error fetching image:', error));

流式处理 (使用 ReadableStream):

对于非常大的响应体,或者需要实时处理数据流的场景,可以直接使用 response.body 这个 ReadableStream 对象。这允许你分块读取数据,而不需要等待整个响应体下载完成。

async function fetchAndProcessStream() {
  try {
    const response = await fetch('https://api.example.com/large-data-stream'); // 假设此端点返回一个大文件或数据流
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }

    const reader = response.body.getReader();
    const decoder = new TextDecoder('utf-8');
    let receivedLength = 0;
    let chunks = [];

    // 循环读取数据块
    while (true) {
      const { done, value } = await reader.read(); // value 是 Uint8Array
      if (done) {
        console.log('Stream reading complete.');
        break;
      }

      chunks.push(value);
      receivedLength += value.length;
      console.log(`Received ${receivedLength} bytes. Current chunk size: ${value.length}`);

      // 你可以在这里对每个数据块进行实时处理
      // 例如,解码并显示部分文本:
      // console.log('Partial data:', decoder.decode(value, { stream: true }));
    }

    // 将所有数据块合并并解码为最终字符串
    let fullText = '';
    for (const chunk of chunks) {
      fullText += decoder.decode(chunk, { stream: true });
    }
    fullText += decoder.decode(new Uint8Array(), { stream: false }); // 刷新 decoder 缓冲区

    console.log('Full stream content length:', fullText.length);
    // console.log('Full stream content:', fullText); // 打印完整内容 (可能非常大)

  } catch (error) {
    console.error('Error fetching or processing stream:', error);
  }
}

fetchAndProcessStream();

使用 ReadableStream 提供了更细粒度的控制,但同时也增加了代码的复杂性。对于大多数场景,json(), text() 等简便方法已足够。

阶段四:错误处理与请求取消

健壮的网络请求需要完善的错误处理。Fetch API 的 Promise 机制使得错误处理变得直观。

4.1 错误类型与处理

在 Fetch API 的整个生命周期中,可能遇到多种类型的错误:

  1. 网络错误 (TypeError/NetworkError):

    • 发生在请求调度或网络传输阶段。
    • 例如:DNS 解析失败、无网络连接、CORS 预检请求失败、请求被用户代理阻止、服务器无响应。
    • fetch() 返回的 Promise 会直接进入 rejected 状态。
    • 错误对象通常是 TypeErrorNetworkError
    fetch('https://nonexistent-domain-12345.com/data')
      .then(response => console.log('Success (should not happen)'))
      .catch(error => {
        console.error('Network-level error:', error.message);
        // "Network-level error: Failed to fetch"
      });
  2. HTTP 错误 (4xx, 5xx 状态码):

    • 发生在响应头部接收阶段。
    • 例如:404 Not Found, 401 Unauthorized, 500 Internal Server Error。
    • fetch() 返回的 Promise 不会拒绝,而是解析为一个 Response 对象。
    • 你需要手动检查 response.ok 属性或 response.status 状态码来判断。
    fetch('https://api.example.com/nonexistent-resource')
      .then(response => {
        if (!response.ok) {
          // 明确抛出错误,以便后续的 .catch 能够捕获
          throw new Error(`HTTP error! status: ${response.status}`);
        }
        return response.json();
      })
      .then(data => console.log('Data:', data))
      .catch(error => {
        console.error('Application-level HTTP error:', error.message);
        // "Application-level HTTP error: HTTP error! status: 404"
      });
  3. 解析错误:

    • 发生在响应体读取和解析阶段。
    • 例如:调用 response.json() 尝试解析一个非 JSON 格式的响应体。
    • response.json() (或 text(), blob() 等) 返回的 Promise 会进入 rejected 状态。
    // 假设这个端点返回纯文本 "Hello World" 而不是 JSON
    fetch('https://api.example.com/hello-text')
      .then(response => {
        if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
        return response.json(); // 尝试解析非 JSON 数据
      })
      .then(data => console.log('Data:', data))
      .catch(error => {
        console.error('Parsing error:', error.name, error.message);
        // "Parsing error: SyntaxError Unexpected token 'H', "Hello World" is not valid JSON"
      });
  4. 请求取消错误 (AbortError):

    • 当使用 AbortController 取消正在进行的 Fetch 请求时发生。
    • fetch() 返回的 Promise 会进入 rejected 状态,错误对象是 DOMException,其 name 属性为 'AbortError'

4.2 使用 AbortController 取消请求

AbortController 接口提供了一种取消一个或多个 Web 请求的机制。它通过一个 AbortSignal 对象与 fetch() 请求关联。

  1. 创建 AbortController 实例:
    const controller = new AbortController();
    const signal = controller.signal;
  2. signal 传递给 fetch():
    fetch('https://api.example.com/long-task', { signal: signal })
      .then(response => { /* ... */ })
      .catch(error => {
        if (error.name === 'AbortError') {
          console.log('Fetch request was aborted.');
        } else {
          console.error('Fetch error:', error);
        }
      });
  3. 调用 controller.abort() 取消请求:
    // 假设 500ms 后取消请求
    setTimeout(() => {
      controller.abort();
      console.log('Attempting to abort fetch...');
    }, 500);

完整的错误处理和取消示例:

const controller = new AbortController();
const signal = controller.signal;

async function performFetchWithCancellation() {
  try {
    console.log('Starting fetch request...');
    const response = await fetch('https://api.example.com/data', { signal }); // 使用 signal

    if (!response.ok) {
      // 尝试读取服务器返回的错误信息,如果有的话
      const errorBody = await response.text();
      throw new Error(`HTTP error! Status: ${response.status}, Details: ${errorBody}`);
    }

    const data = await response.json();
    console.log('Data received successfully:', data);
  } catch (error) {
    if (error.name === 'AbortError') {
      console.warn('Fetch request was aborted by AbortController.');
    } else if (error instanceof TypeError) { // 或 NetworkError
      console.error('Network error (e.g., no internet, CORS):', error.message);
    } else if (error instanceof SyntaxError) { // 例如 JSON 解析失败
      console.error('Data parsing error:', error.message);
    } else {
      console.error('An unexpected error occurred:', error.message);
    }
  }
}

// 启动请求
performFetchWithCancellation();

// 模拟在请求进行中取消
setTimeout(() => {
  controller.abort();
  console.log('Request cancellation initiated.');
}, 200); // 假设 200ms 后取消

Fetch API 内部状态机:综合视图表格

为了更清晰地展示 Fetch API 的内部状态转换,我们可以用一个表格来概括:

阶段/状态 事件/动作 下一状态 Promise 返回状态 描述
初始 fetch() 被调用 Request Pending Promise<Response> (pending) fetch() 函数被调用,立即返回一个 pending 状态的 Promise。浏览器开始构建和调度请求。
Request Pending 1. 网络连接建立成功 Response Headers Ready Promise<Response> (fulfilled) DNS 解析、TCP/TLS 握手、请求发送完成。服务器响应头部被接收。fetch() Promise 解析。
2. 网络错误发生 Rejected (网络错误) Promise<Response> (rejected) DNS 失败、连接超时、CORS 预检失败、无网络连接。fetch() Promise 被拒绝(TypeError/NetworkError)。
3. controller.abort() Rejected (AbortError) Promise<Response> (rejected) 请求被 AbortController 明确取消。fetch() Promise 被拒绝 (AbortError)。
Response Headers Ready 1. 业务逻辑检查 response.ok Body Reading Pending Promise<DataType> (pending) fetch() Promise 已解析。开发者调用 response.json()/text() 等方法,返回一个新的 pending Promise。
2. 业务逻辑判断失败(如 !response.ok)并抛出错误 Rejected (HTTP Error) Promise<DataType> (rejected) 开发者手动检查 response.statusresponse.ok 后抛出自定义错误。
Body Reading Pending 1. 响应体数据开始传输 Body Reading In Progress Promise<DataType> (pending) 浏览器开始从网络中接收响应体的第一个数据块。
Body Reading In Progress 1. 整个响应体接收完成并解析 Fulfilled (数据可用) Promise<DataType> (fulfilled) 所有响应体数据已被下载,并成功解析为指定的数据类型。体读取 Promise 解析。
2. 解析错误发生 Rejected (解析错误) Promise<DataType> (rejected) 响应体数据与所选的解析方法不匹配(如 json() 解析非 JSON)。体读取 Promise 被拒绝 (SyntaxError)。
3. 网络断开/超时(下载期间) Rejected (网络错误) Promise<DataType> (rejected) 在下载响应体数据过程中发生网络中断或超时。体读取 Promise 被拒绝 (TypeError/NetworkError)。
Fulfilled (数据可用) 数据已准备好被应用程序使用 终端状态 应用程序可以安全地使用获取到的数据。
Rejected (错误) 错误已发生 终端状态 请求或数据处理失败,应用程序需要处理错误。

结论与展望

Fetch API 提供了一个强大且现代化的方式来处理 Web 请求。其内部状态机,通过 Promise 驱动的异步流程,将请求的发送、响应头部的接收和响应体的读取清晰地分离开来。这种分阶段处理机制,不仅提升了网络请求的灵活性和效率,也为开发者提供了细粒度的控制,从而能够构建出更具弹性、响应更快的 Web 应用程序。

理解 Fetch API 的状态转换和错误处理机制,是利用其全部潜力的基石。通过恰当的错误检查和请求取消策略,我们可以确保应用在面对各种网络条件和服务器响应时,都能保持稳定和用户友好。随着 Web 平台的不断演进,Fetch API 及其核心概念将继续作为构建高性能、高可靠性 Web 体验的关键组成部分。

发表回复

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